Skip to content

Commit

Permalink
Update error handlers to be content-type aware
Browse files Browse the repository at this point in the history
Ensure that Error, NotAllowed and NotFound return JSON, XML or HTML as
determined by the request's Accept header. This makes Slim much more
API friendly.
  • Loading branch information
akrabat committed Sep 6, 2015
1 parent faaf470 commit bdcf93d
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 bdcf93d

Please sign in to comment.