2019-01-27 20:00:39 +00:00
< ? php
/*
* This file is part of the Symfony package .
*
* ( c ) Fabien Potencier < fabien @ symfony . com >
*
* For the full copyright and license information , please view the LICENSE
* file that was distributed with this source code .
*/
namespace Symfony\Component\HttpClient\Response ;
use Symfony\Component\HttpClient\Chunk\DataChunk ;
use Symfony\Component\HttpClient\Chunk\ErrorChunk ;
2019-03-26 00:54:03 +00:00
use Symfony\Component\HttpClient\Chunk\FirstChunk ;
2019-01-27 20:00:39 +00:00
use Symfony\Component\HttpClient\Chunk\LastChunk ;
use Symfony\Component\HttpClient\Exception\ClientException ;
2019-03-09 16:12:38 +00:00
use Symfony\Component\HttpClient\Exception\JsonException ;
2019-01-27 20:00:39 +00:00
use Symfony\Component\HttpClient\Exception\RedirectionException ;
use Symfony\Component\HttpClient\Exception\ServerException ;
use Symfony\Component\HttpClient\Exception\TransportException ;
2019-04-07 10:02:11 +01:00
use Symfony\Component\HttpClient\Internal\ClientState ;
2019-07-16 07:40:46 +01:00
use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface ;
use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface ;
use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface ;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface ;
2019-01-27 20:00:39 +00:00
/**
* Implements the common logic for response classes .
*
* @ author Nicolas Grekas < p @ tchwork . com >
*
* @ internal
*/
trait ResponseTrait
{
2019-04-04 15:02:57 +01:00
private $logger ;
2019-01-27 20:00:39 +00:00
private $headers = [];
/**
* @ var callable | null A callback that initializes the two previous properties
*/
private $initializer ;
private $info = [
2019-04-02 11:03:40 +01:00
'response_headers' => [],
2019-03-08 13:46:03 +00:00
'http_code' => 0 ,
2019-01-27 20:00:39 +00:00
'error' => null ,
2019-10-21 09:45:40 +01:00
'canceled' => false ,
2019-01-27 20:00:39 +00:00
];
2019-04-07 10:02:11 +01:00
/** @var resource */
2019-01-27 20:00:39 +00:00
private $handle ;
private $id ;
2019-10-14 18:04:51 +01:00
private $timeout = 0 ;
2020-01-05 13:56:11 +00:00
private $inflate ;
private $shouldBuffer ;
private $content ;
2019-01-27 20:00:39 +00:00
private $finalInfo ;
private $offset = 0 ;
2019-03-09 16:12:38 +00:00
private $jsonData ;
2019-01-27 20:00:39 +00:00
/**
* { @ inheritdoc }
*/
public function getStatusCode () : int
{
if ( $this -> initializer ) {
2020-01-05 13:56:11 +00:00
self :: initialize ( $this );
2019-01-27 20:00:39 +00:00
}
2019-03-08 13:46:03 +00:00
return $this -> info [ 'http_code' ];
2019-01-27 20:00:39 +00:00
}
/**
* { @ inheritdoc }
*/
public function getHeaders ( bool $throw = true ) : array
{
if ( $this -> initializer ) {
2020-01-05 13:56:11 +00:00
self :: initialize ( $this );
2019-01-27 20:00:39 +00:00
}
if ( $throw ) {
$this -> checkStatusCode ();
}
return $this -> headers ;
}
/**
* { @ inheritdoc }
*/
public function getContent ( bool $throw = true ) : string
{
if ( $this -> initializer ) {
2020-01-05 13:56:11 +00:00
self :: initialize ( $this );
2019-01-27 20:00:39 +00:00
}
if ( $throw ) {
$this -> checkStatusCode ();
}
if ( null === $this -> content ) {
2019-03-11 09:55:59 +00:00
$content = null ;
2019-01-27 20:00:39 +00:00
foreach ( self :: stream ([ $this ]) as $chunk ) {
2019-03-11 09:55:59 +00:00
if ( ! $chunk -> isLast ()) {
$content .= $chunk -> getContent ();
}
2019-01-27 20:00:39 +00:00
}
2019-10-30 11:53:18 +00:00
if ( null !== $content ) {
return $content ;
2019-01-27 20:00:39 +00:00
}
2019-10-30 11:53:18 +00:00
if ( 'HEAD' === $this -> info [ 'http_method' ] || \in_array ( $this -> info [ 'http_code' ], [ 204 , 304 ], true )) {
return '' ;
}
2019-10-30 12:55:29 +00:00
throw new TransportException ( 'Cannot get the content of the response twice: buffering is disabled.' );
2019-01-27 20:00:39 +00:00
}
foreach ( self :: stream ([ $this ]) as $chunk ) {
// Chunks are buffered in $this->content already
}
rewind ( $this -> content );
return stream_get_contents ( $this -> content );
}
2019-03-09 16:12:38 +00:00
/**
* { @ inheritdoc }
*/
public function toArray ( bool $throw = true ) : array
{
if ( '' === $content = $this -> getContent ( $throw )) {
throw new TransportException ( 'Response body is empty.' );
}
if ( null !== $this -> jsonData ) {
return $this -> jsonData ;
}
$contentType = $this -> headers [ 'content-type' ][ 0 ] ? ? 'application/json' ;
if ( ! preg_match ( '/\bjson\b/i' , $contentType )) {
2020-02-21 22:42:54 +00:00
throw new JsonException ( sprintf ( 'Response content-type is "%s" while a JSON-compatible one was expected for "%s".' , $contentType , $this -> getInfo ( 'url' )));
2019-03-09 16:12:38 +00:00
}
try {
2020-02-04 09:29:10 +00:00
$content = json_decode ( $content , true , 512 , JSON_BIGINT_AS_STRING | ( \PHP_VERSION_ID >= 70300 ? JSON_THROW_ON_ERROR : 0 ));
2019-03-09 16:12:38 +00:00
} catch ( \JsonException $e ) {
2020-02-21 22:42:54 +00:00
throw new JsonException ( sprintf ( '%s for "%s".' , $e -> getMessage (), $this -> getInfo ( 'url' )), $e -> getCode ());
2019-03-09 16:12:38 +00:00
}
if ( \PHP_VERSION_ID < 70300 && JSON_ERROR_NONE !== json_last_error ()) {
2020-02-21 22:42:54 +00:00
throw new JsonException ( sprintf ( '%s for "%s".' , json_last_error_msg (), $this -> getInfo ( 'url' )), json_last_error ());
2019-03-09 16:12:38 +00:00
}
if ( ! \is_array ( $content )) {
2020-03-03 19:07:47 +00:00
throw new JsonException ( sprintf ( 'JSON content was expected to decode to an array, "%s" returned for "%s".' , get_debug_type ( $content ), $this -> getInfo ( 'url' )));
2019-03-09 16:12:38 +00:00
}
if ( null !== $this -> content ) {
// Option "buffer" is true
return $this -> jsonData = $content ;
}
return $content ;
}
2019-06-03 18:56:51 +01:00
/**
* { @ inheritdoc }
*/
public function cancel () : void
{
2019-10-21 09:45:40 +01:00
$this -> info [ 'canceled' ] = true ;
2019-06-03 18:56:51 +01:00
$this -> info [ 'error' ] = 'Response has been canceled.' ;
$this -> close ();
}
2019-06-30 00:27:41 +01:00
/**
* Casts the response to a PHP stream resource .
*
2019-10-14 18:04:51 +01:00
* @ return resource
2019-07-16 07:40:46 +01:00
*
* @ throws TransportExceptionInterface When a network error occurs
* @ throws RedirectionExceptionInterface On a 3 xx when $throw is true and the " max_redirects " option has been reached
* @ throws ClientExceptionInterface On a 4 xx when $throw is true
* @ throws ServerExceptionInterface On a 5 xx when $throw is true
2019-06-30 00:27:41 +01:00
*/
2019-07-16 07:40:46 +01:00
public function toStream ( bool $throw = true )
2019-06-30 00:27:41 +01:00
{
2019-10-14 18:04:51 +01:00
if ( $throw ) {
// Ensure headers arrived
$this -> getHeaders ( $throw );
}
2019-06-30 00:27:41 +01:00
2020-01-03 13:52:20 +00:00
$stream = StreamWrapper :: createResource ( $this );
stream_get_meta_data ( $stream )[ 'wrapper_data' ]
-> bindHandles ( $this -> handle , $this -> content );
return $stream ;
2019-06-30 00:27:41 +01:00
}
2019-01-27 20:00:39 +00:00
/**
* Closes the response and all its network handles .
*/
abstract protected function close () : void ;
/**
* Adds pending responses to the activity list .
*/
abstract protected static function schedule ( self $response , array & $runningResponses ) : void ;
/**
* Performs all pending non - blocking operations .
*/
2019-04-07 10:02:11 +01:00
abstract protected static function perform ( ClientState $multi , array & $responses ) : void ;
2019-01-27 20:00:39 +00:00
/**
* Waits for network activity .
*/
2019-04-07 10:02:11 +01:00
abstract protected static function select ( ClientState $multi , float $timeout ) : int ;
2019-01-27 20:00:39 +00:00
2020-01-05 13:56:11 +00:00
private static function initialize ( self $response ) : void
{
if ( null !== $response -> info [ 'error' ]) {
throw new TransportException ( $response -> info [ 'error' ]);
}
try {
if (( $response -> initializer )( $response )) {
foreach ( self :: stream ([ $response ]) as $chunk ) {
if ( $chunk -> isFirst ()) {
break ;
}
}
}
} catch ( \Throwable $e ) {
// Persist timeouts thrown during initialization
$response -> info [ 'error' ] = $e -> getMessage ();
$response -> close ();
throw $e ;
}
$response -> initializer = null ;
}
2019-05-27 19:15:39 +01:00
private static function addResponseHeaders ( array $responseHeaders , array & $info , array & $headers , string & $debug = '' ) : void
2019-01-27 20:00:39 +00:00
{
2019-04-02 11:03:40 +01:00
foreach ( $responseHeaders as $h ) {
2020-01-13 15:59:16 +00:00
if ( 11 <= \strlen ( $h ) && '/' === $h [ 4 ] && preg_match ( '#^HTTP/\d+(?:\.\d+)? ([12345]\d\d)(?: |$)#' , $h , $m )) {
2019-05-27 19:15:39 +01:00
if ( $headers ) {
$debug .= " < \r \n " ;
$headers = [];
}
2019-03-08 13:46:03 +00:00
$info [ 'http_code' ] = ( int ) $m [ 1 ];
2019-01-27 20:00:39 +00:00
} elseif ( 2 === \count ( $m = explode ( ':' , $h , 2 ))) {
2019-03-08 13:46:03 +00:00
$headers [ strtolower ( $m [ 0 ])][] = ltrim ( $m [ 1 ]);
2019-01-27 20:00:39 +00:00
}
2019-05-27 19:15:39 +01:00
$debug .= " < { $h } \r \n " ;
2019-04-02 11:03:40 +01:00
$info [ 'response_headers' ][] = $h ;
2019-01-27 20:00:39 +00:00
}
2019-05-27 19:15:39 +01:00
$debug .= " < \r \n " ;
2019-03-08 13:46:03 +00:00
if ( ! $info [ 'http_code' ]) {
2019-01-27 20:00:39 +00:00
throw new TransportException ( 'Invalid or missing HTTP status line.' );
}
}
private function checkStatusCode ()
{
2019-03-08 13:46:03 +00:00
if ( 500 <= $this -> info [ 'http_code' ]) {
2019-01-27 20:00:39 +00:00
throw new ServerException ( $this );
}
2019-03-08 13:46:03 +00:00
if ( 400 <= $this -> info [ 'http_code' ]) {
2019-01-27 20:00:39 +00:00
throw new ClientException ( $this );
}
2019-03-08 13:46:03 +00:00
if ( 300 <= $this -> info [ 'http_code' ]) {
2019-01-27 20:00:39 +00:00
throw new RedirectionException ( $this );
}
}
/**
* Ensures the request is always sent and that the response code was checked .
*/
private function doDestruct ()
{
2020-02-11 13:51:01 +00:00
$this -> shouldBuffer = true ;
2019-01-27 20:00:39 +00:00
if ( $this -> initializer && null === $this -> info [ 'error' ]) {
2020-01-05 13:56:11 +00:00
self :: initialize ( $this );
2019-01-27 20:00:39 +00:00
$this -> checkStatusCode ();
}
}
/**
* Implements an event loop based on a buffer activity queue .
*
* @ internal
*/
public static function stream ( iterable $responses , float $timeout = null ) : \Generator
{
$runningResponses = [];
foreach ( $responses as $response ) {
self :: schedule ( $response , $runningResponses );
}
$lastActivity = microtime ( true );
$isTimeout = false ;
while ( true ) {
$hasActivity = false ;
$timeoutMax = 0 ;
$timeoutMin = $timeout ? ? INF ;
2019-04-07 10:02:11 +01:00
/** @var ClientState $multi */
2019-01-27 20:00:39 +00:00
foreach ( $runningResponses as $i => [ $multi ]) {
$responses = & $runningResponses [ $i ][ 1 ];
self :: perform ( $multi , $responses );
foreach ( $responses as $j => $response ) {
$timeoutMax = $timeout ? ? max ( $timeoutMax , $response -> timeout );
$timeoutMin = min ( $timeoutMin , $response -> timeout , 1 );
2019-03-19 09:44:44 +00:00
$chunk = false ;
2019-01-27 20:00:39 +00:00
if ( isset ( $multi -> handlesActivity [ $j ])) {
// no-op
} elseif ( ! isset ( $multi -> openHandles [ $j ])) {
unset ( $responses [ $j ]);
continue ;
} elseif ( $isTimeout ) {
2019-10-31 09:21:58 +00:00
$multi -> handlesActivity [ $j ] = [ new ErrorChunk ( $response -> offset , sprintf ( 'Idle timeout reached for "%s".' , $response -> getInfo ( 'url' )))];
2019-01-27 20:00:39 +00:00
} else {
continue ;
}
while ( $multi -> handlesActivity [ $j ] ? ? false ) {
$hasActivity = true ;
$isTimeout = false ;
if ( \is_string ( $chunk = array_shift ( $multi -> handlesActivity [ $j ]))) {
2020-01-05 13:56:11 +00:00
if ( null !== $response -> inflate && false === $chunk = @ inflate_add ( $response -> inflate , $chunk )) {
$multi -> handlesActivity [ $j ] = [ null , new TransportException ( 'Error while processing content unencoding.' )];
continue ;
}
if ( '' !== $chunk && null !== $response -> content && \strlen ( $chunk ) !== fwrite ( $response -> content , $chunk )) {
2020-01-07 20:22:51 +00:00
$multi -> handlesActivity [ $j ] = [ null , new TransportException ( sprintf ( 'Failed writing %d bytes to the response buffer.' , \strlen ( $chunk )))];
2020-01-05 13:56:11 +00:00
continue ;
}
2019-01-27 20:00:39 +00:00
$response -> offset += \strlen ( $chunk );
$chunk = new DataChunk ( $response -> offset , $chunk );
} elseif ( null === $chunk ) {
2019-03-11 09:55:59 +00:00
$e = $multi -> handlesActivity [ $j ][ 0 ];
unset ( $responses [ $j ], $multi -> handlesActivity [ $j ]);
$response -> close ();
if ( null !== $e ) {
2019-01-27 20:00:39 +00:00
$response -> info [ 'error' ] = $e -> getMessage ();
if ( $e instanceof \Error ) {
throw $e ;
}
2019-03-19 09:44:44 +00:00
$chunk = new ErrorChunk ( $response -> offset , $e );
2019-01-27 20:00:39 +00:00
} else {
$chunk = new LastChunk ( $response -> offset );
}
} elseif ( $chunk instanceof ErrorChunk ) {
unset ( $responses [ $j ]);
$isTimeout = true ;
2019-09-19 19:45:47 +01:00
} elseif ( $chunk instanceof FirstChunk ) {
if ( $response -> logger ) {
$info = $response -> getInfo ();
$response -> logger -> info ( sprintf ( 'Response: "%s %s"' , $info [ 'http_code' ], $info [ 'url' ]));
}
2020-01-05 13:56:11 +00:00
$response -> inflate = \extension_loaded ( 'zlib' ) && $response -> inflate && 'gzip' === ( $response -> headers [ 'content-encoding' ][ 0 ] ? ? null ) ? inflate_init ( ZLIB_ENCODING_GZIP ) : null ;
2020-01-06 12:57:54 +00:00
if ( $response -> shouldBuffer instanceof \Closure ) {
try {
$response -> shouldBuffer = ( $response -> shouldBuffer )( $response -> headers );
if ( null !== $response -> info [ 'error' ]) {
throw new TransportException ( $response -> info [ 'error' ]);
}
} catch ( \Throwable $e ) {
$response -> close ();
$multi -> handlesActivity [ $j ] = [ null , $e ];
}
}
if ( true === $response -> shouldBuffer ) {
$response -> content = fopen ( 'php://temp' , 'w+' );
} elseif ( \is_resource ( $response -> shouldBuffer )) {
$response -> content = $response -> shouldBuffer ;
}
$response -> shouldBuffer = null ;
2020-01-05 13:56:11 +00:00
2019-09-19 19:45:47 +01:00
yield $response => $chunk ;
if ( $response -> initializer && null === $response -> info [ 'error' ]) {
// Ensure the HTTP status code is always checked
$response -> getHeaders ( true );
}
continue ;
2019-01-27 20:00:39 +00:00
}
yield $response => $chunk ;
}
unset ( $multi -> handlesActivity [ $j ]);
2019-09-19 19:45:47 +01:00
if ( $chunk instanceof ErrorChunk && ! $chunk -> didThrow ()) {
2019-01-27 20:00:39 +00:00
// Ensure transport exceptions are always thrown
$chunk -> getContent ();
}
}
if ( ! $responses ) {
unset ( $runningResponses [ $i ]);
}
// Prevent memory leaks
$multi -> handlesActivity = $multi -> handlesActivity ? : [];
$multi -> openHandles = $multi -> openHandles ? : [];
}
if ( ! $runningResponses ) {
break ;
}
if ( $hasActivity ) {
$lastActivity = microtime ( true );
continue ;
}
switch ( self :: select ( $multi , $timeoutMin )) {
case - 1 : usleep ( min ( 500 , 1E6 * $timeoutMin )); break ;
case 0 : $isTimeout = microtime ( true ) - $lastActivity > $timeoutMax ; break ;
}
}
}
}