FormData TRPCClientError
Hi i have this error in my client : I am working with Next 15.2.2 and tRPC 11.0.0-rc.682. NO MATTER what I try, FormData cannot be sent to my server — this is very frustrating. If anybody has encountered this problem, please help me!
I have a smaller project with the exact same uploading form, and it works fine there (even with latest next version) . I don’t know how to debug this — I’ve spent hours and haven’t found any major differences between the codebases.
//DATA TRANSFORMER src: https://github.com/juliangra/trpc-next-formdata-app-router
1 Reply
//NEXT-API : api/trpc/[trpc]/route.ts
//PS : createQueryClient for TRPC PROVIDER
```typescript
export const createQueryClient = () =>
new QueryClient({
defaultOptions: {
queries: {
// With SSR, we usually want to set some default staleTime
// above 0 to avoid refetching immediately on the client
staleTime: 30 * 1000,
},
dehydrate: {
serializeData: transformer.serialize,
shouldDehydrateQuery: (query) =>
defaultShouldDehydrateQuery(query) ||
query.state.status === "pending",
},
hydrate: {
deserializeData: transformer.deserialize,
},
},
});```
//CLIENT FORM
```typescript
const url =
/test/upload/${uuidv4()}
const formData = new FormData();
formData.append('file', data.image);
formData.append('metadata', JSON.stringify({
filename,
type:"IMAGE",
extension,
img_width: width,
img_height: height,
..._.pick(file,["size","name"]),
video_length:null,
mimeType:"IMAGE_JPEG"
}));
formData.append('url', url);
await createPost.mutateAsync(formData)```
//TRPC PROVIDER
```typescript
function TRPCProviders(props: Readonly<{ children: React.ReactNode }>) {
const queryClient = getQueryClient();
const [trpcClient] = useState(() =>
trpc.createClient({
links: [
loggerLink({
enabled: (op) =>
process.env.NODE_ENV === "development" ||
(op.direction === "down" && op.result instanceof Error),
}),
splitLink({
condition: (op) => op.type === 'subscription',
true: unstable_httpSubscriptionLink({
url: getUrl(),
/**
* @see https://trpc.io/docs/v11/data-transformers
*/
transformer,
}),
false: unstable_httpBatchStreamLink({
url: getUrl(),
/**
* @see https://trpc.io/docs/v11/data-transformers
*/
transformer,
}),
})
],
}),
);```
//ENDPOINT TRPC
```typescript
const uploadFileSchema = zfd.formData({
url: zfd.text(),
file: zfd.file(z.instanceof(File)),
metadata: zfd.json(z.any()),
});
export const appRouter = createTRPCRouter({
uploadFile: publicProcedure.input(uploadFileSchema).mutation(async (opts) => {
console.log(opts.input);
})})
```
Now I have the same endpoint and frontend as https://github.com/trpc/examples-next-formdata, but it still doesn't work.
```typescript
export function Upload() {
const mutation = trpc.myEndpoint.create.useMutation({
onError(err) {
alert('Error from server: ' + err.message);
},
});
const schema = zfd.formData({
name: zfd.text(),
image: zfd.file(),
});
const form = useZodFormData({
schema,
});
const [noJs, setNoJs] = useState(false);
return (
<>
<FormProvider {...form}>
<form
method="post"
action={
/api/trpc/${mutation.trpc.path}`}
encType="multipart/form-data"
onSubmit={(_event) => {
if (noJs) {
return;
}
void form.handleSubmit(async (values, event) => {
await mutation.mutateAsync(new FormData(event?.target));
})(_event);
}}
style={{ display: 'flex', flexDirection: 'column', gap: 10 }}
ref={form.formRef}
>
<fieldset>
<div style={{}}>
<label htmlFor="name">Enter your name</label>
<input {...form.register('name')} />
{form.formState.errors.name && (
<div>{form.formState.errors.name.message}</div>
)}
</div>
<div>
<label>Required file, only images</label>
<input type="file" {...form.register('image')} />
{form.formState.errors.image && (
<div>{form.formState.errors.image.message}</div>
)}
</div>
<div>
<button type="submit" disabled={mutation.status === 'pending'}>
submit
</button>
</div>
</fieldset>
</form>
</FormProvider>
</>
);
}typescript
const uploadFileSchema = zfd.formData({
name: zfd.text(),
image: zfd.file(),
});
export const appRouter = createTRPCRouter({
//...
myEndpoint: publicProcedure.input(uploadFileSchema).mutation(async (opts) => {
console.log(opts.input);
})
})typescript
//QueryClient
export const createQueryClient = () =>
new QueryClient({
defaultOptions: {
queries: {
// With SSR, we usually want to set some default staleTime
// above 0 to avoid refetching immediately on the client
staleTime: 30 * 1000,
},
}})
//PROVIDER
function TRPCProviders(props: Readonly<{ children: React.ReactNode }>) {
const queryClient = getQueryClient();
const [trpcClient] = useState(() =>
trpc.createClient({
links: [
loggerLink({
enabled: (op) =>
process.env.NODE_ENV === "development"
(op.direction === "down" && op.result instanceof Error),
}),
splitLink({
condition: (op) => op.type === 'subscription',
true: unstable_httpSubscriptionLink({
url: getUrl(),
/
* @see https://trpc.io/docs/v11/data-transformers
*/
transformer,
}),
false: unstable_httpBatchStreamLink({
url: getUrl(),
/
* @see https://trpc.io/docs/v11/data-transformers
*/
transformer,
}),
}),
],
}),
);
return (
<QueryClientProvider client={getQueryClient()}>
<trpc.Provider client={trpcClient} queryClient={queryClient}>
{props.children}
</trpc.Provider>
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
);
}Uncaught (in promise) TRPCClientError: [
{
"code": "invalid_type",
"expected": "string",
"received": "undefined",
"path": [
"name"
],
"message": "Required"
},
{
"code": "custom",
"message": "Input not instance of File",
"fatal": true,
"path": [
"image"
]
}
]typescript
// TRPC PROVIDER file
export class FormDataTransformer implements DataTransformer {
serialize(object: any) {
if (!(object instanceof FormData)) {
throw new Error("Expected FormData");
}
return object;
}
deserialize(object: any) {
return object as JSON;
}
}
function TRPCProviders(props: Readonly<{ children: React.ReactNode }>) {
const queryClient = getQueryClient();
const [trpcClient] = useState(() =>
trpc.createClient({
links: [
loggerLink({
enabled: (op) =>
process.env.NODE_ENV === "development"
(op.direction === "down" && op.result instanceof Error),
}),
splitLink({
condition: (op) => !isNonJsonSerializable(op.input) && op.type !== "subscription" && !op.context["stream"],
true: httpBatchLink({url: getUrl(),transformer }),
false: splitLink({
condition: (op) => isNonJsonSerializable(op.input) && op.type !== "subscription" && !op.context["stream"],
true: httpLink({
url: getUrl(),transformer: new FormDataTransformer(),
}),
false: splitLink({
condition: (op) => op.type === "subscription" && !op.context["stream"],
true: unstable_httpSubscriptionLink({
url: getUrl(),transformer
}),
false: unstable_httpBatchStreamLink({
url: getUrl(),transformer
}),
}),
}),
}),
],
}),
);
return (
<QueryClientProvider client={getQueryClient()}>
<trpc.Provider client={trpcClient} queryClient={queryClient}>
{props.children}
</trpc.Provider>
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
);
}```
/solve