Skip to navigation

[Middleware]-Standardized Error Handling for API Responses

This page documents the implementation of a centralized error handler that returns consistent application/problem+json responses for schema validation failures (400) and internal server errors (500) in the AutoExtract Platform.

Implementation is available in branch
js_errorHandlerMiddleware_590761

Related user story: #590761


Summary

Fastify’s built-in validator throws AJV-based errors when incoming requests fail schema validation. By default, these errors do not conform to CLA’s problem+json format. This implementation introduces a global error handler to:

  • Intercept validation errors for request body, query, and params
  • Return a structured 400 response in the application/problem+json format
  • Include a canonical type, title, status, detail, validationErrors, and instance
  • Return consistent 500 responses for internal server exceptions

The handler is scoped to schema-validated routes using POST, PUT, or PATCH and is safe to apply across the platform.


Key Behavior

400 - Invalid JSON Body

Triggered when a request fails schema validation.

Response includes:

  • type: https://claconnect.com/platform/autoextract/problems/invalid-json-body
  • title: Invalid JSON body
  • status: 400
  • detail: the validation message
  • validationErrors: raw AJV error array
  • instance: /platform/autoextract/<fileId> if available, otherwise /platform/autoextract

Example curl:

curl -X 'POST' http://localhost:3000/test \
-H 'Content-Type: application/json' \
-d '{ "nasdfame": "string", "age": 0 }'; echo

Example response:

{
"type": "https://claconnect.com/platform/autoextract/problems/invalid-json-body",
"title": "Invalid JSON body",
"status": 400,
"detail": "body must have required property 'name'",
"instance": "/platform/autoextract",
"validationErrors": [...]
}

500 - Internal Server Error

Returned when a controller throws an unhandled error.

Response includes:

  • type: https://claconnect.com/platform/autoextract/problems/internal-server-error
  • title: Internal Server Error
  • status: 500
  • detail: error message
  • instance: derived from fileId in request body or params

Example curl:

curl -X 'PUT' http://localhost:3000/test-500 \
-H 'Content-Type: application/json' \
-d '{ "fileId": "abc123", "name": "string", "age": 0 }'; echo

Example response:

{
"type": "https://claconnect.com/platform/autoextract/problems/internal-server-error",
"title": "Internal Server Error",
"status": 500,
"detail": "This is a simulated internal server error",
"instance": "/platform/autoextract/abc123"
}

Code Additions

1. src/middleware/errorHandler.ts

Centralized error handler registered with Fastify. Detects schema validation failures (AJV) and runtime exceptions and transforms them into problem+json responses.

import { FastifyError, FastifyReply, FastifyRequest } from 'fastify';
import { FastifyInstance } from 'fastify/types/instance';
function getInstanceUrl(request: FastifyRequest): string {
const doesParamsHaveFileId =
typeof request.params === 'object' && request.params && 'fileId' in request.params;
const doesBodyHaveFileId =
typeof request.body === 'object' && request.body && Object.prototype.hasOwnProperty.call(request.body, 'fileId');
const fileId = doesParamsHaveFileId
? (request.params as { fileId?: string }).fileId
: doesBodyHaveFileId
? (request.body as { fileId?: string }).fileId
: undefined;
return fileId
? `/platform/autoextract/${fileId}`
: '/platform/autoextract';
}
export function registerGlobalErrorHandler(frameworkServer: FastifyInstance) {
frameworkServer.setErrorHandler((error: FastifyError, request: FastifyRequest, reply: FastifyReply) => {
const isValidationError = error.validation && error.statusCode === 400;
const isInternalServerError = error.statusCode === 500 || error instanceof Error;
const instance = getInstanceUrl(request);
if (isValidationError) {
return reply
.status(400)
.type('application/problem+json')
.send({
type: 'https://claconnect.com/platform/autoextract/problems/invalid-json-body',
title: 'Invalid JSON body',
status: 400,
detail: error.message,
instance,
validationErrors: error.validation,
});
} else if (isInternalServerError) {
return reply
.status(500)
.type('application/problem+json')
.send({
type: 'https://claconnect.com/platform/autoextract/problems/internal-server-error',
title: 'Internal Server Error',
status: 500,
detail: error.message || 'An unexpected error occurred',
instance,
});
}
reply.send(error);
});
}

2. src/index.ts

Registers the error handler early in the server lifecycle.

import { registerGlobalErrorHandler } from './middleware/errorHandler.js';
registerGlobalErrorHandler(frameworkApp.server);

3. src/routes/index.ts

Adds two test routes for demonstrating 400 and 500 behavior.

import { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify';
import { TestController } from '../controllers/testController.js';
import { testPostSchema, testPutSchema } from './schemas.js';
import type { TestPutBody } from '../types/middleware.js';
export const routes = async (frameworkServer: FastifyInstance) => {
frameworkServer.route({
method: 'POST',
url: '/test',
schema: testPostSchema,
handler: async (request: FastifyRequest, reply: FastifyReply) => {
const body = request.body as { name: string; age: number };
const testController = new TestController();
const result = await testController.handlePost(body);
reply.send(result);
},
});
frameworkServer.route({
method: 'PUT',
url: '/test-500',
schema: testPutSchema,
handler: async (request: FastifyRequest, reply: FastifyReply) => {
const body = request.body as TestPutBody;
const testController = new TestController();
const result = await testController.handlePut(body);
reply.send(result);
},
});
};

4. src/controllers/testController.ts

Controller with two methods: one for successful input, one that throws a 500.

export class TestController {
async handlePost(body: { name: string; age: number }) {
return {
message: `Received name: ${body.name}, age: ${body.age}`,
};
}
async handlePut(_: { fileId: string; name: string; age: number }) {
throw new Error('This is a simulated internal server error');
}
}

5. src/routes/schemas.ts

Schema definitions for the two test routes.

export const testPostSchema = {
tags: ['test'],
body: {
type: 'object',
required: ['name', 'age'],
properties: {
name: { type: 'string' },
age: { type: 'number' },
},
},
response: {
200: {
type: 'object',
properties: {
message: { type: 'string' },
},
},
},
};
export const testPutSchema = {
tags: ['test'],
body: {
type: 'object',
required: ['fileId', 'name', 'age'],
properties: {
fileId: { type: 'string' },
name: { type: 'string' },
age: { type: 'number' },
},
},
response: {
200: {
type: 'object',
properties: {
message: { type: 'string' },
},
},
},
};

6. src/types/middleware.ts

Shared type for use in the PUT test route.

export type TestPutBody = {
fileId: string;
name: string;
age: number;
};

Curl Examples

# Triggers 400 due to malformed "nasdfame" param.
curl -X 'POST' http://localhost:3000/test \
-H 'Content-Type: application/json' \
-d '{ "nasdfame": "string", "age": 0 }'; echo

image.png

# Triggers 500 due to hard-coded throw
curl -X 'PUT' http://localhost:3000/test-500 \
-H 'Content-Type: application/json' \
-d '{ "fileId": "abc123", "name": "string", "age": 0 }'; echo

image.png

  • This implementation is non-invasive and does not interfere with other status codes like 401 or 403
  • The instance property logic can be expanded later to include other request identifiers
  • All changes are encapsulated and ready for future adoption across more routes or services