If you’ve been using Vercel + Next.JS recently, you might have come across a bug where the “fetch” will fail only on Vercel deployments, and obviously only on the back-end. Locally every network request seems to work, but once deployed to production on Vercel you’d get the annoying “fetch failed” error.
This happened to me, both with regular fetch as well as with TRPC. The latter one spat out an unpleasant TRPCClientError: fetch failed
:
TRPCClientError: fetch failed
at TRPCClientError.from (file:///var/task/node_modules/@trpc/client/dist/transformResult-6fb67924.mjs:13:16)
at file:///var/task/node_modules/@trpc/client/dist/links/httpBatchLink.mjs:211:64 {
meta: undefined,
shape: undefined,
data: undefined,
[cause]: TypeError: fetch failed
at Object.fetch (node:internal/deps/undici/undici:14062:11) {
cause: RequestContentLengthMismatchError: Request body length does not match content-length header
at AsyncWriter.end (node:internal/deps/undici/undici:9704:19)
at writeIterable (node:internal/deps/undici/undici:9614:16) {
code: 'UND_ERR_REQ_CONTENT_LENGTH_MISMATCH'
}
}
}
You’ll notice it’s “undici” complaining withTypeError: fetch failed
which makes sense since TRPC uses fetch to perform its network requests, and fetch is polyfilled with “undici” on Vercel + Next.JS.
It turns out since it’s a fetch related issue, others were experiencing this issue through Apollo Client for Next.JS with an unfriendly ApolloError: fetch failed
(and it only took a month for someone else to report on this! 🤯)
The initial solution
When I first encountered this problem I started digging deeper. It turned out Next.JS had an env variable determining which polyfill to use for fetch:
if (!(global as any).fetch) {
function getFetchImpl() {
return (global as any).__NEXT_USE_UNDICI
? require('next/dist/compiled/undici')
: require('next/dist/compiled/node-fetch')
}
Depending on __NEXT_USE_UNDICI, which is set by Vercel, either “undici” or node-fetch polyfill would kick in. The kicker #1: it’s a compiled version of these, so “undici” could have been changed in a breaking way and we wouldn’t be able to tell by reading the code. And since it’s compiled, we also can’t just update the dependencies in case it is indeed a “undici” related issue. I assume there’s some Vercel/Next magic happening in these compiled versions, that does not appear within the origianl “undici” code.
My serverless functions were setup to run on the Frankfurt network. Changing the server to another one, where the environment variable __NEXT_USE_UNDICI was not set to true, solved my issue since Next would default to implementing node-fetch as polyfill instead. Below is an image showing where this config is located:
The kicker #2: Vercel does not offer any info on env variables for given regions, so choosing a server is a guessing game.
This initial solution would save me some headaches for around 3 weeks, until Next.js dropped “note-fetch” in favour of “undici”.
The initial solution was only temporary
Although this fix worked, it was not really a fix. As such, the problem came back haunting me when Next.JS released v13.14.0, in which “node-fetch” was dropped in favour of “undici”.
The new solution
Since we cannot escape from the compiled version of “undici” that is causing issues in Vercel deployments, the next best thing is to use a fetch ponyfill.
My solution was to implement 'fetch-ponyfill’ for all BE calls to fetch.
Here’s how I did it for TRPC:
import fetchPonyfill from 'fetch-ponyfill'
export const trpcClient = createTRPCProxyClient<AppRouter>({
transformer: SuperJSON,
links: [
devtoolsLink({
enabled: process.env.NODE_ENV === 'development',
}),
splitLink({
condition(op) {
// check for context property `skipBatch`
return op.context.skipBatch === true
},
// when condition is true, use normal request
true: httpLink({
// vercel issue with fetch undici
fetch: fetchPonyfill().fetch,
url: `${getBaseUrl()}/api/trpc`,
}),
// when condition is false, use batching
false: httpBatchLink({
fetch: fetchPonyfill().fetch,
url: `${getBaseUrl()}/api/trpc`,
}),
}),
loggerLink({
enabled: (opts) => opts.direction === 'down' && opts.result instanceof Error,
}),
],
})
And for regular fetch requests, I just updated my helper:
export async function fetchPostJSON<T>(
url: string,
data?: {},
usePonyfill = true
): Promise<T | Error> {
try {
// Default options are marked with *
const response = await (usePonyfill ? fetchPonyfill() : { fetch }).fetch(url, {
method: 'POST', // *GET, POST, PUT, DELETE, etc.
...
This enabled me to move forward whilst waiting for Vercel/Next to re-compile undici, with a working version.
WORTH MENTIONING:
If I remember correctly, whilst testing I noticed that node 16.x did not cause the fetch failed issue, so I assume the compiled “undici” is not doing something when the version of Node is 18.x. This could be because of the new features that newer node has, which maybe “undici” assumes are working properly?