Skip to content

Commit 4ec1646

Browse files
authored
Merge pull request #51 from clue-labs/error-event
Improve error reporting for invalid responses by using `ResponseException`
2 parents 445e314 + dfe388d commit 4ec1646

File tree

3 files changed

+86
-12
lines changed

3 files changed

+86
-12
lines changed

README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -193,14 +193,17 @@ The `error` event will be emitted when the EventSource connection fails.
193193
The event receives a single `Exception` argument for the error instance.
194194

195195
```php
196-
$redis->on('error', function (Exception $e) {
196+
$es->on('error', function (Exception $e) {
197197
echo 'Error: ' . $e->getMessage() . PHP_EOL;
198198
});
199199
```
200200

201201
The EventSource connection will be retried automatically when it is temporarily
202202
disconnected. If the server sends a non-successful HTTP status code or an
203203
invalid `Content-Type` response header, the connection will fail permanently.
204+
In this case, the `error` event will receive a
205+
[`ResponseException`](https://github.com/reactphp/http#responseexception)
206+
that provides access to the response via the `getResponse()` method.
204207

205208
```php
206209
$es->on('error', function (Exception $e) use ($es) {

src/EventSource.php

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use React\EventLoop\Loop;
88
use React\EventLoop\LoopInterface;
99
use React\Http\Browser;
10+
use React\Http\Message\ResponseException;
1011
use React\Stream\ReadableStreamInterface;
1112

1213
/**
@@ -193,15 +194,22 @@ private function request()
193194
$this->request->then(function (ResponseInterface $response) {
194195
if ($response->getStatusCode() !== 200) {
195196
$this->readyState = self::CLOSED;
196-
$this->emit('error', array(new \UnexpectedValueException('Unexpected status code')));
197+
$this->emit('error', [new ResponseException(
198+
$response,
199+
'Expected "200 OK" response status, ' . $this->quote($response->getStatusCode() . ' ' . $response->getReasonPhrase()) . ' response status returned'
200+
)]);
197201
$this->close();
198202
return;
199203
}
200204

201205
// match `Content-Type: text/event-stream` (case insensitive and ignore additional parameters)
202-
if (!preg_match('/^text\/event-stream(?:$|;)/i', $response->getHeaderLine('Content-Type'))) {
206+
$contentType = $response->getHeaderLine('Content-Type');
207+
if (!preg_match('/^text\/event-stream(?:$|;)/i', $contentType)) {
203208
$this->readyState = self::CLOSED;
204-
$this->emit('error', array(new \UnexpectedValueException('Unexpected Content-Type')));
209+
$this->emit('error', [new ResponseException(
210+
$response,
211+
'Expected "Content-Type: text/event-stream" response header, ' . (!$response->hasHeader('Content-Type') ? 'no "Content-Type"' : $this->quote('Content-Type: ' . $contentType)) . ' response header returned'
212+
)]);
205213
$this->close();
206214
return;
207215
}
@@ -290,4 +298,14 @@ public function close()
290298

291299
$this->removeAllListeners();
292300
}
301+
302+
/**
303+
* @param string $string
304+
* @return string
305+
* @throws void
306+
*/
307+
private function quote($string)
308+
{
309+
return '"' . \addcslashes(\substr($string, 0, 100), "\x00..\x1f\"\\\x7f..\xff") . '"';
310+
}
293311
}

tests/EventSourceTest.php

Lines changed: 61 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,28 @@ public function testCloseAfterGetRequestFromConstructorFailsWillCancelPendingRet
228228
$es->close();
229229
}
230230

231-
public function testConstructorWillReportFatalErrorWhenGetResponseResolvesWithInvalidStatusCode()
231+
public function provideInvalidStatusCode()
232+
{
233+
return [
234+
[
235+
new Response(400, ['Content-Type' => 'text/event-stream'], ''),
236+
'Expected "200 OK" response status, "400 Bad Request" response status returned'
237+
],
238+
[
239+
new Response(500, ['Content-Type' => 'text/event-stream'], '', '1.1', "Intern\xE4l Server Err\xF6r"),
240+
'Expected "200 OK" response status, "500 Intern\344l Server Err\366r" response status returned'
241+
],
242+
[
243+
new Response(400, ['Content-Type' => 'text/event-stream'], '', '1.1', str_repeat('a', 200)),
244+
'Expected "200 OK" response status, "400 ' . str_repeat('a', 96) . '" response status returned'
245+
]
246+
];
247+
}
248+
249+
/**
250+
* @dataProvider provideInvalidStatusCode
251+
*/
252+
public function testConstructorWillReportFatalErrorWhenGetResponseResolvesWithInvalidStatusCode($response, $expectedMessage)
232253
{
233254
$deferred = new Deferred();
234255
$browser = $this->getMockBuilder('React\Http\Browser')->disableOriginalConstructor()->getMock();
@@ -244,14 +265,45 @@ public function testConstructorWillReportFatalErrorWhenGetResponseResolvesWithIn
244265
$caught = $e;
245266
});
246267

247-
$response = new Response(400, array('Content-Type' => 'text/event-stream'), '');
248268
$deferred->resolve($response);
249269

250270
$this->assertEquals(EventSource::CLOSED, $readyState);
251-
$this->assertInstanceOf('UnexpectedValueException', $caught);
252-
}
253-
254-
public function testConstructorWillReportFatalErrorWhenGetResponseResolvesWithInvalidContentType()
271+
$this->assertInstanceOf('React\Http\Message\ResponseException', $caught);
272+
$this->assertEquals($expectedMessage, $caught->getMessage());
273+
$this->assertEquals($response->getStatusCode(), $caught->getCode());
274+
$this->assertSame($response, $caught->getResponse());
275+
}
276+
277+
public function provideInvalidContentType()
278+
{
279+
return [
280+
[
281+
new Response(200, [], ''),
282+
'Expected "Content-Type: text/event-stream" response header, no "Content-Type" response header returned'
283+
],
284+
[
285+
new Response(200, ['Content-Type' => ''], ''),
286+
'Expected "Content-Type: text/event-stream" response header, "Content-Type: " response header returned'
287+
],
288+
[
289+
new Response(200, ['Content-Type' => 'text/html'], ''),
290+
'Expected "Content-Type: text/event-stream" response header, "Content-Type: text/html" response header returned'
291+
],
292+
[
293+
new Response(200, ['Content-Type' => "application/json; invalid=a\xE4b"], ''),
294+
'Expected "Content-Type: text/event-stream" response header, "Content-Type: application/json; invalid=a\344b" response header returned'
295+
],
296+
[
297+
new Response(200, ['Content-Type' => str_repeat('a', 200)], ''),
298+
'Expected "Content-Type: text/event-stream" response header, "Content-Type: ' . str_repeat('a', 86) . '" response header returned'
299+
]
300+
];
301+
}
302+
303+
/**
304+
* @dataProvider provideInvalidContentType
305+
*/
306+
public function testConstructorWillReportFatalErrorWhenGetResponseResolvesWithInvalidContentType($response, $expectedMessage)
255307
{
256308
$deferred = new Deferred();
257309
$browser = $this->getMockBuilder('React\Http\Browser')->disableOriginalConstructor()->getMock();
@@ -267,11 +319,12 @@ public function testConstructorWillReportFatalErrorWhenGetResponseResolvesWithIn
267319
$caught = $e;
268320
});
269321

270-
$response = new Response(200, array(), '');
271322
$deferred->resolve($response);
272323

273324
$this->assertEquals(EventSource::CLOSED, $readyState);
274-
$this->assertInstanceOf('UnexpectedValueException', $caught);
325+
$this->assertInstanceOf('React\Http\Message\ResponseException', $caught);
326+
$this->assertEquals($expectedMessage, $caught->getMessage());
327+
$this->assertSame($response, $caught->getResponse());
275328
}
276329

277330
public function testConstructorWillReportOpenWhenGetResponseResolvesWithValidResponse()

0 commit comments

Comments
 (0)