Skip to content

Commit

Permalink
Merge pull request #1490 from akrabat/content-type-aware-errors
Browse files Browse the repository at this point in the history
Update error handlers to be content-type aware
  • Loading branch information
silentworks committed Sep 7, 2015
2 parents faaf470 + bdcf93d commit c098a25
Show file tree
Hide file tree
Showing 7 changed files with 371 additions and 31 deletions.
144 changes: 130 additions & 14 deletions Slim/Handlers/Error.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@
use Slim\Http\Body;

/**
* Default error handler
* Default Slim application error handler
*
* This is the default Slim application error handler. All it does is output
* a clean and simple HTML page with diagnostic information.
* It outputs the error message and diagnostic information in either JSON, XML,
* or HTML based on the Accept header.
*/
class Error
{
Expand All @@ -31,15 +31,49 @@ class Error
* @return ResponseInterface
*/
public function __invoke(ServerRequestInterface $request, ResponseInterface $response, Exception $exception)
{
$contentType = $this->determineContentType($request->getHeaderLine('Accept'));
switch ($contentType) {
case 'application/json':
$output = $this->renderJsonErrorMessage($exception);
break;

case 'application/xml':
$output = $this->renderXmlErrorMessage($exception);
break;

case 'text/html':
default:
$contentType = 'text/html';
$output = $this->renderHtmlErrorMessage($exception);
break;
}

$body = new Body(fopen('php://temp', 'r+'));
$body->write($output);

return $response
->withStatus(500)
->withHeader('Content-type', $contentType)
->withBody($body);
}

/**
* Render HTML error page
*
* @param Exception $exception
* @return string
*/
private function renderHtmlErrorMessage(Exception $exception)
{
$title = 'Slim Application Error';
$html = '<p>The application could not run because of the following error:</p>';
$html .= '<h2>Details</h2>';
$html .= $this->renderException($exception);
$html .= $this->renderHtmlException($exception);

while ($exception = $exception->getPrevious()) {
$html .= '<h2>Previous exception</h2>';
$html .= $this->renderException($exception);
$html .= $this->renderHtmlException($exception);
}

$output = sprintf(
Expand All @@ -52,23 +86,17 @@ public function __invoke(ServerRequestInterface $request, ResponseInterface $res
$html
);

$body = new Body(fopen('php://temp', 'r+'));
$body->write($output);

return $response
->withStatus(500)
->withHeader('Content-type', 'text/html')
->withBody($body);
return $output;
}

/**
* Render exception as html.
* Render exception as HTML.
*
* @param Exception $exception
*
* @return string
*/
private function renderException(Exception $exception)
private function renderHtmlException(Exception $exception)
{
$code = $exception->getCode();
$message = $exception->getMessage();
Expand All @@ -95,4 +123,92 @@ private function renderException(Exception $exception)
}
return $html;
}

/**
* Render JSON error
*
* @param Exception $exception
* @return string
*/
private function renderJsonErrorMessage(Exception $exception)
{
$error = ['message' => 'Slim Application Error'];
$error['exception'][] = [
'code' => $exception->getCode(),
'message' => $exception->getMessage(),
'file' => $exception->getFile(),
'line' => $exception->getLine(),
'trace' => explode("\n", $exception->getTraceAsString()),
];

while ($exception = $exception->getPrevious()) {
$error['exception'][] = [
'code' => $exception->getCode(),
'message' => $exception->getMessage(),
'file' => $exception->getFile(),
'line' => $exception->getLine(),
'trace' => explode("\n", $exception->getTraceAsString()),
];
}

return json_encode($error);
}

/**
* Render XML error
*
* @param Exception $exception
* @return string
*/
private function renderXmlErrorMessage(Exception $exception)
{
$xml = "<root>\n <message>Slim Application Error</message>\n";

$xml .= <<<EOT
<exception>
<code>{$exception->getCode()}</code>
<message>{$exception->getMessage()}</message>
<file>{$exception->getFile()}</file>
<line>{$exception->getLine()}</line>
<trace>{$exception->getTraceAsString()}</trace>
</exception>
EOT;

while ($exception = $exception->getPrevious()) {
$xml .= <<<EOT
<exception>
<code>{$exception->getCode()}</code>
<message>{$exception->getMessage()}</message>
<file>{$exception->getFile()}</file>
<line>{$exception->getLine()}</line>
<trace>{$exception->getTraceAsString()}</trace>
</exception>
EOT;
}
$xml .="</root>";
return $xml;
}

/**
* Read the accept header and determine which content type we know about
* is wanted.
*
* @param string $acceptHeader Accept header from request
* @return string
*/
private function determineContentType($acceptHeader)
{
$list = explode(',', $acceptHeader);
$known = ['application/json', 'application/xml', 'text/html'];

foreach ($list as $type) {
if (in_array($type, $known)) {
return $type;
}
}

return 'text/html';
}
}
74 changes: 67 additions & 7 deletions Slim/Handlers/NotAllowed.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@
use Slim\Http\Body;

/**
* Default not allowed handler
* Default Slim application not allowed handler
*
* This is the default Slim application error handler. All it does is output
* a clean and simple HTML page with diagnostic information.
* It outputs a simple message in either JSON, XML or HTML based on the
* Accept header.
*/
class NotAllowed
{
Expand All @@ -32,22 +32,82 @@ class NotAllowed
public function __invoke(ServerRequestInterface $request, ResponseInterface $response, array $methods)
{
$allow = implode(', ', $methods);
$body = new Body(fopen('php://temp', 'r+'));

if ($request->getMethod() === 'OPTIONS') {
$status = 200;
$contentType = 'text/plain';
$body->write('Allowed methods: ' . $allow);
$output = 'Allowed methods: ' . $allow;
} else {
$status = 405;
$contentType = 'text/html';
$body->write('<p>Method not allowed. Must be one of: ' . $allow . '</p>');
$contentType = $this->determineContentType($request->getHeaderLine('Accept'));
switch ($contentType) {
case 'application/json':
$output = '{"message":"Method not allowed. Must be one of: ' . $allow . '"}';
break;

case 'application/xml':
$output = "<root><message>Method not allowed. Must be one of: $allow</message></root>";
break;

case 'text/html':
default:
$contentType = 'text/html';
$output = <<<END
<html>
<head>
<title>Method not allowed</title>
<style>
body{
margin:0;
padding:30px;
font:12px/1.5 Helvetica,Arial,Verdana,sans-serif;
}
h1{
margin:0;
font-size:48px;
font-weight:normal;
line-height:48px;
}
</style>
</head>
<body>
<h1>Method not allowed</h1>
<p>Method not allowed. Must be one of: <strong>$allow</strong></p>
</body>
</html>
END;
break;
}
}

$body = new Body(fopen('php://temp', 'r+'));
$body->write($output);

return $response
->withStatus($status)
->withHeader('Content-type', $contentType)
->withHeader('Allow', $allow)
->withBody($body);
}

/**
* Read the accept header and determine which content type we know about
* is wanted.
*
* @param string $acceptHeader Accept header from request
* @return string
*/
private function determineContentType($acceptHeader)
{
$list = explode(',', $acceptHeader);
$known = ['application/json', 'application/xml', 'text/html'];

foreach ($list as $type) {
if (in_array($type, $known)) {
return $type;
}
}

return 'text/html';
}
}
48 changes: 42 additions & 6 deletions Slim/Handlers/NotFound.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@
use Slim\Http\Body;

/**
* Default not found handler
* Default Slim application not found handler.
*
* This is the default Slim application not found handler. All it does is output
* a clean and simple HTML page with diagnostic information.
* It outputs a simple message in either JSON, XML or HTML based on the
* Accept header.
*/
class NotFound
{
Expand All @@ -30,9 +30,22 @@ class NotFound
*/
public function __invoke(ServerRequestInterface $request, ResponseInterface $response)
{
$homeUrl = (string)($request->getUri()->withPath('')->withQuery('')->withFragment(''));

$output = <<<END
$contentType = $this->determineContentType($request->getHeaderLine('Accept'));
switch ($contentType) {
case 'application/json':
$output = '{"message":"Not found"}';
break;

case 'application/xml':
$output = '<root><message>Not found</message></root>';
break;

case 'text/html':
default:
$homeUrl = (string)($request->getUri()->withPath('')->withQuery('')->withFragment(''));
$contentType = 'text/html';
$output = <<<END
<html>
<head>
<title>Page Not Found</title>
Expand Down Expand Up @@ -65,12 +78,35 @@ public function __invoke(ServerRequestInterface $request, ResponseInterface $res
</body>
</html>
END;
break;
}

$body = new Body(fopen('php://temp', 'r+'));
$body->write($output);

return $response->withStatus(404)
->withHeader('Content-Type', 'text/html')
->withHeader('Content-Type', $contentType)
->withBody($body);
}

/**
* Read the accept header and determine which content type we know about
* is wanted.
*
* @param string $acceptHeader Accept header from request
* @return string
*/
private function determineContentType($acceptHeader)
{
$list = explode(',', $acceptHeader);
$known = ['application/json', 'application/xml', 'text/html'];

foreach ($list as $type) {
if (in_array($type, $known)) {
return $type;
}
}

return 'text/html';
}
}
2 changes: 1 addition & 1 deletion tests/AppTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,7 @@ public function testInvokeReturnMethodNotAllowed()
$this->assertInstanceOf('\Psr\Http\Message\ResponseInterface', $resOut);
$this->assertEquals(405, (string)$resOut->getStatusCode());
$this->assertEquals(['GET'], $resOut->getHeader('Allow'));
$this->assertEquals('<p>Method not allowed. Must be one of: GET</p>', (string)$resOut->getBody());
$this->assertContains('<p>Method not allowed. Must be one of: <strong>GET</strong></p>', (string)$resOut->getBody());
}

public function testInvokeWithMatchingRoute()
Expand Down
Loading

0 comments on commit c098a25

Please sign in to comment.