From 93d38e7cff0651214661e2ce9e12963b5dc45e25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 10 Sep 2021 18:29:30 +0200 Subject: [PATCH 1/8] Improve error formatting and HTML layout --- src/App.php | 81 +++++++++++++++++++++++-------------- tests/AppMiddlewareTest.php | 8 +++- tests/AppTest.php | 45 +++++++++++++++------ 3 files changed, 89 insertions(+), 45 deletions(-) diff --git a/src/App.php b/src/App.php index 7566337..b494683 100644 --- a/src/App.php +++ b/src/App.php @@ -345,11 +345,7 @@ private function routeRequest(ServerRequestInterface $request) case \FastRoute\Dispatcher::NOT_FOUND: return $this->errorNotFound($request); case \FastRoute\Dispatcher::METHOD_NOT_ALLOWED: - $allowedMethods = $routeInfo[1]; - - return $this->errorMethodNotAllowed( - $request->withAttribute('allowed', $allowedMethods) - ); + return $this->errorMethodNotAllowed($routeInfo[1]); case \FastRoute\Dispatcher::FOUND: $handler = $routeInfo[1]; $vars = $routeInfo[2]; @@ -422,59 +418,78 @@ private function log(string $message): void } } - private function error(int $statusCode, ?string $info = null): ResponseInterface + private function error(int $statusCode, string $title, string ...$info): ResponseInterface { - $response = new Response( + $info = \implode('', \array_map(function (string $info) { return "

$info

\n"; }, $info)); + $html = << + + +Error $statusCode: $title + + + +
+

$statusCode

+$title +$info
+ + + +HTML; + + return new Response( $statusCode, [ 'Content-Type' => 'text/html' ], - (string)$statusCode + $html ); - - $body = $response->getBody(); - $body->seek(0, SEEK_END); - - $reason = $response->getReasonPhrase(); - if ($reason !== '') { - $body->write(' (' . $reason . ')'); - } - - if ($info !== null) { - $body->write(': ' . $info); - } - $body->write("\n"); - - return $response; } private function errorProxy(): ResponseInterface { return $this->error( 400, - 'Proxy requests not allowed' + 'Proxy Requests Not Allowed', + 'Please check your settings and retry.' ); } private function errorNotFound(): ResponseInterface { - return $this->error(404); + return $this->error( + 404, + 'Page Not Found', + 'Please check the URL in the address bar and try again.' + ); } - private function errorMethodNotAllowed(ServerRequestInterface $request): ResponseInterface + private function errorMethodNotAllowed(array $allowedMethods): ResponseInterface { return $this->error( 405, - implode(', ', $request->getAttribute('allowed')) - )->withHeader('Allowed', implode(', ', $request->getAttribute('allowed'))); + 'Method Not Allowed', + 'Please check the URL in the address bar and try again.', + 'Try ' . \implode(', ', \array_map(function (string $method) { return '' . $method . ''; }, $allowedMethods)) . '.' + )->withHeader('Allowed', implode(', ', $allowedMethods)); } private function errorHandlerException(\Throwable $e): ResponseInterface { - $where = ' (' . \basename($e->getFile()) . ':' . $e->getLine() . ')'; + $where = ' in ' . \basename($e->getFile()) . ':' . $e->getLine() . ''; return $this->error( 500, + 'Internal Server Error', + 'The requested page failed to load, please try again later.', 'Expected request handler to return ' . ResponseInterface::class . ' but got uncaught ' . \get_class($e) . '' . $where . ': ' . $e->getMessage() ); } @@ -483,7 +498,9 @@ private function errorHandlerResponse($value): ResponseInterface { return $this->error( 500, - 'Expected request handler to return ' . ResponseInterface::class . ' but got ' . $this->describeType($value) . '' + 'Internal Server Error', + 'The requested page failed to load, please try again later.', + 'Expected request handler to return ' . ResponseInterface::class . ' but got ' . $this->describeType($value) . '.' ); } @@ -491,7 +508,9 @@ private function errorHandlerCoroutine($value): ResponseInterface { return $this->error( 500, - 'Expected request handler to yield ' . PromiseInterface::class . ' but got ' . $this->describeType($value) . '' + 'Internal Server Error', + 'The requested page failed to load, please try again later.', + 'Expected request handler to yield ' . PromiseInterface::class . ' but got ' . $this->describeType($value) . '.' ); } diff --git a/tests/AppMiddlewareTest.php b/tests/AppMiddlewareTest.php index 8a6227c..029ece5 100644 --- a/tests/AppMiddlewareTest.php +++ b/tests/AppMiddlewareTest.php @@ -389,7 +389,9 @@ public function testMiddlewareCallsNextWhichThrowsExceptionReturnsInternalServer $this->assertInstanceOf(ResponseInterface::class, $response); $this->assertEquals(500, $response->getStatusCode()); $this->assertEquals('text/html', $response->getHeaderLine('Content-Type')); - $this->assertEquals("500 (Internal Server Error): Expected request handler to return Psr\Http\Message\ResponseInterface but got uncaught RuntimeException (AppMiddlewareTest.php:$line): Foo\n", (string) $response->getBody()); + $this->assertStringContainsString("Error 500: Internal Server Error\n", (string) $response->getBody()); + $this->assertStringContainsString("

The requested page failed to load, please try again later.

\n", (string) $response->getBody()); + $this->assertStringContainsString("

Expected request handler to return Psr\Http\Message\ResponseInterface but got uncaught RuntimeException in AppMiddlewareTest.php:$line: Foo

\n", (string) $response->getBody()); } public function testMiddlewareWhichThrowsExceptionReturnsInternalServerErrorResponse() @@ -416,7 +418,9 @@ public function testMiddlewareWhichThrowsExceptionReturnsInternalServerErrorResp $this->assertInstanceOf(ResponseInterface::class, $response); $this->assertEquals(500, $response->getStatusCode()); $this->assertEquals('text/html', $response->getHeaderLine('Content-Type')); - $this->assertEquals("500 (Internal Server Error): Expected request handler to return Psr\Http\Message\ResponseInterface but got uncaught RuntimeException (AppMiddlewareTest.php:$line): Foo\n", (string) $response->getBody()); + $this->assertStringContainsString("Error 500: Internal Server Error\n", (string) $response->getBody()); + $this->assertStringContainsString("

The requested page failed to load, please try again later.

\n", (string) $response->getBody()); + $this->assertStringContainsString("

Expected request handler to return Psr\Http\Message\ResponseInterface but got uncaught RuntimeException in AppMiddlewareTest.php:$line: Foo

\n", (string) $response->getBody()); } public function testGlobalMiddlewareCallsNextReturnsResponseFromController() diff --git a/tests/AppTest.php b/tests/AppTest.php index 9f0876e..5659855 100644 --- a/tests/AppTest.php +++ b/tests/AppTest.php @@ -383,7 +383,7 @@ public function testHandleRequestWithProxyRequestReturnsResponseWithMessageThatP $this->assertInstanceOf(ResponseInterface::class, $response); $this->assertEquals(400, $response->getStatusCode()); $this->assertEquals('text/html', $response->getHeaderLine('Content-Type')); - $this->assertEquals("400 (Bad Request): Proxy requests not allowed\n", (string) $response->getBody()); + $this->assertStringContainsString("Error 400: Proxy Requests Not Allowed\n", (string) $response->getBody()); } public function testHandleRequestWithUnknownRouteReturnsResponseWithFileNotFoundMessage() @@ -401,7 +401,8 @@ public function testHandleRequestWithUnknownRouteReturnsResponseWithFileNotFound $this->assertInstanceOf(ResponseInterface::class, $response); $this->assertEquals(404, $response->getStatusCode()); $this->assertEquals('text/html', $response->getHeaderLine('Content-Type')); - $this->assertEquals("404 (Not Found)\n", (string) $response->getBody()); + $this->assertStringContainsString("Error 404: Page Not Found\n", (string) $response->getBody()); + $this->assertStringContainsString("

Please check the URL in the address bar and try again.

\n", (string) $response->getBody()); } public function testHandleRequestWithInvalidRequestMethodReturnsResponseWithMethodNotAllowedMessage() @@ -423,7 +424,9 @@ public function testHandleRequestWithInvalidRequestMethodReturnsResponseWithMeth $this->assertEquals(405, $response->getStatusCode()); $this->assertEquals('text/html', $response->getHeaderLine('Content-Type')); $this->assertEquals('GET, POST', $response->getHeaderLine('Allowed')); - $this->assertEquals("405 (Method Not Allowed): GET, POST\n", (string) $response->getBody()); + $this->assertStringContainsString("Error 405: Method Not Allowed\n", (string) $response->getBody()); + $this->assertStringContainsString("

Please check the URL in the address bar and try again.

\n", (string) $response->getBody()); + $this->assertStringContainsString("

Try GET, POST.

\n", (string) $response->getBody()); } public function testHandleRequestWithMatchingRouteReturnsResponseFromMatchingRouteHandler() @@ -677,7 +680,9 @@ public function testHandleRequestWithMatchingRouteReturnsInternalServerErrorResp $this->assertInstanceOf(ResponseInterface::class, $response); $this->assertEquals(500, $response->getStatusCode()); $this->assertEquals('text/html', $response->getHeaderLine('Content-Type')); - $this->assertEquals("500 (Internal Server Error): Expected request handler to return Psr\Http\Message\ResponseInterface but got uncaught RuntimeException (AppTest.php:$line): Foo\n", (string) $response->getBody()); + $this->assertStringContainsString("Error 500: Internal Server Error\n", (string) $response->getBody()); + $this->assertStringContainsString("

The requested page failed to load, please try again later.

\n", (string) $response->getBody()); + $this->assertStringContainsString("

Expected request handler to return Psr\Http\Message\ResponseInterface but got uncaught RuntimeException in AppTest.php:$line: Foo

\n", (string) $response->getBody()); } public function testHandleRequestWithMatchingRouteReturnsPromiseWhichFulfillsWithInternalServerErrorResponseWhenHandlerReturnsPromiseWhichRejectsWithException() @@ -708,7 +713,9 @@ public function testHandleRequestWithMatchingRouteReturnsPromiseWhichFulfillsWit $this->assertInstanceOf(ResponseInterface::class, $response); $this->assertEquals(500, $response->getStatusCode()); $this->assertEquals('text/html', $response->getHeaderLine('Content-Type')); - $this->assertEquals("500 (Internal Server Error): Expected request handler to return Psr\Http\Message\ResponseInterface but got uncaught RuntimeException (AppTest.php:$line): Foo\n", (string) $response->getBody()); + $this->assertStringContainsString("Error 500: Internal Server Error\n", (string) $response->getBody()); + $this->assertStringContainsString("

The requested page failed to load, please try again later.

\n", (string) $response->getBody()); + $this->assertStringContainsString("

Expected request handler to return Psr\Http\Message\ResponseInterface but got uncaught RuntimeException in AppTest.php:$line: Foo

\n", (string) $response->getBody()); } public function testHandleRequestWithMatchingRouteReturnsPromiseWhichFulfillsWithInternalServerErrorResponseWhenHandlerReturnsPromiseWhichRejectsWithNull() @@ -738,7 +745,9 @@ public function testHandleRequestWithMatchingRouteReturnsPromiseWhichFulfillsWit $this->assertInstanceOf(ResponseInterface::class, $response); $this->assertEquals(500, $response->getStatusCode()); $this->assertEquals('text/html', $response->getHeaderLine('Content-Type')); - $this->assertEquals("500 (Internal Server Error): Expected request handler to return Psr\Http\Message\ResponseInterface but got React\Promise\RejectedPromise\n", (string) $response->getBody()); + $this->assertStringContainsString("Error 500: Internal Server Error\n", (string) $response->getBody()); + $this->assertStringContainsString("

The requested page failed to load, please try again later.

\n", (string) $response->getBody()); + $this->assertStringContainsString("

Expected request handler to return Psr\Http\Message\ResponseInterface but got React\Promise\RejectedPromise.

\n", (string) $response->getBody()); } public function testHandleRequestWithMatchingRouteReturnsPromiseWhichFulfillsWithInternalServerErrorResponseWhenHandlerReturnsCoroutineWhichYieldsRejectedPromise() @@ -769,7 +778,9 @@ public function testHandleRequestWithMatchingRouteReturnsPromiseWhichFulfillsWit $this->assertInstanceOf(ResponseInterface::class, $response); $this->assertEquals(500, $response->getStatusCode()); $this->assertEquals('text/html', $response->getHeaderLine('Content-Type')); - $this->assertEquals("500 (Internal Server Error): Expected request handler to return Psr\Http\Message\ResponseInterface but got uncaught RuntimeException (AppTest.php:$line): Foo\n", (string) $response->getBody()); + $this->assertStringContainsString("Error 500: Internal Server Error\n", (string) $response->getBody()); + $this->assertStringContainsString("

The requested page failed to load, please try again later.

\n", (string) $response->getBody()); + $this->assertStringContainsString("

Expected request handler to return Psr\Http\Message\ResponseInterface but got uncaught RuntimeException in AppTest.php:$line: Foo

\n", (string) $response->getBody()); } public function testHandleRequestWithMatchingRouteReturnsPromiseWhichFulfillsWithInternalServerErrorResponseWhenHandlerReturnsCoroutineWhichThrowsExceptionAfterYielding() @@ -801,7 +812,9 @@ public function testHandleRequestWithMatchingRouteReturnsPromiseWhichFulfillsWit $this->assertInstanceOf(ResponseInterface::class, $response); $this->assertEquals(500, $response->getStatusCode()); $this->assertEquals('text/html', $response->getHeaderLine('Content-Type')); - $this->assertEquals("500 (Internal Server Error): Expected request handler to return Psr\Http\Message\ResponseInterface but got uncaught RuntimeException (AppTest.php:$line): Foo\n", (string) $response->getBody()); + $this->assertStringContainsString("Error 500: Internal Server Error\n", (string) $response->getBody()); + $this->assertStringContainsString("

The requested page failed to load, please try again later.

\n", (string) $response->getBody()); + $this->assertStringContainsString("

Expected request handler to return Psr\Http\Message\ResponseInterface but got uncaught RuntimeException in AppTest.php:$line: Foo

\n", (string) $response->getBody()); } public function testHandleRequestWithMatchingRouteReturnsPromiseWhichFulfillsWithInternalServerErrorResponseWhenHandlerReturnsCoroutineWhichReturnsNull() @@ -832,7 +845,9 @@ public function testHandleRequestWithMatchingRouteReturnsPromiseWhichFulfillsWit $this->assertInstanceOf(ResponseInterface::class, $response); $this->assertEquals(500, $response->getStatusCode()); $this->assertEquals('text/html', $response->getHeaderLine('Content-Type')); - $this->assertEquals("500 (Internal Server Error): Expected request handler to return Psr\Http\Message\ResponseInterface but got null\n", (string) $response->getBody()); + $this->assertStringContainsString("Error 500: Internal Server Error\n", (string) $response->getBody()); + $this->assertStringContainsString("

The requested page failed to load, please try again later.

\n", (string) $response->getBody()); + $this->assertStringContainsString("

Expected request handler to return Psr\Http\Message\ResponseInterface but got null.

\n", (string) $response->getBody()); } public function testHandleRequestWithMatchingRouteReturnsPromiseWhichFulfillsWithInternalServerErrorResponseWhenHandlerReturnsCoroutineWhichYieldsNull() @@ -862,7 +877,9 @@ public function testHandleRequestWithMatchingRouteReturnsPromiseWhichFulfillsWit $this->assertInstanceOf(ResponseInterface::class, $response); $this->assertEquals(500, $response->getStatusCode()); $this->assertEquals('text/html', $response->getHeaderLine('Content-Type')); - $this->assertEquals("500 (Internal Server Error): Expected request handler to yield React\Promise\PromiseInterface but got null\n", (string) $response->getBody()); + $this->assertStringContainsString("Error 500: Internal Server Error\n", (string) $response->getBody()); + $this->assertStringContainsString("

The requested page failed to load, please try again later.

\n", (string) $response->getBody()); + $this->assertStringContainsString("

Expected request handler to yield React\Promise\PromiseInterface but got null.

\n", (string) $response->getBody()); } public function provideInvalidReturnValue() @@ -927,7 +944,9 @@ public function testHandleRequestWithMatchingRouteReturnsInternalServerErrorResp $this->assertInstanceOf(ResponseInterface::class, $response); $this->assertEquals(500, $response->getStatusCode()); $this->assertEquals('text/html', $response->getHeaderLine('Content-Type')); - $this->assertEquals("500 (Internal Server Error): Expected request handler to return Psr\Http\Message\ResponseInterface but got $name\n", (string) $response->getBody()); + $this->assertStringContainsString("Error 500: Internal Server Error\n", (string) $response->getBody()); + $this->assertStringContainsString("

The requested page failed to load, please try again later.

\n", (string) $response->getBody()); + $this->assertStringContainsString("

Expected request handler to return Psr\Http\Message\ResponseInterface but got $name.

\n", (string) $response->getBody()); } /** @@ -962,7 +981,9 @@ public function testHandleRequestWithMatchingRouteReturnsPromiseWhichFulfillsWit $this->assertInstanceOf(ResponseInterface::class, $response); $this->assertEquals(500, $response->getStatusCode()); $this->assertEquals('text/html', $response->getHeaderLine('Content-Type')); - $this->assertEquals("500 (Internal Server Error): Expected request handler to return Psr\Http\Message\ResponseInterface but got $name\n", (string) $response->getBody()); + $this->assertStringContainsString("Error 500: Internal Server Error\n", (string) $response->getBody()); + $this->assertStringContainsString("

The requested page failed to load, please try again later.

\n", (string) $response->getBody()); + $this->assertStringContainsString("

Expected request handler to return Psr\Http\Message\ResponseInterface but got $name.

\n", (string) $response->getBody()); } public function testLogRequestResponsePrintsRequestLogWithCurrentDateAndTime() From e91e63b412f374689c24dcb012268e3c047da5bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sat, 11 Sep 2021 11:40:59 +0200 Subject: [PATCH 2/8] Improve error formatting for 405 (Method Not Allowed) --- src/App.php | 7 ++++--- tests/AppTest.php | 30 ++++++++++++++++++++++++++---- 2 files changed, 30 insertions(+), 7 deletions(-) diff --git a/src/App.php b/src/App.php index b494683..949310c 100644 --- a/src/App.php +++ b/src/App.php @@ -474,12 +474,13 @@ private function errorNotFound(): ResponseInterface private function errorMethodNotAllowed(array $allowedMethods): ResponseInterface { + $methods = \implode('/', \array_map(function (string $method) { return '' . $method . ''; }, $allowedMethods)); + return $this->error( 405, 'Method Not Allowed', - 'Please check the URL in the address bar and try again.', - 'Try ' . \implode(', ', \array_map(function (string $method) { return '' . $method . ''; }, $allowedMethods)) . '.' - )->withHeader('Allowed', implode(', ', $allowedMethods)); + 'Please check the URL in the address bar and try again with ' . $methods . ' request.' + )->withHeader('Allow', implode(', ', $allowedMethods)); } private function errorHandlerException(\Throwable $e): ResponseInterface diff --git a/tests/AppTest.php b/tests/AppTest.php index 5659855..e7d689e 100644 --- a/tests/AppTest.php +++ b/tests/AppTest.php @@ -405,11 +405,34 @@ public function testHandleRequestWithUnknownRouteReturnsResponseWithFileNotFound $this->assertStringContainsString("

Please check the URL in the address bar and try again.

\n", (string) $response->getBody()); } - public function testHandleRequestWithInvalidRequestMethodReturnsResponseWithMethodNotAllowedMessage() + public function testHandleRequestWithInvalidRequestMethodReturnsResponseWithSingleMethodNotAllowedMessage() { $app = new App(); $app->get('/users', function () { }); + + $request = new ServerRequest('POST', 'http://localhost/users'); + + // $response = $app->handleRequest($request); + $ref = new ReflectionMethod($app, 'handleRequest'); + $ref->setAccessible(true); + $response = $ref->invoke($app, $request); + + /** @var ResponseInterface $response */ + $this->assertInstanceOf(ResponseInterface::class, $response); + $this->assertEquals(405, $response->getStatusCode()); + $this->assertEquals('text/html', $response->getHeaderLine('Content-Type')); + $this->assertEquals('GET', $response->getHeaderLine('Allow')); + $this->assertStringContainsString("Error 405: Method Not Allowed\n", (string) $response->getBody()); + $this->assertStringContainsString("

Please check the URL in the address bar and try again with GET request.

\n", (string) $response->getBody()); + } + + public function testHandleRequestWithInvalidRequestMethodReturnsResponseWithMultipleMethodNotAllowedMessage() + { + $app = new App(); + + $app->get('/users', function () { }); + $app->head('/users', function () { }); $app->post('/users', function () { }); $request = new ServerRequest('DELETE', 'http://localhost/users'); @@ -423,10 +446,9 @@ public function testHandleRequestWithInvalidRequestMethodReturnsResponseWithMeth $this->assertInstanceOf(ResponseInterface::class, $response); $this->assertEquals(405, $response->getStatusCode()); $this->assertEquals('text/html', $response->getHeaderLine('Content-Type')); - $this->assertEquals('GET, POST', $response->getHeaderLine('Allowed')); + $this->assertEquals('GET, HEAD, POST', $response->getHeaderLine('Allow')); $this->assertStringContainsString("Error 405: Method Not Allowed\n", (string) $response->getBody()); - $this->assertStringContainsString("

Please check the URL in the address bar and try again.

\n", (string) $response->getBody()); - $this->assertStringContainsString("

Try GET, POST.

\n", (string) $response->getBody()); + $this->assertStringContainsString("

Please check the URL in the address bar and try again with GET/HEAD/POST request.

\n", (string) $response->getBody()); } public function testHandleRequestWithMatchingRouteReturnsResponseFromMatchingRouteHandler() From d20b01c7c7b3ebd95614f861bb09db0256b69f85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sat, 11 Sep 2021 14:08:01 +0200 Subject: [PATCH 3/8] Improve error layout for small screens --- src/App.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/App.php b/src/App.php index 949310c..5b37948 100644 --- a/src/App.php +++ b/src/App.php @@ -427,12 +427,17 @@ private function error(int $statusCode, string $title, string ...$info): Respons Error $statusCode: $title From e438b43a95fbe6d9a0c8acf82e140d352ec8ac98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sun, 12 Sep 2021 12:38:06 +0200 Subject: [PATCH 4/8] Escape exception messages to prevent XSS attacks and invalid UTF-8 data --- src/App.php | 17 +++++- tests/AppMiddlewareTest.php | 8 +-- tests/AppTest.php | 108 ++++++++++++++++++++++++++---------- tests/acceptance.sh | 6 +- 4 files changed, 101 insertions(+), 38 deletions(-) diff --git a/src/App.php b/src/App.php index 5b37948..f8b999a 100644 --- a/src/App.php +++ b/src/App.php @@ -453,7 +453,7 @@ private function error(int $statusCode, string $title, string ...$info): Respons return new Response( $statusCode, [ - 'Content-Type' => 'text/html' + 'Content-Type' => 'text/html; charset=utf-8' ], $html ); @@ -491,12 +491,13 @@ private function errorMethodNotAllowed(array $allowedMethods): ResponseInterface private function errorHandlerException(\Throwable $e): ResponseInterface { $where = ' in ' . \basename($e->getFile()) . ':' . $e->getLine() . ''; + $message = '' . $this->escapeHtml($e->getMessage()) . ''; return $this->error( 500, 'Internal Server Error', 'The requested page failed to load, please try again later.', - 'Expected request handler to return ' . ResponseInterface::class . ' but got uncaught ' . \get_class($e) . '' . $where . ': ' . $e->getMessage() + 'Expected request handler to return ' . ResponseInterface::class . ' but got uncaught ' . \get_class($e) . ' with message ' . $message . $where . '.' ); } @@ -529,4 +530,16 @@ private function describeType($value): string } return \is_object($value) ? \get_class($value) : \gettype($value); } + + private function escapeHtml(string $s): string + { + return \addcslashes( + \str_replace( + ' ', + ' ', + \htmlspecialchars($s, \ENT_NOQUOTES | \ENT_SUBSTITUTE | \ENT_DISALLOWED, 'utf-8') + ), + "\0..\032\\" + ); + } } diff --git a/tests/AppMiddlewareTest.php b/tests/AppMiddlewareTest.php index 029ece5..7c7b417 100644 --- a/tests/AppMiddlewareTest.php +++ b/tests/AppMiddlewareTest.php @@ -388,10 +388,10 @@ public function testMiddlewareCallsNextWhichThrowsExceptionReturnsInternalServer /** @var ResponseInterface $response */ $this->assertInstanceOf(ResponseInterface::class, $response); $this->assertEquals(500, $response->getStatusCode()); - $this->assertEquals('text/html', $response->getHeaderLine('Content-Type')); + $this->assertEquals('text/html; charset=utf-8', $response->getHeaderLine('Content-Type')); $this->assertStringContainsString("Error 500: Internal Server Error\n", (string) $response->getBody()); $this->assertStringContainsString("

The requested page failed to load, please try again later.

\n", (string) $response->getBody()); - $this->assertStringContainsString("

Expected request handler to return Psr\Http\Message\ResponseInterface but got uncaught RuntimeException in AppMiddlewareTest.php:$line: Foo

\n", (string) $response->getBody()); + $this->assertStringContainsString("

Expected request handler to return Psr\Http\Message\ResponseInterface but got uncaught RuntimeException with message Foo in AppMiddlewareTest.php:$line.

\n", (string) $response->getBody()); } public function testMiddlewareWhichThrowsExceptionReturnsInternalServerErrorResponse() @@ -417,10 +417,10 @@ public function testMiddlewareWhichThrowsExceptionReturnsInternalServerErrorResp /** @var ResponseInterface $response */ $this->assertInstanceOf(ResponseInterface::class, $response); $this->assertEquals(500, $response->getStatusCode()); - $this->assertEquals('text/html', $response->getHeaderLine('Content-Type')); + $this->assertEquals('text/html; charset=utf-8', $response->getHeaderLine('Content-Type')); $this->assertStringContainsString("Error 500: Internal Server Error\n", (string) $response->getBody()); $this->assertStringContainsString("

The requested page failed to load, please try again later.

\n", (string) $response->getBody()); - $this->assertStringContainsString("

Expected request handler to return Psr\Http\Message\ResponseInterface but got uncaught RuntimeException in AppMiddlewareTest.php:$line: Foo

\n", (string) $response->getBody()); + $this->assertStringContainsString("

Expected request handler to return Psr\Http\Message\ResponseInterface but got uncaught RuntimeException with message Foo in AppMiddlewareTest.php:$line.

\n", (string) $response->getBody()); } public function testGlobalMiddlewareCallsNextReturnsResponseFromController() diff --git a/tests/AppTest.php b/tests/AppTest.php index e7d689e..1d77db5 100644 --- a/tests/AppTest.php +++ b/tests/AppTest.php @@ -382,7 +382,7 @@ public function testHandleRequestWithProxyRequestReturnsResponseWithMessageThatP /** @var ResponseInterface $response */ $this->assertInstanceOf(ResponseInterface::class, $response); $this->assertEquals(400, $response->getStatusCode()); - $this->assertEquals('text/html', $response->getHeaderLine('Content-Type')); + $this->assertEquals('text/html; charset=utf-8', $response->getHeaderLine('Content-Type')); $this->assertStringContainsString("Error 400: Proxy Requests Not Allowed\n", (string) $response->getBody()); } @@ -400,7 +400,7 @@ public function testHandleRequestWithUnknownRouteReturnsResponseWithFileNotFound /** @var ResponseInterface $response */ $this->assertInstanceOf(ResponseInterface::class, $response); $this->assertEquals(404, $response->getStatusCode()); - $this->assertEquals('text/html', $response->getHeaderLine('Content-Type')); + $this->assertEquals('text/html; charset=utf-8', $response->getHeaderLine('Content-Type')); $this->assertStringContainsString("Error 404: Page Not Found\n", (string) $response->getBody()); $this->assertStringContainsString("

Please check the URL in the address bar and try again.

\n", (string) $response->getBody()); } @@ -421,7 +421,7 @@ public function testHandleRequestWithInvalidRequestMethodReturnsResponseWithSing /** @var ResponseInterface $response */ $this->assertInstanceOf(ResponseInterface::class, $response); $this->assertEquals(405, $response->getStatusCode()); - $this->assertEquals('text/html', $response->getHeaderLine('Content-Type')); + $this->assertEquals('text/html; charset=utf-8', $response->getHeaderLine('Content-Type')); $this->assertEquals('GET', $response->getHeaderLine('Allow')); $this->assertStringContainsString("Error 405: Method Not Allowed\n", (string) $response->getBody()); $this->assertStringContainsString("

Please check the URL in the address bar and try again with GET request.

\n", (string) $response->getBody()); @@ -445,7 +445,7 @@ public function testHandleRequestWithInvalidRequestMethodReturnsResponseWithMult /** @var ResponseInterface $response */ $this->assertInstanceOf(ResponseInterface::class, $response); $this->assertEquals(405, $response->getStatusCode()); - $this->assertEquals('text/html', $response->getHeaderLine('Content-Type')); + $this->assertEquals('text/html; charset=utf-8', $response->getHeaderLine('Content-Type')); $this->assertEquals('GET, HEAD, POST', $response->getHeaderLine('Allow')); $this->assertStringContainsString("Error 405: Method Not Allowed\n", (string) $response->getBody()); $this->assertStringContainsString("

Please check the URL in the address bar and try again with GET/HEAD/POST request.

\n", (string) $response->getBody()); @@ -682,13 +682,54 @@ public function testHandleRequestWithMatchingRouteAndRouteVariablesReturnsRespon $this->assertEquals("Hello alice\n", (string) $response->getBody()); } - public function testHandleRequestWithMatchingRouteReturnsInternalServerErrorResponseWhenHandlerThrowsException() + public function provideExceptionMessage() + { + return [ + [ + 'Foo', + 'Foo' + ], + [ + 'Ünicöde!', + 'Ünicöde!' + ], + [ + ' spa ces ', + ' spa  ces ' + ], + [ + 'sla/she\'s\\n', + 'sla/she\'s\\\\n' + ], + [ + "hello\r\nworld", + 'hello\r\nworld' + ], + [ + '"with"', + '"with"<html>' + ], + [ + "bin\0\1\2\3\4\5\6\7ary", + "bin��������ary" + ], + [ + utf8_decode("hellö!"), + "hell�!" + ] + ]; + } + + /** + * @dataProvider provideExceptionMessage + */ + public function testHandleRequestWithMatchingRouteReturnsInternalServerErrorResponseWhenHandlerThrowsException(string $in, string $expected) { $app = new App(); $line = __LINE__ + 2; - $app->get('/users', function () { - throw new \RuntimeException('Foo'); + $app->get('/users', function () use ($in) { + throw new \RuntimeException($in); }); $request = new ServerRequest('GET', 'http://localhost/users'); @@ -701,19 +742,22 @@ public function testHandleRequestWithMatchingRouteReturnsInternalServerErrorResp /** @var ResponseInterface $response */ $this->assertInstanceOf(ResponseInterface::class, $response); $this->assertEquals(500, $response->getStatusCode()); - $this->assertEquals('text/html', $response->getHeaderLine('Content-Type')); + $this->assertEquals('text/html; charset=utf-8', $response->getHeaderLine('Content-Type')); $this->assertStringContainsString("Error 500: Internal Server Error\n", (string) $response->getBody()); $this->assertStringContainsString("

The requested page failed to load, please try again later.

\n", (string) $response->getBody()); - $this->assertStringContainsString("

Expected request handler to return Psr\Http\Message\ResponseInterface but got uncaught RuntimeException in AppTest.php:$line: Foo

\n", (string) $response->getBody()); + $this->assertStringContainsString("

Expected request handler to return Psr\Http\Message\ResponseInterface but got uncaught RuntimeException with message $expected in AppTest.php:$line.

\n", (string) $response->getBody()); } - public function testHandleRequestWithMatchingRouteReturnsPromiseWhichFulfillsWithInternalServerErrorResponseWhenHandlerReturnsPromiseWhichRejectsWithException() + /** + * @dataProvider provideExceptionMessage + */ + public function testHandleRequestWithMatchingRouteReturnsPromiseWhichFulfillsWithInternalServerErrorResponseWhenHandlerReturnsPromiseWhichRejectsWithException(string $in, string $expected) { $app = new App(); $line = __LINE__ + 2; - $app->get('/users', function () { - return \React\Promise\reject(new \RuntimeException('Foo')); + $app->get('/users', function () use ($in) { + return \React\Promise\reject(new \RuntimeException($in)); }); $request = new ServerRequest('GET', 'http://localhost/users'); @@ -734,10 +778,10 @@ public function testHandleRequestWithMatchingRouteReturnsPromiseWhichFulfillsWit /** @var ResponseInterface $response */ $this->assertInstanceOf(ResponseInterface::class, $response); $this->assertEquals(500, $response->getStatusCode()); - $this->assertEquals('text/html', $response->getHeaderLine('Content-Type')); + $this->assertEquals('text/html; charset=utf-8', $response->getHeaderLine('Content-Type')); $this->assertStringContainsString("Error 500: Internal Server Error\n", (string) $response->getBody()); $this->assertStringContainsString("

The requested page failed to load, please try again later.

\n", (string) $response->getBody()); - $this->assertStringContainsString("

Expected request handler to return Psr\Http\Message\ResponseInterface but got uncaught RuntimeException in AppTest.php:$line: Foo

\n", (string) $response->getBody()); + $this->assertStringContainsString("

Expected request handler to return Psr\Http\Message\ResponseInterface but got uncaught RuntimeException with message $expected in AppTest.php:$line.

\n", (string) $response->getBody()); } public function testHandleRequestWithMatchingRouteReturnsPromiseWhichFulfillsWithInternalServerErrorResponseWhenHandlerReturnsPromiseWhichRejectsWithNull() @@ -766,19 +810,22 @@ public function testHandleRequestWithMatchingRouteReturnsPromiseWhichFulfillsWit /** @var ResponseInterface $response */ $this->assertInstanceOf(ResponseInterface::class, $response); $this->assertEquals(500, $response->getStatusCode()); - $this->assertEquals('text/html', $response->getHeaderLine('Content-Type')); + $this->assertEquals('text/html; charset=utf-8', $response->getHeaderLine('Content-Type')); $this->assertStringContainsString("Error 500: Internal Server Error\n", (string) $response->getBody()); $this->assertStringContainsString("

The requested page failed to load, please try again later.

\n", (string) $response->getBody()); $this->assertStringContainsString("

Expected request handler to return Psr\Http\Message\ResponseInterface but got React\Promise\RejectedPromise.

\n", (string) $response->getBody()); } - public function testHandleRequestWithMatchingRouteReturnsPromiseWhichFulfillsWithInternalServerErrorResponseWhenHandlerReturnsCoroutineWhichYieldsRejectedPromise() + /** + * @dataProvider provideExceptionMessage + */ + public function testHandleRequestWithMatchingRouteReturnsPromiseWhichFulfillsWithInternalServerErrorResponseWhenHandlerReturnsCoroutineWhichYieldsRejectedPromise(string $in, string $expected) { $app = new App(); $line = __LINE__ + 2; - $app->get('/users', function () { - yield \React\Promise\reject(new \RuntimeException('Foo')); + $app->get('/users', function () use ($in) { + yield \React\Promise\reject(new \RuntimeException($in)); }); $request = new ServerRequest('GET', 'http://localhost/users'); @@ -799,20 +846,23 @@ public function testHandleRequestWithMatchingRouteReturnsPromiseWhichFulfillsWit /** @var ResponseInterface $response */ $this->assertInstanceOf(ResponseInterface::class, $response); $this->assertEquals(500, $response->getStatusCode()); - $this->assertEquals('text/html', $response->getHeaderLine('Content-Type')); + $this->assertEquals('text/html; charset=utf-8', $response->getHeaderLine('Content-Type')); $this->assertStringContainsString("Error 500: Internal Server Error\n", (string) $response->getBody()); $this->assertStringContainsString("

The requested page failed to load, please try again later.

\n", (string) $response->getBody()); - $this->assertStringContainsString("

Expected request handler to return Psr\Http\Message\ResponseInterface but got uncaught RuntimeException in AppTest.php:$line: Foo

\n", (string) $response->getBody()); + $this->assertStringContainsString("

Expected request handler to return Psr\Http\Message\ResponseInterface but got uncaught RuntimeException with message $expected in AppTest.php:$line.

\n", (string) $response->getBody()); } - public function testHandleRequestWithMatchingRouteReturnsPromiseWhichFulfillsWithInternalServerErrorResponseWhenHandlerReturnsCoroutineWhichThrowsExceptionAfterYielding() + /** + * @dataProvider provideExceptionMessage + */ + public function testHandleRequestWithMatchingRouteReturnsPromiseWhichFulfillsWithInternalServerErrorResponseWhenHandlerReturnsCoroutineWhichThrowsExceptionAfterYielding(string $in, string $expected) { $app = new App(); $line = __LINE__ + 3; - $app->get('/users', function () { + $app->get('/users', function () use ($in) { yield \React\Promise\resolve(null); - throw new \RuntimeException('Foo'); + throw new \RuntimeException($in); }); $request = new ServerRequest('GET', 'http://localhost/users'); @@ -833,10 +883,10 @@ public function testHandleRequestWithMatchingRouteReturnsPromiseWhichFulfillsWit /** @var ResponseInterface $response */ $this->assertInstanceOf(ResponseInterface::class, $response); $this->assertEquals(500, $response->getStatusCode()); - $this->assertEquals('text/html', $response->getHeaderLine('Content-Type')); + $this->assertEquals('text/html; charset=utf-8', $response->getHeaderLine('Content-Type')); $this->assertStringContainsString("Error 500: Internal Server Error\n", (string) $response->getBody()); $this->assertStringContainsString("

The requested page failed to load, please try again later.

\n", (string) $response->getBody()); - $this->assertStringContainsString("

Expected request handler to return Psr\Http\Message\ResponseInterface but got uncaught RuntimeException in AppTest.php:$line: Foo

\n", (string) $response->getBody()); + $this->assertStringContainsString("

Expected request handler to return Psr\Http\Message\ResponseInterface but got uncaught RuntimeException with message $expected in AppTest.php:$line.

\n", (string) $response->getBody()); } public function testHandleRequestWithMatchingRouteReturnsPromiseWhichFulfillsWithInternalServerErrorResponseWhenHandlerReturnsCoroutineWhichReturnsNull() @@ -866,7 +916,7 @@ public function testHandleRequestWithMatchingRouteReturnsPromiseWhichFulfillsWit /** @var ResponseInterface $response */ $this->assertInstanceOf(ResponseInterface::class, $response); $this->assertEquals(500, $response->getStatusCode()); - $this->assertEquals('text/html', $response->getHeaderLine('Content-Type')); + $this->assertEquals('text/html; charset=utf-8', $response->getHeaderLine('Content-Type')); $this->assertStringContainsString("Error 500: Internal Server Error\n", (string) $response->getBody()); $this->assertStringContainsString("

The requested page failed to load, please try again later.

\n", (string) $response->getBody()); $this->assertStringContainsString("

Expected request handler to return Psr\Http\Message\ResponseInterface but got null.

\n", (string) $response->getBody()); @@ -898,7 +948,7 @@ public function testHandleRequestWithMatchingRouteReturnsPromiseWhichFulfillsWit /** @var ResponseInterface $response */ $this->assertInstanceOf(ResponseInterface::class, $response); $this->assertEquals(500, $response->getStatusCode()); - $this->assertEquals('text/html', $response->getHeaderLine('Content-Type')); + $this->assertEquals('text/html; charset=utf-8', $response->getHeaderLine('Content-Type')); $this->assertStringContainsString("Error 500: Internal Server Error\n", (string) $response->getBody()); $this->assertStringContainsString("

The requested page failed to load, please try again later.

\n", (string) $response->getBody()); $this->assertStringContainsString("

Expected request handler to yield React\Promise\PromiseInterface but got null.

\n", (string) $response->getBody()); @@ -965,7 +1015,7 @@ public function testHandleRequestWithMatchingRouteReturnsInternalServerErrorResp /** @var ResponseInterface $response */ $this->assertInstanceOf(ResponseInterface::class, $response); $this->assertEquals(500, $response->getStatusCode()); - $this->assertEquals('text/html', $response->getHeaderLine('Content-Type')); + $this->assertEquals('text/html; charset=utf-8', $response->getHeaderLine('Content-Type')); $this->assertStringContainsString("Error 500: Internal Server Error\n", (string) $response->getBody()); $this->assertStringContainsString("

The requested page failed to load, please try again later.

\n", (string) $response->getBody()); $this->assertStringContainsString("

Expected request handler to return Psr\Http\Message\ResponseInterface but got $name.

\n", (string) $response->getBody()); @@ -1002,7 +1052,7 @@ public function testHandleRequestWithMatchingRouteReturnsPromiseWhichFulfillsWit /** @var ResponseInterface $response */ $this->assertInstanceOf(ResponseInterface::class, $response); $this->assertEquals(500, $response->getStatusCode()); - $this->assertEquals('text/html', $response->getHeaderLine('Content-Type')); + $this->assertEquals('text/html; charset=utf-8', $response->getHeaderLine('Content-Type')); $this->assertStringContainsString("Error 500: Internal Server Error\n", (string) $response->getBody()); $this->assertStringContainsString("

The requested page failed to load, please try again later.

\n", (string) $response->getBody()); $this->assertStringContainsString("

Expected request handler to return Psr\Http\Message\ResponseInterface but got $name.

\n", (string) $response->getBody()); diff --git a/tests/acceptance.sh b/tests/acceptance.sh index b3fcc57..1bea3af 100755 --- a/tests/acceptance.sh +++ b/tests/acceptance.sh @@ -14,11 +14,11 @@ skipif() { } out=$(curl -v $base/ 2>&1); match "HTTP/.* 200" && match -iv "Content-Type:" -out=$(curl -v $base/invalid 2>&1); match "HTTP/.* 404" && match -iP "Content-Type: text/html[\r\n]" +out=$(curl -v $base/invalid 2>&1); match "HTTP/.* 404" && match -iP "Content-Type: text/html; charset=utf-8[\r\n]" out=$(curl -v $base// 2>&1); match "HTTP/.* 404" out=$(curl -v $base/ 2>&1 -X POST); match "HTTP/.* 405" -out=$(curl -v $base/error 2>&1); match "HTTP/.* 500" && match -iP "Content-Type: text/html[\r\n]" -out=$(curl -v $base/error/null 2>&1); match "HTTP/.* 500" && match -iP "Content-Type: text/html[\r\n]" +out=$(curl -v $base/error 2>&1); match "HTTP/.* 500" && match -iP "Content-Type: text/html; charset=utf-8[\r\n]" +out=$(curl -v $base/error/null 2>&1); match "HTTP/.* 500" && match -iP "Content-Type: text/html; charset=utf-8[\r\n]" out=$(curl -v $base/uri 2>&1); match "HTTP/.* 200" && match "$base/uri" out=$(curl -v $base/uri/ 2>&1); match "HTTP/.* 200" && match "$base/uri/" From 9c38246270ddf6b8c5013c997dede602206fad51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sun, 12 Sep 2021 12:53:50 +0200 Subject: [PATCH 5/8] Add `Content-Security-Policy` (CSP) to limit possible XSS surface --- src/App.php | 6 ++++-- tests/AppTest.php | 2 ++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/App.php b/src/App.php index f8b999a..b9a2f49 100644 --- a/src/App.php +++ b/src/App.php @@ -420,13 +420,14 @@ private function log(string $message): void private function error(int $statusCode, string $title, string ...$info): ResponseInterface { + $nonce = \base64_encode(\random_bytes(16)); $info = \implode('', \array_map(function (string $info) { return "

$info

\n"; }, $info)); $html = << Error $statusCode: $title - From b009c15c4bb5e81d8b426555d54e732582bf8f5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sun, 12 Sep 2021 16:35:22 +0200 Subject: [PATCH 7/8] Preserve original EOL (LF) instead of converting to CRLF on Windows --- .gitattributes | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitattributes b/.gitattributes index 9709aa5..65a00ff 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,3 +1,4 @@ +# exclude dev files from export to reduce archive download size /.gitattributes export-ignore /.github/workflows/ export-ignore /.gitignore export-ignore @@ -7,3 +8,6 @@ /phpunit.xml.dist export-ignore /phpunit.xml.legacy export-ignore /tests/ export-ignore + +# preserve original EOL with no automatic conversation +* -text From 256c167ba12365011758a0ea612495b5cd828b9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sun, 12 Sep 2021 18:35:24 +0200 Subject: [PATCH 8/8] Reuse 404 (Not Found) error page for `FilesystemHandler` --- src/App.php | 11 +++++++---- src/FilesystemHandler.php | 8 +------- tests/FilesystemHandlerTest.php | 24 ++++++++++++------------ 3 files changed, 20 insertions(+), 23 deletions(-) diff --git a/src/App.php b/src/App.php index 0746f0f..a07446e 100644 --- a/src/App.php +++ b/src/App.php @@ -343,7 +343,7 @@ private function routeRequest(ServerRequestInterface $request) $routeInfo = $this->routeDispatcher->dispatch($request->getMethod(), $request->getUri()->getPath()); switch ($routeInfo[0]) { case \FastRoute\Dispatcher::NOT_FOUND: - return $this->errorNotFound($request); + return self::errorNotFound($request); case \FastRoute\Dispatcher::METHOD_NOT_ALLOWED: return $this->errorMethodNotAllowed($routeInfo[1]); case \FastRoute\Dispatcher::FOUND: @@ -418,7 +418,7 @@ private function log(string $message): void } } - private function error(int $statusCode, string $title, string ...$info): ResponseInterface + private static function error(int $statusCode, string $title, string ...$info): ResponseInterface { $nonce = \base64_encode(\random_bytes(16)); $info = \implode('', \array_map(function (string $info) { return "

$info

\n"; }, $info)); @@ -465,9 +465,12 @@ private function errorProxy(): ResponseInterface ); } - private function errorNotFound(): ResponseInterface + /** + * @internal + */ + public static function errorNotFound(): ResponseInterface { - return $this->error( + return self::error( 404, 'Page Not Found', 'Please check the URL in the address bar and try again.' diff --git a/src/FilesystemHandler.php b/src/FilesystemHandler.php index cf48920..1c1372b 100644 --- a/src/FilesystemHandler.php +++ b/src/FilesystemHandler.php @@ -124,13 +124,7 @@ public function __invoke(ServerRequestInterface $request) \file_get_contents($path) ); } else { - return new Response( - 404, - [ - 'Content-Type' => 'text/plain; charset=utf-8' - ], - "Error 404: Not Found\n" - ); + return App::errorNotFound(); } } diff --git a/tests/FilesystemHandlerTest.php b/tests/FilesystemHandlerTest.php index 4dcd9e0..b1b3942 100644 --- a/tests/FilesystemHandlerTest.php +++ b/tests/FilesystemHandlerTest.php @@ -106,8 +106,8 @@ public function testInvokeWithInvalidPathWillReturnNotFoundResponse() /** @var ResponseInterface $response */ $this->assertInstanceOf(ResponseInterface::class, $response); $this->assertEquals(404, $response->getStatusCode()); - $this->assertEquals('text/plain; charset=utf-8', $response->getHeaderLine('Content-Type')); - $this->assertEquals("Error 404: Not Found\n", (string) $response->getBody()); + $this->assertEquals('text/html; charset=utf-8', $response->getHeaderLine('Content-Type')); + $this->assertStringContainsString("Error 404: Page Not Found\n", (string) $response->getBody()); } public function testInvokeWithDoubleSlashWillReturnNotFoundResponse() @@ -122,8 +122,8 @@ public function testInvokeWithDoubleSlashWillReturnNotFoundResponse() /** @var ResponseInterface $response */ $this->assertInstanceOf(ResponseInterface::class, $response); $this->assertEquals(404, $response->getStatusCode()); - $this->assertEquals('text/plain; charset=utf-8', $response->getHeaderLine('Content-Type')); - $this->assertEquals("Error 404: Not Found\n", (string) $response->getBody()); + $this->assertEquals('text/html; charset=utf-8', $response->getHeaderLine('Content-Type')); + $this->assertStringContainsString("Error 404: Page Not Found\n", (string) $response->getBody()); } public function testInvokeWithPathWithLeadingSlashWillReturnNotFoundResponse() @@ -138,8 +138,8 @@ public function testInvokeWithPathWithLeadingSlashWillReturnNotFoundResponse() /** @var ResponseInterface $response */ $this->assertInstanceOf(ResponseInterface::class, $response); $this->assertEquals(404, $response->getStatusCode()); - $this->assertEquals('text/plain; charset=utf-8', $response->getHeaderLine('Content-Type')); - $this->assertEquals("Error 404: Not Found\n", (string) $response->getBody()); + $this->assertEquals('text/html; charset=utf-8', $response->getHeaderLine('Content-Type')); + $this->assertStringContainsString("Error 404: Page Not Found\n", (string) $response->getBody()); } public function testInvokeWithPathWithDotSegmentWillReturnNotFoundResponse() @@ -154,8 +154,8 @@ public function testInvokeWithPathWithDotSegmentWillReturnNotFoundResponse() /** @var ResponseInterface $response */ $this->assertInstanceOf(ResponseInterface::class, $response); $this->assertEquals(404, $response->getStatusCode()); - $this->assertEquals('text/plain; charset=utf-8', $response->getHeaderLine('Content-Type')); - $this->assertEquals("Error 404: Not Found\n", (string) $response->getBody()); + $this->assertEquals('text/html; charset=utf-8', $response->getHeaderLine('Content-Type')); + $this->assertStringContainsString("Error 404: Page Not Found\n", (string) $response->getBody()); } public function testInvokeWithPathBelowRootWillReturnNotFoundResponse() @@ -170,8 +170,8 @@ public function testInvokeWithPathBelowRootWillReturnNotFoundResponse() /** @var ResponseInterface $response */ $this->assertInstanceOf(ResponseInterface::class, $response); $this->assertEquals(404, $response->getStatusCode()); - $this->assertEquals('text/plain; charset=utf-8', $response->getHeaderLine('Content-Type')); - $this->assertEquals("Error 404: Not Found\n", (string) $response->getBody()); + $this->assertEquals('text/html; charset=utf-8', $response->getHeaderLine('Content-Type')); + $this->assertStringContainsString("Error 404: Page Not Found\n", (string) $response->getBody()); } public function testInvokeWithBinaryPathWillReturnNotFoundResponse() @@ -186,8 +186,8 @@ public function testInvokeWithBinaryPathWillReturnNotFoundResponse() /** @var ResponseInterface $response */ $this->assertInstanceOf(ResponseInterface::class, $response); $this->assertEquals(404, $response->getStatusCode()); - $this->assertEquals('text/plain; charset=utf-8', $response->getHeaderLine('Content-Type')); - $this->assertEquals("Error 404: Not Found\n", (string) $response->getBody()); + $this->assertEquals('text/html; charset=utf-8', $response->getHeaderLine('Content-Type')); + $this->assertStringContainsString("Error 404: Page Not Found\n", (string) $response->getBody()); } public function testInvokeWithoutPathWillReturnResponseWithDirectoryListing()