Files
File and Blob values in inputs and outputs, sent as multipart, raw bodies or streamed chunks.
On this page
File and Blob values can sit anywhere in an input. httpLink sends that call alone (outside a batch) as multipart/form-data, and the handler rebuilds them before validation. maxBodySize covers the whole body.
import { import zz } from 'zod';
const const setCover: Procedure<{
type: "mutation";
ctx: Context;
meta: {};
input: {
postId: string;
cover: File;
};
output: void;
errors: SchemaValidationError<z.ZodObject<{
postId: z.ZodString;
cover: z.ZodInstanceOf<File>;
}, z.core.$strip>>;
services: never;
}>
setCover = const t: TrpcRoot<Context, {}, RootDef<Context, {}>>t.TrpcRoot<Context, {}, RootDef<Context, {}>>.procedure: ProcedureBuilder<RootDef<Context, {}>>procedure
.ProcedureBuilder<RootDef<Context, {}>>.input<z.ZodObject<{
postId: z.ZodString;
cover: z.ZodInstanceOf<File>;
}, z.core.$strip>>(schema: z.ZodObject<{
postId: z.ZodString;
cover: z.ZodInstanceOf<File>;
}, z.core.$strip>): ProcedureBuilder<InputDef<RootDef<Context, {}>, z.ZodObject<{
postId: z.ZodString;
cover: z.ZodInstanceOf<File>;
}, z.core.$strip>>> (+1 overload)
input(import zz.function object<{
postId: z.ZodString;
cover: z.ZodInstanceOf<File>;
}>(shape?: {
postId: z.ZodString;
cover: z.ZodInstanceOf<File>;
} | undefined, params?: string | {
error?: string | z.core.$ZodErrorMap<NonNullable<z.core.$ZodIssueInvalidType<unknown> | z.core.$ZodIssueUnrecognizedKeys>> | undefined;
message?: string | undefined | undefined;
} | undefined): z.ZodObject<{
postId: z.ZodString;
cover: z.ZodInstanceOf<File>;
}, z.core.$strip>
object({ postId: z.ZodStringpostId: import zz.function string(params?: string | z.core.$ZodStringParams): z.ZodString (+1 overload)string(), cover: z.ZodInstanceOf<File>cover: import zz.instanceof<{
new (fileBits: BlobPart[], fileName: string, options?: FilePropertyBag): File;
prototype: File;
}>(cls: {
new (fileBits: BlobPart[], fileName: string, options?: FilePropertyBag): File;
prototype: File;
}, params?: {
when?: ((payload: z.core.ParsePayload) => boolean) | undefined | undefined;
error?: string | z.core.$ZodErrorMap<z.core.$ZodIssueCustom> | undefined;
message?: string | undefined | undefined;
}): z.ZodInstanceOf<File>
export instanceof
instanceof(var File: {
new (fileBits: BlobPart[], fileName: string, options?: FilePropertyBag): File;
prototype: File;
}
File) }))
.ProcedureBuilder<InputDef<RootDef<Context, {}>, ZodObject<{ postId: ZodString; cover: ZodInstanceOf<File>; }, $strip>>>.mutation<Promise<void>>(resolver: (opts: ResolverOpts<InputDef<RootDef<Context, {}>, z.ZodObject<{
postId: z.ZodString;
cover: z.ZodInstanceOf<File>;
}, z.core.$strip>>>) => Promise<void>): Procedure<{
type: "mutation";
ctx: Context;
meta: {};
input: {
postId: string;
cover: File;
};
output: void;
errors: SchemaValidationError<z.ZodObject<{
postId: z.ZodString;
cover: z.ZodInstanceOf<File>;
}, z.core.$strip>>;
services: never;
}>
mutation((opts: ResolverOpts<InputDef<RootDef<Context, {}>, z.ZodObject<{
postId: z.ZodString;
cover: z.ZodInstanceOf<File>;
}, z.core.$strip>>>
opts) => const storage: {
put(id: string, file: File): Promise<void>;
}
storage.function put(id: string, file: File): Promise<void>put(opts: ResolverOpts<InputDef<RootDef<Context, {}>, z.ZodObject<{
postId: z.ZodString;
cover: z.ZodInstanceOf<File>;
}, z.core.$strip>>>
opts.ResolverOpts<InputDef<RootDef<Context, {}>, ZodObject<{ postId: ZodString; cover: ZodInstanceOf<File>; }, $strip>>>.input: {
postId: string;
cover: File;
}
input.postId: stringpostId, opts: ResolverOpts<InputDef<RootDef<Context, {}>, z.ZodObject<{
postId: z.ZodString;
cover: z.ZodInstanceOf<File>;
}, z.core.$strip>>>
opts.ResolverOpts<InputDef<RootDef<Context, {}>, ZodObject<{ postId: ZodString; cover: ZodInstanceOf<File>; }, $strip>>>.input: {
postId: string;
cover: File;
}
input.cover: Filecover));await const client: TrpcClient<{
setCover: Procedure<{
type: "mutation";
ctx: Context;
meta: {};
input: {
postId: string;
cover: File;
};
output: void;
errors: SchemaValidationError<z.ZodObject<{
postId: z.ZodString;
cover: z.ZodInstanceOf<File>;
}, z.core.$strip>>;
services: never;
}>;
}, {}, undefined, LinksTypes<readonly [TrpcLink<{
form: JsonForm;
provides: "response";
}, any>]>>
client.setCover: {
mutate(input: {
postId: string;
cover: File;
}, opts?: CallOptions<{}> | undefined): TrpcPromise<undefined, CallError<{
type: "mutation";
ctx: Context;
meta: {};
input: {
postId: string;
cover: File;
};
output: void;
errors: SchemaValidationError<z.ZodObject<{
postId: z.ZodString;
cover: z.ZodInstanceOf<File>;
}, z.core.$strip>>;
services: never;
}, JsonForm, never>>;
}
setCover.function mutate(input: {
postId: string;
cover: File;
}, opts?: CallOptions<{}> | undefined): TrpcPromise<undefined, CallError<{
type: "mutation";
ctx: Context;
meta: {};
input: {
postId: string;
cover: File;
};
output: void;
errors: SchemaValidationError<z.ZodObject<{
postId: z.ZodString;
cover: z.ZodInstanceOf<File>;
}, z.core.$strip>>;
services: never;
}, JsonForm, never>>
mutate({ postId: stringpostId: '1', cover: Filecover: const file: Filefile });The handler only takes a multipart body with the trpc-version header, which httpLink sends and a cross-site form can't, so a form on another site can't run your mutations (other POST bodies must be application/json for the same reason).
wsLink doesn't send files; the call fails with a BAD_REQUEST. For large uploads, use presigned URLs.
Files in outputs
Outputs can hold files too, over every link:
import { import zz } from 'zod';
const const report: Procedure<{
type: "query";
ctx: Context;
meta: {};
input: {
id: string;
};
output: File;
errors: SchemaValidationError<z.ZodObject<{
id: z.ZodString;
}, z.core.$strip>>;
services: never;
}>
report = const t: TrpcRoot<Context, {}, RootDef<Context, {}>>t.TrpcRoot<Context, {}, RootDef<Context, {}>>.procedure: ProcedureBuilder<RootDef<Context, {}>>procedure
.ProcedureBuilder<RootDef<Context, {}>>.input<z.ZodObject<{
id: z.ZodString;
}, z.core.$strip>>(schema: z.ZodObject<{
id: z.ZodString;
}, z.core.$strip>): ProcedureBuilder<InputDef<RootDef<Context, {}>, z.ZodObject<{
id: z.ZodString;
}, z.core.$strip>>> (+1 overload)
input(import zz.function object<{
id: z.ZodString;
}>(shape?: {
id: z.ZodString;
} | undefined, params?: string | {
error?: string | z.core.$ZodErrorMap<NonNullable<z.core.$ZodIssueInvalidType<unknown> | z.core.$ZodIssueUnrecognizedKeys>> | undefined;
message?: string | undefined | undefined;
} | undefined): z.ZodObject<{
id: z.ZodString;
}, z.core.$strip>
object({ id: z.ZodStringid: import zz.function string(params?: string | z.core.$ZodStringParams): z.ZodString (+1 overload)string() }))
.ProcedureBuilder<InputDef<RootDef<Context, {}>, ZodObject<{ id: ZodString; }, $strip>>>.query<File>(resolver: (opts: ResolverOpts<InputDef<RootDef<Context, {}>, z.ZodObject<{
id: z.ZodString;
}, z.core.$strip>>>) => File): Procedure<{
type: "query";
ctx: Context;
meta: {};
input: {
id: string;
};
output: File;
errors: SchemaValidationError<z.ZodObject<{
id: z.ZodString;
}, z.core.$strip>>;
services: never;
}>
query(
(opts: ResolverOpts<InputDef<RootDef<Context, {}>, z.ZodObject<{
id: z.ZodString;
}, z.core.$strip>>>
opts) =>
new var File: new (fileBits: BlobPart[], fileName: string, options?: FilePropertyBag) => FileFile([function csv(id: string): stringcsv(opts: ResolverOpts<InputDef<RootDef<Context, {}>, z.ZodObject<{
id: z.ZodString;
}, z.core.$strip>>>
opts.ResolverOpts<InputDef<RootDef<Context, {}>, ZodObject<{ id: ZodString; }, $strip>>>.input: {
id: string;
}
input.id: stringid)], 'report.csv', { BlobPropertyBag.type?: string | undefinedtype: 'text/csv' }),
);const const report: Filereport = await const client: TrpcClient<{
report: Procedure<{
type: "query";
ctx: Context;
meta: {};
input: {
id: string;
};
output: File;
errors: SchemaValidationError<z.ZodObject<{
id: z.ZodString;
}, z.core.$strip>>;
services: never;
}>;
}, {}, undefined, LinksTypes<readonly [TrpcLink<{
form: JsonForm;
provides: "response";
}, any>]>>
client.report: {
query(input: {
id: string;
}, opts?: CallOptions<{}> | undefined): TrpcPromise<File, CallError<{
type: "query";
ctx: Context;
meta: {};
input: {
id: string;
};
output: File;
errors: SchemaValidationError<z.ZodObject<{
id: z.ZodString;
}, z.core.$strip>>;
services: never;
}, JsonForm, never>>;
}
report.function query(input: {
id: string;
}, opts?: CallOptions<{}> | undefined): TrpcPromise<File, CallError<{
type: "query";
ctx: Context;
meta: {};
input: {
id: string;
};
output: File;
errors: SchemaValidationError<z.ZodObject<{
id: z.ZodString;
}, z.core.$strip>>;
services: never;
}, JsonForm, never>>
query({ id: stringid }); // File- When a
FileorBlobis the whole output of a call that isn't batched,httpLinkgets it as the response body, with itscontent-typeand acontent-dispositionnaming the file, so the URL works as a download link too. - Anywhere else (nested in an object, yielded by an async iterable, or in a batch), its bytes stream after the rest of the output as base64 chunks. The value holding it resolves once they've arrived, so a nested
Fileis aFile, not a promise. A file that appears twice is sent once.
Binary frames
Those chunks are base64 in JSONL, SSE and WebSocket messages. httpLink({ binary: true }) asks for binary frames instead (application/vnd.trpc.frames: the same lines, length-prefixed, with the bytes unencoded), a third smaller; servers answer with JSONL to clients that don't ask. wsLink({ binary: true }) gets each chunk of bytes as a binary message. localLink always passes the bytes as they are.
Serializers
A root File or Blob output of an unbatched HTTP call is the response body, with any serializer. On a batching chain, plain JSON can't batch it, so such calls take { batch: false } (a type error otherwise). Files anywhere else, and files in inputs, need danSON; the other serializers fail before sending (Serializers).
The examples/media-pipeline app uploads photos as multipart, returns files as response bodies and inside streamed outputs, and reads them as binary frames.