Error handling
ApiPresenter never builds error responses itself. It calls the ErrorHandlerInterface service, usually DefaultErrorHandler.
The interface and the default answers
| Method | Called when | DefaultErrorHandler answers |
|---|---|---|
handle(Throwable, array $params) | handle() threw | 500 Internal server error, logs to Tracy |
handleInputParams(array $errors) | params are invalid | 400 wrong input |
handleSchema(array $errors, array $params) | the output check failed | 500 Internal server error, logs to Tracy |
handleAuthorization(ApiAuthorizationInterface, array $params) | authorized() is false | 401 with getErrorMessage() |
handleAuthorizationException(Throwable, array $params) | authorized() threw | 500 Internal server error, logs to Tracy |
handleNotFound(EndpointInterface) | no endpoint matches the URL | 404 Unknown api endpoint |
handleRateLimit(RateLimitResponse) | the rate limit is reached and has no own response | 429 Too many requests., plus Retry after N seconds. when the time is known |
All of them return a ResponseInterface; DefaultErrorHandler returns a JsonApiResponse with status: error and message.
Since 4.0.0
handleNotFound() and handleRateLimit() are new, so 404 and 429 can be changed in one place. The methods return ResponseInterface instead of JsonApiResponse. handleAuthorizationException() no longer sends the exception message unless error details are on. See the upgrade guide.
For new projects we recommend RFC 9457 application/problem+json errors with ProblemDetailsErrorHandler, see Problem Details. DefaultErrorHandler stays the default so that clients reading message keep working.
Error details
DefaultErrorHandler(ConfiguratorInterface) adds detail when the configurator's showErrorDetail() is true: the exception message for 500 (also from authorized()), the errors per field for 400, the schema errors for a broken output.
{"status":"error","message":"Internal server error","detail":"Database connection refused"}Which configurator to use is described in Outputs. In production details should be off.
Your own error handler
DefaultErrorHandler is final, so wrap it. The logging example turns one exception into 503 and leaves the rest to the default:
<?php
declare(strict_types=1);
namespace App;
use Throwable;
use Tomaj\NetteApi\Authorization\ApiAuthorizationInterface;
use Tomaj\NetteApi\EndpointInterface;
use Tomaj\NetteApi\Error\ErrorHandlerInterface;
use Tomaj\NetteApi\RateLimit\RateLimitResponse;
use Tomaj\NetteApi\Response\JsonApiResponse;
use Tomaj\NetteApi\Response\ResponseInterface;
/**
* Answers MaintenanceException with 503 and leaves everything else to DefaultErrorHandler.
*/
final class AppErrorHandler implements ErrorHandlerInterface
{
public function __construct(private ErrorHandlerInterface $inner)
{
}
public function handle(Throwable $exception, array $params): ResponseInterface
{
if ($exception instanceof MaintenanceException) {
return new JsonApiResponse(503, ['status' => 'error', 'message' => 'Down for maintenance, try again later']);
}
return $this->inner->handle($exception, $params);
}
public function handleInputParams(array $errors): ResponseInterface
{
return $this->inner->handleInputParams($errors);
}
public function handleSchema(array $errors, array $params): ResponseInterface
{
return $this->inner->handleSchema($errors, $params);
}
public function handleAuthorization(ApiAuthorizationInterface $auth, array $params): ResponseInterface
{
return $this->inner->handleAuthorization($auth, $params);
}
public function handleAuthorizationException(Throwable $exception, array $params): ResponseInterface
{
return $this->inner->handleAuthorizationException($exception, $params);
}
public function handleNotFound(EndpointInterface $endpoint): ResponseInterface
{
return $this->inner->handleNotFound($endpoint);
}
public function handleRateLimit(RateLimitResponse $rateLimit): ResponseInterface
{
return $this->inner->handleRateLimit($rateLimit);
}
}services:
defaultErrorHandler:
factory: Tomaj\NetteApi\Error\DefaultErrorHandler
autowired: false
- App\AppErrorHandler(@defaultErrorHandler)autowired: false keeps a single ErrorHandlerInterface for autowiring.
FAQ
Why does production answer 400 without saying which field is wrong?
DefaultErrorHandler hides detail for every code when showErrorDetail() is false, including 400. DefaultErrorHandler is final, so implement ErrorHandlerInterface, delegate every method to the wrapped DefaultErrorHandler as in Your own error handler, and in handleInputParams() return new JsonApiResponse(400, ['status' => 'error', 'message' => 'wrong input', 'detail' => $errors]).
How do I send a 404 for a missing record?
Return it from the handler: new JsonApiResponse(404, [...]). Error handlers are for unexpected failures. See Handlers.
Where do I see the exception?
In the Tracy log directory, exception.log and an HTML file per exception. The logging example checks it.
Can authorization errors carry their own status code?
Your error handler decides; handleAuthorization() gets the authorization object and can read anything from it. See Authorization.
Is a broken output an error in production?
Only if the configurator validates outputs there. With EnvConfigurator and APP_ENV=production the response is sent unchecked.