> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://cla-demo.postmansolutions.com/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://cla-demo.postmansolutions.com/_mcp/server.

# [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**](https://dev.azure.com/CLADevOps/Development/_git/poc-autoextract-platform-file-intake/pullrequestcreate?sourceRef=js_errorHandlerMiddleware_590761\&targetRef=main\&sourceRepositoryId=ebcd9b49-a69b-4a5c-8584-298d0b3c06c4\&targetRepositoryId=ebcd9b49-a69b-4a5c-8584-298d0b3c06c4)

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`:

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

Example response:

```json
{
  "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`:

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

Example response:

```json
{
  "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.

```ts
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.

```ts
import { registerGlobalErrorHandler } from './middleware/errorHandler.js';

registerGlobalErrorHandler(frameworkApp.server);
```

---

### 3. `src/routes/index.ts`

Adds two test routes for demonstrating 400 and 500 behavior.

```ts
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.

```ts
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.

```ts
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.

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

---

## Curl Examples

```bash
# 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](/_fern-img/2598bdd442e429b7b1279ff2fa793b1e1e849ab9c288eb2c2c38ef2574b94bc6.webp)

```bash
# 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](/_fern-files/cla-autoextract.docs.buildwithfern.com/158cc5ddde7ffad2d70fcd087276a867a272bdef72928101e70d132024612b91/assets/attachments/image-940cb1ac-a6b1-412e-bf73-e77125a04ad8.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

---