Tratamento e padronização de erros no Slim Framework: exceções e respostas consistentes

Capítulo 7

Tempo estimado de leitura: 8 minutos

+ Exercício

Por que padronizar erros no Slim

Em uma API, erros precisam ser previsíveis para quem consome: o cliente deve conseguir identificar rapidamente o que aconteceu, onde aconteceu (em termos de regra/validação/autorização) e como reagir (corrigir payload, autenticar, tentar novamente etc.). No Slim, exceções podem surgir em qualquer ponto do pipeline (actions, serviços, repositórios, middlewares). Sem um tratamento centralizado, o resultado costuma ser inconsistente: mensagens diferentes, status codes incorretos e, pior, vazamento de detalhes internos em produção.

A estratégia recomendada é: (1) lançar exceções semânticas no domínio/aplicação para erros esperados; (2) capturar tudo em um error handler global; (3) transformar em respostas JSON padronizadas; (4) registrar logs estruturados com contexto; (5) expor mensagens seguras ao cliente.

Formato de resposta de erro (contrato)

Defina um envelope único para erros. Um formato comum e fácil de consumir:

{  "error": {    "code": "validation_error",    "message": "Dados inválidos.",    "details": [      { "field": "email", "issue": "invalid_format" }    ],    "traceId": "..."  }}
  • code: identificador estável para o front/cliente (não depende do idioma).
  • message: mensagem curta e segura para produção.
  • details: opcional; lista de problemas (validação, conflito, etc.).
  • traceId: id para correlacionar com logs (útil em suporte/observabilidade).

Erros esperados vs. inesperados

Erros esperados

São falhas que fazem parte do fluxo normal do negócio e podem acontecer por ação do usuário/cliente: validação, recurso não encontrado, conflito de estado, autenticação/autorização, regra de domínio. Esses erros devem retornar status codes 4xx e mensagens orientadas ao cliente.

Erros inesperados

São falhas que não deveriam ocorrer em condições normais: bugs, falhas de infraestrutura, exceções não mapeadas, erros de integração, problemas de banco, etc. Devem retornar 500 com mensagem genérica e sem detalhes internos. O detalhe vai para o log.

Continue em nosso aplicativo e ...
  • Ouça o áudio com a tela desligada
  • Ganhe Certificado após a conclusão
  • + de 5000 cursos para você explorar!
ou continue lendo abaixo...
Download App

Baixar o aplicativo

Hierarquia de exceções do projeto

Crie uma base para exceções da aplicação e derive tipos específicos. Isso permite mapear status codes e códigos de erro de forma consistente.

namespace App\Shared\Exception;use RuntimeException;class AppException extends RuntimeException{    public function __construct(        string $message,        private readonly string $errorCode = 'app_error',        private readonly array $details = [],        private readonly int $httpStatus = 400,        ?\Throwable $previous = null    ) {        parent::__construct($message, 0, $previous);    }    public function errorCode(): string { return $this->errorCode; }    public function details(): array { return $this->details; }    public function httpStatus(): int { return $this->httpStatus; }}

Agora, especialize por categoria. A ideia é que o status code fique “embutido” no tipo, reduzindo repetição e evitando mapeamentos errados.

namespace App\Shared\Exception;class ValidationException extends AppException{    public function __construct(array $details, string $message = 'Dados inválidos.') {        parent::__construct($message, 'validation_error', $details, 422);    }}class NotFoundException extends AppException{    public function __construct(string $message = 'Recurso não encontrado.', array $details = []) {        parent::__construct($message, 'not_found', $details, 404);    }}class UnauthorizedException extends AppException{    public function __construct(string $message = 'Não autenticado.') {        parent::__construct($message, 'unauthorized', [], 401);    }}class ForbiddenException extends AppException{    public function __construct(string $message = 'Acesso negado.') {        parent::__construct($message, 'forbidden', [], 403);    }}class ConflictException extends AppException{    public function __construct(string $message = 'Conflito de estado.', array $details = []) {        parent::__construct($message, 'conflict', $details, 409);    }}class BadRequestException extends AppException{    public function __construct(string $message = 'Requisição inválida.', array $details = []) {        parent::__construct($message, 'bad_request', $details, 400);    }}

Quando usar cada status:

StatusQuando usarExemplo
400Payload malformado, parâmetros inválidos em nível de protocoloJSON inválido, query param fora do formato
401Não autenticadoToken ausente/expirado
403Autenticado, mas sem permissãoUsuário sem role necessária
404Recurso não encontradoID inexistente
409Conflito de estado/concorrênciaE-mail já cadastrado, versão divergente
422Entidade semanticamente inválidaValidação de campos/regra de negócio

Implementando um Error Handler global no Slim

No Slim 4, o tratamento centralizado é feito via ErrorMiddleware. Você registra um handler padrão que recebe qualquer Throwable e devolve uma resposta.

Passo a passo: registrar ErrorMiddleware e handler

Em um arquivo de bootstrap (por exemplo, onde você monta o container e cria o app), registre o middleware de erro e aponte para seu handler.

use Slim\Factory\AppFactory;use Slim\Middleware\ErrorMiddleware;use App\Shared\Http\AppErrorHandler;$app = AppFactory::create();$displayErrorDetails = false; // produção: false$logErrors = true;$logErrorDetails = true;$errorMiddleware = new ErrorMiddleware(    $app->getCallableResolver(),    $app->getResponseFactory(),    $displayErrorDetails,    $logErrors,    $logErrorDetails);$errorHandler = new AppErrorHandler(    $app->getResponseFactory(),    $displayErrorDetails);$errorMiddleware->setDefaultErrorHandler($errorHandler);$app->add($errorMiddleware);

Observação: a forma exata de obter dependências pode variar conforme seu container. O importante é: ErrorMiddleware + setDefaultErrorHandler.

Criando o AppErrorHandler (JSON consistente)

O handler deve: (1) gerar traceId; (2) decidir se o erro é esperado (ex.: AppException) ou inesperado; (3) montar payload padronizado; (4) logar com contexto; (5) retornar JSON com status correto.

namespace App\Shared\Http;use App\Shared\Exception\AppException;use Psr\Http\Message\ResponseFactoryInterface;use Psr\Http\Message\ResponseInterface;use Psr\Http\Message\ServerRequestInterface;use Slim\Exception\HttpNotFoundException;use Throwable;class AppErrorHandler{    public function __construct(        private readonly ResponseFactoryInterface $responseFactory,        private readonly bool $displayErrorDetails = false,        private readonly ?\Psr\Log\LoggerInterface $logger = null    ) {}    public function __invoke(        ServerRequestInterface $request,        Throwable $exception,        bool $displayErrorDetails,        bool $logErrors,        bool $logErrorDetails    ): ResponseInterface {        $traceId = $request->getHeaderLine('X-Request-Id') ?: bin2hex(random_bytes(16));        $mapped = $this->mapException($exception);        $status = $mapped['status'];        $payload = [            'error' => [                'code' => $mapped['code'],                'message' => $mapped['message'],                'details' => $mapped['details'],                'traceId' => $traceId,            ]        ];        if ($this->logger) {            $this->logger->error('request_error', [                'traceId' => $traceId,                'status' => $status,                'errorCode' => $mapped['code'],                'exceptionClass' => get_class($exception),                'exceptionMessage' => $exception->getMessage(),                'method' => $request->getMethod(),                'path' => (string)$request->getUri(),            ]);        }        $response = $this->responseFactory->createResponse($status);        $response->getBody()->write(json_encode($payload, JSON_UNESCAPED_UNICODE));        return $response->withHeader('Content-Type', 'application/json')                        ->withHeader('X-Trace-Id', $traceId);    }    private function mapException(Throwable $e): array {        if ($e instanceof AppException) {            return [                'status' => $e->httpStatus(),                'code' => $e->errorCode(),                'message' => $e->getMessage(),                'details' => $e->details(),            ];        }        if ($e instanceof HttpNotFoundException) {            return [                'status' => 404,                'code' => 'not_found',                'message' => 'Rota não encontrada.',                'details' => [],            ];        }        return [            'status' => 500,            'code' => 'internal_error',            'message' => 'Ocorreu um erro inesperado.',            'details' => $this->displayErrorDetails ? [                'exception' => get_class($e),                'reason' => $e->getMessage(),            ] : [],        ];    }}

Pontos importantes do handler

  • Mensagem segura em produção: para 500, use texto genérico. Detalhes só quando $displayErrorDetails estiver habilitado (ambiente de desenvolvimento).
  • Mapeamento explícito: exceções do projeto (AppException) já carregam status/code/details.
  • 404 de rota: o Slim pode lançar HttpNotFoundException quando não há rota correspondente; trate separadamente para manter consistência.
  • TraceId: devolva no header e no body para facilitar suporte.

Como lançar exceções corretamente (sem vazar detalhes)

O local onde você lança a exceção deve fornecer informação útil ao cliente, mas não expor detalhes internos (SQL, stack trace, nomes de tabelas, etc.).

Exemplo: conflito (409) ao tentar criar um recurso duplicado

use App\Shared\Exception\ConflictException;public function registerUser(array $input): void{    if ($this->users->emailExists($input['email'])) {        throw new ConflictException(            'E-mail já cadastrado.',            ['field' => 'email', 'issue' => 'already_exists']        );    }    // ... cria usuário}

Exemplo: não encontrado (404)

use App\Shared\Exception\NotFoundException;public function getOrder(string $id): array{    $order = $this->orders->findById($id);    if (!$order) {        throw new NotFoundException('Pedido não encontrado.', ['id' => $id]);    }    return $order;}

Exemplo: validação sem repetir camada de validação

Mesmo que exista validação de entrada, regras de domínio podem falhar (ex.: datas inconsistentes). Use ValidationException com details estruturado.

use App\Shared\Exception\ValidationException;public function schedule(array $data): void{    $errors = [];    if ($data['start'] >= $data['end']) {        $errors[] = ['field' => 'end', 'issue' => 'must_be_after_start'];    }    if ($errors) {        throw new ValidationException($errors);    }}

Logs estruturados: o que registrar

Para depuração e auditoria, registre logs em formato estruturado (contexto em array) para facilitar busca e correlação. Recomendações:

  • traceId e, se existir, userId (sem dados sensíveis).
  • status, errorCode, exceptionClass.
  • method, path, query (cuidado com dados sensíveis).
  • tempo de resposta (se você já mede em outro ponto do pipeline).

Evite logar: senha, token, dados completos de cartão, documentos completos. Se precisar, aplique mascaramento.

Exemplo: mascarando campos sensíveis antes de logar

function maskSensitive(array $data): array{    $sensitiveKeys = ['password', 'token', 'authorization'];    foreach ($sensitiveKeys as $key) {        if (array_key_exists($key, $data)) {            $data[$key] = '***';        }    }    return $data;}

Mapeamento de status codes: checklist rápido

  • 400 Bad Request: request inválida em nível de formato/protocolo (ex.: JSON quebrado).
  • 401 Unauthorized: falta autenticação válida.
  • 403 Forbidden: autenticado, mas sem permissão.
  • 404 Not Found: recurso/rota inexistente.
  • 409 Conflict: conflito de estado (unicidade, concorrência, transição inválida).
  • 422 Unprocessable Entity: dados semanticamente inválidos (regras e validações).
  • 500 Internal Server Error: inesperado; mensagem genérica ao cliente, detalhes no log.

Boas práticas para mensagens seguras

  • Não retorne mensagens de exceções técnicas diretamente (PDOException, mensagens do driver, stack traces).
  • Padronize code para permitir tratamento no cliente sem depender do texto.
  • Use details apenas com informações necessárias para correção (campo/issue), sem expor implementação.
  • Em desenvolvimento, habilite detalhes apenas via configuração de ambiente; em produção, mantenha desabilitado.

Agora responda o exercício sobre o conteúdo:

Ao implementar um handler global de erros no Slim para retornar JSON consistente, qual prática garante segurança em produção ao lidar com erros inesperados?

Você acertou! Parabéns, agora siga para a próxima página

Você errou! Tente novamente.

Erros inesperados devem retornar 500 com mensagem segura e genérica, evitando vazar detalhes internos. As informações técnicas ficam nos logs e só podem ser expostas se a configuração de ambiente permitir (ex.: desenvolvimento).

Próximo capitúlo

Injeção de dependências no Slim Framework: container, factories e acoplamento mínimo

Arrow Right Icon
Capa do Ebook gratuito Back-end com Slim Framework (PHP): Rotas, Middlewares e Arquitetura Limpa
44%

Back-end com Slim Framework (PHP): Rotas, Middlewares e Arquitetura Limpa

Novo curso

16 páginas

Baixe o app para ganhar Certificação grátis e ouvir os cursos em background, mesmo com a tela desligada.