Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Request_Client triggers callbacks on presence of specified headers [r…

…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...
commit a8a150539c2dd6ff1bbdabb2036bffdee41a10c8 1 parent 90bc6d6
Andrew Coulton authored
Showing with 224 additions and 38 deletions.
  1. +120 −38 classes/Kohana/Request/Client.php
  2. +104 −0 tests/kohana/request/ClientTest.php
158 classes/Kohana/Request/Client.php
View
@@ -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
@@ -34,6 +34,13 @@
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.
*
@@ -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;
@@ -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 tests/kohana/request/ClientTest.php
View
@@ -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
Please sign in to comment.
Something went wrong with that request. Please try again.