/**
* Problem Details for HTTP APIs (RFC 9457)
*
* Provides standardized error response format for better debugging
* and client error handling.
*
* @see https://www.rfc-editor.org/rfc/rfc9457
*/
import { ErrorCode } from '../types/index.js';
/**
* Problem Details response structure per RFC 9457
*/
export interface ProblemDetails {
/** URI reference identifying the problem type */
type: string;
/** Short, human-readable summary */
title: string;
/** HTTP status code */
status: number;
/** Human-readable explanation specific to this occurrence */
detail?: string;
/** URI reference identifying the specific occurrence */
instance?: string;
/** Additional properties */
[key: string]: unknown;
}
/**
* Problem type registry
*/
const PROBLEM_TYPES: Record<ErrorCode, { type: string; title: string; status: number }> = {
NOT_FOUND: {
type: 'https://problems.example.com/not-found',
title: 'Resource Not Found',
status: 404,
},
VALIDATION_ERROR: {
type: 'https://problems.example.com/validation-error',
title: 'Validation Error',
status: 400,
},
RATE_LIMITED: {
type: 'https://problems.example.com/rate-limited',
title: 'Rate Limit Exceeded',
status: 429,
},
UNAUTHORIZED: {
type: 'https://problems.example.com/unauthorized',
title: 'Unauthorized',
status: 401,
},
FORBIDDEN: {
type: 'https://problems.example.com/forbidden',
title: 'Forbidden',
status: 403,
},
INTERNAL_ERROR: {
type: 'https://problems.example.com/internal-error',
title: 'Internal Server Error',
status: 500,
},
TIMEOUT: {
type: 'https://problems.example.com/timeout',
title: 'Request Timeout',
status: 504,
},
CACHE_ERROR: {
type: 'https://problems.example.com/cache-error',
title: 'Cache Error',
status: 500,
},
EXTERNAL_API_ERROR: {
type: 'https://problems.example.com/external-api-error',
title: 'External API Error',
status: 502,
},
};
/**
* Create a Problem Details response from an error code
*/
export function createProblemDetails(
code: ErrorCode,
detail?: string,
extensions?: Record<string, unknown>
): ProblemDetails {
const base = PROBLEM_TYPES[code] ?? PROBLEM_TYPES.INTERNAL_ERROR;
return {
type: base.type,
title: base.title,
status: base.status,
detail,
...extensions,
};
}
/**
* Create a validation error with field-level details
*/
export function createValidationProblem(
errors: Array<{ field: string; message: string; code?: string }>
): ProblemDetails {
return {
type: 'https://problems.example.com/validation-error',
title: 'Validation Error',
status: 400,
detail: 'One or more fields failed validation',
errors,
};
}
/**
* Create a rate limit error with retry information
*/
export function createRateLimitProblem(retryAfterSeconds: number): ProblemDetails {
return {
type: 'https://problems.example.com/rate-limited',
title: 'Rate Limit Exceeded',
status: 429,
detail: `Rate limit exceeded. Retry after ${retryAfterSeconds} seconds.`,
retryAfter: retryAfterSeconds,
};
}
/**
* Create an authentication error
*/
export function createAuthProblem(detail?: string): ProblemDetails {
return {
type: 'https://problems.example.com/unauthorized',
title: 'Unauthorized',
status: 401,
detail: detail ?? 'Authentication required',
};
}
/**
* Create a not found error
*/
export function createNotFoundProblem(resource: string, identifier?: string): ProblemDetails {
return {
type: 'https://problems.example.com/not-found',
title: 'Resource Not Found',
status: 404,
detail: identifier ? `${resource} not found: ${identifier}` : `${resource} not found`,
resource,
identifier,
};
}
/**
* Create an external API error
*/
export function createExternalApiProblem(
service: string,
detail: string,
upstreamStatus?: number
): ProblemDetails {
return {
type: 'https://problems.example.com/external-api-error',
title: 'External API Error',
status: 502,
detail: `${service}: ${detail}`,
service,
upstreamStatus,
};
}
/**
* Convert Problem Details to MCP error format
*/
export function problemToMcpError(problem: ProblemDetails): {
error: string;
code: string;
details?: Record<string, unknown>;
} {
const { type, title, status, detail, ...rest } = problem;
return {
error: detail ?? title,
code: type.split('/').pop()?.toUpperCase().replace(/-/g, '_') ?? 'INTERNAL_ERROR',
details: Object.keys(rest).length > 0 ? rest : undefined,
};
}
/**
* Check if an object is a Problem Details response
*/
export function isProblemDetails(obj: unknown): obj is ProblemDetails {
if (typeof obj !== 'object' || obj === null) {
return false;
}
const problem = obj as Record<string, unknown>;
return (
typeof problem['type'] === 'string' &&
typeof problem['title'] === 'string' &&
typeof problem['status'] === 'number'
);
}
/**
* Express-compatible error handler that returns Problem Details
*/
export function problemDetailsMiddleware() {
return (
err: Error & { code?: ErrorCode; status?: number },
_req: unknown,
res: { status: (code: number) => { json: (body: unknown) => void }; set: (header: string, value: string) => void },
_next: unknown
): void => {
const code = err.code ?? 'INTERNAL_ERROR';
const problem = createProblemDetails(code as ErrorCode, err.message);
res.set('Content-Type', 'application/problem+json');
res.status(problem.status).json(problem);
};
}