PHP HTTP POST fails when cURL data > 1024 - php

Note: solution at the end
If I attempt to do a HTTP POST of over 1024 characters, it fails. Why? Here is a minimal example:
recipient.php:
<?php
if (strlen(file_get_contents('php://input')) > 1000
|| strlen($HTTP_RAW_POST_DATA) > 1000) {
echo "This was a triumph.";
}
?>
sender.php:
<?php
function try_to_post($char_count) {
$url = 'http://gpx3quaa.joyent.us/test/recipient.php';
$post_data = str_repeat('x', $char_count);
$c = curl_init();
curl_setopt_array($c,
array( CURLOPT_URL => $url,
CURLOPT_HEADER => false,
CURLOPT_CONNECTTIMEOUT => 999,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => 1,
CURLOPT_POSTFIELDS => $post_data
)
);
$result = curl_exec($c);
echo "{$result}\n";
curl_close($c);
}
for ($i=1020;$i<1030;$i++) {
echo "Trying {$i} - ";
try_to_post($i);
}
?>
output:
Trying 1020 - This was a triumph.
Trying 1021 - This was a triumph.
Trying 1022 - This was a triumph.
Trying 1023 - This was a triumph.
Trying 1024 - This was a triumph.
Trying 1025 -
Trying 1026 -
Trying 1027 -
Trying 1028 -
Trying 1029 -
configuration:
PHP Version 5.2.6
libcurl/7.18.0 OpenSSL/0.9.8g zlib/1.2.3 libidn/1.8
lighttpd-1.4.19
Solution
Add the following option for cURL:
curl_setopt($ch,CURLOPT_HTTPHEADER,array("Expect:"));
The reason seems to be that any POST over 1024 character causes the "Expect: 100-continue" HTTP header to be sent, and Lighttpd 1.4.* does not support it. I found a ticket for it: http://redmine.lighttpd.net/issues/show/1017
They say it works in 1.5.

You can convince PHP's curl backend to stop doing the 100-continue-thing by setting an explicit request header:
curl_setopt($ch, CURLOPT_HTTPHEADER, array('Expect:'));
This way you can post a request however long you would ever want and curl will not do the dual phase post.
I've blogged about this nearly two years ago.

First thoughts...
The manual page for curl_setopt says of CURLOPT_POSTFIELDS
"The full data to post in a HTTP "POST"
operation. To post a file, prepend a
filename with # and use the full path.
This can either be passed as a
urlencoded string like
'para1=val1&para2=val2&...' or as an
array with the field name as key and
field data as value."
Could it be that your value is being treated as if it were urlencoded, and thus looks like a big long name with no value. Something somewhere is deciding to truncate that name.
Maybe you could alter it to something like
$post_data = "data=".str_repeat('x', $char_count);
Turns out this was too easy, and the problem was a little deeper. So, how to debug?
Find out exactly what CURL sends to the server
Another debugging tactic might be to formulate a curl command line which achieves the same thing, and have it output the HTTP request details as it makes them.
Testing the server by hand
You can eliminate the server from the equation by perform a request by hand, e.g. telnetting to port 80 on your server and sending it a request >1024 chars
POST /test/recipient.php HTTP/1.0
Host: gpx3quaa.joyent.us
Content-Length:1028
xxxxx(I put 1028 chars here, no point copying them all here!)
I got this response
HTTP/1.0 200 OK
Connection: close
Content-type: text/html; charset=UTF-8
Content-Length: 19
Date: Tue, 20 Jan 2009 21:35:16 GMT
Server: lighttpd/1.4.19
This was a triumph.Connection closed by foreign host.
So at least you now know it's all on the client side, possible some CURL option or configuration setting somewhere :(
Final Answer!
The problem intrigued me so I dug deeper
If you use CURLOPT_VERBOSE=>true, you'll see that CURL sends an extra header on the bigger posts:Expect: 100-Continue. Your lighttpd server doesn't like this, it would seem.
You can stop CURL from doing this by forcing it to use HTTP/1.0 with CURLOPT_HTTP_VERSION=>CURL_HTTP_VERSION_1_0 in your curl_setopt options array.

I had a similar problem with a IIS server, using SSL v3.
I kept getting the following cURL error when CURLOPT_POSTFIELDS was longer than 1024 :
52 - SSL read: error:1408F10B:SSL routines:SSL3_GET_RECORD:wrong version number, errno 0
Adding CURLOPT_HTTPHEADER : "Expect:" solved the problem for me.
Thank you so much for this thread!

Check if you have Suhosin patch enabled. By default, it cuts off POST data after certain number of indexes. You can bypass it in Suhosin config tho.

Related

PHP + Curl: Connection closure while negotiation auth (HTTP 1.0?)

I have a problem between CURL and PHP. I tried to connect to an Exchange 2010 Server via EWS. The connection to the server works fine in command line using CURL but not with PHP Curl extension.
I attached an image with the debug information. On the left you see command line output, on the right PHP verbose output. When the PHP Curl Extension throws an error "Connection closure while negotiation auth (HTTP 1.0?)" the command line continues with a third HTTP request with results in an HTTP/1.1 302 Found:
Some additional information:
I use this library for CURL-Requests: https://github.com/jamesiarmes/php-ntlm/blob/master/src/SoapClient.php
we have about 80 exchange servers where there is no problem. Only with this server there is a problem. Our customer told about a software called "Sophos" used as a proxy for the webserver
CURLOPT_HTTPAUTH is CURLAUTH_NTLM
PHP Version 7.3.1 / 7.3.9 also tested
cURL Information 7.63.0 / 7.52.1 also tested
Does anybody know why the PHP Curl Extension closes the connection before the third request? Is this a bug of the extension, can I use a constant of PHP Curl to avoid that or is there another solution?
The connection is closed because the server says so. See your screenshot, one line above where the "What is the problem here?" points.
HTTP/1.1 401 Unauthorized
[...]
Connection: close
Content-Type: application/x-asmx
And probably the server closes the connection afterwards.
So that is not an action, but a result. The message is emitted in Curl_http_readwrite_headers:
#if defined(USE_NTLM)
if(conn->bits.close &&
(((data->req.httpcode == 401) &&
(conn->http_ntlm_state == NTLMSTATE_TYPE2)) ||
((data->req.httpcode == 407) &&
(conn->proxy_ntlm_state == NTLMSTATE_TYPE2)))) {
infof(data, "Connection closure while negotiating auth (HTTP 1.0?)\n");
data->state.authproblem = TRUE;
}
#endif
#if defined(USE_SPNEGO)
if(conn->bits.close &&
(((data->req.httpcode == 401) &&
(conn->http_negotiate_state == GSS_AUTHRECV)) ||
((data->req.httpcode == 407) &&
(conn->proxy_negotiate_state == GSS_AUTHRECV)))) {
infof(data, "Connection closure while negotiating auth (HTTP 1.0?)\n");
data->state.authproblem = TRUE;
}
Presumably from the first block (NTLM), but these are the two occurrences, and they are next to each other anyway.
Fun fact: the same function only a lot later checks for the presence of a Connection: close header, so having the mystical flag conn->bits.close set probably means that the server dropped the connection already and it was detected on socket-level.
Side remark: the two sides of the comparison show very dissimilar interactions. On the left there is a practically empty GET request (Host, Authorization, User-Agent and Accept headers are provided), while on the right side there is a lot more complicated POST request (same headers plus Method, SOAPAction, an empty content with Expect for continuation).
I have faced such like this problem by using SoapClient
and Microsoft Exchange 2010 Server, and the trick was by changing the header array option 'Expect: 100-continue'
to 'Expect: 200-ok'
protected function buildHeaders($action)
{
return array(
'Method: POST',
'Connection: Keep-Alive',
'User-Agent: PHP-SOAP-CURL',
'Content-Type: text/xml; charset=utf-8',
"SOAPAction: \"$action\"",
'Expect: 200-ok',
);
}
The 100 (Continue) status code indicates that the initial part of a request has been received and has not yet been rejected by the server.
The 200 (OK) status code indicats that the request has succeeded. The meaning of a success varies depending on the HTTP method.
You can also check this out HTTP status codes
I wish this could help

Why does a cURL connection fail (without error) if no timeout is set?

I have a PHP script that connects to an URL through cURL and then does something, depending on the returned HTTP status code:
$ch = curl_init();
$options = array(
CURLOPT_RETURNTRANSFER => true,
CURLOPT_URL => $url,
CURLOPT_USERAGENT => "What?!?"
);
curl_setopt_array($ch, $options);
$out = curl_exec($ch);
$code = curl_getinfo($ch)["http_code"];
curl_close($ch);
if ($code == "200") {
echo "200";
} else {
echo "not 200";
}
Some webservers are slow to reply, and although the page is loaded in my browser after a few seconds my script, when it tries to connect to that server, tells me that it did not receive a positive ("200") reply. So, apparently, the connection initiated by cURL timed out.
But why? I don't set a timeout in my script, and according to other answers on this site the default timeout for cURL is definitely longer than the three or four seconds it takes for the page to load in my browser.
So why does the connecion time out, and how can I get it to last longer, if, apparently, it is already set to infinite?
Notes:
The same URL doesn't always time out. So sometimes cURL can connect.
It is not one specific URL that sometimes times out, but different URLs at different times.
I'm on a shared server, so I don't have root access to any files.
I tried to look at curl_getinfo($ch) and curl_error($ch) – as per #drew010's suggestion in the comments – but both were empty whenever the problem happened.
The whole script runs for a little more than one minute. In this time it connects to 300+ URLs successfully. Even when one of the URLs fails, the other connections are successfully made. So the script does not time out.
cURL does not time out either, because when I try to connect to an URL with a script sleeping for 59 seconds, cURL successfully connects. So apparently the slowness of the failing URL is not a problem in itself for cURL.
Update
Following #Karlos' suggestion in his answer, I used:
CURLOPT_VERBOSE => 1,
CURLOPT_STDERR => $curl_log
(using code from this answer) and found the following in $curl_log when an URL failed (URL and IP changed):
* About to connect() to www.somesite.com port 80 (#0)
* Trying 104.16.37.249... * connected
* Connected to www.somesite.com (104.16.37.249) port 80 (#0)
GET /wp_german/?feed=rss2 HTTP/1.1
User-Agent: myURL
Host: www.somesite.com
Accept: */*
* Recv failure: Connection reset by peer
* Closing connection #0
So, I have found the why – thank you #Karlos! – and apparently #Axalix was right and it is a network problem. I'll now follow suggestions given on this site for that kind of failure. Thanks to everyone for their help!
My experience working with curl showed me that sometimes when using the option:
CURLOPT_RETURNTRANSFER => true
the server might not give a successful reply or, at least, a successful reply within the timeframe that curl has to receive the response and cache it, so the results are returned by the curl into the variable you assign. In your code:
$out = curl_exec($ch);
In this stackoverflow question CURLOPT_RETURNTRANSFER set to true doesnt work on hosting server, you can see that that the option CURLOPT_RETURNTRANSFER is directly affected by the requested host web server implementation.
As you are using explicitly the response body, and your code relies on the response headers, a good way to solve this might be to:
CURLOPT_RETURNTRANSFER => false
and execute the curl code to work on the response headers.
Once you have the header with the code you are interested, you could run a php script that echoes the curl response and parse it by yourself:
<?php
$url=isset($_GET['url']) ? $_GET['url'] : 'http://www.example.com';
$ch= curl_init();
$options = array(
CURLOPT_RETURNTRANSFER => false,
CURLOPT_URL => $url,
CURLOPT_USERAGENT => "myURL"
);
curl_setopt_array($ch, $options);
curl_exec($ch);
curl_close($ch);
?>
In any case the reply to your question why your request does not get an error, I guess that the use of the option CURLOPT_NOSIGNAL and the different timeout options explained in the set_opt php manual might get you closer to it.
In order to dig further, the option CURLOPT_VERBOSE might help you to have extra information about the request behavior through the STDERR.
The reason may be your hosting provider is imposing some limits on outgoing connections.
Here is what can be done to secure your script:
Create a queue in DB with all the URLs that need to be fetched.
Run cron every minute or 5 minutes, take a few URLs from DB - mark them as in progress.
Try to fetch those URLs. Mark every fetched URL as success in DB.
Increment failure count for unsuccessful ones.
Continue going through queue until its empty.
If you implement such a solution you will be able to process every single URL under any unfavourable conditions.

Twitter 1.1 OAuth authenticity_token_error(99)

I use the following code to get the bearer token:
$token = base64_encode($client_id.':'.$client_sec);
$data = array ('grant_type' => 'client_credentials');
$data = http_build_query($data);
$header = array(
'Authorization: Basic '.$token,
'Content-type: application/x-www-form-urlencoded;charset=UTF-8',
'Content-Length: ' . strlen($data)
);
$options = array(
CURLOPT_HTTPHEADER => $header,
CURLOPT_HEADER => false,
CURLOPT_URL => 'https://api.twitter.com/oauth2/token',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POSTFIELDS => $data
);
$ch = curl_init();
curl_setopt_array($ch, $options);
$result = curl_exec($ch);
curl_close($ch);
print_r($result);
exit;
But output all the time:
{"errors":[{"label":"authenticity_token_error","code":99,"message":"Unable to verify your credentials"}]}
What I doing wrong?
After fighting with this problem for a while I found the problem was I was making the call to /oauth2/token using Advanced Rest Client from a browser I was already logged into Twitter with. After logging out of Twitter and making the API call again it worked fine.
Short answer: make sure you do not already have an active session logged into Twitter when attempting to request a Bearer token.
I struggled with this for awhile and none of the answers I've found seemed to help. The documentation for the error is also a vague "something went wrong".
My problem is that I was using a mashup of code I found, and the headers weren't used correctly:
$headers = array(
'Authorization' => 'Basic ' . base64_encode($appid . ':' . $secret), // WRONG!!!
'Authorization: Basic ' . base64_encode($appid . ':' . $secret), // Correct!
'Content-Type: application/x-www-form-urlencoded;charset=UTF-8', // Correct!
);
For me, the problem was that the Authorization header was using key-value format, while the content-type header was not. This broke the authorization header.
Here are some other things to check that also relate to error 99:
Verify that your credentials are correct and base64 encoded (see above)
Make sure the request is using POST
Ensure the content-type is set (see above)
Make sure you included grant_type=client_credentials as a post field.
SSL is required, make sure that is being used (https://)
Try verbose logging to help debugging. It should include SSL certificate information, your authorization header, and content type header. This won't show the grant_type field though, only headers.
If everything looks OK but it still won't work, you might be getting rate limited. Rate limits reset every 15 minutes.
When you finally get your access token, make sure you cache it to avoid rate limiting. You get 450 requests every 15 minutes, I believe. Half of that will be spent on getting your access token if you don't cache it!
There's an accepted answer here already but just in case someone stroll to this post and had the same issue I did...
Twitter docs for reference -> OAuth 2.0 docs
Misconception #1: The Authorization String is generated using the consumer key (aka API-Key) and consumer secret (aka API Secret Key). The display of those credentials in the UI on developer.twitter.com is less apparent than that of apps.twitter.com. Nonetheless common RIF problem.
Misconception #2: This one is not really an misconception but an implementation error when base64 encoding the url concatenated Consumer Key+":"+Consumer Secret. If not doing this programmatically be sure to check for whitespaces anywhere (especially around the :) in the concatenated string you are base64 encoding.
Just a tad bit advice as well postman has a wonderful utility that makes the rest call to retrieve an oauth2.0 token (as well as other auth tokens) this was useful for me when trying to consume api's with the that required an oauth1.0 token
After fighting with this problem, i finally come up with the solution. Twitter is not actually sending the right message if error exist anywhere.
When i send request from curl, it works fine but when through code. i was having same error {"errors":[{"label":"authenticity_token_error","code":99,"message":"Unable to verify your credentials"}]}
So what i got, problem was lying with Access control header. setting these header does not work for me
xhttp.setRequestHeader("Content-Type","application/x-www-form-urlencoded")
xhttp.setRequestHeader('Access-Control-Allow-Headers', '*');
xhttp.setRequestHeader('Access-Control-Allow-Origin', '*')
xhttp.setRequestHeader('X-Requested-With', 'XMLHttpRequest')
as a workaround i just used this url to bypass request to handler cors
https://cors-anywhere.herokuapp.com/https://api.twitter.com/oauth2/token
added "https://cors-anywhere.herokuapp.com/" before the actual url and it began to work. hope someone may face this issue in problem
Twitter OAuth 2.0 Bearer Token:
Step 1: Encode consumer key and secret
A - Concatenate the encoded consumer key, a colon character ":", and the encoded consumer secret into a single string.
B - Base64 encode the string from the previous step.
Example function: convertStringBase64("Api key" +":"+"Api key secret")
C- This steps generate you "Authorization"
Step 2: Obtain a Bearer Token
URL: https://api.twitter.com/oauth2/token
The request must be an HTTP POST request.
The request must include an Authorization header with the value of Basic <base64 encoded value from step 1>.
The request must include a Content-Type header with the value of application/x-www-form-urlencoded;charset=UTF-8.
The body of the request must be grant_type=client_credentials.
Example request (Authorization header has been wrapped):
POST /oauth2/token HTTP/1.1
Host: api.twitter.com
User-Agent: My Twitter App v1.0.23
Authorization: Basic eHZ6MWV2RlM0d0VFUFRHRUZQSEJvZzpMOHFxOVBaeVJn
NmllS0dFS2hab2xHQzB2SldMdzhpRUo4OERSZHlPZw==
Content-Type: application/x-www-form-urlencoded;charset=UTF-8
Content-Length: 29
Accept-Encoding: gzip
grant_type=client_credentials
If the request was formatted correctly, the server would respond with a JSON-encoded payload:
Example response:
HTTP/1.1 200 OK
Status: 200 OK
Content-Type: application/json; charset=utf-8
...
Content-Encoding: gzip
Content-Length: 140
{"token_type":"bearer","access_token":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA%2FAAAAAAAAAAAAAAAAAAAA%3DAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"}
For more informtaion look this in Twitter Developer API: https://developer.twitter.com/en/docs/authentication/oauth-2-0/application-only

Xml stream read by browser, but fail when loaded with Php [duplicate]

I'm trying to download the contents of a web page using PHP.
When I issue the command:
$f = file_get_contents("http://mobile.mybustracker.co.uk/mobile.php?searchMode=2");
It returns a page that reports that the server is down. Yet when I paste the same URL into my browser I get the expected page.
Does anyone have any idea what's causing this? Does file_get_contents transmit any headers that differentiate it from a browser request?
Yes, there are differences -- the browser tends to send plenty of additionnal HTTP headers, I'd say ; and the ones that are sent by both probably don't have the same value.
Here, after doing a couple of tests, it seems that passing the HTTP header called Accept is necessary.
This can be done using the third parameter of file_get_contents, to specify additionnal context informations :
$opts = array('http' =>
array(
'method' => 'GET',
//'user_agent ' => "Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.2) Gecko/20100301 Ubuntu/9.10 (karmic) Firefox/3.6",
'header' => array(
'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*\/*;q=0.8
'
),
)
);
$context = stream_context_create($opts);
$f = file_get_contents("http://mobile.mybustracker.co.uk/mobile.php?searchMode=2", false, $context);
echo $f;
With this, I'm able to get the HTML code of the page.
Notes :
I first tested passing the User-Agent, but it doesn't seem to be necessary -- which is why the corresponding line is here as a comment
The value is used for the Accept header is the one Firefox used when I requested that page with Firefox before trying with file_get_contents.
Some other values might be OK, but I didn't do any test to determine which value is the required one.
For more informations, you can take a look at :
file_get_contents
stream_context_create
Context options and parameters
HTTP context options -- that's the interesting page, here ;-)
replace all spaces with %20

Can't Set "Host:" Header with CURL Request

I am trying to make a CURL request with some basic authentication (through sending an encrypted header). However, when I try to set the "Host: _" header, the remote server responds with:
Bad Request: Your browser sent a request that this server could not understand.
Here's the code that makes the CURL call. Note that it works as soon as I comment out the "Host: url" header in the $http_header variable. However, it is used on the target server as part of the authentication procedure, so I can not simply remove it.
$curl = curl_init();
$opt = array(
CURLOPT_URL=>$url,
CURLOPT_RETURNTRANSFER=>1,
CURLOPT_HTTPHEADER=>$http_header,
CURLOPT_POST=>1,
CURLOPT_POSTFIELDS=>$data_params,
CURLOPT_CONNECTTIMEOUT=>5, // Timeout to 5 seconds
);
curl_setopt_array($curl, $opt);
// $output contains the output string
$output = curl_exec($curl);
// it closes the session
curl_close($curl);
The contents of $http_header (an associative array):
array
0 => string 'Host: http://localhost/work/myproject'
1 => string 'Content-Type: multipart/form-data'
2 => string 'X-Authorization: Basicauth Z2xZjA2YzJhMzIxYTI1ZmIzZTgxYQ=='
The Host header doesn't accept a full URL, but only a hostname.
In this case the solution is to replace this header:
"Host: $url"
with:
"Host: ". parse_url($url, PHP_URL_HOST) ."
The Host header takes a hostname, not a URL. i.e. localhost, not http://localhost/work/myproject.
cURL should be generating it automatically from CURLOPT_URL though.
The Host header should not contain the protocol, only the host's name. So in this case all you need is localhost:
'Host: localhost'
I don't think either answer answers the asker's question. I've ran into the same problem. From looking at what cURL is sending, it is sending it's own Host header. This means that when you add one you now have two. All servers MUST respond with a 400 error when they see more than one host header.
The trick is get cURL not to generate it's own header (which I haven't figured out how to do yet) or don't send yours. In your case it seems not sending yours is fine. But, for what I'm doing, I have to send mine.

Categories