Skip to content

Commit 3508476

Browse files
committed
test redirects
1 parent 167c671 commit 3508476

File tree

3 files changed

+259
-5
lines changed

3 files changed

+259
-5
lines changed

src/Psr18/Client.php

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -227,10 +227,12 @@ protected function doSendRequest(RequestInterface $request, ClientOptions $optio
227227
$response = $headerParser->applyToResponse($response);
228228

229229
if ($this->isRedirect($response)) {
230-
try {
231-
$fiber->throw(new RequestRedirectedException());
232-
} catch (Throwable $e) {
233-
throw new RequestException($request, "Could not close request before redirect", previous: $e);
230+
if (!$fiber->isTerminated()) {
231+
try {
232+
$fiber->throw(new RequestRedirectedException());
233+
} catch (Throwable $e) {
234+
throw new RequestException($request, "Could not close request before redirect", previous: $e);
235+
}
234236
}
235237
return $this->handleRedirect($request, $response, $options, $redirects);
236238
}

tests/HttpClientTest.php

Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
namespace Tests;
44

5+
use Aternos\CurlPsr\Exception\RequestException;
6+
use Aternos\CurlPsr\Exception\RequestRedirectedException;
7+
use Aternos\CurlPsr\Psr7\Stream\StringStream;
8+
use Exception;
59
use PHPUnit\Framework\Attributes\TestWith;
610
use Psr\Http\Client\NetworkExceptionInterface;
711
use Psr\Http\Client\RequestExceptionInterface;
@@ -241,6 +245,7 @@ public function testProgressCallbackHasTotalSizeIfKnown(): void
241245
$uTotal = $uploadTotal;
242246
}
243247
});
248+
$this->assertIsCallable($this->client->getProgressCallback());
244249

245250
$request = $this->requestFactory->createRequest("POST", "https://example.com")
246251
->withBody($requestBody);
@@ -367,6 +372,7 @@ public function testCustomCurlOption(): void
367372
{
368373
$this->client->setCurlOption(CURLOPT_BUFFERSIZE, 1024);
369374
$this->assertEquals(1024, $this->client->getCurlOption(CURLOPT_BUFFERSIZE));
375+
$this->assertCount(1, $this->client->getCurlOptions());
370376

371377
$request = $this->requestFactory->createRequest("GET", "https://example.com");
372378
$this->client->sendRequest($request);
@@ -415,4 +421,219 @@ public function testDefaultHeaderOverwrittenByRequestHeader(): void
415421
$this->assertContains("X-Test: Request", $this->curlHandle->getOption(CURLOPT_HTTPHEADER));
416422
$this->assertNotContains("X-Test: Test", $this->curlHandle->getOption(CURLOPT_HTTPHEADER));
417423
}
424+
425+
public function testRedirect(): void
426+
{
427+
$body1 = $this->streamFactory->createStream();
428+
$this->curlHandle->setInfo(["http_code" => 302])
429+
->setResponseHeaders([
430+
"Location: https://example.com/redirect"
431+
])
432+
->setRequestBodySink($body1);
433+
434+
$body2 = $this->streamFactory->createStream();
435+
$target = $this->curlHandleFactory->nextTestHandle()
436+
->setRequestBodySink($body2);
437+
438+
$request = $this->requestFactory->createRequest("POST", "https://example.com")
439+
->withBody($this->streamFactory->createStream("test1234"));
440+
$this->client->sendRequest($request);
441+
442+
$this->assertEquals("POST", $target->getOption(CURLOPT_CUSTOMREQUEST));
443+
$this->assertEquals("https://example.com/redirect", (string)$target->getOption(CURLOPT_URL));
444+
$this->assertEquals("test1234", (string) $body1);
445+
$this->assertEquals("test1234", (string) $body2);
446+
}
447+
448+
public function testThrowOnRedirectIfBodyIsNotSeekable(): void
449+
{
450+
$this->curlHandle->setInfo(["http_code" => 302])
451+
->setResponseHeaders([
452+
"Location: https://example.com/redirect"
453+
]);
454+
455+
$this->curlHandleFactory->nextTestHandle();
456+
$request = $this->requestFactory->createRequest("POST", "https://example.com")
457+
->withBody(new StringStream("test1234", false));
458+
459+
$this->expectException(RequestException::class);
460+
$this->expectExceptionMessage("Could not rewind body for redirect");
461+
$this->client->sendRequest($request);
462+
}
463+
464+
public function testThrowOnMultipleLocationHeaders(): void
465+
{
466+
$this->curlHandle->setInfo(["http_code" => 302])
467+
->setResponseHeaders([
468+
"Location: https://example.com/redirect",
469+
"Location: https://example.com/redirect1"
470+
]);
471+
472+
$this->curlHandleFactory->nextTestHandle();
473+
$request = $this->requestFactory->createRequest("POST", "https://example.com");
474+
475+
$this->expectException(RequestException::class);
476+
$this->expectExceptionMessage("Multiple location headers in redirect");
477+
$this->client->sendRequest($request);
478+
}
479+
480+
public function testRedirectLimit(): void
481+
{
482+
$this->curlHandle->setInfo(["http_code" => 302])
483+
->setResponseHeaders([
484+
"Location: https://example.com/redirect1"
485+
]);
486+
487+
$this->curlHandleFactory->nextTestHandle()
488+
->setInfo(["http_code" => 302])
489+
->setResponseHeaders([
490+
"Location: https://example.com/redirect2"
491+
]);
492+
493+
$this->curlHandleFactory->nextTestHandle()
494+
->setInfo(["http_code" => 302])
495+
->setResponseHeaders([
496+
"Location: https://example.com/redirect3"
497+
]);
498+
499+
$request = $this->requestFactory->createRequest("POST", "https://example.com");
500+
501+
$this->client->setMaxRedirects(2);
502+
503+
$this->expectException(RequestException::class);
504+
$this->expectExceptionMessage("Redirect limit of 2 reached");
505+
$this->client->sendRequest($request);
506+
}
507+
508+
public function testRedirectToGetOn303(): void
509+
{
510+
$body1 = $this->streamFactory->createStream();
511+
$this->curlHandle->setInfo(["http_code" => 303])
512+
->setResponseHeaders([
513+
"Location: https://example.com/redirect"
514+
])
515+
->setRequestBodySink($body1);
516+
517+
$body2 = $this->streamFactory->createStream();
518+
$target = $this->curlHandleFactory->nextTestHandle()
519+
->setRequestBodySink($body2);
520+
521+
$request = $this->requestFactory->createRequest("POST", "https://example.com")
522+
->withBody($this->streamFactory->createStream("test1234"));
523+
$this->client->sendRequest($request);
524+
525+
$this->assertEquals("GET", $target->getOption(CURLOPT_CUSTOMREQUEST));
526+
$this->assertEquals("https://example.com/redirect", (string)$target->getOption(CURLOPT_URL));
527+
$this->assertEquals("test1234", (string) $body1);
528+
$this->assertEquals("", (string) $body2);
529+
}
530+
531+
public function testRedirectToGetIfConfigured(): void
532+
{
533+
$this->curlHandle->setInfo(["http_code" => 302])
534+
->setResponseHeaders([
535+
"Location: https://example.com/redirect"
536+
]);
537+
538+
$target = $this->curlHandleFactory->nextTestHandle();
539+
540+
$request = $this->requestFactory->createRequest("POST", "https://example.com")
541+
->withBody($this->streamFactory->createStream("test1234"));
542+
$this->client->setRedirectToGetStatusCodes([302]);
543+
$this->assertEquals([302], $this->client->getRedirectToGetStatusCodes());
544+
$this->client->sendRequest($request);
545+
546+
$this->assertEquals("GET", $target->getOption(CURLOPT_CUSTOMREQUEST));
547+
$this->assertEquals("https://example.com/redirect", (string)$target->getOption(CURLOPT_URL));
548+
}
549+
550+
public function testThrowOnRedirectWithoutLocation(): void
551+
{
552+
$this->curlHandle->setInfo(["http_code" => 303]);
553+
554+
$this->expectException(RequestException::class);
555+
$this->expectExceptionMessage("Redirect without location header");
556+
$request = $this->requestFactory->createRequest("GET", "https://example.com");
557+
$this->client->sendRequest($request);
558+
}
559+
560+
public function testThrowOnRedirectToInvalidTarget(): void
561+
{
562+
$this->curlHandle->setInfo(["http_code" => 303])
563+
->setResponseHeaders([
564+
"Location: http://"
565+
]);
566+
567+
$this->expectException(RequestException::class);
568+
$this->expectExceptionMessage("Invalid location header in redirect");
569+
$request = $this->requestFactory->createRequest("GET", "https://example.com");
570+
$this->client->sendRequest($request);
571+
}
572+
573+
public function testRedirectOn300IfLocationIsSet(): void
574+
{
575+
$this->curlHandle->setInfo(["http_code" => 300])
576+
->setResponseHeaders([
577+
"Location: https://example.com/redirect"
578+
]);
579+
580+
$target = $this->curlHandleFactory->nextTestHandle();
581+
582+
$request = $this->requestFactory->createRequest("GET", "https://example.com");
583+
$this->client->sendRequest($request);
584+
585+
$this->assertEquals("GET", $target->getOption(CURLOPT_CUSTOMREQUEST));
586+
$this->assertEquals("https://example.com/redirect", (string)$target->getOption(CURLOPT_URL));
587+
}
588+
589+
public function testDoNotRedirectOn300IfLocationIsMissing(): void
590+
{
591+
$this->curlHandle->setInfo(["http_code" => 300])
592+
->setResponseHeaders([
593+
'Link: <https://example.com/redirect>; rel="alternate"'
594+
]);
595+
596+
$target = $this->curlHandleFactory->nextTestHandle();
597+
598+
$request = $this->requestFactory->createRequest("GET", "https://example.com");
599+
$this->client->sendRequest($request);
600+
601+
$this->assertNull($target->getOption(CURLOPT_CUSTOMREQUEST));
602+
}
603+
604+
public function testRedirectCancelsInitialRequest(): void
605+
{
606+
$this->curlHandle->setInfo(["http_code" => 302])
607+
->setResponseHeaders([
608+
"Location: https://example.com/redirect"
609+
])
610+
->setResponseBody($this->streamFactory->createStream(random_bytes(1024)));
611+
612+
$this->curlHandleFactory->nextTestHandle();
613+
614+
$request = $this->requestFactory->createRequest("POST", "https://example.com");
615+
$this->client->sendRequest($request);
616+
617+
$this->assertInstanceOf(RequestRedirectedException::class, $this->curlHandle->getExecError());
618+
}
619+
620+
public function testThrowOnErrorWhileStoppingInitialRequest(): void
621+
{
622+
$this->curlHandle->setInfo(["http_code" => 302])
623+
->setResponseHeaders([
624+
"Location: https://example.com/redirect"
625+
])
626+
->setResponseBody($this->streamFactory->createStream(random_bytes(1024)))
627+
->setOnWriteError(function () {
628+
throw new Exception("Test");
629+
});
630+
631+
$this->curlHandleFactory->nextTestHandle();
632+
633+
$request = $this->requestFactory->createRequest("POST", "https://example.com");
634+
635+
$this->expectException(RequestException::class);
636+
$this->expectExceptionMessage("Could not close request before redirect");
637+
$this->client->sendRequest($request);
638+
}
418639
}

tests/Util/TestCurlHandle.php

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ class TestCurlHandle implements CurlHandleInterface
3232
protected ?Closure $onAfterRead = null;
3333
protected ?Closure $onBeforeWrite = null;
3434
protected ?Closure $onAfterWrite = null;
35+
protected ?Closure $onBeforeRequest = null;
36+
protected ?Closure $onWriteError = null;
3537
protected ?Throwable $execError = null;
3638

3739
/**
@@ -48,6 +50,10 @@ public function setopt(int $option, mixed $value): bool
4850
*/
4951
public function exec(): string|bool
5052
{
53+
$beforeRequest = $this->onBeforeRequest;
54+
if ($beforeRequest !== null) {
55+
$beforeRequest($this);
56+
}
5157
try {
5258
return $this->doExec();
5359
} catch (Throwable $e) {
@@ -92,7 +98,12 @@ public function doExec(): string|bool
9298
if (isset($this->options[CURLOPT_WRITEFUNCTION]) && $this->responseBody !== null) {
9399
while (!$this->responseBody->eof()) {
94100
$this->onBeforeWrite?->call($this);
95-
$this->options[CURLOPT_WRITEFUNCTION](null, $this->responseBody->read($this->responseChunkSize));
101+
try {
102+
$this->options[CURLOPT_WRITEFUNCTION](null, $this->responseBody->read($this->responseChunkSize));
103+
} catch (Throwable $e) {
104+
$this->onWriteError?->call($this, $e);
105+
throw $e;
106+
}
96107
$this->progressUpdate(
97108
$this->responseBody->getSize() ?? $this->getResponseHeader("Content-Length") ?? 0,
98109
$this->responseBody->tell(), 0, 0);
@@ -444,4 +455,24 @@ public function getExecError(): ?Throwable
444455
return $this->execError;
445456
}
446457

458+
/**
459+
* @param Closure|null $onBeforeRequest
460+
* @return $this
461+
*/
462+
public function setOnBeforeRequest(?Closure $onBeforeRequest): static
463+
{
464+
$this->onBeforeRequest = $onBeforeRequest;
465+
return $this;
466+
}
467+
468+
/**
469+
* @param Closure|null $onWriteError
470+
* @return $this
471+
*/
472+
public function setOnWriteError(?Closure $onWriteError): static
473+
{
474+
$this->onWriteError = $onWriteError;
475+
return $this;
476+
}
477+
447478
}

0 commit comments

Comments
 (0)