Tratamento de erros
Três perguntas que todo pipeline acaba fazendo, e que esta página responde na ordem: o que acontece quando um step falha, como isso vira a resposta HTTP, e como evitar devolver 500 quando a falha era esperada.
O caminho de uma falha
Um step que falha não lança exceção: ele devolve uma falha estruturada com quatro campos.
| Campo | O que é |
|---|---|
code |
O identificador da falha — db_persist_failed, http_call_failed, ou o que você declarou num fail |
message |
Texto legível |
status |
O status HTTP que a falha sugere, quando ela tem um |
details |
Dado estruturado extra, quando o step tem algo a anexar |
A partir daí, a falha interrompe o pipeline e a camada HTTP a converte numa resposta
RFC 9457 ProblemDetails, consultando a tabela
errors: da rota pelo code:
{
"type": "https://pipevine.dev/errors/CUSTOMER_NOT_FOUND",
"title": "Cliente não encontrado",
"detail": "cliente não encontrado",
"status": 404,
"code": "CUSTOMER_NOT_FOUND"
}
title vem da entrada em errors: quando existe; senão, da message da falha. details, quando a
falha tem, aparece como um campo extra de mesmo nome.
Todo code que uma execução pode produzir está no
catálogo de códigos de erro —
inclusive os que nenhum fail seu gerou.
A tabela errors: da rota
- id: createOrder
method: POST
path: /orders
pipeline:
- { type: db.find, id: customer, entity: Customer, where: { id: "input.body.customerId" } }
- { type: fail, when: "steps.customer.result = null", status: 404, code: CUSTOMER_NOT_FOUND }
- { type: http.call, id: price, integration: pricing, method: POST, path: /quote }
errors:
- { code: CUSTOMER_NOT_FOUND, status: 404, title: "Cliente não encontrado" }
- { code: http_call_failed, status: 503, title: "Serviço de preços indisponível" }
A segunda entrada é a resposta direta para "como faço para não devolver 500": a falha do
http.call passa a sair como 503 com um título próprio, em vez do status cru do upstream.
status:é obrigatório na práticaUma entrada de
errors:que casa sempre impõe o própriostatus:— e omiti-lo não significa "herda o status da falha", significa500.# ARMADILHA: responde 500, não 404 - { type: fail, when: "...", status: 404, code: CUSTOMER_NOT_FOUND } errors: - { code: CUSTOMER_NOT_FOUND, title: "Cliente não encontrado" } # sem status:O
404dofailé descartado. Declarestatus:em toda entrada deerrors:, ou não declare a entrada — sem entrada nenhuma, o status da própria falha é respeitado.
O que não passa pela tabela errors:
Três respostas de erro são produzidas antes de o pipeline começar, e por isso nenhuma entrada de
errors: as alcança:
| Código | Quando | Status |
|---|---|---|
binding_failed |
Header ou parâmetro de query obrigatório ausente, corpo JSON malformado | 400 |
unauthorized |
Token ausente, expirado, ou com issuer/audience/assinatura inválidos | 401 |
forbidden |
Autenticado, mas sem o escopo/papel/claim exigido, ou authorize.expr falso |
403 |
Se você precisa customizar essas respostas, o lugar é a configuração de auth e de request, não a
tabela errors:.
Os três mecanismos, e quando usar cada um
| Mecanismo | Onde age | Use quando |
|---|---|---|
resilience.fallback |
Na integração, antes de o step falhar | A dependência está fora do ar e você tem um valor aceitável para seguir em frente |
errors: |
Depois da falha, ao montar a resposta | A falha deve mesmo interromper, mas com status/título melhores que o default |
try/catch |
Dentro do pipeline | O pipeline deve continuar apesar da falha, fazendo outra coisa |
Eles compõem, e a ordem importa: o fallback age primeiro (o step nem chega a falhar), o
try/catch intercepta a falha antes que ela suba, e a tabela errors: só entra em cena se a
falha chegou ao fim do pipeline.
try/catch: seguir apesar da falha
- type: try
try:
- { type: http.call, id: price, integration: pricing, method: POST, path: /quote }
catch:
- { type: log, message: "consulta de preço falhou, usando preço padrão" }
- { type: map, to: vars.price, expr: "0" }
Dentro do catch, a falha fica disponível em error — mas com duas formas diferentes, e essa
diferença já causou surpresa:
| Origem | Forma de error |
|---|---|
| Falha de um step | { code, message, status, details } |
Exceção não tratada dentro do try |
{ code: "exception", message, type } — sem status nem details |
Ou seja: error.status existe no primeiro caso e some no segundo, sem aviso. Se o seu catch
lê error.status, trate a ausência.
Três detalhes do try que não são óbvios:
trysemcatchnão engole nada: a falha continua subindo. O bloco só serve, nesse caso, para pendurar umfinally.- Uma exceção só é capturada se houver
catch. Sem ele, ela sobe como exceção — e vira o 500 opaco da seção seguinte. finallysobrepõe o resultado: se o blocofinallyterminar com qualquer coisa que não seja "continuar" (umfail, umreturn), é esse o resultado que vale, descartando o que otryou ocatchhaviam produzido. Ele também roda sem cancelamento — um cliente que desistiu não interrompe ofinally.
Exceção não tratada: o 500 opaco
Uma exceção que ninguém capturou não é um erro de aplicação esperado — é um bug. A resposta é deliberadamente vazia de detalhes:
{
"type": "https://pipevine.dev/errors/internal-error",
"title": "An unexpected error occurred.",
"status": 500
}
Nem a mensagem da exceção nem o stack trace vazam para o cliente. Para diagnosticar, o caminho é a telemetria — veja Observabilidade.
Exemplo trabalhado: http.call
O http.call é o step onde as três perguntas do começo aparecem juntas, porque o resultado dele
não fica no mesmo lugar em todos os casos:
| Situação | Onde o valor fica | Falha |
|---|---|---|
2xx |
steps.<id>.result, só o corpo (sem status, sem headers) — e só se houver id: |
— |
4xx |
Em lugar nenhum. O corpo vai para details da falha |
http_call_failed, status do upstream |
5xx com fallback.expr |
steps.<id>.result recebe o valor da expressão |
— |
5xx com fallback em sub-pipeline |
Nada em steps.<id>.result — os steps de dentro gravam o próprio |
— |
5xx sem fallback |
— | http_call_failed, status do upstream |
| Timeout / circuito aberto | — | http_call_failed, 502 |
Juntando tudo, para que uma indisponibilidade do serviço de preços não derrube a requisição:
integrations:
- id: pricing
kind: http
baseUrl: "${config:Pricing:Url}"
resilience:
timeout: { perAttempt: 2s }
retry: { attempts: 3, on: [502, 503, 504] }
fallback: { expr: "{ 'price': 0, 'degraded': true }" } # 1: nem chega a falhar
routes:
- id: quote
method: POST
path: /quote
pipeline:
- { type: http.call, id: price, integration: pricing, method: POST, path: /quote }
- { type: map, to: vars.total, expr: "steps.price.result.price" }
errors:
- { code: http_call_failed, status: 503, title: "Serviço de preços indisponível" } # 2: se ainda assim falhar
Com o fallback, um 5xx vira { price: 0, degraded: true } publicado em steps.price.result, e o
pipeline segue como se tivesse dado certo — repare que vars.total funciona igual nos dois
caminhos. A entrada em errors: é a rede de segurança para o que o fallback não cobre: um 4xx,
que nunca aciona fallback.
Para onde ir daqui
- Catálogo de códigos de erro — todo
codeque um pipeline pode produzir, com o status que cada um carrega. - Controle de fluxo —
try/catch/finallyno contexto dos demais blocos. - Integrações outbound — a configuração completa de
resilience. - Blob storage e Bancos NoSQL — os dois últimos capítulos.