From f322e19ef8f5b42486ad3e606539e50a7159cb34 Mon Sep 17 00:00:00 2001 From: 7eliassen Date: Sun, 29 Mar 2026 20:25:21 +0300 Subject: [PATCH 1/3] fix: add json parse error handler --- src/presentation/http/http-api.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/presentation/http/http-api.ts b/src/presentation/http/http-api.ts index 6e5cb37a..939e58fc 100644 --- a/src/presentation/http/http-api.ts +++ b/src/presentation/http/http-api.ts @@ -34,6 +34,7 @@ import UploadRouter from './router/upload.js'; import { ajvFilePlugin } from '@fastify/multipart'; import { UploadSchema } from './schema/Upload.js'; import { NoteHierarchySchema } from './schema/NoteHierarchy.js'; +import { StatusCodes } from 'http-status-codes'; const appServerLogger = getLogger('appServer'); @@ -372,6 +373,21 @@ export default class HttpApi implements Api { return; } + /** + * JSON parse errors (invalid request body) + */ + if (error instanceof SyntaxError && error.message.includes('JSON')) { + this.log.warn({ reqId: request.id }, 'Invalid JSON in request body'); + + return reply + .code(StatusCodes.BAD_REQUEST) + .type('application/json') + .send({ + message: 'Invalid JSON in request body', + error: 'Bad Request', + statusCode: StatusCodes.BAD_REQUEST, + }); + } /** * If error is not a domain error, we route it to the default error handler */ From dcce6d27a43a0d289412c69f205799dcce71b666 Mon Sep 17 00:00:00 2001 From: 7eliassen Date: Tue, 7 Apr 2026 15:59:45 +0300 Subject: [PATCH 2/3] chore: add FastifyError handling --- src/presentation/http/http-api.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/presentation/http/http-api.ts b/src/presentation/http/http-api.ts index 939e58fc..ffc41d8e 100644 --- a/src/presentation/http/http-api.ts +++ b/src/presentation/http/http-api.ts @@ -36,6 +36,10 @@ import { UploadSchema } from './schema/Upload.js'; import { NoteHierarchySchema } from './schema/NoteHierarchy.js'; import { StatusCodes } from 'http-status-codes'; +interface FastifyError extends Error { + code: string; +} + const appServerLogger = getLogger('appServer'); /** @@ -375,8 +379,10 @@ export default class HttpApi implements Api { } /** * JSON parse errors (invalid request body) + * Errors can be either SyntaxError or FastifyError. */ - if (error instanceof SyntaxError && error.message.includes('JSON')) { + if ((error instanceof SyntaxError && error.message.includes('JSON')) + || ((error as FastifyError).code?.startsWith('FST_ERR_CTP_') ?? false)) { this.log.warn({ reqId: request.id }, 'Invalid JSON in request body'); return reply @@ -384,8 +390,6 @@ export default class HttpApi implements Api { .type('application/json') .send({ message: 'Invalid JSON in request body', - error: 'Bad Request', - statusCode: StatusCodes.BAD_REQUEST, }); } /** From b29d04b16388ae8418027cc717afafdca1c1b601 Mon Sep 17 00:00:00 2001 From: 7eliassen Date: Tue, 7 Apr 2026 16:07:43 +0300 Subject: [PATCH 3/3] chore: add tests --- src/presentation/http/http-api.test.ts | 79 ++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 src/presentation/http/http-api.test.ts diff --git a/src/presentation/http/http-api.test.ts b/src/presentation/http/http-api.test.ts new file mode 100644 index 00000000..b3e82fb1 --- /dev/null +++ b/src/presentation/http/http-api.test.ts @@ -0,0 +1,79 @@ +import { describe, test, expect } from 'vitest'; + +describe('HTTP API Error Handler', () => { + describe('JSON Parse Error Handler', () => { + test('Returns 400 when request body contains not complete JSON', async () => { + const response = await global.api?.fakeRequest({ + method: 'POST', + url: '/join/test-hash1', + headers: { + // eslint-disable-next-line @typescript-eslint/naming-convention + 'Content-Type': 'application/json', + }, + body: '{invalid json', + }); + + expect(response?.statusCode).toBe(400); + + const body = await response?.json(); + + expect(body).toStrictEqual({ + message: 'Invalid JSON in request body', + }); + }); + + test('Returns 400 when request body contains malformed JSON', async () => { + const response = await global.api?.fakeRequest({ + method: 'POST', + url: '/join/test-hash1', + headers: { + // eslint-disable-next-line @typescript-eslint/naming-convention + 'Content-Type': 'application/json', + }, + body: '{"key": "value",}', + }); + + expect(response?.statusCode).toBe(400); + + const body = await response?.json(); + + expect(body).toStrictEqual({ + message: 'Invalid JSON in request body', + }); + }); + + test('Returns 400 when JSON body is empty', async () => { + const response = await global.api?.fakeRequest({ + method: 'POST', + url: '/join/test-hash1', + headers: { + // eslint-disable-next-line @typescript-eslint/naming-convention + 'Content-Type': 'application/json', + }, + body: '', + }); + + expect(response?.statusCode).toBe(400); + + const body = await response?.json(); + + expect(body).toStrictEqual({ + message: 'Invalid JSON in request body', + }); + }); + + test('Does not return 400 for valid JSON', async () => { + const response = await global.api?.fakeRequest({ + method: 'POST', + url: '/join/test-hash1', + headers: { + // eslint-disable-next-line @typescript-eslint/naming-convention + 'Content-Type': 'application/json', + }, + body: '{"key": "value"}', + }); + + expect(response?.statusCode).not.toBe(400); + }); + }); +});