cURL: forward POST over HTTP redirections

Recently, I've been stuck on a Web service call that wouldn't be called as supposed to be. When having troubles with WS, it's very important to dump client request and server response.

Using cURL withing PHP, I couldn't understand why my code was building a POST request and cURL returned me a GET request instead when the URI was getting a 301 redirection.

Turned out that it was a configuration issue because the URI wasn't the right one. Thanks to the config team!

What cURL says

When curl follows a redirect and the request is not a plain GET (for example POST or PUT), it will do  the  following  request  with  a  GET  if the HTTP response was 301, 302, or 303. If the response code was any other 3xx code, curl will re-send the following request using the same unmodified method.

Make it work

Server

Create a server.php file and just dump the $_POST variable:

<?php
var_export($_POST);

Client

Create client.php and build a POST cURL request to server.php:

<?php
header('Content-Type: text/plain');

$ch = curl_init();

curl_setopt($ch, CURLOPT_URL, 'http://localhost/server.php');
curl_setopt($ch, CURLOPT_HEADER, 1);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_VERBOSE, 1);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
curl_setopt($ch, CURLOPT_FRESH_CONNECT, 1);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLINFO_HEADER_OUT, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, array('params' => 'foo-bar-baz'));

$response = curl_exec($ch);
echo 'REQUEST HEADERS:' . PHP_EOL . PHP_EOL;
echo curl_getinfo($ch)['request_header'];
echo 'RESPONSE:' . PHP_EOL . PHP_EOL;
echo $response;

curl_close($ch);

Runs the client.php and it should output this:

REQUEST HEADERS:

POST /server.php HTTP/1.1
Host: localhost
Accept: */*
Content-Length: 152
Expect: 100-continue
Content-Type: multipart/form-data; boundary=----------------------------f8cc685e6441

RESPONSE:

HTTP/1.1 100 Continue

HTTP/1.1 200 OK
Date: Wed, 07 May 2014 10:41:36 GMT
Server: Apache/2.2.24 (Unix) mod_ssl/2.2.24 OpenSSL/0.9.8y PHP/5.4.27
X-Powered-By: PHP/5.4.27
Content-Length: 38
Content-Type: text/html; charset=UTF-8

Array
(
    [params] => foo-bar-baz
)

Redirection

Create a redirect.php file and put this in it:

<?php
header('Location: http://localhost/server.php');
exit;

Update the client URI from http://localhost/server.php to http://localhost/redirect.php and run again the client:

REQUEST HEADERS:

GET /server.php HTTP/1.1
Host: localhost
Accept: */*

RESPONSE:

HTTP/1.1 100 Continue

HTTP/1.1 302 Found
Date: Wed, 07 May 2014 10:45:19 GMT
Server: Apache/2.2.24 (Unix) mod_ssl/2.2.24 OpenSSL/0.9.8y PHP/5.4.27
X-Powered-By: PHP/5.4.27
Location: http://localhost/server.php
Content-Length: 0
Content-Type: text/html; charset=UTF-8

HTTP/1.1 200 OK
Date: Wed, 07 May 2014 10:45:19 GMT
Server: Apache/2.2.24 (Unix) mod_ssl/2.2.24 OpenSSL/0.9.8y PHP/5.4.27
X-Powered-By: PHP/5.4.27
Content-Length: 10
Content-Type: text/html; charset=UTF-8

Array
(
)

It's obvious that the POST has been converted to GET and no more data is forwarded.

Some people say that you will need to set the CURLOPT_CUSTOMREQUEST cURL option to go through. Indeed, when adding this option, the HTTP verb is well kept:

curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');

However, the data is still lost. What's the solution then?

Using a 307 redirection

To keep your data, you mustn't use a 301, 302 or 303 redirection but the 307 redirection:

307 Temporary Redirect (since HTTP/1.1)
In this case, the request should be repeated with another URI; however, future requests should still use the original URI. In contrast to how 302 was historically implemented, the request method is not allowed to be changed when reissuing the original request. For instance, a POST request should be repeated using another POST request.

Give it a try, update your redirect.php file:

<?php
header('HTTP/1.1 307 Temporary Redirect');
header('Location: http://localhost/server.php');
exit;

Then run the client again:

REQUEST HEADERS:

POST /server.php HTTP/1.1
Host: localhost
Accept: */*
Content-Length: 152
Expect: 100-continue
Content-Type: multipart/form-data; boundary=----------------------------cebd8214326b

RESPONSE:

HTTP/1.1 100 Continue

HTTP/1.1 307 Temporary Redirect
Date: Wed, 07 May 2014 10:58:33 GMT
Server: Apache/2.2.24 (Unix) mod_ssl/2.2.24 OpenSSL/0.9.8y PHP/5.4.27
X-Powered-By: PHP/5.4.27
Location: http://localhost/server.php
Content-Length: 0
Content-Type: text/html; charset=UTF-8

HTTP/1.1 100 Continue

HTTP/1.1 200 OK
Date: Wed, 07 May 2014 10:58:33 GMT
Server: Apache/2.2.24 (Unix) mod_ssl/2.2.24 OpenSSL/0.9.8y PHP/5.4.27
X-Powered-By: PHP/5.4.27
Content-Length: 38
Content-Type: text/html; charset=UTF-8

Array
(
    [params] => foo-bar-baz
)

You're all set!

There is a 308 redirection which should do the same as the 307 but for permanent redirection but doesn't seem to be supported by cURL yet.

Thanks for reading and happy data forwarding!