Skip to content

Commit b1102dc

Browse files
committed
Preserve method on redirect
1 parent b5a66a4 commit b1102dc

File tree

2 files changed

+127
-10
lines changed

2 files changed

+127
-10
lines changed

src/Io/Transaction.php

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use Psr\Http\Message\RequestInterface;
66
use Psr\Http\Message\ResponseInterface;
77
use Psr\Http\Message\UriInterface;
8+
use React\Http\Message\Response;
89
use RingCentral\Psr7\Request;
910
use RingCentral\Psr7\Uri;
1011
use React\EventLoop\LoopInterface;
@@ -234,6 +235,8 @@ public function onResponse(ResponseInterface $response, RequestInterface $reques
234235
/**
235236
* @param ResponseInterface $response
236237
* @param RequestInterface $request
238+
* @param Deferred $deferred
239+
* @param ClientRequestState $state
237240
* @return PromiseInterface
238241
* @throws \RuntimeException
239242
*/
@@ -242,7 +245,7 @@ private function onResponseRedirect(ResponseInterface $response, RequestInterfac
242245
// resolve location relative to last request URI
243246
$location = Uri::resolve($request->getUri(), $response->getHeaderLine('Location'));
244247

245-
$request = $this->makeRedirectRequest($request, $location);
248+
$request = $this->makeRedirectRequest($request, $location, $response->getStatusCode());
246249
$this->progress('redirect', array($request));
247250

248251
if ($state->numRequests >= $this->maxRedirects) {
@@ -255,25 +258,32 @@ private function onResponseRedirect(ResponseInterface $response, RequestInterfac
255258
/**
256259
* @param RequestInterface $request
257260
* @param UriInterface $location
261+
* @param int $statusCode
258262
* @return RequestInterface
263+
* @throws \RuntimeException
259264
*/
260-
private function makeRedirectRequest(RequestInterface $request, UriInterface $location)
265+
private function makeRedirectRequest(RequestInterface $request, UriInterface $location, $statusCode)
261266
{
262-
$originalHost = $request->getUri()->getHost();
263-
$request = $request
264-
->withoutHeader('Host')
265-
->withoutHeader('Content-Type')
266-
->withoutHeader('Content-Length');
267267

268268
// Remove authorization if changing hostnames (but not if just changing ports or protocols).
269+
$originalHost = $request->getUri()->getHost();
269270
if ($location->getHost() !== $originalHost) {
270271
$request = $request->withoutHeader('Authorization');
271272
}
272273

273-
// naïve approach..
274-
$method = ($request->getMethod() === 'HEAD') ? 'HEAD' : 'GET';
274+
if ($statusCode === Response::STATUS_TEMPORARY_REDIRECT || $statusCode === Response::STATUS_PERMANENT_REDIRECT) {
275+
if ($request->getBody() instanceof ReadableStreamInterface) {
276+
throw new \RuntimeException('Unable to redirect request with streaming body');
277+
}
278+
} else {
279+
$request = $request
280+
->withMethod($request->getMethod() === 'HEAD' ? 'HEAD' : 'GET')
281+
->withoutHeader('Content-Type')
282+
->withoutHeader('Content-Length')
283+
->withBody(new EmptyBodyStream());
284+
}
275285

276-
return new Request($method, $location, $request->getHeaders());
286+
return $request;
277287
}
278288

279289
private function progress($name, array $args = array())
@@ -295,4 +305,5 @@ private function progress($name, array $args = array())
295305

296306
echo PHP_EOL;
297307
}
308+
298309
}

tests/Io/TransactionTest.php

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -671,6 +671,112 @@ public function testSomeRequestHeadersShouldBeRemovedWhenRedirecting()
671671
$transaction->send($requestWithCustomHeaders);
672672
}
673673

674+
public function testRequestMethodShouldBeChangedWhenRedirectingWithSeeOther()
675+
{
676+
$loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
677+
678+
$customHeaders = array(
679+
'Content-Type' => 'text/html; charset=utf-8',
680+
'Content-Length' => '111',
681+
);
682+
683+
$request = new Request('POST', 'http://example.com', $customHeaders);
684+
$sender = $this->makeSenderMock();
685+
686+
// mock sender to resolve promise with the given $redirectResponse in
687+
// response to the given $request
688+
$redirectResponse = new Response(303, array('Location' => 'http://example.com/new'));
689+
690+
// mock sender to resolve promise with the given $okResponse in
691+
// response to the given $request
692+
$okResponse = new Response(200);
693+
$that = $this;
694+
$sender->expects($this->exactly(2))->method('send')->withConsecutive(
695+
array($this->anything()),
696+
array($this->callback(function (RequestInterface $request) use ($that) {
697+
$that->assertEquals('GET', $request->getMethod());
698+
return true;
699+
}))
700+
)->willReturnOnConsecutiveCalls(
701+
Promise\resolve($redirectResponse),
702+
Promise\resolve($okResponse)
703+
);
704+
705+
$transaction = new Transaction($sender, $loop);
706+
$transaction->send($request);
707+
}
708+
709+
public function testRequestMethodAndBodyShouldNotBeChangedWhenRedirectingWith307Or308()
710+
{
711+
$loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
712+
713+
$customHeaders = array(
714+
'Content-Type' => 'text/html; charset=utf-8',
715+
'Content-Length' => '111',
716+
);
717+
718+
$request = new Request('POST', 'http://example.com', $customHeaders, '{"key":"value"}');
719+
$sender = $this->makeSenderMock();
720+
721+
// mock sender to resolve promise with the given $redirectResponse in
722+
// response to the given $request
723+
$redirectResponse = new Response(307, array('Location' => 'http://example.com/new'));
724+
725+
// mock sender to resolve promise with the given $okResponse in
726+
// response to the given $request
727+
$okResponse = new Response(200);
728+
$that = $this;
729+
$sender->expects($this->exactly(2))->method('send')->withConsecutive(
730+
array($this->anything()),
731+
array($this->callback(function (RequestInterface $request) use ($that) {
732+
$that->assertEquals('POST', $request->getMethod());
733+
$that->assertEquals('{"key":"value"}', (string)$request->getBody());
734+
return true;
735+
}))
736+
)->willReturnOnConsecutiveCalls(
737+
Promise\resolve($redirectResponse),
738+
Promise\resolve($okResponse)
739+
);
740+
741+
$transaction = new Transaction($sender, $loop);
742+
$transaction->send($request);
743+
}
744+
745+
public function testRedirectingStreamingBodyWith307Or308ShouldThrowCantRedirectStreamException()
746+
{
747+
$loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
748+
749+
$customHeaders = array(
750+
'Content-Type' => 'text/html; charset=utf-8',
751+
'Content-Length' => '111',
752+
);
753+
754+
$stream = new ThroughStream();
755+
$request = new Request('POST', 'http://example.com', $customHeaders, new ReadableBodyStream($stream));
756+
$sender = $this->makeSenderMock();
757+
758+
// mock sender to resolve promise with the given $redirectResponse in
759+
// response to the given $request
760+
$redirectResponse = new Response(307, array('Location' => 'http://example.com/new'));
761+
762+
$sender->expects($this->once())->method('send')->withConsecutive(
763+
array($this->anything())
764+
)->willReturnOnConsecutiveCalls(
765+
Promise\resolve($redirectResponse)
766+
);
767+
768+
$transaction = new Transaction($sender, $loop);
769+
$promise = $transaction->send($request);
770+
771+
$exception = null;
772+
$promise->then(null, function ($reason) use (&$exception) {
773+
$exception = $reason;
774+
});
775+
776+
assert($exception instanceof RuntimeException);
777+
$this->assertEquals('Unable to redirect request with streaming body', $exception->getMessage());
778+
}
779+
674780
public function testCancelTransactionWillCancelRequest()
675781
{
676782
$loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();

0 commit comments

Comments
 (0)