Explicación de RFC 9457: Estándarización de las respuestas de error de la API HTTP
RFC 9457 Problem Details proporciona a las API HTTP un formato de error estándar. Qué significa cada campo y cómo implementarlo correctamente en NestJS con un filtro global.
Casi todas las API terminan generando un payload de error que se parece más o menos a esto:
{
"message": "Something went wrong",
"error": "Bad Request",
"statusCode": 400
}
El problema es que esta estructura está casi siempre vinculada al framework que la generó. NestJS, Express, Spring, Rails y numerosos otros stacks producen cada uno su propio formato de objeto de error, con nombres de campos y estructuras diferentes.
La RFC 9457, titulada Problem Details for HTTP APIs, aborda este problema proponiendo una estructura estandarizada para los cuerpos de error de las API HTTP.
La especificación fue publicada en julio de 2023 y sustituye a la anterior RFC 7807. Su idea central es sencilla: en lugar de que cada API diseñe su propio formato de error personalizado, todos pueden confiar en una estructura compartida y legible por máquinas.
El problema que resuelve
Un código de estado HTTP le indica una categoría general de lo que ocurrió:
400 Bad Request
401 Unauthorized
403 Forbidden
404 Not Found
409 Conflict
422 Unprocessable Content
500 Internal Server Error
Pero saber que se obtuvo un 400 no explica la causa real del fallo.
Tomemos esta respuesta, por ejemplo:
HTTP/1.1 409 Conflict
Esa única línea de estado podría representar un nombre de usuario ya ocupado, un recurso que ya existe, una incompatibilidad de versiones o cualquier otro tipo de conflicto.
RFC 9457 resuelve esto al incluir detalles estructurados y descriptivos dentro del propio cuerpo de la respuesta, de modo que el cliente pueda determinar qué salió mal sin necesidad de analizar la estructura específica de errores de un framework.
El objeto Details del problema
Una respuesta que sigue esta especificación es un documento JSON servido con este tipo de medio:
Content-Type: application/problem+json
La especificación define cinco miembros estándar:
{
"type": "https://example.com/problems/username-taken",
"title": "Username already exists",
"status": 409,
"detail": "The username 'ali' is already registered.",
"instance": "/users"
}
Ninguno de estos cinco campos es obligatorio, pero cada uno cumple una función distinta.
type
Este campo identifica la categoría del problema que se está reportando.
{
"type": "https://example.com/problems/username-taken"
}
Piénselo como la etiqueta legible por máquinas para el error. Idealmente, el URI en type apunta a documentación real que explica el problema y cómo debería reaccionar un cliente ante él.
Cuando se omite type, se trata como si estuviera establecido en:
about:blank
Se espera que los clientes utilicen este URI de type como identificador canónico, en lugar de intentar interpretar el significado del texto de detail.
title
Se trata de una etiqueta breve y fácil de entender que describe la clase de problema.
{
"title": "Username already exists"
}
Dado que title describe el tipo general de problema y no una instancia específica, se espera que mantenga su consistencia en las diferentes apariciones del mismo error.
status
Esto refleja el código de estado HTTP asociado al error.
{
"status": 409
}
Una sutileza que cabe destacar: este campo es solo informativo.
El estado oficial real es el que indica la propia respuesta HTTP:
HTTP/1.1 409 Conflict
Cuando el servidor genera el cuerpo de la respuesta, el valor status debe coincidir con ese código de respuesta real.
detail
{
"detail": "The username 'ali' is already registered."
}
No dependa de detail para ninguna lógica automatizada.
if (error.detail.includes("already registered")) {
// ...
}
Para todo lo que necesite ser verificado de forma programática, confíe en type o en un miembro de extensión personalizado en su lugar.
instance
Este campo apunta a la ocurrencia específica del problema que se está reportando.
{
"instance": "/users"
}
Es útil cuando es necesario rastrear un fallo, brindar soporte a un usuario, correlacionar registros de log o investigar una solicitud concreta.
Sirve un propósito diferente al de type:
type -> what kind of problem is this?
instance -> which occurrence of that problem is this?
Extensiones
La especificación deja intencionalmente espacio para campos adicionales más allá del conjunto básico.
Por ejemplo, una respuesta por fallo de validación podría verse así:
{
"type": "https://example.com/problems/validation-error",
"title": "Validation failed",
"status": 422,
"detail": "One or more fields are invalid.",
"errors": [
{
"field": "email",
"message": "Invalid email address"
},
{
"field": "password",
"message": "Password is too short"
}
]
}
Aquí, errors no forma parte de la propia especificación RFC 9457; es un miembro de extensión que su API define por sí misma.
Esta flexibilidad es una de las mayores fortalezas de la especificación: se obtiene un formato compartido y predecible, al tiempo que se puede añadir cualquier detalle específico del dominio que necesite la aplicación.
Cabeceras HTTP
Las respuestas que sigan este formato deben establecer:
Content-Type: application/problem+json
Los clientes pueden indicar su soporte para este formato utilizando:
Accept: application/problem+json
Los campos legibles por humanos como title y detail también pueden negociarse a través de los encabezados estándar del lenguaje HTTP:
Accept-Language: en
con el servidor respondiendo algo similar a:
Content-Language: en
Cuando un cliente debe esperar antes de intentarlo nuevamente, se puede incluir el encabezado estándar Retry-After:
Retry-After: 30
Como ejemplo:
HTTP/1.1 429 Too Many Requests
Content-Type: application/problem+json
Retry-After: 30
Con un cuerpo como este:
{
"type": "https://example.com/problems/rate-limit",
"title": "Too many requests",
"status": 429,
"detail": "Rate limit exceeded."
}
RFC 9457 no inventa un nuevo mecanismo de reintentos; simplemente reutiliza el encabezado HTTP Retry-After cuando es apropiado para el tipo de problema dado.
Qué no define RFC 9457
La especificación no aborda:
- códigos de error a nivel de aplicación
Su alcance se limita a proporcionarte una estructura estándar para describir problemas a nivel HTTP.
Eso significa que una respuesta como esta cumple plenamente con los requisitos:
{
"type": "https://api.example.com/problems/validation-error",
"title": "Validation failed",
"status": 422,
"detail": "Request validation failed.",
"code": "VALIDATION_ERROR",
"errors": [
{
"field": "email",
"message": "Invalid email address"
}
]
}
En este ejemplo, code y errors son extensiones específicas de esta API, no forman parte de la especificación base.
Reglas prácticas para desarrolladores
Nunca incluyas trazas de pila ni mensajes de excepción sin procesar en las respuestas para producción.
Evita esto:
{
"detail": "QueryFailedError: duplicate key value violates unique constraint..."
}
Preferir algo como:
{
"type": "https://api.example.com/problems/conflict",
"title": "Resource conflict",
"status": 409,
"detail": "The username is already in use."
}
También evita que los clientes dependan del análisis de la cadena detail.
No hagas esto:
if (error.detail === "User already exists") {
}
En su lugar, verifique el campo type:
if (error.type === "https://api.example.com/problems/user-exists") {
}
Trate type como un identificador estable.
Un determinado type debe representar siempre la misma categoría de problema, aunque detail variará naturalmente entre cada ocurrencia individual.
Resista la tentación de inventar una nueva estructura de error para cada controlador o punto final. Elija una capa consistente de Detalles del Problema en toda su API, y úntela con campos personalizados solo cuando sea realmente necesario.
Por último, no confunda una respuesta de Detalles del Problema con un payload normal de éxito. Este formato existe específicamente para describir fallos, y pertenece a los códigos de estado 4xx y 5xx.
Implementación de RFC 9457 en NestJS
Una forma ordenada de manejar esto en NestJS es centralizar la lógica dentro de un filtro global de excepciones.
import {
ArgumentsHost,
Catch,
ExceptionFilter,
HttpException,
HttpStatus,
} from '@nestjs/common';
import type { Request, Response } from 'express';
interface ProblemDetails {
type?: string;
title?: string;
status?: number;
detail?: string;
instance?: string;
[key: string]: unknown;
}
@Catch()
export class ProblemDetailsExceptionFilter implements ExceptionFilter {
catch(exception: unknown, host: ArgumentsHost): void {
const ctx = host.switchToHttp();
const request = ctx.getRequest<Request>();
const response = ctx.getResponse<Response>();
let status = HttpStatus.INTERNAL_SERVER_ERROR;
let detail = 'An unexpected error occurred.';
let extensions: Record<string, unknown> = {};
if (exception instanceof HttpException) {
status = exception.getStatus();
const errorResponse = exception.getResponse();
if (typeof errorResponse === 'string') {
detail = errorResponse;
} else if (typeof errorResponse === 'object' && errorResponse !== null) {
const body = errorResponse as Record<string, unknown>;
if (typeof body.message === 'string') {
detail = body.message;
}
const {
statusCode: _statusCode,
error: _error,
message: _message,
...rest
} = body;
extensions = rest;
}
}
const problem: ProblemDetails = {
type: 'about:blank',
title: HttpStatus[status] ?? 'HTTP Error',
status,
detail,
instance: request.originalUrl,
...extensions,
};
response
.status(status)
.type('application/problem+json')
.json(problem);
}
}
Luego, intégralo en toda la aplicación:
app.useGlobalFilters(
new ProblemDetailsExceptionFilter(),
);
En lugar de devolver la estructura de error predeterminada de NestJS, como:
{
"statusCode": 404,
"message": "User not found",
"error": "Not Found"
}
sus puntos finales emitirán de manera consistente:
HTTP/1.1 404 Not Found
Content-Type: application/problem+json
{
"type": "about:blank",
"title": "Not Found",
"status": 404,
"detail": "User not found",
"instance": "/users/42"
}
A partir de ahí, puede definir sus propios URI de type estables para casos específicos de fallo:
https://api.example.com/problems/user-not-found
https://api.example.com/problems/validation-error
https://api.example.com/problems/username-taken
https://api.example.com/problems/rate-limit
Esta configuración le brinda a su API un contrato de error predecible y uniforme, mientras que las excepciones integradas de NestJS y sus errores personalizados de la aplicación siguen funcionando en el fondo.
Lectura relacionada
- Por qué NestJS gana para equipos y codigos de backend en crecimiento — Explore cómo la estructura de NestJS, la inyección de dependencias y su diseño basado en TypeScript ayudan a los equipos de ingeniería a escalar sin caer en el caos.