Permalink
Browse files

You can now send any EntityEnclosingRequest with POST fields or POST

files and cURL will handle creating bodies
* Lots of cleanup to CurlHandle::factory and RequestFactory::createRequest
* POST requests using a custom entity body are now treated exactly like PUT
requests but with a custom cURL method. This means that the redirect
behavior of POST requests with custom bodies will not be the same as POST
requests that use POST fields or files (the latter is only used when
emulating a form POST in the browser).
* Closes #266
  • Loading branch information...
1 parent 4f818c9 commit 9da0ac2123ee3072078cd9da22e2a1dc3bc5dc49 @mtdowling mtdowling committed Mar 29, 2013
View
@@ -1,6 +1,15 @@
CHANGELOG
=========
+Next Version (TBD):
+-------------------
+
+* You can now send any EntityEnclosingRequest with POST fields or POST files and cURL will handle creating bodies
+* POST requests using a custom entity body are now treated exactly like PUT requests but with a custom cURL method. This
+ means that the redirect behavior of POST requests with custom bodies will not be the same as POST requests that use
+ POST fields or files (the latter is only used when emulating a form POST in the browser).
+* Lots of cleanup to CurlHandle::factory and RequestFactory::createRequest
+
3.3.1 (2013-03-10)
------------------
@@ -5,9 +5,9 @@
use Guzzle\Common\Exception\InvalidArgumentException;
use Guzzle\Common\Exception\RuntimeException;
use Guzzle\Common\Collection;
+use Guzzle\Http\Message\EntityEnclosingRequest;
use Guzzle\Http\Message\RequestInterface;
use Guzzle\Parser\ParserRegistry;
-use Guzzle\Http\Message\EntityEnclosingRequestInterface;
use Guzzle\Http\Url;
/**
@@ -62,6 +62,7 @@ public static function factory(RequestInterface $request)
CURLOPT_HEADER => false,
CURLOPT_PORT => $request->getPort(),
CURLOPT_HTTPHEADER => array(),
+ CURLOPT_WRITEFUNCTION => array($mediator, 'writeResponseBody'),
CURLOPT_HEADERFUNCTION => array($mediator, 'receiveResponseHeader'),
CURLOPT_HTTP_VERSION => $request->getProtocolVersion() === '1.0'
? CURL_HTTP_VERSION_1_0 : CURL_HTTP_VERSION_1_1,
@@ -100,78 +101,74 @@ public static function factory(RequestInterface $request)
$curlOptions[CURLOPT_VERBOSE] = true;
}
- // HEAD requests need no response body, everything else might
- if ($method != 'HEAD') {
- $curlOptions[CURLOPT_WRITEFUNCTION] = array($mediator, 'writeResponseBody');
- }
-
// Specify settings according to the HTTP method
- switch ($method) {
- case 'GET':
- $curlOptions[CURLOPT_HTTPGET] = true;
- break;
- case 'HEAD':
- $curlOptions[CURLOPT_NOBODY] = true;
- break;
- case 'POST':
- $curlOptions[CURLOPT_POST] = true;
- // Special handling for POST specific fields and files
- if (count($request->getPostFiles())) {
- $fields = $request->getPostFields()->useUrlEncoding(false)->urlEncode();
- foreach ($request->getPostFiles() as $key => $data) {
- $prefixKeys = count($data) > 1;
- foreach ($data as $index => $file) {
- // Allow multiple files in the same key
- $fieldKey = $prefixKeys ? "{$key}[{$index}]" : $key;
- $fields[$fieldKey] = $file->getCurlString();
- }
- }
- $curlOptions[CURLOPT_POSTFIELDS] = $fields;
- $request->removeHeader('Content-Length');
- } elseif (count($request->getPostFields())) {
- $curlOptions[CURLOPT_POSTFIELDS] = (string) $request->getPostFields()->useUrlEncoding(true);
- $request->removeHeader('Content-Length');
- } elseif (!$request->getBody()) {
- // Need to remove CURLOPT_POST to prevent chunked encoding for an empty POST
- unset($curlOptions[CURLOPT_POST]);
- $curlOptions[CURLOPT_CUSTOMREQUEST] = 'POST';
- }
- break;
- case 'PUT':
- case 'PATCH':
- case 'DELETE':
- default:
- $curlOptions[CURLOPT_CUSTOMREQUEST] = $method;
- if (!$bodyAsString) {
- $curlOptions[CURLOPT_UPLOAD] = true;
- // Let cURL handle setting the Content-Length header
- if ($tempContentLength = $request->getHeader('Content-Length')) {
- $tempContentLength = (int) (string) $tempContentLength;
- $curlOptions[CURLOPT_INFILESIZE] = $tempContentLength;
- }
- } elseif (!$request->hasHeader('Content-Type')) {
- // Remove the curl generated Content-Type header if none was set manually
- $curlOptions[CURLOPT_HTTPHEADER][] = 'Content-Type:';
- }
- }
+ if ($method == 'GET') {
+ $curlOptions[CURLOPT_HTTPGET] = true;
+ } elseif ($method == 'HEAD') {
+ $curlOptions[CURLOPT_NOBODY] = true;
+ // HEAD requests do not use a write function
+ unset($curlOptions[CURLOPT_WRITEFUNCTION]);
+ } elseif (!($request instanceof EntityEnclosingRequest)) {
+ $curlOptions[CURLOPT_CUSTOMREQUEST] = $method;
+ } else {
+
+ $curlOptions[CURLOPT_CUSTOMREQUEST] = $method;
- // Special handling for requests sending raw data
- if ($request instanceof EntityEnclosingRequestInterface) {
+ // Handle sending raw bodies in a request
if ($request->getBody()) {
+ // You can send the body as a string using curl's CURLOPT_POSTFIELDS
if ($bodyAsString) {
$curlOptions[CURLOPT_POSTFIELDS] = (string) $request->getBody();
// Allow curl to add the Content-Length for us to account for the times when
// POST redirects are followed by GET requests
if ($tempContentLength = $request->getHeader('Content-Length')) {
$tempContentLength = (int) (string) $tempContentLength;
}
+ // Remove the curl generated Content-Type header if none was set manually
+ if (!$request->hasHeader('Content-Type')) {
+ $curlOptions[CURLOPT_HTTPHEADER][] = 'Content-Type:';
+ }
} else {
+ $curlOptions[CURLOPT_UPLOAD] = true;
+ // Let cURL handle setting the Content-Length header
+ if ($tempContentLength = $request->getHeader('Content-Length')) {
+ $tempContentLength = (int) (string) $tempContentLength;
+ $curlOptions[CURLOPT_INFILESIZE] = $tempContentLength;
+ }
// Add a callback for curl to read data to send with the request only if a body was specified
$curlOptions[CURLOPT_READFUNCTION] = array($mediator, 'readRequestBody');
// Attempt to seek to the start of the stream
$request->getBody()->seek(0);
}
+
+ } else {
+
+ // Special handling for POST specific fields and files
+ $postFields = false;
+ if (count($request->getPostFiles())) {
+ $postFields = $request->getPostFields()->useUrlEncoding(false)->urlEncode();
+ foreach ($request->getPostFiles() as $key => $data) {
+ $prefixKeys = count($data) > 1;
+ foreach ($data as $index => $file) {
+ // Allow multiple files in the same key
+ $fieldKey = $prefixKeys ? "{$key}[{$index}]" : $key;
+ $postFields[$fieldKey] = $file->getCurlString();
+ }
+ }
+ } elseif (count($request->getPostFields())) {
+ $postFields = (string) $request->getPostFields()->useUrlEncoding(true);
+ }
+
+ if ($postFields !== false) {
+ if ($method == 'POST') {
+ unset($curlOptions[CURLOPT_CUSTOMREQUEST]);
+ $curlOptions[CURLOPT_POST] = true;
+ }
+ $curlOptions[CURLOPT_POSTFIELDS] = $postFields;
+ $request->removeHeader('Content-Length');
+ }
}
+
// If the Expect header is not present, prevent curl from adding it
if (!$request->hasHeader('Expect')) {
$curlOptions[CURLOPT_HTTPHEADER][] = 'Expect:';
@@ -91,39 +91,37 @@ public function create($method, $url, $headers = null, $body = null)
$method = strtoupper($method);
if ($method == 'GET' || $method == 'HEAD' || $method == 'TRACE' || $method == 'OPTIONS') {
- $c = $this->requestClass;
- $request = new $c($method, $url, $headers);
+ // Handle non-entity-enclosing request methods
+ $request = new $this->requestClass($method, $url, $headers);
if ($body) {
// The body is where the response body will be stored
- $request->setResponseBody(EntityBody::factory($body));
+ $request->setResponseBody($body);
}
return $request;
}
- $c = $this->entityEnclosingRequestClass;
- $request = new $c($method, $url, $headers);
+ // Create an entity enclosing request by default
+ $request = new $this->entityEnclosingRequestClass($method, $url, $headers);
if ($body) {
-
- $isChunked = (string) $request->getHeader('Transfer-Encoding') == 'chunked';
-
- if ($method == 'POST' && (is_array($body) || $body instanceof Collection)) {
-
+ // Add POST fields and files to an entity enclosing request if an array is used
+ if (is_array($body) || $body instanceof Collection) {
// Normalize PHP style cURL uploads with a leading '@' symbol
foreach ($body as $key => $value) {
if (is_string($value) && substr($value, 0, 1) == '@') {
$request->addPostFile($key, $value);
unset($body[$key]);
}
}
-
// Add the fields if they are still present and not all files
$request->addPostFields($body);
-
- } elseif (is_resource($body) || $body instanceof EntityBody) {
- $request->setBody($body, (string) $request->getHeader('Content-Type'), $isChunked);
} else {
- $request->setBody((string) $body, (string) $request->getHeader('Content-Type'), $isChunked);
+ // Add a raw entity body body to the request
+ $request->setBody(
+ $body,
+ (string) $request->getHeader('Content-Type'),
+ (string) $request->getHeader('Transfer-Encoding') == 'chunked'
+ );
}
}
@@ -402,13 +402,14 @@ public function dataProvider()
CURLOPT_CONNECTTIMEOUT => 10,
CURLOPT_WRITEFUNCTION => 'callback',
CURLOPT_HEADERFUNCTION => 'callback',
- CURLOPT_POST => 1,
+ CURLOPT_CUSTOMREQUEST => 'POST',
+ CURLOPT_UPLOAD => true,
+ CURLOPT_INFILESIZE => 14,
CURLOPT_HTTPHEADER => array (
'Expect:',
'Accept:',
'Host: localhost:8124',
'Content-Type: application/json',
- 'Content-Length: 14',
'User-Agent: ' . $userAgent
),
), array(
@@ -429,7 +430,8 @@ public function dataProvider()
CURLOPT_CONNECTTIMEOUT => 10,
CURLOPT_WRITEFUNCTION => 'callback',
CURLOPT_HEADERFUNCTION => 'callback',
- CURLOPT_POST => 1,
+ CURLOPT_CUSTOMREQUEST => 'POST',
+ CURLOPT_UPLOAD => true,
CURLOPT_HTTPHEADER => array (
'Expect:',
'Accept:',
@@ -795,7 +797,7 @@ public function testCanSendPostBodyAsString()
$this->assertContains('POST /', $requests[0]);
$this->assertContains("\nfoo", $requests[0]);
$this->assertContains('content-length: 3', $requests[0]);
- $this->assertContains('application/x-www-form-urlencoded', $requests[0]);
+ $this->assertNotContains('content-type', $requests[0]);
}
public function testAllowsWireTransferInfoToBeEnabled()
@@ -925,7 +927,7 @@ public function testAllowsCurloptEncodingToBeSet()
$this->getServer()->enqueue("HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n");
$client = new Client($this->getServer()->getUrl());
- $request = $client->get('/', null, 'test');
+ $request = $client->get('/', null);
$request->getCurlOptions()->set(CURLOPT_ENCODING, '');
$this->updateForHandle($request);
$request->send();
@@ -957,9 +959,9 @@ public function testSetsCurloptEncodingWhenAcceptEncodingHeaderIsSet()
$this->getServer()->enqueue("HTTP/1.1 200 OK\r\nContent-Length: 4\r\n\r\ndata");
$client = new Client($this->getServer()->getUrl());
$request = $client->get('/', array(
- 'Accept' => 'application/json',
- 'Accept-Encoding' => 'gzip, deflate',
- ));
+ 'Accept' => 'application/json',
+ 'Accept-Encoding' => 'gzip, deflate',
+ ));
$this->updateForHandle($request);
$request->send();
$options = $this->requestHandle->getOptions()->getAll();
@@ -968,4 +970,40 @@ public function testSetsCurloptEncodingWhenAcceptEncodingHeaderIsSet()
$this->assertContainsIns('accept: application/json', $received[0]);
$this->assertContainsIns('accept-encoding: gzip, deflate', $received[0]);
}
+
+ public function testSendsPostFieldsForNonPostRequests()
+ {
+ $this->getServer()->flush();
+ $this->getServer()->enqueue("HTTP/1.1 200 OK\r\n\r\nContent-Length: 0\r\n\r\n");
+
+ $client = new Client();
+ $request = $client->put($this->getServer()->getUrl(), null, array(
+ 'foo' => 'baz',
+ 'baz' => 'bar'
+ ));
+
+ $request->send();
+ $requests = $this->getServer()->getReceivedRequests(true);
+ $this->assertEquals('PUT', $requests[0]->getMethod());
+ $this->assertEquals('application/x-www-form-urlencoded', (string) $requests[0]->getHeader('Content-Type'));
+ $this->assertEquals(15, (string) $requests[0]->getHeader('Content-Length'));
+ $this->assertEquals('foo=baz&baz=bar', (string) $requests[0]->getBody());
+ }
+
+ public function testSendsPostFilesForNonPostRequests()
+ {
+ $this->getServer()->flush();
+ $this->getServer()->enqueue("HTTP/1.1 200 OK\r\n\r\nContent-Length: 0\r\n\r\n");
+
+ $client = new Client();
+ $request = $client->put($this->getServer()->getUrl(), null, array(
+ 'foo' => '@' . __FILE__
+ ));
+
+ $request->send();
+ $requests = $this->getServer()->getReceivedRequests(true);
+ $this->assertEquals('PUT', $requests[0]->getMethod());
+ $this->assertContains('multipart/form-data', (string) $requests[0]->getHeader('Content-Type'));
+ $this->assertContains('testSendsPostFilesForNonPostRequests', (string) $requests[0]->getBody());
+ }
}
@@ -43,15 +43,15 @@ public function cacheRevalidationDataProvider()
// Forces revalidation that overwrites what is in cache
array(
false,
- "\r\n\r\n",
+ "\r\n",
"HTTP/1.1 200 OK\r\nCache-Control: must-revalidate, no-cache\r\nDate: " . $this->getHttpDate('-10 hours') . "\r\nContent-Length: 4\r\n\r\nData",
"HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nDatas",
"HTTP/1.1 200 OK\r\nContent-Length: 5\r\nDate: " . $this->getHttpDate('now') . "\r\n\r\nDatas"
),
// Must get a fresh copy because the request is declining revalidation
array(
false,
- "\r\n\r\n",
+ "\r\n",
"HTTP/1.1 200 OK\r\nCache-Control: no-cache\r\nDate: " . $this->getHttpDate('-3 hours') . "\r\nContent-Length: 4\r\n\r\nData",
null,
null,
@@ -60,7 +60,7 @@ public function cacheRevalidationDataProvider()
// Skips revalidation because the request is accepting the cached copy
array(
true,
- "\r\n\r\n",
+ "\r\n",
"HTTP/1.1 200 OK\r\nCache-Control: no-cache\r\nDate: " . $this->getHttpDate('-3 hours') . "\r\nContent-Length: 4\r\n\r\nData",
null,
null,
@@ -69,14 +69,14 @@ public function cacheRevalidationDataProvider()
// Throws an exception during revalidation
array(
false,
- "\r\n\r\n",
+ "\r\n",
"HTTP/1.1 200 OK\r\nCache-Control: no-cache\r\nDate: " . $this->getHttpDate('-3 hours') . "\r\n\r\nData",
"HTTP/1.1 500 INTERNAL SERVER ERROR\r\nContent-Length: 0\r\n\r\n"
),
// ETag mismatch
array(
false,
- "\r\n\r\n",
+ "\r\n",
"HTTP/1.1 200 OK\r\nCache-Control: no-cache\r\nETag: \"123\"\r\nDate: " . $this->getHttpDate('-10 hours') . "\r\n\r\nData",
"HTTP/1.1 304 NOT MODIFIED\r\nETag: \"123456\"\r\n\r\n",
),

0 comments on commit 9da0ac2

Please sign in to comment.