Skip to content

Commit

Permalink
fix: Throwing errors with superjson (msw 2) (#34)
Browse files Browse the repository at this point in the history
* fix: Throwing errors with superjson (msw 2)

* fix: Use correct transformers for input and output

* fix: More reliable TRPCError detection

* fix: Allow custom error fields
  • Loading branch information
danilofuchs authored Apr 22, 2024
1 parent 6a9ebd8 commit 94bdb5e
Show file tree
Hide file tree
Showing 4 changed files with 185 additions and 39 deletions.
37 changes: 20 additions & 17 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@
},
"homepage": "https://github.com/maloguertin/msw-trpc#readme",
"devDependencies": {
"@trpc/client": "^10.9.0",
"@trpc/react-query": "^10.9.0",
"@trpc/server": "^10.9.0",
"@trpc/client": "^10.44.1",
"@trpc/react-query": "^10.44.1",
"@trpc/server": "^10.44.1",
"@types/jest": "^29.2.6",
"expect-type": "^0.15.0",
"jest": "^29.3.1",
Expand All @@ -40,7 +40,7 @@
"ts-jest": "^29.0.5",
"ts-node": "^10.9.1",
"typescript": "^4.9.5",
"zod": "^3.20.2"
"zod": "^3.22.4"
},
"peerDependencies": {
"@trpc/server": ">=10.9.0",
Expand Down
31 changes: 16 additions & 15 deletions src/createTRPCMsw.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { getHTTPStatusCodeFromError } from '@trpc/server/http'

import { HttpResponse, http } from 'msw'
import { MswTrpc } from './types'
import { TRPC_ERROR_CODES_BY_KEY } from '@trpc/server/rpc'
import { TRPC_ERROR_CODES_BY_KEY, TRPC_ERROR_CODE_KEY } from '@trpc/server/rpc'

const getQueryInput = (req: Request, transformer: CombinedDataTransformer) => {
const inputString = new URL(req.url).searchParams.get('input')
Expand Down Expand Up @@ -53,23 +53,24 @@ const createUntypedTRPCMsw = (
async (params): Promise<any> => {
try {
const body = await handler(await getInput(params.request, transformer))
return HttpResponse.json({ result: { data: transformer.input.serialize(body) } })
return HttpResponse.json({ result: { data: transformer.output.serialize(body) } })
} catch (e) {
if (e instanceof TRPCError) {
const status = getHTTPStatusCodeFromError(e)
return HttpResponse.json(
{
error: {
message: e.message,
code: TRPC_ERROR_CODES_BY_KEY[e.code],
data: { code: e.code, httpStatus: status },
},
},
{ status }
)
} else {
if (!(e instanceof Error)) {
throw e
}
if (!('code' in e)) {
throw e
}

const status = getHTTPStatusCodeFromError(e as TRPCError)
const path = pathParts.slice(1).join('.')
const { name: _, ...otherErrorData } = e
const jsonError = {
message: e.message,
code: TRPC_ERROR_CODES_BY_KEY[e.code as TRPC_ERROR_CODE_KEY],
data: { ...otherErrorData, code: e.code, httpStatus: status, path },
}
return HttpResponse.json({ error: transformer.output.serialize(jsonError) }, { status })
}
}
)
Expand Down
148 changes: 145 additions & 3 deletions test/integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { setupServer } from 'msw/node'
import { createTRPCMsw } from '../src'
import { TRPCError } from '@trpc/server'
import { TRPCClientError } from '@trpc/client'
import { TRPC_ERROR_CODE_KEY } from '@trpc/server/rpc'

type MswTrpc = typeof mswTrpc
type NestedMswTrpc = typeof nestedMswTrpc
Expand Down Expand Up @@ -82,12 +83,153 @@ describe('queries and mutations', () => {
test('throwing error works', async () => {
server.use(
mswTrpc.userById.query(() => {
throw new TRPCError({ code: 'BAD_REQUEST' })
throw new TRPCError({ code: 'NOT_FOUND', message: 'Resource not found' })
})
)
await expect(async () => {

let error
try {
await trpc.userById.query('1')
} catch (e) {
error = e
}
const clientError = error as TRPCClientError<any>

expect(clientError).toBeInstanceOf(TRPCClientError)
expect(clientError.message).toBe('Resource not found')
expect(clientError.data).toEqual({
code: 'NOT_FOUND',
httpStatus: 404,
path: 'userById',
})
expect(clientError.meta?.response).toBeInstanceOf(Response)
expect(clientError.meta?.responseJSON).toEqual({
error: {
message: 'Resource not found',
code: -32004,
data: clientError.data,
},
})
expect(clientError.shape).toEqual({
message: 'Resource not found',
code: -32004,
data: clientError.data,
})
})

test('throwing error with superjson works', async () => {
server.use(
mswTrpcWithSuperJson.userById.query(() => {
throw new TRPCError({ code: 'NOT_FOUND', message: 'Resource not found' })
})
)

let error
try {
await trpcWithSuperJson.userById.query('1')
} catch (e) {
error = e
}
const clientError = error as TRPCClientError<any>

expect(clientError).toBeInstanceOf(TRPCClientError)
expect(clientError.message).toBe('Resource not found')
expect(clientError.data).toEqual({
code: 'NOT_FOUND',
httpStatus: 404,
path: 'userById',
})
expect(clientError.meta?.response).toBeInstanceOf(Response)
expect(clientError.meta?.responseJSON).toEqual({
error: {
json: {
message: 'Resource not found',
code: -32004,
data: clientError.data,
},
},
})
expect(clientError.shape).toEqual({
message: 'Resource not found',
code: -32004,
data: clientError.data,
})
})

test('throwing custom error works with custom properties', async () => {
class CustomError extends TRPCError {
constructor(
opts: {
message?: string
code: TRPC_ERROR_CODE_KEY
cause?: unknown
},
public validationError: unknown
) {
super(opts)
}
}

server.use(
mswTrpc.userById.query(() => {
throw new CustomError({ code: 'UNPROCESSABLE_CONTENT', message: 'Validation failed' }, { code: 'invalid-uuid' })
})
)

let error
try {
await trpc.userById.query('1')
}).rejects.toThrow(new TRPCClientError('BAD_REQUEST'))
} catch (e) {
error = e
}
const clientError = error as TRPCClientError<any>

expect(clientError).toBeInstanceOf(TRPCClientError)
expect(clientError.message).toBe('Validation failed')
expect(clientError.data).toEqual({
code: 'UNPROCESSABLE_CONTENT',
httpStatus: 422,
path: 'userById',
validationError: {
code: 'invalid-uuid',
},
})
expect(clientError.meta?.response).toBeInstanceOf(Response)
expect(clientError.meta?.responseJSON).toEqual({
error: {
message: 'Validation failed',
code: -32022,
data: clientError.data,
},
})
expect(clientError.shape).toEqual({
message: 'Validation failed',
code: -32022,
data: clientError.data,
})
})

test('throwing error with nested router works', async () => {
server.use(
nestedMswTrpc.users.userById.query(() => {
throw new TRPCError({ code: 'NOT_FOUND', message: 'Resource not found' })
})
)

let error
try {
await nestedTrpc.users.userById.query('1')
} catch (e) {
error = e
}
const clientError = error as TRPCClientError<any>

expect(clientError).toBeInstanceOf(TRPCClientError)
expect(clientError.data).toEqual({
code: 'NOT_FOUND',
httpStatus: 404,
path: 'users.userById',
})
})
})

Expand Down

0 comments on commit 94bdb5e

Please sign in to comment.