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.
- Ouça o áudio com a tela desligada
- Ganhe Certificado após a conclusão
- + de 5000 cursos para você explorar!
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:
| Status | Quando usar | Exemplo |
|---|---|---|
| 400 | Payload malformado, parâmetros inválidos em nível de protocolo | JSON inválido, query param fora do formato |
| 401 | Não autenticado | Token ausente/expirado |
| 403 | Autenticado, mas sem permissão | Usuário sem role necessária |
| 404 | Recurso não encontrado | ID inexistente |
| 409 | Conflito de estado/concorrência | E-mail já cadastrado, versão divergente |
| 422 | Entidade semanticamente inválida | Validaçã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$displayErrorDetailsestiver 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
HttpNotFoundExceptionquando 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
codepara permitir tratamento no cliente sem depender do texto. - Use
detailsapenas 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.