4 use GuzzleHttp\Exception\BadResponseException;
5 use GuzzleHttp\Exception\TooManyRedirectsException;
6 use GuzzleHttp\Promise\PromiseInterface;
8 use Psr\Http\Message\RequestInterface;
9 use Psr\Http\Message\ResponseInterface;
10 use Psr\Http\Message\UriInterface;
13 * Request redirect middleware.
15 * Apply this middleware like other middleware using
16 * {@see GuzzleHttp\Middleware::redirect()}.
18 class RedirectMiddleware
20 const HISTORY_HEADER = 'X-Guzzle-Redirect-History';
22 public static $defaultSettings = [
24 'protocols' => ['http', 'https'],
27 'track_redirects' => false,
34 * @param callable $nextHandler Next handler to invoke.
36 public function __construct(callable $nextHandler)
38 $this->nextHandler = $nextHandler;
42 * @param RequestInterface $request
43 * @param array $options
45 * @return PromiseInterface
47 public function __invoke(RequestInterface $request, array $options)
49 $fn = $this->nextHandler;
51 if (empty($options['allow_redirects'])) {
52 return $fn($request, $options);
55 if ($options['allow_redirects'] === true) {
56 $options['allow_redirects'] = self::$defaultSettings;
57 } elseif (!is_array($options['allow_redirects'])) {
58 throw new \InvalidArgumentException('allow_redirects must be true, false, or array');
60 // Merge the default settings with the provided settings
61 $options['allow_redirects'] += self::$defaultSettings;
64 if (empty($options['allow_redirects']['max'])) {
65 return $fn($request, $options);
68 return $fn($request, $options)
69 ->then(function (ResponseInterface $response) use ($request, $options) {
70 return $this->checkRedirect($request, $options, $response);
75 * @param RequestInterface $request
76 * @param array $options
77 * @param ResponseInterface|PromiseInterface $response
79 * @return ResponseInterface|PromiseInterface
81 public function checkRedirect(
82 RequestInterface $request,
84 ResponseInterface $response
86 if (substr($response->getStatusCode(), 0, 1) != '3'
87 || !$response->hasHeader('Location')
92 $this->guardMax($request, $options);
93 $nextRequest = $this->modifyRequest($request, $options, $response);
95 if (isset($options['allow_redirects']['on_redirect'])) {
97 $options['allow_redirects']['on_redirect'],
100 $nextRequest->getUri()
104 /** @var PromiseInterface|ResponseInterface $promise */
105 $promise = $this($nextRequest, $options);
107 // Add headers to be able to track history of redirects.
108 if (!empty($options['allow_redirects']['track_redirects'])) {
109 return $this->withTracking(
111 (string) $nextRequest->getUri()
118 private function withTracking(PromiseInterface $promise, $uri)
120 return $promise->then(
121 function (ResponseInterface $response) use ($uri) {
122 // Note that we are pushing to the front of the list as this
123 // would be an earlier response than what is currently present
124 // in the history header.
125 $header = $response->getHeader(self::HISTORY_HEADER);
126 array_unshift($header, $uri);
127 return $response->withHeader(self::HISTORY_HEADER, $header);
132 private function guardMax(RequestInterface $request, array &$options)
134 $current = isset($options['__redirect_count'])
135 ? $options['__redirect_count']
137 $options['__redirect_count'] = $current + 1;
138 $max = $options['allow_redirects']['max'];
140 if ($options['__redirect_count'] > $max) {
141 throw new TooManyRedirectsException(
142 "Will not follow more than {$max} redirects",
149 * @param RequestInterface $request
150 * @param array $options
151 * @param ResponseInterface $response
153 * @return RequestInterface
155 public function modifyRequest(
156 RequestInterface $request,
158 ResponseInterface $response
160 // Request modifications to apply.
162 $protocols = $options['allow_redirects']['protocols'];
164 // Use a GET request if this is an entity enclosing request and we are
165 // not forcing RFC compliance, but rather emulating what all browsers
167 $statusCode = $response->getStatusCode();
168 if ($statusCode == 303 ||
169 ($statusCode <= 302 && $request->getBody() && !$options['allow_redirects']['strict'])
171 $modify['method'] = 'GET';
172 $modify['body'] = '';
175 $modify['uri'] = $this->redirectUri($request, $response, $protocols);
176 Psr7\rewind_body($request);
178 // Add the Referer header if it is told to do so and only
179 // add the header if we are not redirecting from https to http.
180 if ($options['allow_redirects']['referer']
181 && $modify['uri']->getScheme() === $request->getUri()->getScheme()
183 $uri = $request->getUri()->withUserInfo('', '');
184 $modify['set_headers']['Referer'] = (string) $uri;
186 $modify['remove_headers'][] = 'Referer';
189 // Remove Authorization header if host is different.
190 if ($request->getUri()->getHost() !== $modify['uri']->getHost()) {
191 $modify['remove_headers'][] = 'Authorization';
194 return Psr7\modify_request($request, $modify);
198 * Set the appropriate URL on the request based on the location header
200 * @param RequestInterface $request
201 * @param ResponseInterface $response
202 * @param array $protocols
204 * @return UriInterface
206 private function redirectUri(
207 RequestInterface $request,
208 ResponseInterface $response,
211 $location = Psr7\UriResolver::resolve(
213 new Psr7\Uri($response->getHeaderLine('Location'))
216 // Ensure that the redirect URI is allowed based on the protocols.
217 if (!in_array($location->getScheme(), $protocols)) {
218 throw new BadResponseException(
220 'Redirect URI, %s, does not use one of the allowed redirect protocols: %s',
222 implode(', ', $protocols)