Skip to content

Commit 9125edb

Browse files
authored
feat(server): enhance error handling for malformed requests (#145)
1 parent 989a435 commit 9125edb

2 files changed

Lines changed: 78 additions & 2 deletions

File tree

packages/server/src/adapters/standard/handler.test.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,73 @@ describe('standardHandler', () => {
213213
})
214214
})
215215

216+
it('on decode error', async () => {
217+
matcher.match.mockResolvedValue({
218+
path: ['ping'],
219+
procedure: ping,
220+
params: { id: '__id__' },
221+
})
222+
223+
const error = new Error('Something bad')
224+
codec.decode.mockRejectedValueOnce(error)
225+
const client = vi.fn()
226+
vi.mocked(createProcedureClient).mockReturnValueOnce(client)
227+
228+
codec.decode.mockReturnValueOnce('__input__')
229+
230+
codec.encodeError.mockReturnValueOnce(response)
231+
232+
const result = await handler.handle(request, {
233+
context: { db: 'postgres' },
234+
prefix: '/api/v1',
235+
})
236+
237+
expect(result).toEqual({ matched: true, response })
238+
239+
expect(matcher.match).toHaveBeenCalledOnce()
240+
expect(matcher.match).toHaveBeenCalledWith('GET', '/users/1')
241+
242+
expect(createProcedureClient).toHaveBeenCalledOnce()
243+
expect(createProcedureClient).toHaveBeenCalledWith(ping, {
244+
context: { db: 'postgres' },
245+
path: ['ping'],
246+
})
247+
248+
expect(codec.decode).toHaveBeenCalledOnce()
249+
expect(codec.decode).toHaveBeenCalledWith(request, { id: '__id__' }, ping)
250+
251+
expect(client).not.toHaveBeenCalledOnce()
252+
expect(codec.encode).not.toBeCalled()
253+
254+
expect(codec.encodeError).toHaveBeenCalledOnce()
255+
expect(codec.encodeError.mock.calls[0]![0]).toSatisfy((e: any) => {
256+
expect(e).toBeInstanceOf(ORPCError)
257+
expect(e.code).toEqual('BAD_REQUEST')
258+
expect(e.message).toEqual(
259+
`Malformed request. Ensure the request body is properly formatted and the 'Content-Type' header is set correctly.`,
260+
)
261+
expect(e.cause).toEqual(error)
262+
263+
return true
264+
})
265+
266+
expect(interceptor).toHaveBeenCalledOnce()
267+
expect(interceptor).toHaveBeenCalledWith({
268+
request,
269+
next: expect.any(Function),
270+
context: { db: 'postgres' },
271+
prefix: '/api/v1',
272+
})
273+
274+
expect(interceptorRoot).toHaveBeenCalledOnce()
275+
expect(interceptorRoot).toHaveBeenCalledWith({
276+
request,
277+
next: expect.any(Function),
278+
context: { db: 'postgres' },
279+
prefix: '/api/v1',
280+
})
281+
})
282+
216283
it('work without context and prefix', async () => {
217284
matcher.match.mockResolvedValue({
218285
path: ['ping'],

packages/server/src/adapters/standard/handler.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import type { Plugin } from '../../plugins'
66
import type { CreateProcedureClientOptions } from '../../procedure-client'
77
import type { Router } from '../../router'
88
import type { StandardCodec, StandardMatcher } from './types'
9-
import { toORPCError } from '@orpc/contract'
9+
import { ORPCError, toORPCError } from '@orpc/contract'
1010
import { intercept, trim } from '@orpc/shared'
1111
import { CompositePlugin } from '../../plugins'
1212
import { createProcedureClient } from '../../procedure-client'
@@ -64,6 +64,8 @@ export class StandardHandler<T extends Context> {
6464
context: options?.context ?? {} as T, // context is optional only when all fields are optional so we can safely force it to have a context
6565
},
6666
async (interceptorOptions) => {
67+
let isDecoding = false
68+
6769
try {
6870
return await intercept(
6971
this.options.interceptors ?? [],
@@ -88,7 +90,9 @@ export class StandardHandler<T extends Context> {
8890

8991
const client = createProcedureClient(match.procedure, clientOptions)
9092

93+
isDecoding = true
9194
const input = await this.codec.decode(request, match.params, match.procedure)
95+
isDecoding = false
9296

9397
const output = await client(input, { signal: request.signal })
9498

@@ -102,7 +106,12 @@ export class StandardHandler<T extends Context> {
102106
)
103107
}
104108
catch (e) {
105-
const error = toORPCError(e)
109+
const error = isDecoding
110+
? new ORPCError('BAD_REQUEST', {
111+
message: `Malformed request. Ensure the request body is properly formatted and the 'Content-Type' header is set correctly.`,
112+
cause: e,
113+
})
114+
: toORPCError(e)
106115

107116
const response = this.codec.encodeError(error)
108117

0 commit comments

Comments
 (0)