Skip to content

Commit c9b514e

Browse files
authored
Make ResponseBody rewindable (#411)
* Make ResponseBody rewindable * move the cursor at original position while yielding * Simplify the getChunk method * Add comment on ResponseBodyStream * Move resolve on top of stream method * Fix CS * Revert getChunk and add documentation * Document exceptions
1 parent 241f1f5 commit c9b514e

File tree

3 files changed

+140
-21
lines changed

3 files changed

+140
-21
lines changed

src/Response.php

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
use AsyncAws\Core\Exception\Http\RedirectionException;
1111
use AsyncAws\Core\Exception\Http\ServerException;
1212
use AsyncAws\Core\Exception\RuntimeException;
13+
use AsyncAws\Core\Stream\ResponseBodyResourceStream;
1314
use AsyncAws\Core\Stream\ResponseBodyStream;
1415
use AsyncAws\Core\Stream\ResultStream;
1516
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
@@ -129,20 +130,32 @@ public function cancel(): void
129130
$this->resolveResult = false;
130131
}
131132

133+
/**
134+
* @throws NetworkException
135+
* @throws HttpException
136+
*/
132137
public function getHeaders(): array
133138
{
134139
$this->resolve();
135140

136141
return $this->httpResponse->getHeaders(false);
137142
}
138143

144+
/**
145+
* @throws NetworkException
146+
* @throws HttpException
147+
*/
139148
public function getContent(): string
140149
{
141150
$this->resolve();
142151

143152
return $this->httpResponse->getContent(false);
144153
}
145154

155+
/**
156+
* @throws NetworkException
157+
* @throws HttpException
158+
*/
146159
public function toArray(): array
147160
{
148161
$this->resolve();
@@ -155,8 +168,18 @@ public function getStatusCode(): int
155168
return $this->httpResponse->getStatusCode();
156169
}
157170

171+
/**
172+
* @throws NetworkException
173+
* @throws HttpException
174+
*/
158175
public function toStream(): ResultStream
159176
{
177+
$this->resolve();
178+
179+
if (\is_callable([$this->httpResponse, 'toStream'])) {
180+
return new ResponseBodyResourceStream($this->httpResponse->toStream());
181+
}
182+
160183
return new ResponseBodyStream($this->httpClient->stream($this->httpResponse));
161184
}
162185
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace AsyncAws\Core\Stream;
6+
7+
use AsyncAws\Core\Exception\RuntimeException;
8+
9+
/**
10+
* Provides a ResultStream from a resource filled by an HTTP response body.
11+
*
12+
* @author Jérémy Derussé <[email protected]>
13+
*/
14+
class ResponseBodyResourceStream implements ResultStream
15+
{
16+
/**
17+
* @var resource
18+
*/
19+
private $resource;
20+
21+
public function __construct($resource)
22+
{
23+
$this->resource = $resource;
24+
}
25+
26+
public function __toString()
27+
{
28+
return $this->getContentAsString();
29+
}
30+
31+
/**
32+
* {@inheritdoc}
33+
*/
34+
public function getChunks(): iterable
35+
{
36+
$pos = \ftell($this->resource);
37+
if (0 !== $pos && !\rewind($this->resource)) {
38+
throw new RuntimeException('The stream is not rewindable');
39+
}
40+
41+
try {
42+
while (!\feof($this->resource)) {
43+
yield \fread($this->resource, 64 * 1024);
44+
}
45+
} finally {
46+
\fseek($this->resource, $pos);
47+
}
48+
}
49+
50+
/**
51+
* {@inheritdoc}
52+
*/
53+
public function getContentAsString(): string
54+
{
55+
$pos = \ftell($this->resource);
56+
57+
try {
58+
if (!\rewind($this->resource)) {
59+
throw new RuntimeException('Failed to rewind the stream');
60+
}
61+
62+
return \stream_get_contents($this->resource);
63+
} finally {
64+
\fseek($this->resource, $pos);
65+
}
66+
}
67+
68+
/**
69+
* {@inheritdoc}
70+
*/
71+
public function getContentAsResource()
72+
{
73+
if (!\rewind($this->resource)) {
74+
throw new RuntimeException('Failed to rewind the stream');
75+
}
76+
77+
return $this->resource;
78+
}
79+
}

src/Stream/ResponseBodyStream.php

Lines changed: 38 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,17 @@
44

55
namespace AsyncAws\Core\Stream;
66

7+
use AsyncAws\Core\Exception\LogicException;
78
use Symfony\Contracts\HttpClient\ResponseStreamInterface;
89

910
/**
1011
* Stream a HTTP response body.
12+
* This class is a BC layer for Http Response that does not support `toStream()`.
13+
* When calling `getChunks` you must read all the chunks before being able to call this method (or another method) again.
14+
* When calling `getContentAsResource`, it first, fully read the Response Body in a blocking way.
1115
*
1216
* @author Tobias Nyholm <[email protected]>
17+
* @author Jérémy Derussé <[email protected]>
1318
*/
1419
class ResponseBodyStream implements ResultStream
1520
{
@@ -18,6 +23,13 @@ class ResponseBodyStream implements ResultStream
1823
*/
1924
private $responseStream;
2025

26+
/**
27+
* @var ResponseBodyResourceStream|null
28+
*/
29+
private $fallback;
30+
31+
private $partialRead = false;
32+
2133
public function __construct(ResponseStreamInterface $responseStream)
2234
{
2335
$this->responseStream = $responseStream;
@@ -33,45 +45,50 @@ public function __toString()
3345
*/
3446
public function getChunks(): iterable
3547
{
48+
if (null !== $this->fallback) {
49+
return $this->fallback->getChunks();
50+
}
51+
if ($this->partialRead) {
52+
throw new LogicException(\sprintf('You can not call "%s". Another process doesn\'t reading "getChunks" till the end.', __METHOD__));
53+
}
54+
55+
$resource = \fopen('php://temp', 'rb+');
3656
foreach ($this->responseStream as $chunk) {
37-
yield $chunk->getContent();
57+
$this->partialRead = true;
58+
$chunkContent = $chunk->getContent();
59+
\fwrite($resource, $chunkContent);
60+
yield $chunkContent;
3861
}
62+
63+
$this->fallback = new ResponseBodyResourceStream($resource);
64+
$this->partialRead = false;
3965
}
4066

4167
/**
4268
* {@inheritdoc}
4369
*/
4470
public function getContentAsString(): string
4571
{
46-
$resource = $this->getContentAsResource();
47-
48-
try {
49-
return \stream_get_contents($resource);
50-
} finally {
51-
\fclose($resource);
72+
if (null === $this->fallback) {
73+
// Use getChunks() to read stream content to $this->fallback
74+
foreach ($this->getChunks() as $chunk) {
75+
}
5276
}
77+
78+
return $this->fallback->getContentAsString();
5379
}
5480

5581
/**
5682
* {@inheritdoc}
5783
*/
5884
public function getContentAsResource()
5985
{
60-
$resource = \fopen('php://temp', 'rw+');
61-
62-
try {
63-
foreach ($this->responseStream as $chunk) {
64-
fwrite($resource, $chunk->getContent());
86+
if (null === $this->fallback) {
87+
// Use getChunks() to read stream content to $this->fallback
88+
foreach ($this->getChunks() as $chunk) {
6589
}
66-
67-
// Rewind
68-
\fseek($resource, 0, \SEEK_SET);
69-
70-
return $resource;
71-
} catch (\Throwable $e) {
72-
\fclose($resource);
73-
74-
throw $e;
7590
}
91+
92+
return $this->fallback->getContentAsResource();
7693
}
7794
}

0 commit comments

Comments
 (0)