/
githubmirror
/
trpc
Обзор
Документация
Войти
/
githubmirror
/
trpc
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
packages/tests/server/errors.test.ts
582 строки
16 KB
Chris Cook
fix(server): unwrap error from cause for streaming onError (#7286)
28 мар 2026, 14:58
Не верифицирован
28 мар 2026, 14:58
2b8a4f8
Код
Авторство
О чём код?
/* eslint-disable @typescript-eslint/no-empty-function */ import http from 'http'; import { routerToServerAndClientNew } from './___testHelpers'; import { testServerAndClientResource } from '@trpc/client/__tests__/testClientResource'; import { waitError } from '@trpc/server/__tests__/waitError'; import type { TRPCLink } from '@trpc/client'; import { createTRPCClient, httpBatchLink, httpLink, TRPCClientError, } from '@trpc/client'; import { initTRPC, TRPCError } from '@trpc/server'; import type { CreateHTTPContextOptions } from '@trpc/server/adapters/standalone'; import type { HTTPErrorHandler } from '@trpc/server/http'; import { observable } from '@trpc/server/observable'; import { isObject } from '@trpc/server/unstable-core-do-not-import'; import { konn } from 'konn'; import fetch from 'node-fetch'; import { z, ZodError } from 'zod'; function getMessageFromUnknownError(err: unknown, fallback: string): string { if (typeof err === 'string') { return err; } if (isObject(err) && typeof err['message'] === 'string') { return err['message']; } return fallback; } test('basic', async () => { class MyError extends Error { constructor(message: string) { super(message); Object.setPrototypeOf(this, MyError.prototype); } } const t = initTRPC.create(); const router = t.router({ err: t.procedure.query(() => { throw new MyError('woop'); }), }); const onError = vi.fn(); await using ctx = testServerAndClientResource(router, { server: { onError, }, }); const clientError = await waitError(ctx.client.err.query(), TRPCClientError); expect(clientError.shape.message).toMatchInlineSnapshot(`"woop"`); expect(clientError.shape.code).toMatchInlineSnapshot(`-32603`); expect(onError).toHaveBeenCalledTimes(1); const serverError = onError.mock.calls[0]![0]!.error; expect(serverError).toBeInstanceOf(TRPCError); if (!(serverError instanceof TRPCError)) { throw new Error('Wrong error'); } expect(serverError.cause).toBeInstanceOf(MyError); }); test('input error', async () => { const onError = vi.fn(); const t = initTRPC.create(); const router = t.router({ err: t.procedure.input(z.string()).mutation(() => { return null; }), }); await using ctx = testServerAndClientResource(router, { server: { onError, }, }); const clientError = await waitError( ctx.client.err.mutate(1 as any), TRPCClientError, ); expect(clientError.shape.message).toMatchInlineSnapshot(` "[ { "expected": "string", "code": "invalid_type", "path": [], "message": "Invalid input: expected string, received number" } ]" `); expect(clientError.shape.code).toMatchInlineSnapshot(`-32600`); expect(onError).toHaveBeenCalledTimes(1); const serverError = onError.mock.calls[0]![0]!.error; // if (!(serverError instanceof TRPCError)) { // console.log('err', serverError); // throw new Error('Wrong error'); // } expect(serverError.cause).toBeInstanceOf(ZodError); }); test('unauthorized()', async () => { const onError = vi.fn(); const t = initTRPC.create(); const router = t.router({ err: t.procedure.query(() => { throw new TRPCError({ code: 'UNAUTHORIZED' }); }), }); await using ctx = testServerAndClientResource(router, { server: { onError, }, }); const clientError = await waitError(ctx.client.err.query(), TRPCClientError); expect(clientError).toMatchInlineSnapshot(`[TRPCClientError: UNAUTHORIZED]`); expect(onError).toHaveBeenCalledTimes(1); const serverError = onError.mock.calls[0]![0]!.error; expect(serverError).toBeInstanceOf(TRPCError); }); test('getMessageFromUnknownError()', () => { expect(getMessageFromUnknownError('test', 'nope')).toBe('test'); expect(getMessageFromUnknownError(1, 'test')).toBe('test'); expect(getMessageFromUnknownError({}, 'test')).toBe('test'); }); describe('formatError()', () => { test('simple', async () => { const onError = vi.fn(); const t = initTRPC.create({ errorFormatter({ shape, error }) { if (error.cause instanceof ZodError) { return { ...shape, data: { ...shape.data, type: 'zod' as const, errors: error.cause.issues, }, }; } return shape; }, }); const router = t.router({ err: t.procedure.input(z.string()).mutation(() => { return null; }), }); await using ctx = testServerAndClientResource(router, { server: { onError, }, }); const clientError = await waitError( ctx.client.err.mutate(1 as any), TRPCClientError, ); delete clientError.data.stack; expect(clientError.data).toMatchInlineSnapshot(` Object { "code": "BAD_REQUEST", "errors": Array [ Object { "code": "invalid_type", "expected": "string", "message": "Invalid input: expected string, received number", "path": Array [], }, ], "httpStatus": 400, "path": "err", "type": "zod", } `); expect(clientError.shape).toMatchInlineSnapshot(` Object { "code": -32600, "data": Object { "code": "BAD_REQUEST", "errors": Array [ Object { "code": "invalid_type", "expected": "string", "message": "Invalid input: expected string, received number", "path": Array [], }, ], "httpStatus": 400, "path": "err", "type": "zod", }, "message": "[ { "expected": "string", "code": "invalid_type", "path": [], "message": "Invalid input: expected string, received number" } ]", } `); expect(onError).toHaveBeenCalledTimes(1); const serverError = onError.mock.calls[0]![0]!.error; expect(serverError.cause).toBeInstanceOf(ZodError); }); test('setting custom http response code', async () => { const TEAPOT_ERROR_CODE = 418; const onError = vi.fn(); const t = initTRPC.create({ errorFormatter: ({ error, shape }) => { if (!(error.cause instanceof ZodError)) { return shape; } return { ...shape, data: { ...shape.data, httpStatus: TEAPOT_ERROR_CODE, }, }; }, }); const router = t.router({ q: t.procedure.input(z.string()).query(() => null), }); await using ctx = testServerAndClientResource(router, { server: { onError, }, }); const res = await fetch(`${ctx.httpUrl}/q`); expect(res.ok).toBeFalsy(); expect(res.status).toBe(TEAPOT_ERROR_CODE); }); test('do not override response status set by middleware or resolver', async () => { const TEAPOT_ERROR_CODE = 418; const onError = vi.fn(); const t = initTRPC.context<CreateHTTPContextOptions>().create({}); const middleware = t.middleware((opts) => { opts.ctx.res.statusCode = TEAPOT_ERROR_CODE; throw new Error('Some error'); }); const router = t.router({ q: t.procedure.use(middleware).query(() => null), }); await using ctx = testServerAndClientResource(router, { server: { onError, }, }); const res = await fetch(`${ctx.httpUrl}/q`); expect(res.ok).toBeFalsy(); expect(res.status).toBe(TEAPOT_ERROR_CODE); }); }); test('make sure object is ignoring prototype', async () => { const onError = vi.fn(); const t = initTRPC.create(); const router = t.router({ hello: t.procedure.query(() => 'there'), }); await using ctx = testServerAndClientResource(router, { server: { onError, }, }); const clientError = await waitError( (ctx.client as any).toString.query(), TRPCClientError, ); expect(clientError.shape.message).toMatchInlineSnapshot( `"No procedure found on path "toString""`, ); expect(clientError.shape.code).toMatchInlineSnapshot(`-32004`); expect(onError).toHaveBeenCalledTimes(1); const serverError = onError.mock.calls[0]![0]!.error; expect(serverError.code).toMatchInlineSnapshot(`"NOT_FOUND"`); }); test('allow using built-in Object-properties', async () => { const t = initTRPC.create(); const router = t.router({ toString: t.procedure.query(() => 'toStringValue'), hasOwnProperty: t.procedure.query(() => 'hasOwnPropertyValue'), }); await using ctx = testServerAndClientResource(router); expect(await ctx.client.toString.query()).toBe('toStringValue'); expect(await ctx.client.hasOwnProperty.query()).toBe('hasOwnPropertyValue'); }); test('retain stack trace', async () => { class CustomError extends Error { constructor() { super('CustomError.msg'); this.name = 'CustomError'; Object.setPrototypeOf(this, new.target.prototype); } } const onErrorFn: HTTPErrorHandler<any, any> = () => {}; const onError = vi.fn(onErrorFn); const t = initTRPC.create(); const router = t.router({ hello: t.procedure.query(() => { if (true) { throw new CustomError(); } return 'toStringValue'; }), }); await using ctx = testServerAndClientResource(router, { server: { onError, }, }); const clientError = await waitError(() => ctx.client.hello.query()); expect(clientError.name).toBe('TRPCClientError'); expect(onError).toHaveBeenCalledTimes(1); const serverOnErrorOpts = onError.mock.calls[0]![0]; const serverError = serverOnErrorOpts.error; expect(serverError).toBeInstanceOf(TRPCError); expect(serverError.cause).toBeInstanceOf(CustomError); expect(serverError.stack).not.toContain('getErrorFromUnknown'); const stackParts = serverError.stack!.split('\n'); // first line of stack trace expect(stackParts[1]).toContain(__filename); }); describe('links have meta data about http failures', async () => { type Handler = (opts: { req: http.IncomingMessage; res: http.ServerResponse; }) => void; function createServer(handler: Handler) { const server = http.createServer((req, res) => { handler({ req, res }); }); server.listen(0); const port = (server.address() as any).port as number; return { url: `http://localhost:${port}`, async close() { await new Promise((resolve) => { server.close(resolve); }); }, }; } const ctx = konn() .beforeEach(() => { return { server: createServer(({ res }) => { res.setHeader('content-type', 'application/json'); res.write( JSON.stringify({ __error: { foo: 'bar', }, }), ); res.end(); }), }; }) .afterEach((opts) => { opts.server?.close(); }) .done(); test('httpLink', async () => { let meta = undefined as Record<string, unknown> | undefined; const client: any = createTRPCClient<any>({ links: [ () => { return ({ next, op }) => { return observable((observer) => { const unsubscribe = next(op).subscribe({ error(err) { observer.error(err); meta = err.meta; }, }); return unsubscribe; }); }; }, httpLink({ url: ctx.server.url, fetch: fetch as any, }), ], }); const error = await waitError(client.test.query(), TRPCClientError); expect(error).toMatchInlineSnapshot( `[TRPCClientError: Unable to transform response from server]`, ); expect(meta).not.toBeUndefined(); expect(meta?.['responseJSON']).not.toBeFalsy(); expect(meta?.['responseJSON']).not.toBeFalsy(); expect(meta?.['responseJSON']).toMatchInlineSnapshot(` Object { "__error": Object { "foo": "bar", }, } `); }); test('httpBatchLink', async () => { let meta = undefined as Record<string, unknown> | undefined; const client: any = createTRPCClient<any>({ links: [ () => { return ({ next, op }) => { return observable((observer) => { const unsubscribe = next(op).subscribe({ error(err) { observer.error(err); meta = err.meta; }, }); return unsubscribe; }); }; }, httpBatchLink({ url: ctx.server.url, fetch: fetch as any, }), ], }); const error = await waitError(client.test.query(), TRPCClientError); expect(error).toMatchInlineSnapshot( `[TRPCClientError: Unable to transform response from server]`, ); expect(meta).not.toBeUndefined(); expect(meta?.['responseJSON']).not.toBeFalsy(); expect(meta?.['responseJSON']).not.toBeFalsy(); expect(meta?.['responseJSON']).toMatchInlineSnapshot(` Object { "__error": Object { "foo": "bar", }, } `); }); test('rethrow custom error', async () => { type AppRouter = any; class MyCustomError extends TRPCClientError<AppRouter> { constructor(message: string) { super(message); Object.setPrototypeOf(this, new.target.prototype); } } const customErrorLink: TRPCLink<AppRouter> = (_runtime) => (opts) => observable((observer) => { const unsubscribe = opts.next(opts.op).subscribe({ error(err) { if ( err.meta && isObject(err.meta['responseJSON']) && '__error' in err.meta['responseJSON'] // <----- you need to modify this ) { // custom error handling observer.error( new MyCustomError( `custom error: ${JSON.stringify( err.meta['responseJSON']['__error'], )}`, ), ); } observer.error(err); }, }); return unsubscribe; }); const client: any = createTRPCClient<any>({ links: [ customErrorLink, httpLink({ url: ctx.server.url, fetch: fetch as any, }), ], }); const error = await waitError(client.test.query()); expect(error).toMatchInlineSnapshot( '[TRPCClientError: custom error: {"foo":"bar"}]', ); expect(error).toBeInstanceOf(TRPCClientError); expect(error).toBeInstanceOf(MyCustomError); }); }); describe('onError', () => { test('streaming onError should unwrap { error, path } and preserve error message and path', async () => { const t = initTRPC.create(); const router = t.router({ failingIterable: t.procedure.query(async function* () { yield 1; throw new Error('stream broke'); }), }); await using ctx = testServerAndClientResource(router); // Act const iterable = await ctx.client.failingIterable.query(); const aggregated: Array<unknown> = []; const error = await waitError( async () => { for await (const value of iterable) { aggregated.push(value); } }, TRPCClientError<typeof router>, ); expect(aggregated).toEqual([1]); expect(error.message).toBe('stream broke'); expect(ctx.onErrorSpy.mock.calls.length).toBe(1); const serverErrorOpts = ctx.onErrorSpy.mock.calls[0]![0]; expect(serverErrorOpts.error).toBeInstanceOf(TRPCError); expect(serverErrorOpts.error.message).toBe('stream broke'); expect(serverErrorOpts.error.cause).toBeInstanceOf(Error); expect(serverErrorOpts.error.cause!.message).toBe('stream broke'); }); });