mirror of
https://github.com/php-curl-class/php-curl-class.git
synced 2026-09-01 05:02:39 +00:00
Implement Curl::setStop() for stopping requests early
Use Curl::setStop() to stop requests early without downloading the full response.
This commit is contained in:
@@ -290,6 +290,7 @@ Curl::setRange($range)
|
||||
Curl::setReferer($referer)
|
||||
Curl::setReferrer($referrer)
|
||||
Curl::setRetry($mixed)
|
||||
Curl::setStop($callback)
|
||||
Curl::setTimeout($seconds)
|
||||
Curl::setUrl($url, $mixed_data = '')
|
||||
Curl::setUserAgent($user_agent)
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
require __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
use Curl\Curl;
|
||||
|
||||
$curl = new Curl();
|
||||
$curl->setStop(function ($ch, $header) {
|
||||
// Stop requests returning error responses early without downloading the
|
||||
// full error response.
|
||||
//
|
||||
// Check the header for the status line starting with "HTTP/".
|
||||
// Status-Line per RFC 2616:
|
||||
// 6.1 Status-Line:
|
||||
// Status-Line = HTTP-Version SP Status-Code SP Reason-Phrase CRLF
|
||||
if (stripos($header, 'HTTP/') === 0) {
|
||||
$status_line_parts = explode(' ', $header);
|
||||
if (isset($status_line_parts['1'])) {
|
||||
$http_status_code = $status_line_parts['1'];
|
||||
$http_error = in_array((int) floor($http_status_code / 100), [4, 5], true);
|
||||
if ($http_error) {
|
||||
// Return true to stop receiving the response.
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Return false to continue receiving the response.
|
||||
return false;
|
||||
});
|
||||
|
||||
$curl->get('https://www.example.com/large-500-error');
|
||||
if ($curl->error) {
|
||||
echo 'Error: ' . $curl->errorCode . ': ' . $curl->errorMessage . "\n";
|
||||
echo 'Response content-length: ' . $curl->responseHeaders['content-length'] . "\n";
|
||||
echo 'Actual response size downloaded: ' . $curl->getInfo(CURLINFO_SIZE_DOWNLOAD) . "\n";
|
||||
} else {
|
||||
echo 'Response content-length: ' . $curl->responseHeaders['content-length'] . "\n";
|
||||
echo 'Actual response size downloaded: ' . $curl->getInfo(CURLINFO_SIZE_DOWNLOAD) . "\n";
|
||||
}
|
||||
@@ -51,6 +51,7 @@ class Curl
|
||||
public $jsonDecoder = null;
|
||||
public $xmlDecoder = null;
|
||||
|
||||
private $headerCallbackData;
|
||||
private $cookies = [];
|
||||
private $headers = [];
|
||||
private $options = [];
|
||||
@@ -488,6 +489,8 @@ class Curl
|
||||
$this->responseCookies = $this->headerCallbackData->responseCookies;
|
||||
$this->headerCallbackData->rawResponseHeaders = '';
|
||||
$this->headerCallbackData->responseCookies = [];
|
||||
$this->headerCallbackData->stopRequestDecider = null;
|
||||
$this->headerCallbackData->stopRequest = false;
|
||||
|
||||
// Include additional error code information in error message when possible.
|
||||
if ($this->curlError && function_exists('curl_strerror')) {
|
||||
@@ -2055,6 +2058,8 @@ class Curl
|
||||
$header_callback_data = new \stdClass();
|
||||
$header_callback_data->rawResponseHeaders = '';
|
||||
$header_callback_data->responseCookies = [];
|
||||
$header_callback_data->stopRequestDecider = null;
|
||||
$header_callback_data->stopRequest = false;
|
||||
$this->headerCallbackData = $header_callback_data;
|
||||
$this->setOpt(CURLOPT_HEADERFUNCTION, createHeaderCallback($header_callback_data));
|
||||
|
||||
@@ -2065,6 +2070,42 @@ class Curl
|
||||
$this->setUrl($base_url);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set Stop
|
||||
*
|
||||
* Specify a callable decider to stop the request early without waiting for
|
||||
* the full response to be received.
|
||||
*
|
||||
* The callable is passed two parameters. The first is the cURL resource,
|
||||
* the second is a string with header data. Both parameters match the
|
||||
* parameters in the CURLOPT_HEADERFUNCTION callback.
|
||||
*
|
||||
* The callable must return a truthy value for the request to be stopped
|
||||
* early.
|
||||
*
|
||||
* @access public
|
||||
* @param $callback callable
|
||||
*/
|
||||
public function setStop($callback)
|
||||
{
|
||||
$this->headerCallbackData->stopRequestDecider = $callback;
|
||||
$this->headerCallbackData->stopRequest = false;
|
||||
|
||||
$header_callback_data = $this->headerCallbackData;
|
||||
$this->progress(function (
|
||||
$resource,
|
||||
$download_size,
|
||||
$downloaded,
|
||||
$upload_size,
|
||||
$uploaded
|
||||
) use (
|
||||
$header_callback_data
|
||||
) {
|
||||
// Abort the transfer when the stop request flag has been set by returning a non-zero value.
|
||||
return $header_callback_data->stopRequest ? 1 : 0;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2083,6 +2124,14 @@ function createHeaderCallback($header_callback_data) {
|
||||
if (preg_match('/^Set-Cookie:\s*([^=]+)=([^;]+)/mi', $header, $cookie) === 1) {
|
||||
$header_callback_data->responseCookies[$cookie[1]] = trim($cookie[2], " \n\r\t\0\x0B");
|
||||
}
|
||||
|
||||
if ($header_callback_data->stopRequestDecider !== null) {
|
||||
$stop_request_decider = $header_callback_data->stopRequestDecider;
|
||||
if ($stop_request_decider($ch, $header)) {
|
||||
$header_callback_data->stopRequest = true;
|
||||
}
|
||||
}
|
||||
|
||||
$header_callback_data->rawResponseHeaders .= $header;
|
||||
return strlen($header);
|
||||
};
|
||||
|
||||
@@ -4066,4 +4066,73 @@ class CurlTest extends \PHPUnit\Framework\TestCase
|
||||
$this->assertStringContainsString($expected_string, $test_3_output);
|
||||
}
|
||||
}
|
||||
|
||||
public function testStopRequest() {
|
||||
$response_length_bytes = 1e6; // 1e6 = 1 megabyte
|
||||
|
||||
$stop_request_early = function ($ch, $header) {
|
||||
// Stop requests returning error responses early without downloading the
|
||||
// full error response.
|
||||
//
|
||||
// Check the header for the status line starting with "HTTP/".
|
||||
// Status-Line per RFC 2616:
|
||||
// 6.1 Status-Line:
|
||||
// Status-Line = HTTP-Version SP Status-Code SP Reason-Phrase CRLF
|
||||
if (stripos($header, 'HTTP/') === 0) {
|
||||
$status_line_parts = explode(' ', $header);
|
||||
if (isset($status_line_parts['1'])) {
|
||||
$http_status_code = $status_line_parts['1'];
|
||||
$http_error = in_array((int) floor($http_status_code / 100), [4, 5], true);
|
||||
if ($http_error) {
|
||||
// Return true to stop receiving the response.
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Return false to continue receiving the response.
|
||||
return false;
|
||||
};
|
||||
|
||||
// Verify that full response is fetched for an error.
|
||||
$test_1 = new Test();
|
||||
$test_1->server('download_file_size', 'GET', [
|
||||
'bytes' => $response_length_bytes,
|
||||
'http_response_code' => '500',
|
||||
]);
|
||||
$this->assertEquals($response_length_bytes, $test_1->curl->responseHeaders['Content-Length']);
|
||||
$this->assertEquals($response_length_bytes, $test_1->curl->getInfo(CURLINFO_SIZE_DOWNLOAD));
|
||||
$this->assertEquals($response_length_bytes, strlen($test_1->curl->rawResponse));
|
||||
$this->assertTrue($test_1->curl->error);
|
||||
$this->assertFalse($test_1->curl->curlError);
|
||||
$this->assertTrue($test_1->curl->httpError);
|
||||
|
||||
// Verify that full response is not fetched for an error.
|
||||
$test_2 = new Test();
|
||||
$test_2->curl->setStop($stop_request_early);
|
||||
$test_2->server('download_file_size', 'GET', [
|
||||
'bytes' => $response_length_bytes,
|
||||
'http_response_code' => '500',
|
||||
]);
|
||||
$this->assertEquals($response_length_bytes, $test_2->curl->responseHeaders['Content-Length']);
|
||||
$this->assertLessThan($response_length_bytes, $test_2->curl->getInfo(CURLINFO_SIZE_DOWNLOAD));
|
||||
$this->assertLessThan($response_length_bytes, strlen($test_2->curl->rawResponse));
|
||||
$this->assertTrue($test_2->curl->error);
|
||||
$this->assertTrue($test_2->curl->curlError);
|
||||
$this->assertTrue($test_2->curl->httpError);
|
||||
|
||||
// Verify that full response is still fetched for a non-error.
|
||||
$test_3 = new Test();
|
||||
$test_3->curl->setStop($stop_request_early);
|
||||
$test_3->server('download_file_size', 'GET', [
|
||||
'bytes' => $response_length_bytes,
|
||||
'http_response_code' => '200',
|
||||
]);
|
||||
$this->assertEquals($response_length_bytes, $test_3->curl->responseHeaders['Content-Length']);
|
||||
$this->assertEquals($response_length_bytes, $test_3->curl->getInfo(CURLINFO_SIZE_DOWNLOAD));
|
||||
$this->assertEquals($response_length_bytes, strlen($test_3->curl->rawResponse));
|
||||
$this->assertFalse($test_3->curl->error);
|
||||
$this->assertFalse($test_3->curl->curlError);
|
||||
$this->assertFalse($test_3->curl->httpError);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -263,6 +263,9 @@ if ($test === 'http_basic_auth') {
|
||||
readfile($unsafe_file_path);
|
||||
exit;
|
||||
} elseif ($test === 'download_file_size') {
|
||||
if (isset($_GET['http_response_code'])) {
|
||||
http_response_code((int) $_GET['http_response_code']);
|
||||
}
|
||||
$bytes = isset($_GET['bytes']) ? $_GET['bytes'] : 1234;
|
||||
$str = str_repeat('.', (int) $bytes);
|
||||
header('Content-Type: application/octet-stream');
|
||||
|
||||
Reference in New Issue
Block a user