Skip to content

[bug]: Network request failed on Expo SDK 55 / React Native 0.83 — onBeforeUploadBegin never called #1273

@EL-BADI

Description

@EL-BADI

Provide environment information

el_badi_dev@ELs-MacBook-Air birinia % npx envinfo --system --binaries --browsers --npmPackages "typescript,uploadthing,@uploadthing/react,@uploadthing/solid"
Need to install the following packages:
envinfo@7.21.0
Ok to proceed? (y) y


  System:
    OS: macOS 26.2
    CPU: (8) arm64 Apple M1
    Memory: 219.23 MB / 8.00 GB
    Shell: 5.9 - /bin/zsh
  Binaries:
    Node: 24.4.1 - /Users/el_badi_dev/.nvm/versions/node/v24.4.1/bin/node
    npm: 11.4.2 - /Users/el_badi_dev/.nvm/versions/node/v24.4.1/bin/npm
    pnpm: 10.12.1 - /opt/homebrew/bin/pnpm
    bun: 1.0.35 - /opt/homebrew/bin/bun
    Watchman: 2025.09.15.00 - /opt/homebrew/bin/watchman
  Browsers:
    Chrome: 146.0.7680.81
    Edge: 146.0.3856.62
    Safari: 26.2
  npmPackages:
    typescript: ~5.9.2 => 5.9.3 
    uploadthing: ^7.7.3 => 7.7.3 

el_badi_dev@ELs-MacBook-Air birinia %

Describe the bug

🐛 [expo] Network request failed on Expo SDK 55 / React Native 0.83 — onBeforeUploadBegin never called

Description

After upgrading from Expo SDK 53 (RN 0.79) to Expo SDK 55 (RN 0.83), all uploads fail immediately with a Network request failed error. The crash happens before onBeforeUploadBegin is ever called, meaning the failure occurs during the initial presigned URL request — before the upload pipeline even starts.

The same codebase, same uploadthing versions, same backend — works perfectly on SDK 53.

Error

ERROR [Error: Uncaught (in promise, id: 0) TypeError: Network request failed]
Code: fetch.umd.js
  565 |       xhr.onerror = function() {
  566 |         setTimeout(function() {
> 567 |           reject(new TypeError('Network request failed'));
      |                               ^
  568 |         }, 0);
  569 |       };

Call Stack:
setTimeout$argument_0 (node_modules/whatwg-fetch/dist/fetch.umd.js:567:31)

Reproduction

  1. Working project on Expo SDK 53 / RN 0.79 with @uploadthing/expo
  2. Upgrade to Expo SDK 55 / RN 0.83 (New Architecture mandatory, no opt-out)
  3. Trigger openImagePicker
  4. onBeforeUploadBegin is never called — crash happens before it

Environment

  SDK 53 (working) SDK 55 (broken)
expo ^53.0.20 ^55.0.6
react-native 0.79.5 0.83.2
@uploadthing/expo 7.2.5 7.2.6
uploadthing 7.7.3 7.7.4
expo-image-picker ~16.1.4 ~55.0.12
Architecture Old (opt-in new) New Architecture only

Backend: Convex HTTP action (https://xxx.convex.site)
Platform: Android (physical device), both WiFi and mobile data

What was tried

  • @bacons/text-decoder polyfill
  • expo/fetch polyfill via polyfillGlobal
  • ✅ Blocking whatwg-fetch in metro resolveRequest
  • ✅ Blocking specifically when origin is Libraries/Network/fetch.js
  • ✅ Downgrading expo-image-picker and expo-document-picker to SDK 53 versions
  • ✅ Simplifying onBeforeUploadBegin to just pass the file through unchanged
  • ✅ Removing react-native-compressor entirely
  • ✅ Verified EXPO_PUBLIC_SERVER_URL is correct (https://xxx.convex.site)

Key finding

react-native/Libraries/Network/fetch.js does require('whatwg-fetch') in both RN 0.79 and RN 0.83 — so this is not a new change. The whatwg-fetch polyfill was present and working in SDK 53, but causes Network request failed in SDK 55 / RN 0.83 New Architecture (Bridgeless mode).

// node_modules/react-native/Libraries/Network/fetch.js
'use strict';
require('whatwg-fetch'); // <-- present in both RN 0.79 and RN 0.83
export const fetch = global.fetch;
...

This suggests the issue is specifically with how Bridgeless / New Architecture in RN 0.83 handles XHR-based requests from whatwg-fetch when uploadthing makes the initial presigned URL fetch.

Question

Is @uploadthing/expo tested against Expo SDK 55 / RN 0.83 with New Architecture (Bridgeless)? Is there a known workaround or fix in progress?

Link to reproduction

https//:google.com

To reproduce

Steps to Reproduce

  1. Set up a React Native / Expo project using Expo SDK 55 (React Native 0.83, New Architecture mandatory)
  2. Install uploadthing packages:
    npm install uploadthing @uploadthing/expo expo-image-picker expo-document-picker
  3. Set up a Convex HTTP action as the uploadthing backend and set EXPO_PUBLIC_SERVER_URL=https://xxx.convex.site
  4. Generate helpers in uploadthing.ts:
    import { generateReactNativeHelpers } from "@uploadthing/expo";
    export const { useImageUploader } = generateReactNativeHelpers({
      url: process.env.EXPO_PUBLIC_SERVER_URL,
    });
  5. Use useImageUploader in a component and call openImagePicker
  6. Add a console.log inside onBeforeUploadBegin to verify it is called
  7. Run on a physical Android device and trigger the image picker
  8. Expected: onBeforeUploadBegin is called, upload proceeds
  9. Actual: App crashes with TypeError: Network request failed from whatwg-fetch/dist/fetch.umd.js:567onBeforeUploadBegin is never reached

Additional information

Additional Context — Uploadthing Implementation Files

uploadthing.ts (client helper setup)

import { generateReactNativeHelpers } from "@uploadthing/expo";
import { OurFileRouter } from "~/convex/uploadthing/index";

export const { useImageUploader, useDocumentUploader } =
  generateReactNativeHelpers<OurFileRouter>({
    /**
     * Your server url.
     * @default process.env.EXPO_PUBLIC_SERVER_URL
     * @remarks In dev we will also try to use Expo.debuggerHost
     */
    url: process.env.EXPO_PUBLIC_SERVER_URL,
  });

Note: Backend is a Convex HTTP action, not Expo API routes. EXPO_PUBLIC_SERVER_URL is set to https://xxx.convex.site.


photo-picker.tsx (upload component)

import { useAuthToken } from "@convex-dev/auth/react";
import { openSettings } from "expo-linking";
import React from "react";
import { Alert } from "react-native";
import { PhotoIcon } from "react-native-heroicons/outline";
import { useImageUploader } from "~/src/lib/uploadthing";
import { Button } from "../ui/button";
import { useTheme } from "~/src/hooks/use-theme";

interface PhotoPickerProps {
  onImageSelected: (localUri: string) => string;
  onUploadComplete: (imageId: string, uploadedUrl: string) => void;
  onUploadError: (imageId: string, error: string) => void;
}

const PhotoPicker = ({
  onImageSelected,
  onUploadComplete,
  onUploadError,
}: PhotoPickerProps) => {
  const { theme } = useTheme();
  const token = useAuthToken();

  let currentImageId: string | null = null;

  const { openImagePicker } = useImageUploader("imageUploader", {
    headers: (): Record<string, string> => {
      if (!token) return {};
      return { Authorization: `Bearer ${token}` };
    },

    onClientUploadComplete: async (e) => {
      if (e[0].ufsUrl && e[0].customId && currentImageId) {
        const urlParts = e[0].ufsUrl.split("/");
        urlParts.pop();
        const baseUrl = urlParts.join("/");
        const imageUrl = `${baseUrl}/${e[0].customId}`;
        onUploadComplete(currentImageId, imageUrl);
        currentImageId = null;
      }
    },

    // ⚠️ This callback is NEVER reached — crash happens before it
    onBeforeUploadBegin: async (files: any[]) => {
      const file = files[0];
      currentImageId = onImageSelected(file.uri);
      return [file];
    },

    onUploadError: (error) => {
      if (currentImageId) {
        onUploadError(currentImageId, error.message || "فشل في رفع الصورة");
        currentImageId = null;
      }
      Alert.alert("خطأ في الرفع", error.message || "فشل في رفع الصورة");
    },
  });

  const handlePress = () => {
    openImagePicker({
      source: "library",
      onInsufficientPermissions: () => {
        Alert.alert(
          "لا توجد صلاحيات",
          "تحتاج إلى منح صلاحية للوصول إلى الصور لاستخدام هذه الميزة",
          [{ text: "إلغاء" }, { text: "فتح الإعدادات", onPress: openSettings }]
        );
      },
    });
  };

  return (
    <Button
      onPress={handlePress}
      variant="outline"
      className="w-20 h-20 rounded-xl border-2 border-dashed border-gray-300 bg-gray-50"
    >
      <PhotoIcon size={24} color={theme.foreground} />
    </Button>
  );
};

export default PhotoPicker;

Key observations

  • onBeforeUploadBegin is never called — confirmed via console log
  • The crash is in whatwg-fetch/dist/fetch.umd.js at the XHR onerror handler
  • This means the failure is in the initial presigned URL request to the Convex backend, before the upload pipeline starts
  • The same code works on SDK 53 / RN 0.79 with no changes

👨‍👧‍👦 Contributing

  • 🙋‍♂️ Yes, I'd be down to file a PR fixing this bug!

Code of Conduct

  • I agree to follow this project's Code of Conduct

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:packagesissue regarding one of the uploadthing packagesstaleNo activity in the past 10 days🐛 bug: unconfirmed

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions