trpc/trpc · error · TRPCError
BAD_REQUEST
BAD_REQUEST
Error message
Invalid input
What it means
This is the generic catch-all inside the content-type `memo().read()` used for input parsing. Any non-TRPCError thrown while reading or deserializing input (e.g. `req.json()` failing, or the transformer's `deserialize` throwing) is rethrown as BAD_REQUEST with the original cause preserved; existing TRPCErrors pass through unchanged.
Source
Thrown at packages/server/src/unstable-core-do-not-import/http/contentType.ts:57
function memo<TReturn>(fn: () => Promise<TReturn>) {
let promise: Promise<TReturn> | null = null;
const sym = Symbol.for('@trpc/server/http/memo');
let value: TReturn | typeof sym = sym;
return {
/**
* Lazily read the value
*/
read: async (): Promise<TReturn> => {
if (value !== sym) {
return value;
}
// dedupes promises and catches errors
promise ??= fn().catch((cause) => {
if (cause instanceof TRPCError) {
throw cause;
}
throw new TRPCError({
code: 'BAD_REQUEST',
message: cause instanceof Error ? cause.message : 'Invalid input',
cause,
});
});
value = await promise;
promise = null;
return value;
},
/**
* Get an already stored result
*/
result: (): TReturn | undefined => {
return value !== sym ? value : undefined;
},
};View on GitHub (pinned to acff82332d)
Solutions
- Inspect `error.cause` (and `error.data.cause`) for the real underlying reason.
- Ensure transformer type registration matches exactly on client and server.
- Validate the payload shape on the client before sending, and surface transformer errors clearly during development.
Example fix
// before - superjson custom type registered only on client
// client: superjson.registerCustom(MyDate, 'date')
// server: (missing)
// after - register on BOTH sides, shared module
// shared/registry.ts
export const s = SuperJSON;
s.registerCustom(Date, (v) => v.toISOString(), (v) => new Date(v));
// import { s } from '../shared/registry' on both client and server Defensive patterns
Strategy: try-catch
Validate before calling
// Validate the body parses with the configured transformer before sending
import { transformer } from '../shared/transformer';
function safeSerialize(value: unknown): string {
const wire = transformer.input.serialize(value);
return JSON.stringify(wire); // throws here if the type is unregistered
} Type guard
function isTransformerRegistered(
transformer: { input: { serialize: (x: unknown) => unknown } },
sample: unknown,
): boolean {
try { transformer.input.serialize(sample); return true; } catch { return false; }
} Try / catch
try {
await trpc.proc.query(input);
} catch (e) {
if (e instanceof TRPCError && e.code === 'BAD_REQUEST' && /Invalid input/.test(e.message)) {
// Inspect e.cause: usually JSON.parse or transformer.deserialize failed
}
throw e;
} Prevention
- Register all custom transformer types identically on client and server.
- Share the transformer/registry module between both sides.
- Surface `error.cause` in dev logging to catch the real deserialize failure.
When it happens
Trigger: A malformed JSON body; a superjson transformer failing to deserialize a value whose custom type is not registered; a custom transformer throwing on unexpected input.
Common situations: Client forgot to register a custom Superjson type on the server; client sent values like unregistered dates/classes; transformer version skew between client and server.
Related errors
- FormData is only supported for mutations
- Octet type input is only supported for mutations
- Subscriptions are unsupported by `httpLink` - use `httpSubsc
- BAD_REQUEST
- METHOD_NOT_SUPPORTED
AI-assisted analysis of trpc/trpc@acff82332d (2026-08-12).
Data as JSON: /api/errors/0411a04449a7e99f.
Report an issue: GitHub.