Skip to content

Commit

Permalink
Request_Client triggers callbacks on presence of specified headers [r…
Browse files Browse the repository at this point in the history
…efs #4353]

Request_Client checks the server response and triggers user-definable callbacks
when specified headers are present. The existing logic for following redirects
is implemented as a callback - other uses would include for eg dealing with
a WWW-Authenticate header to refresh an OAuth token.

Callbacks can return a request (which will be executed with the same callbacks
and client parameters as the original request) or a response.
  • Loading branch information
acoulton committed Mar 5, 2012
1 parent 90bc6d6 commit a8a1505
Show file tree
Hide file tree
Showing 2 changed files with 224 additions and 38 deletions.
158 changes: 120 additions & 38 deletions classes/Kohana/Request/Client.php
@@ -1,4 +1,4 @@
<?php defined('SYSPATH') or die('No direct script access.');
<?php defined('SYSPATH') OR die('No direct script access.');
/**
* Request Client. Processes a [Request] and handles [HTTP_Caching] if
* available. Will usually return a [Response] object as a result of the
Expand Down Expand Up @@ -33,6 +33,13 @@ abstract class Kohana_Request_Client {
*/
protected $_strict_redirect = TRUE;

/**
* @var array Callbacks to use when response contains given headers
*/
protected $_header_callbacks = array(
'Location' => 'Request_Client::on_header_location'
);

/**
* Creates a new `Request_Client` object,
* allows for dependency injection.
Expand Down Expand Up @@ -74,54 +81,42 @@ public function __construct(array $params = array())
*/
public function execute(Request $request)
{
$response = Response::factory();
$orig_response = $response = Response::factory();

if (($cache = $this->cache()) instanceof HTTP_Cache)
return $cache->execute($this, $request, $response);

$response = $this->execute_request($request, $response);

// Do we need to follow a Location header ?
if ($this->follow() AND in_array($response->status(), array(201, 301, 302, 303, 307))
AND $response->headers('Location'))
// Execute response callbacks
foreach ($this->header_callbacks() as $header => $callback)
{
// Figure out which method to use for the follow request
switch ($response->status())
if ($response->headers($header))
{
default:
case 301:
case 307:
$follow_method = $request->method();
break;
case 201:
case 303:
$follow_method = Request::GET;
break;
case 302:
// Cater for sites with broken HTTP redirect implementations
if ($this->strict_redirect())
{
$follow_method = $request->method();
}
else
{
$follow_method = Request::GET;
}
break;
}
$cb_result = call_user_func($callback, $request, $response, $this);

// Prepare the additional request
$follow_request = Request::factory($response->headers('Location'))
->method($follow_method)
->headers(Arr::extract($request->headers(), $this->follow_headers()));
if ($cb_result instanceof Request)
{
// If the callback returns a request, automatically assign client params
$client = $cb_result->client();
$client->cache($this->cache());
$client->follow($this->follow());
$client->follow_headers($this->follow_headers());
$client->header_callbacks($this->header_callbacks());

if ($follow_method !== Request::GET)
{
$follow_request->body($request->body());
}
// Execute the request
$response = $cb_result->execute();
}
elseif ($cb_result instanceof Response)
{
// Assign the returned response
$response = $cb_result;
}

// Execute the additional request
$response = $follow_request->execute();
// If the callback has created a new response, do not process any further
if ($response !== $orig_response)
break;
}
}

return $response;
Expand Down Expand Up @@ -215,4 +210,91 @@ public function strict_redirect($strict_redirect = NULL)

return $this;
}

/**
* Getter and setter for the header callbacks array.
*
* Accepts an array with HTTP response headers as keys and a PHP callback
* function as values. These callbacks will be triggered if a response contains
* the given header and can either issue a subsequent request or manipulate
* the response as required.
*
* By default, the [Request_Client::on_header_location] callback is assigned
* to the Location header to support automatic redirect following.
*
* $client->header_callbacks(array(
* 'Location' => 'Request_Client::on_header_location',
* 'WWW-Authenticate' => function($request, $response, $client) {return $new_response;},
* );
*
* @param array $header_callbacks Array of callbacks to trigger on presence of given headers
* @return Request_Client
*/
public function header_callbacks($header_callbacks = NULL)
{
if ($header_callbacks === NULL)
return $this->_header_callbacks;

$this->_header_callbacks = $header_callbacks;

return $this;
}

/**
* The default handler for following redirects, triggered by the presence of
* a Location header in the response.
*
* The client's follow property must be set TRUE and the HTTP response status
* one of 201, 301, 302, 303 or 307 for the redirect to be followed.
*
* @param Request $request
* @param Response $response
* @param Request_Client $client
*/
public static function on_header_location(Request $request, Response $response, Request_Client $client)
{
// Do we need to follow a Location header ?
if ($client->follow() AND in_array($response->status(), array(201, 301, 302, 303, 307)))
{
// Figure out which method to use for the follow request
switch ($response->status())
{
default:
case 301:
case 307:
$follow_method = $request->method();
break;
case 201:
case 303:
$follow_method = Request::GET;
break;
case 302:
// Cater for sites with broken HTTP redirect implementations
if ($client->strict_redirect())
{
$follow_method = $request->method();
}
else
{
$follow_method = Request::GET;
}
break;
}

// Prepare the additional request
$follow_request = Request::factory($response->headers('Location'))
->method($follow_method)
->headers(Arr::extract($request->headers(), $client->follow_headers()));

if ($follow_method !== Request::GET)
{
$follow_request->body($request->body());
}

return $follow_request;
}

return NULL;
}

}
104 changes: 104 additions & 0 deletions tests/kohana/request/ClientTest.php
Expand Up @@ -259,6 +259,110 @@ public function test_follows_with_body_if_not_get($original_method, $status, $ex
$this->assertEquals($expect_body, $data['rq_body']);
}

/**
* Provider for test_triggers_header_callbacks
*
* @return array
*/
public function provider_triggers_header_callbacks()
{
return array(
// Straightforward response manipulation
array(
array('X-test-1' =>
function($request, $response, $client){
$response->body(json_encode(array('body'=>'test1-body-changed')));
return $response;
}),
$this->_dummy_uri(200, array('X-test-1' => 'foo'), 'test1-body'),
'test1-body-changed'
),
// Subsequent request execution
array(
array('X-test-2' =>
function($request, $response, $client){
return Request::factory($response->headers('X-test-2'));
}),
$this->_dummy_uri(200,
array('X-test-2' => $this->_dummy_uri(200, NULL, 'test2-subsequent-body')),
'test2-orig-body'),
'test2-subsequent-body'
),
// No callbacks triggered
array(
array('X-test-3' =>
function ($request, $response, $client) {
throw new Exception("Unexpected execution of X-test-3 callback");
}),
$this->_dummy_uri(200, array('X-test-1' => 'foo'), 'test3-body'),
'test3-body'
),
// Callbacks not triggered once a previous callback has created a new response
array(
array(
'X-test-1' =>
function($request, $response, $client){
return Request::factory($response->headers('X-test-1'));
},
'X-test-2' =>
function($request, $response, $client){
return Request::factory($response->headers('X-test-2'));
}
),
$this->_dummy_uri(200,
array(
'X-test-1' => $this->_dummy_uri(200, NULL, 'test1-subsequent-body'),
'X-test-2' => $this->_dummy_uri(200, NULL, 'test2-subsequent-body')
),
'test2-orig-body'),
'test1-subsequent-body'
),
// Nested callbacks are supported if callback creates new request
array(
array(
'X-test-1' =>
function($request, $response, $client){
return Request::factory($response->headers('X-test-1'));
},
'X-test-2' =>
function($request, $response, $client){
return Request::factory($response->headers('X-test-2'));
}
),
$this->_dummy_uri(200,
array(
'X-test-1' => $this->_dummy_uri(
200,
array('X-test-2' => $this->_dummy_uri(200, NULL, 'test2-subsequent-body')),
'test1-subsequent-body'),
),
'test-orig-body'),
'test2-subsequent-body'
),
);
}

/**
* Tests that header callbacks are triggered in sequence when specific headers
* are present in the response
*
* @dataProvider provider_triggers_header_callbacks
*
* @param array $callbacks Array of header callbacks
* @param array $headers Headers that will be received in the response
* @param string $expect_body Response body content to expect
*/
public function test_triggers_header_callbacks($callbacks, $uri, $expect_body)
{
$response = Request::factory($uri,
array('header_callbacks' => $callbacks))
->execute();

$data = json_decode($response->body(), TRUE);

$this->assertEquals($expect_body, $data['body']);
}

} // End Kohana_Request_ClientTest


Expand Down

0 comments on commit a8a1505

Please sign in to comment.