From d30f7d6969a26f4809b6f3f024306a35fb866aa5 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Mon, 21 Jul 2025 12:58:51 +0200 Subject: [PATCH] Make 404 errors consistent with other reponses --- .changeset/fifty-pens-hammer.md | 5 ++ .changeset/sharp-bees-serve.md | 5 ++ .../src/routes/configure-fastify.ts | 4 +- .../service-core/src/routes/route-register.ts | 56 ++++++++++++++----- packages/service-errors/src/errors.ts | 9 ++- 5 files changed, 61 insertions(+), 18 deletions(-) create mode 100644 .changeset/fifty-pens-hammer.md create mode 100644 .changeset/sharp-bees-serve.md diff --git a/.changeset/fifty-pens-hammer.md b/.changeset/fifty-pens-hammer.md new file mode 100644 index 000000000..760b8ff69 --- /dev/null +++ b/.changeset/fifty-pens-hammer.md @@ -0,0 +1,5 @@ +--- +'@powersync/service-errors': patch +--- + +Report HTTP method in `RouteNotFound` error diff --git a/.changeset/sharp-bees-serve.md b/.changeset/sharp-bees-serve.md new file mode 100644 index 000000000..73c01828a --- /dev/null +++ b/.changeset/sharp-bees-serve.md @@ -0,0 +1,5 @@ +--- +'@powersync/service-core': patch +--- + +Make 404 error body consistent with other error responses. diff --git a/packages/service-core/src/routes/configure-fastify.ts b/packages/service-core/src/routes/configure-fastify.ts index 192010d7e..ba8906d15 100644 --- a/packages/service-core/src/routes/configure-fastify.ts +++ b/packages/service-core/src/routes/configure-fastify.ts @@ -1,7 +1,7 @@ import type fastify from 'fastify'; import * as uuid from 'uuid'; -import { registerFastifyRoutes } from './route-register.js'; +import { registerFastifyNotFoundHandler, registerFastifyRoutes } from './route-register.js'; import * as system from '../system/system-index.js'; @@ -76,6 +76,8 @@ export function configureFastifyServer(server: fastify.FastifyInstance, options: */ server.register(async function (childContext) { registerFastifyRoutes(childContext, generateContext, routes.api?.routes ?? DEFAULT_ROUTE_OPTIONS.api.routes); + registerFastifyNotFoundHandler(childContext); + // Limit the active concurrent requests childContext.addHook( 'onRequest', diff --git a/packages/service-core/src/routes/route-register.ts b/packages/service-core/src/routes/route-register.ts index f5d71c050..970e014cd 100644 --- a/packages/service-core/src/routes/route-register.ts +++ b/packages/service-core/src/routes/route-register.ts @@ -1,8 +1,17 @@ import type fastify from 'fastify'; import * as uuid from 'uuid'; -import { errors, HTTPMethod, logger, router } from '@powersync/lib-services-framework'; +import { + ErrorCode, + errors, + HTTPMethod, + logger, + RouteNotFound, + router, + ServiceError +} from '@powersync/lib-services-framework'; import { Context, ContextProvider, RequestEndpoint, RequestEndpointHandlerPayload } from './router.js'; +import { FastifyReply } from 'fastify'; export type FastifyEndpoint = RequestEndpoint & { parse?: boolean; @@ -69,23 +78,11 @@ export function registerFastifyRoutes( const serviceError = errors.asServiceError(ex); requestLogger.error(`Request failed`, serviceError); - response = new router.RouterResponse({ - status: serviceError.errorData.status || 500, - headers: { - 'Content-Type': 'application/json' - }, - data: { - error: serviceError.errorData - } - }); + response = serviceErrorToResponse(serviceError); } - Object.keys(response.headers).forEach((key) => { - reply.header(key, response.headers[key]); - }); - reply.status(response.status); try { - await reply.send(response.data); + await respond(reply, response); } finally { await response.afterSend?.({ clientClosed: request.socket.closed }); requestLogger.info(`${e.method} ${request.url}`, { @@ -106,3 +103,32 @@ export function registerFastifyRoutes( }); } } + +/** + * Registers a custom not-found handler to ensure 404 error responses have the same schema as other service errors. + */ +export function registerFastifyNotFoundHandler(app: fastify.FastifyInstance) { + app.setNotFoundHandler(async (request, reply) => { + await respond(reply, serviceErrorToResponse(new RouteNotFound(request.originalUrl, request.method))); + }); +} + +function serviceErrorToResponse(error: ServiceError): router.RouterResponse { + return new router.RouterResponse({ + status: error.errorData.status || 500, + headers: { + 'Content-Type': 'application/json' + }, + data: { + error: error.errorData + } + }); +} + +async function respond(reply: FastifyReply, response: router.RouterResponse) { + Object.keys(response.headers).forEach((key) => { + reply.header(key, response.headers[key]); + }); + reply.status(response.status); + await reply.send(response.data); +} diff --git a/packages/service-errors/src/errors.ts b/packages/service-errors/src/errors.ts index fdafcfca9..393b35eff 100644 --- a/packages/service-errors/src/errors.ts +++ b/packages/service-errors/src/errors.ts @@ -218,12 +218,17 @@ export class InternalServerError extends ServiceError { export class RouteNotFound extends ServiceError { static readonly CODE = ErrorCode.PSYNC_S2002; - constructor(path: string) { + constructor(path: string, method?: string) { + let pathDescription = JSON.stringify(path); + if (method != null) { + pathDescription = `${method} ${pathDescription}`; + } + super({ code: RouteNotFound.CODE, status: 404, description: 'The path does not exist on this server', - details: `The path ${JSON.stringify(path)} does not exist on this server`, + details: `The path ${pathDescription} does not exist on this server`, severity: ErrorSeverity.INFO }); }