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

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions