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 const STATUS_HISTORY_HEADER = 'X-Guzzle-Redirect-Status-History';
24 public static $defaultSettings = [
26 'protocols' => ['http', 'https'],
29 'track_redirects' => false,
36 * @param callable $nextHandler Next handler to invoke.
38 public function __construct(callable $nextHandler)
40 $this->nextHandler = $nextHandler;
44 * @param RequestInterface $request
45 * @param array $options
47 * @return PromiseInterface
49 public function __invoke(RequestInterface $request, array $options)
51 $fn = $this->nextHandler;
53 if (empty($options['allow_redirects'])) {
54 return $fn($request, $options);
57 if ($options['allow_redirects'] === true) {
58 $options['allow_redirects'] = self::$defaultSettings;
59 } elseif (!is_array($options['allow_redirects'])) {
60 throw new \InvalidArgumentException('allow_redirects must be true, false, or array');
62 // Merge the default settings with the provided settings
63 $options['allow_redirects'] += self::$defaultSettings;
66 if (empty($options['allow_redirects']['max'])) {
67 return $fn($request, $options);
70 return $fn($request, $options)
71 ->then(function (ResponseInterface $response) use ($request, $options) {
72 return $this->checkRedirect($request, $options, $response);
77 * @param RequestInterface $request
78 * @param array $options
79 * @param ResponseInterface|PromiseInterface $response
81 * @return ResponseInterface|PromiseInterface
83 public function checkRedirect(
84 RequestInterface $request,
86 ResponseInterface $response
88 if (substr($response->getStatusCode(), 0, 1) != '3'
89 || !$response->hasHeader('Location')
94 $this->guardMax($request, $options);
95 $nextRequest = $this->modifyRequest($request, $options, $response);
97 if (isset($options['allow_redirects']['on_redirect'])) {
99 $options['allow_redirects']['on_redirect'],
102 $nextRequest->getUri()
106 /** @var PromiseInterface|ResponseInterface $promise */
107 $promise = $this($nextRequest, $options);
109 // Add headers to be able to track history of redirects.
110 if (!empty($options['allow_redirects']['track_redirects'])) {
111 return $this->withTracking(
113 (string) $nextRequest->getUri(),
114 $response->getStatusCode()
121 private function withTracking(PromiseInterface $promise, $uri, $statusCode)
123 return $promise->then(
124 function (ResponseInterface $response) use ($uri, $statusCode) {
125 // Note that we are pushing to the front of the list as this
126 // would be an earlier response than what is currently present
127 // in the history header.
128 $historyHeader = $response->getHeader(self::HISTORY_HEADER);
129 $statusHeader = $response->getHeader(self::STATUS_HISTORY_HEADER);
130 array_unshift($historyHeader, $uri);
131 array_unshift($statusHeader, $statusCode);
132 return $response->withHeader(self::HISTORY_HEADER, $historyHeader)
133 ->withHeader(self::STATUS_HISTORY_HEADER, $statusHeader);
138 private function guardMax(RequestInterface $request, array &$options)
140 $current = isset($options['__redirect_count'])
141 ? $options['__redirect_count']
143 $options['__redirect_count'] = $current + 1;
144 $max = $options['allow_redirects']['max'];
146 if ($options['__redirect_count'] > $max) {
147 throw new TooManyRedirectsException(
148 "Will not follow more than {$max} redirects",
155 * @param RequestInterface $request
156 * @param array $options
157 * @param ResponseInterface $response
159 * @return RequestInterface
161 public function modifyRequest(
162 RequestInterface $request,
164 ResponseInterface $response
166 // Request modifications to apply.
168 $protocols = $options['allow_redirects']['protocols'];
170 // Use a GET request if this is an entity enclosing request and we are
171 // not forcing RFC compliance, but rather emulating what all browsers
173 $statusCode = $response->getStatusCode();
174 if ($statusCode == 303 ||
175 ($statusCode <= 302 && $request->getBody() && !$options['allow_redirects']['strict'])
177 $modify['method'] = 'GET';
178 $modify['body'] = '';
181 $modify['uri'] = $this->redirectUri($request, $response, $protocols);
182 Psr7\rewind_body($request);
184 // Add the Referer header if it is told to do so and only
185 // add the header if we are not redirecting from https to http.
186 if ($options['allow_redirects']['referer']
187 && $modify['uri']->getScheme() === $request->getUri()->getScheme()
189 $uri = $request->getUri()->withUserInfo('', '');
190 $modify['set_headers']['Referer'] = (string) $uri;
192 $modify['remove_headers'][] = 'Referer';
195 // Remove Authorization header if host is different.
196 if ($request->getUri()->getHost() !== $modify['uri']->getHost()) {
197 $modify['remove_headers'][] = 'Authorization';
200 return Psr7\modify_request($request, $modify);
204 * Set the appropriate URL on the request based on the location header
206 * @param RequestInterface $request
207 * @param ResponseInterface $response
208 * @param array $protocols
210 * @return UriInterface
212 private function redirectUri(
213 RequestInterface $request,
214 ResponseInterface $response,
217 $location = Psr7\UriResolver::resolve(
219 new Psr7\Uri($response->getHeaderLine('Location'))
222 // Ensure that the redirect URI is allowed based on the protocols.
223 if (!in_array($location->getScheme(), $protocols)) {
224 throw new BadResponseException(
226 'Redirect URI, %s, does not use one of the allowed redirect protocols: %s',
228 implode(', ', $protocols)