Randomly get errors when doing client blob upload

Hi, I encounter a strange bug when doing client upload on preview env (it works as expected locally with ngrok and I did not try it in production as huh… test in production is never a good idea).

The problem is related to the random hidden use of window object inside the upload api route.
Random because I use the upload function client side to upload a lot of files, sometimes the api route handle the request normally and all works as expected, sometimes not without any difference in upload() parameters client side.
Here is a simple implementation :

import { getServerSession } from "next-auth";
import { NextRequest, NextResponse } from "next/server";
import { authOptions } from "../../auth/[...nextauth]/route";
import { handleUpload, type HandleUploadBody } from "@vercel/blob/client";

export async function POST(request: NextRequest) {
  const body = (await request.json()) as HandleUploadBody;

  try {
    const jsonResponse = await handleUpload({
      body,
      request,
      onBeforeGenerateToken: async (
        pathname: string
        /* clientPayload?: string, */
      ) => {
        const session = await getServerSession(authOptions);
        if (!session?.organization?.id) {
          throw new Error("Not authorized");
        }
        if (typeof window !== "undefined") {
          console.log("Window is defined unexpectedly!");
          // @ts-ignore
          window = undefined;
        } else {
          console.log("window is not defined");
        }
        return {
          allowedContentTypes: ["image/jpeg", "image/png", "image/avif", "image/webp"],
          tokenPayload: JSON.stringify({
            organizationId: session.organization.id,
          }),
        };
      },
      onUploadCompleted: async ({ blob, tokenPayload }) => {
        console.log("blob upload completed", blob.pathname, tokenPayload);
      },
    });

    return NextResponse.json(jsonResponse);
  } catch (error) {
    console.log("error", error);
    console.log("error saving images", body);
    return NextResponse.json(
      { error: (error as Error).message },
      { status: 400 } // The webhook will retry 5 times waiting for a 200
    );
  }
}

and here is the client side implementation :

const res = await upload(fileName, imageFile.rawFile, {
        access: "public",
        handleUploadUrl: uploadUrl + "/api/images/upload",
      });

Without setting windows to undefined if it is defined I got the following error :
“Vercel Blob: "generateClientTokenFromReadWriteToken" must be called from a server environment”

After checking the @vercel/blob code, i found a check on window in generateClientTokenFromReadWriteToken() function.
So, even if I do not know why window is randomly defined hence I don’t think to use any library on server side which create the window variable, I tried to set it to undefined. But after that I got the error :
“window is not defined”

How this error could be thrown ?

Is any of you have been confronted to this issue ?
Do you have any clue ?

I use the latest @vercel/blob realease.

Hey @briancharlesdev thanks for reaching out and already doing some debugging, much appreciated.

It is indeed extremely weird that window is sometimes defined. We do not define it as part of the Vercel Blob package.

Do you know of any other package or Vercel/Next.js configuration you’re using the could be defining window?

Could you do something like:

if (typeof window !== "undefined") {
  console.log(window);
  console.log(JSON.stringify(process.env);
}

So perhaps we learn a bit more about why we’re seeing window being defined.

I will also investigate on my side.

4 Likes

Hi @vvoyer ,

Thank you a lot to answer this that fast!
The previous code wasn’t up to date to what I used sorry, I forgot to put the small modification “window || …)”, it was :

if (window || typeof window !== "undefined") {
          console.log("Window is defined unexpectedly!");
          // @ts-ignore
          window = undefined;
        } else {
          console.log("window is not defined");
        }

, I had made this small modification to better understand the problem as typeof window return undefined, but logging window directly give following result :

<ref *1> Window {
  onafterprint: [Getter/Setter],
  onbeforeprint: [Getter/Setter],
  onbeforeunload: [Getter/Setter],
  onhashchange: [Getter/Setter],
  onlanguagechange: [Getter/Setter],
  onmessage: [Getter/Setter],
  onmessageerror: [Getter/Setter],
  onoffline: [Getter/Setter],
  ononline: [Getter/Setter],
  onpagehide: [Getter/Setter],
  onpageshow: [Getter/Setter],
 ....
}

Can’t understand why… But for sure typeof window do not return undefined in generateClientTokenFromReadWriteToken() function as if it did not I would not have the
“Vercel Blob: “generateClientTokenFromReadWriteToken” must be called from a server environment”
error.

I tried to remove nextAuth part to see if it is related to it, but just keeping as imports

import { NextRequest, NextResponse } from "next/server";
import { handleUpload, type HandleUploadBody } from "@vercel/blob/client";

It didn’t worked better.

I give you the process.env object (I removed some unrelated parts and hide some passwords/token) :

{
  "APP_ENV": "preview",
  "AWS_ACCESS_KEY_ID": "****",
  "AWS_DEFAULT_REGION": "eu-central-1",
  "AWS_EXECUTION_ENV": "AWS_Lambda_nodejs20.x",
  "AWS_LAMBDA_EXEC_WRAPPER": "/opt/rust/bootstrap",
  "AWS_LAMBDA_FUNCTION_MEMORY_SIZE": "1024",
  "AWS_LAMBDA_FUNCTION_NAME": "team_R2Uk4lhiXoxetvGde1vVUBjx-4eecaa1466aee3e558e90d96f7eb2e337c",
  "AWS_LAMBDA_FUNCTION_VERSION": "$LATEST",
  "AWS_LAMBDA_INITIALIZATION_TYPE": "on-demand",
  "AWS_LAMBDA_LOG_GROUP_NAME": "/aws/lambda/team_R2Uk4lhiXoxetvGde1vVUBjx-4eecaa1466aee3e558e90d96f7eb2e337c",
  "AWS_LAMBDA_LOG_STREAM_NAME": "2025/01/15/[$LATEST]479331ec981546059a17862a5d4f01e5",
  "AWS_LAMBDA_RUNTIME_API": "169.254.100.1:9001",
  "AWS_REGION": "eu-central-1",
  "AWS_SECRET_ACCESS_KEY": "****",
  "AWS_SESSION_TOKEN": "****",
  "AWS_XRAY_CONTEXT_MISSING": "LOG_ERROR",
  "AWS_XRAY_DAEMON_ADDRESS": "169.254.100.1:2000",
  "BLOB_READ_WRITE_TOKEN": "****",
  "CRON_SECRET": "****",
  "LAMBDA_RUNTIME_DIR": "/var/runtime",
  "LAMBDA_TASK_ROOT": "/var/task",
  "LANG": "en_US.UTF-8",
  "LD_LIBRARY_PATH": "/var/lang/lib:/lib64:/usr/lib64:/var/runtime:/var/runtime/lib:/var/task:/var/task/lib:/opt/lib",
  "NEXTAUTH_SECRET": "****",
  "NODE_PATH": "/opt/nodejs/node20/node_modules:/opt/nodejs/node_modules:/var/runtime/node_modules:/var/runtime:/var/task",
  "NOW_REGION": "fra1",
  "NX_DAEMON": "false",
  "PATH": "/var/lang/bin:/usr/local/bin:/usr/bin/:/bin:/opt/bin",
  "PWD": "/var/task",
  "SHLVL": "0",
  "TURBO_CACHE": "remote:rw",
  "TURBO_DOWNLOAD_LOCAL_ENABLED": "true",
  "TURBO_PLATFORM_ENV": "POSTGRES_URL,POSTGRES_URL_NON_POOLING,POSTGRES_URL_NO_SSL,POSTGRES_PRISMA_URL,POSTGRES_USER,POSTGRES_PASSWORD,POSTGRES_HOST,POSTGRES_DATABASE,APP_ENV,STRIPE_SECRET_KEY,STRIPE_WEBHOOK_SECRET_LOCAL,NEXT_PUBLIC_STRIPE_PUBLIC_KEY,NEXTAUTH_SECRET,CLOUDINARY_NAME,CLOUDINARY_API_KEY,CLOUDINARY_API_SECRET,BREVO_MAIL_API_URL,BREVO_MAIL_API_KEY,BLOB_READ_WRITE_TOKEN,VERCEL_API_TOKEN,VERCEL_TEAM_ID,DEEPL_API_KEY,CRON_SECRET,VERCEL_ADMIN_PROJECT_ID,VERCEL_ADMIN_DEPLOY_URL,OVH_CONSUMER_KEY,OVH_SECRET_KEY,OVH_APPLICATION_KEY,STRIPE_WEBHOOK_SECRET,BUNNY_CDN_STORAGE_URL,BUNNY_CDN_API_KEY,BUNNY_CDN_PULL_URL,VERCEL_WEB_ANALYTICS_ID",
  "TURBO_REMOTE_ONLY": "true",
  "TURBO_RUN_SUMMARY": "true",
  "TZ": ":UTC",
  "VERCEL": "1",
  "VERCEL_ADMIN_DEPLOY_URL": "https://api.vercel.com/v1/integrations/deploy/prj_Lg78Ti1iB3JjMmco1CRvrILWnIFa/q9yeJbf2Z3",
  "VERCEL_ADMIN_PROJECT_ID": "prj_Lg78Ti1iB3JjMmco1CRvrILWnIFa",
  "VERCEL_API_TOKEN": "****",
  "VERCEL_BRANCH_URL": "mon-trail-git-develop-brian-charles-development.vercel.app",
  "VERCEL_DEPLOYMENT_ID": "dpl_J8oK9QpJjecGzxL1cEraX2Loxutg",
  "VERCEL_ENV": "preview",
  "VERCEL_ENV_ENC_KEY": "****",
  "VERCEL_ENV_FILE": "___vc/__env.encrypted",
  "VERCEL_IPC_FD": "3",
  "VERCEL_NODE_PRELOAD_SCRIPTS": "/opt/rust/next-data.js",
  "VERCEL_PARENT_SPAN_ID": "2a74228345edc48b",
  "VERCEL_PROJECT_PRODUCTION_URL": "www.mon-trail.fr",
  "VERCEL_REGION": "fra1",
  "VERCEL_TARGET_ENV": "preview",
  "VERCEL_TEAM_ID": "team_R2Uk4lhiXoxetvGde1vVUBjx",
  "VERCEL_URL": "mon-trail-by3itgzpo-brian-charles-development.vercel.app",
  "VERCEL_WEB_ANALYTICS_ID": "H9xrRVTtCtIpLBztgSkpSjn2r",
  "_AWS_XRAY_DAEMON_ADDRESS": "169.254.100.1",
  "_AWS_XRAY_DAEMON_PORT": "2000",
  "_HANDLER": "___next_launcher.cjs",
  "_LAMBDA_TELEMETRY_LOG_FD": "62",
  "NODE_ENV": "production",
  "__NEXT_PRIVATE_PREBUNDLED_REACT": "next",
  "VERCEL_GIT_PREVIOUS_SHA": "",
  "VERCEL_GIT_PULL_REQUEST_ID": "",
  "NEXT_DEPLOYMENT_ID": "",
  "__NEXT_PRIVATE_RUNTIME_TYPE": "",
  "SSL_CERT_FILE": "/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem",
  "SSL_CERT_DIR": "/etc/pki/tls/certs"
}

Here my next config :

/** @type {import('next').NextConfig} */
const nextConfig = {
  transpilePackages: ["@pqina/pintura", "@pqina/react-pintura"],
  experimental: {
    turbo: {
      rules: {
        "*.svg": {
          loaders: ["@svgr/webpack"],
          as: "*.js",
        },
      },
    },
  },
  experimental: {
    turbo: {
      resolveAlias: {
        canvas: "./empty-module.ts",
      },
    },
  },
  compiler: {
    styledComponents: true,
  },
  webpack: (config) => {
    // Add rule for SVG files
    config.module.rules.push({
      test: /\.svg$/,
      use: ["@svgr/webpack", "url-loader"],
    });
    return config;
  },
};

module.exports = nextConfig;

Any thoughts?
In my understanding, which could be wrong, Api routes are isolated from the others, so libraries not imported inside them should not be imported even if they’re imported elsewhere in the projet isn’t it ?
There is unrelated files in api folder where I use librairies that can create the window property as @react-pdf/renderer, could it be coming from that imports ?
The fact that sometimes the call to the upload route works and sometimes not is very strange, nearly 50% 50%, maybe it could be a clue.

1 Like

@briancharlesdev this is all VERY good debugging, let’s continue: I can tell from some logs that the environment when the error appears is jsdom. Are you using any library even in a different Route handler that could inject jsdom?

I am not saying the current behavior from Vercel is correct, it’s not. I just want to debug more. You can also reach out to me in French (if you speak french?) on X:x.com or email: vincent.voyer@vercel.com

2 Likes

@briancharlesdev One reason for typeof window === undefined would be OK while console.log(window) would be undefined is because there must be some code like this somewhere:

const window = createJSDOMWindow();
Object.defineProperty(global, 'window', {
  get() {
    return window;
  },
  configurable: true
});
1 Like

Nice guess for the typeof window === ‘undefined’ but console.log(window) do not give undefined . And thank you for having potentially identify the source of the problem with jsdom that I use in another API route. I send you an email to continue this discussion without putting too much stuff that nobody will read on this forum!

For anyone reading here: we dug into this issue with @briancharlesdev and people internally at Vercel, here’s the summary:

Sometimes Next.js API routes are bundled in the same function and when that’s the case they’ll share the same global scope. This means if one route modifies global objects (like global.window), it can affect other routes in the same bundle.

Workaround: If you need to isolate routes that use libraries affecting the global scope (like jsdom), you can force them into separate bundles by setting different maxDuration values in your vercel.json:

{
  "functions": {
    "api/route-with-jsdom/**": {
      "maxDuration": 10
    },
    "api/other-routes/**": {
      "maxDuration": 5
    }
  }
}

Note: This is a workaround and shouldn’t be relied upon long-term as bundling behavior may change in future versions. As a best practice, avoid modifying the global scope in your API routes.

In this specific case, the issue was caused by a dependency chain (GPXParser.js → jsdom-global) that was modifying the global scope by injecting jsdom, which affected other routes in the same bundle.

1 Like

This topic was automatically closed 7 days after the last reply. New replies are no longer allowed.