Skip to content

Error handling ​

ApiPresenter never builds error responses itself. It calls the ErrorHandlerInterface service, usually DefaultErrorHandler.

The interface and the default answers ​

MethodCalled whenDefaultErrorHandler answers
handle(Throwable, array $params)handle() threw500 Internal server error, logs to Tracy
handleInputParams(array $errors)params are invalid400 wrong input
handleSchema(array $errors, array $params)the output check failed500 Internal server error, logs to Tracy
handleAuthorization(ApiAuthorizationInterface, array $params)authorized() is false401 with getErrorMessage()
handleAuthorizationException(Throwable, array $params)authorized() threw500 with the exception message

All of them return a JsonApiResponse with status: error and message.

Error details ​

DefaultErrorHandler(ConfiguratorInterface) adds detail when the configurator's showErrorDetail() is true: the exception message for 500, the errors per field for 400, the schema errors for a broken output.

json
{"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
<?php

declare(strict_types=1);

namespace App;

use Throwable;
use Tomaj\NetteApi\Authorization\ApiAuthorizationInterface;
use Tomaj\NetteApi\Error\ErrorHandlerInterface;
use Tomaj\NetteApi\Response\JsonApiResponse;

/**
 * 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): JsonApiResponse
    {
        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): JsonApiResponse
    {
        return $this->inner->handleInputParams($errors);
    }

    public function handleSchema(array $errors, array $params): JsonApiResponse
    {
        return $this->inner->handleSchema($errors, $params);
    }

    public function handleAuthorization(ApiAuthorizationInterface $auth, array $params): JsonApiResponse
    {
        return $this->inner->handleAuthorization($auth, $params);
    }

    public function handleAuthorizationException(Throwable $exception, array $params): JsonApiResponse
    {
        return $this->inner->handleAuthorizationException($exception, $params);
    }
}
yaml
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.

Released under the MIT License.