From 77fccdb28678b4d6df235d414340775340e432eb Mon Sep 17 00:00:00 2001 From: unnoq Date: Thu, 13 Feb 2025 15:53:30 +0700 Subject: [PATCH] feat(server): enhance error handling for malformed requests --- .../src/adapters/standard/handler.test.ts | 67 +++++++++++++++++++ .../server/src/adapters/standard/handler.ts | 13 +++- 2 files changed, 78 insertions(+), 2 deletions(-) diff --git a/packages/server/src/adapters/standard/handler.test.ts b/packages/server/src/adapters/standard/handler.test.ts index 91cf59520..aedbc21a9 100644 --- a/packages/server/src/adapters/standard/handler.test.ts +++ b/packages/server/src/adapters/standard/handler.test.ts @@ -213,6 +213,73 @@ describe('standardHandler', () => { }) }) + it('on decode error', async () => { + matcher.match.mockResolvedValue({ + path: ['ping'], + procedure: ping, + params: { id: '__id__' }, + }) + + const error = new Error('Something bad') + codec.decode.mockRejectedValueOnce(error) + const client = vi.fn() + vi.mocked(createProcedureClient).mockReturnValueOnce(client) + + codec.decode.mockReturnValueOnce('__input__') + + codec.encodeError.mockReturnValueOnce(response) + + const result = await handler.handle(request, { + context: { db: 'postgres' }, + prefix: '/api/v1', + }) + + expect(result).toEqual({ matched: true, response }) + + expect(matcher.match).toHaveBeenCalledOnce() + expect(matcher.match).toHaveBeenCalledWith('GET', '/users/1') + + expect(createProcedureClient).toHaveBeenCalledOnce() + expect(createProcedureClient).toHaveBeenCalledWith(ping, { + context: { db: 'postgres' }, + path: ['ping'], + }) + + expect(codec.decode).toHaveBeenCalledOnce() + expect(codec.decode).toHaveBeenCalledWith(request, { id: '__id__' }, ping) + + expect(client).not.toHaveBeenCalledOnce() + expect(codec.encode).not.toBeCalled() + + expect(codec.encodeError).toHaveBeenCalledOnce() + expect(codec.encodeError.mock.calls[0]![0]).toSatisfy((e: any) => { + expect(e).toBeInstanceOf(ORPCError) + expect(e.code).toEqual('BAD_REQUEST') + expect(e.message).toEqual( + `Malformed request. Ensure the request body is properly formatted and the 'Content-Type' header is set correctly.`, + ) + expect(e.cause).toEqual(error) + + return true + }) + + expect(interceptor).toHaveBeenCalledOnce() + expect(interceptor).toHaveBeenCalledWith({ + request, + next: expect.any(Function), + context: { db: 'postgres' }, + prefix: '/api/v1', + }) + + expect(interceptorRoot).toHaveBeenCalledOnce() + expect(interceptorRoot).toHaveBeenCalledWith({ + request, + next: expect.any(Function), + context: { db: 'postgres' }, + prefix: '/api/v1', + }) + }) + it('work without context and prefix', async () => { matcher.match.mockResolvedValue({ path: ['ping'], diff --git a/packages/server/src/adapters/standard/handler.ts b/packages/server/src/adapters/standard/handler.ts index 403940b14..6870f2c14 100644 --- a/packages/server/src/adapters/standard/handler.ts +++ b/packages/server/src/adapters/standard/handler.ts @@ -6,7 +6,7 @@ import type { Plugin } from '../../plugins' import type { CreateProcedureClientOptions } from '../../procedure-client' import type { Router } from '../../router' import type { StandardCodec, StandardMatcher } from './types' -import { toORPCError } from '@orpc/contract' +import { ORPCError, toORPCError } from '@orpc/contract' import { intercept, trim } from '@orpc/shared' import { CompositePlugin } from '../../plugins' import { createProcedureClient } from '../../procedure-client' @@ -64,6 +64,8 @@ export class StandardHandler { context: options?.context ?? {} as T, // context is optional only when all fields are optional so we can safely force it to have a context }, async (interceptorOptions) => { + let isDecoding = false + try { return await intercept( this.options.interceptors ?? [], @@ -88,7 +90,9 @@ export class StandardHandler { const client = createProcedureClient(match.procedure, clientOptions) + isDecoding = true const input = await this.codec.decode(request, match.params, match.procedure) + isDecoding = false const output = await client(input, { signal: request.signal }) @@ -102,7 +106,12 @@ export class StandardHandler { ) } catch (e) { - const error = toORPCError(e) + const error = isDecoding + ? new ORPCError('BAD_REQUEST', { + message: `Malformed request. Ensure the request body is properly formatted and the 'Content-Type' header is set correctly.`, + cause: e, + }) + : toORPCError(e) const response = this.codec.encodeError(error)