Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

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
158 classes/Kohana/Request/Client.php
... ... @@ -1,4 +1,4 @@
1   -<?php defined('SYSPATH') or die('No direct script access.');
  1 +<?php defined('SYSPATH') OR die('No direct script access.');
2 2 /**
3 3 * Request Client. Processes a [Request] and handles [HTTP_Caching] if
4 4 * available. Will usually return a [Response] object as a result of the
@@ -34,6 +34,13 @@
34 34 protected $_strict_redirect = TRUE;
35 35
36 36 /**
  37 + * @var array Callbacks to use when response contains given headers
  38 + */
  39 + protected $_header_callbacks = array(
  40 + 'Location' => 'Request_Client::on_header_location'
  41 + );
  42 +
  43 + /**
37 44 * Creates a new `Request_Client` object,
38 45 * allows for dependency injection.
39 46 *
@@ -74,54 +81,42 @@ public function __construct(array $params = array())
74 81 */
75 82 public function execute(Request $request)
76 83 {
77   - $response = Response::factory();
  84 + $orig_response = $response = Response::factory();
78 85
79 86 if (($cache = $this->cache()) instanceof HTTP_Cache)
80 87 return $cache->execute($this, $request, $response);
81 88
82 89 $response = $this->execute_request($request, $response);
83 90
84   - // Do we need to follow a Location header ?
85   - if ($this->follow() AND in_array($response->status(), array(201, 301, 302, 303, 307))
86   - AND $response->headers('Location'))
  91 + // Execute response callbacks
  92 + foreach ($this->header_callbacks() as $header => $callback)
87 93 {
88   - // Figure out which method to use for the follow request
89   - switch ($response->status())
  94 + if ($response->headers($header))
90 95 {
91   - default:
92   - case 301:
93   - case 307:
94   - $follow_method = $request->method();
95   - break;
96   - case 201:
97   - case 303:
98   - $follow_method = Request::GET;
99   - break;
100   - case 302:
101   - // Cater for sites with broken HTTP redirect implementations
102   - if ($this->strict_redirect())
103   - {
104   - $follow_method = $request->method();
105   - }
106   - else
107   - {
108   - $follow_method = Request::GET;
109   - }
110   - break;
111   - }
  96 + $cb_result = call_user_func($callback, $request, $response, $this);
112 97
113   - // Prepare the additional request
114   - $follow_request = Request::factory($response->headers('Location'))
115   - ->method($follow_method)
116   - ->headers(Arr::extract($request->headers(), $this->follow_headers()));
  98 + if ($cb_result instanceof Request)
  99 + {
  100 + // If the callback returns a request, automatically assign client params
  101 + $client = $cb_result->client();
  102 + $client->cache($this->cache());
  103 + $client->follow($this->follow());
  104 + $client->follow_headers($this->follow_headers());
  105 + $client->header_callbacks($this->header_callbacks());
117 106
118   - if ($follow_method !== Request::GET)
119   - {
120   - $follow_request->body($request->body());
121   - }
  107 + // Execute the request
  108 + $response = $cb_result->execute();
  109 + }
  110 + elseif ($cb_result instanceof Response)
  111 + {
  112 + // Assign the returned response
  113 + $response = $cb_result;
  114 + }
122 115
123   - // Execute the additional request
124   - $response = $follow_request->execute();
  116 + // If the callback has created a new response, do not process any further
  117 + if ($response !== $orig_response)
  118 + break;
  119 + }
125 120 }
126 121
127 122 return $response;
@@ -215,4 +210,91 @@ public function strict_redirect($strict_redirect = NULL)
215 210
216 211 return $this;
217 212 }
  213 +
  214 + /**
  215 + * Getter and setter for the header callbacks array.
  216 + *
  217 + * Accepts an array with HTTP response headers as keys and a PHP callback
  218 + * function as values. These callbacks will be triggered if a response contains
  219 + * the given header and can either issue a subsequent request or manipulate
  220 + * the response as required.
  221 + *
  222 + * By default, the [Request_Client::on_header_location] callback is assigned
  223 + * to the Location header to support automatic redirect following.
  224 + *
  225 + * $client->header_callbacks(array(
  226 + * 'Location' => 'Request_Client::on_header_location',
  227 + * 'WWW-Authenticate' => function($request, $response, $client) {return $new_response;},
  228 + * );
  229 + *
  230 + * @param array $header_callbacks Array of callbacks to trigger on presence of given headers
  231 + * @return Request_Client
  232 + */
  233 + public function header_callbacks($header_callbacks = NULL)
  234 + {
  235 + if ($header_callbacks === NULL)
  236 + return $this->_header_callbacks;
  237 +
  238 + $this->_header_callbacks = $header_callbacks;
  239 +
  240 + return $this;
  241 + }
  242 +
  243 + /**
  244 + * The default handler for following redirects, triggered by the presence of
  245 + * a Location header in the response.
  246 + *
  247 + * The client's follow property must be set TRUE and the HTTP response status
  248 + * one of 201, 301, 302, 303 or 307 for the redirect to be followed.
  249 + *
  250 + * @param Request $request
  251 + * @param Response $response
  252 + * @param Request_Client $client
  253 + */
  254 + public static function on_header_location(Request $request, Response $response, Request_Client $client)
  255 + {
  256 + // Do we need to follow a Location header ?
  257 + if ($client->follow() AND in_array($response->status(), array(201, 301, 302, 303, 307)))
  258 + {
  259 + // Figure out which method to use for the follow request
  260 + switch ($response->status())
  261 + {
  262 + default:
  263 + case 301:
  264 + case 307:
  265 + $follow_method = $request->method();
  266 + break;
  267 + case 201:
  268 + case 303:
  269 + $follow_method = Request::GET;
  270 + break;
  271 + case 302:
  272 + // Cater for sites with broken HTTP redirect implementations
  273 + if ($client->strict_redirect())
  274 + {
  275 + $follow_method = $request->method();
  276 + }
  277 + else
  278 + {
  279 + $follow_method = Request::GET;
  280 + }
  281 + break;
  282 + }
  283 +
  284 + // Prepare the additional request
  285 + $follow_request = Request::factory($response->headers('Location'))
  286 + ->method($follow_method)
  287 + ->headers(Arr::extract($request->headers(), $client->follow_headers()));
  288 +
  289 + if ($follow_method !== Request::GET)
  290 + {
  291 + $follow_request->body($request->body());
  292 + }
  293 +
  294 + return $follow_request;
  295 + }
  296 +
  297 + return NULL;
  298 + }
  299 +
218 300 }
104 tests/kohana/request/ClientTest.php
@@ -259,6 +259,110 @@ public function test_follows_with_body_if_not_get($original_method, $status, $ex
259 259 $this->assertEquals($expect_body, $data['rq_body']);
260 260 }
261 261
  262 + /**
  263 + * Provider for test_triggers_header_callbacks
  264 + *
  265 + * @return array
  266 + */
  267 + public function provider_triggers_header_callbacks()
  268 + {
  269 + return array(
  270 + // Straightforward response manipulation
  271 + array(
  272 + array('X-test-1' =>
  273 + function($request, $response, $client){
  274 + $response->body(json_encode(array('body'=>'test1-body-changed')));
  275 + return $response;
  276 + }),
  277 + $this->_dummy_uri(200, array('X-test-1' => 'foo'), 'test1-body'),
  278 + 'test1-body-changed'
  279 + ),
  280 + // Subsequent request execution
  281 + array(
  282 + array('X-test-2' =>
  283 + function($request, $response, $client){
  284 + return Request::factory($response->headers('X-test-2'));
  285 + }),
  286 + $this->_dummy_uri(200,
  287 + array('X-test-2' => $this->_dummy_uri(200, NULL, 'test2-subsequent-body')),
  288 + 'test2-orig-body'),
  289 + 'test2-subsequent-body'
  290 + ),
  291 + // No callbacks triggered
  292 + array(
  293 + array('X-test-3' =>
  294 + function ($request, $response, $client) {
  295 + throw new Exception("Unexpected execution of X-test-3 callback");
  296 + }),
  297 + $this->_dummy_uri(200, array('X-test-1' => 'foo'), 'test3-body'),
  298 + 'test3-body'
  299 + ),
  300 + // Callbacks not triggered once a previous callback has created a new response
  301 + array(
  302 + array(
  303 + 'X-test-1' =>
  304 + function($request, $response, $client){
  305 + return Request::factory($response->headers('X-test-1'));
  306 + },
  307 + 'X-test-2' =>
  308 + function($request, $response, $client){
  309 + return Request::factory($response->headers('X-test-2'));
  310 + }
  311 + ),
  312 + $this->_dummy_uri(200,
  313 + array(
  314 + 'X-test-1' => $this->_dummy_uri(200, NULL, 'test1-subsequent-body'),
  315 + 'X-test-2' => $this->_dummy_uri(200, NULL, 'test2-subsequent-body')
  316 + ),
  317 + 'test2-orig-body'),
  318 + 'test1-subsequent-body'
  319 + ),
  320 + // Nested callbacks are supported if callback creates new request
  321 + array(
  322 + array(
  323 + 'X-test-1' =>
  324 + function($request, $response, $client){
  325 + return Request::factory($response->headers('X-test-1'));
  326 + },
  327 + 'X-test-2' =>
  328 + function($request, $response, $client){
  329 + return Request::factory($response->headers('X-test-2'));
  330 + }
  331 + ),
  332 + $this->_dummy_uri(200,
  333 + array(
  334 + 'X-test-1' => $this->_dummy_uri(
  335 + 200,
  336 + array('X-test-2' => $this->_dummy_uri(200, NULL, 'test2-subsequent-body')),
  337 + 'test1-subsequent-body'),
  338 + ),
  339 + 'test-orig-body'),
  340 + 'test2-subsequent-body'
  341 + ),
  342 + );
  343 + }
  344 +
  345 + /**
  346 + * Tests that header callbacks are triggered in sequence when specific headers
  347 + * are present in the response
  348 + *
  349 + * @dataProvider provider_triggers_header_callbacks
  350 + *
  351 + * @param array $callbacks Array of header callbacks
  352 + * @param array $headers Headers that will be received in the response
  353 + * @param string $expect_body Response body content to expect
  354 + */
  355 + public function test_triggers_header_callbacks($callbacks, $uri, $expect_body)
  356 + {
  357 + $response = Request::factory($uri,
  358 + array('header_callbacks' => $callbacks))
  359 + ->execute();
  360 +
  361 + $data = json_decode($response->body(), TRUE);
  362 +
  363 + $this->assertEquals($expect_body, $data['body']);
  364 + }
  365 +
262 366 } // End Kohana_Request_ClientTest
263 367
264 368

0 comments on commit a8a1505

Please sign in to comment.
Something went wrong with that request. Please try again.