expo: TypeError: Network request failed when fetching a file:// uri

Environment

Environment:
  OS: Windows 10
  Node: 8.11.0
  Yarn: 1.7.0
  npm: 5.6.0
  Watchman: Not Found
  Xcode: N/A
  Android Studio: Version  3.1.0.0 AI-173.4907809

Packages: (wanted => installed)
  expo: ^30.0.0 => 30.0.1
  react: 16.3.1 => 16.3.1
  react-native: https://github.com/expo/react-native/archive/sdk-30.0.0.tar.gz => 0.55.4

App target - Standalone & Client for testing

Steps to Reproduce

  1. use this expo example of recording audio
  2. mix it with this example of uploading to firebase

Expected Behavior

fetching a file URI from the device file system should return a valid response.

Actual Behavior

I’m getting a network error exception

TypeError: Network request failed

and if I’m running with remote JS debug mode I can see this error in chrome console

Failed to load file:///data/user/0/host.exp.exponent/cache/ExperienceData/…/Audio/recording-eee85da3-7121-4273-86a7-686098180662.m4a: Cross origin requests are only supported for protocol schemes: http, data, chrome, chrome-extension, https.

Some Code

const asyncUpload = async ()=> {
          try {   
                const response = await fetch(recordInfo.uri);
                console.log("response", response);
                const blob = await response.blob();
                const blob = new Blob();
                upload( blob)
            }catch (e) {
                console.log("error fetching",e);
            }
}

About this issue

  • Original URL
  • State: closed
  • Created 6 years ago
  • Reactions: 2
  • Comments: 16 (2 by maintainers)

Most upvoted comments

TL;DR: Replace fetch().blob() with:

  const blob = await new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest();
    xhr.onload = function() {
      resolve(xhr.response);
    };
    xhr.onerror = function() {
      reject(new TypeError('Network request failed'));
    };
    xhr.responseType = 'blob';
    xhr.open('GET', uri, true);
    xhr.send(null);
  });

Let me guide you through some code to understand the problem better:

Let’s start with fetch function. It’s currently not supported natively by JSC on which JS code runs. When we use React Native, it’s being polyfilled by the framework.

Pre-0.57, React Native used a regular whatwg-fetch polyfill, implementing fetch with XMLHttpRequest (XMLHttpRequest polyfill has always been implemented by RN), to enable developers to use fetch (usage, dependency definition). (whatwg-fetch has set xhr.responseType by default to "blob".) React Native’s XMLHttpRequest implementation, in turn, uses NetworkingModule to issue the request. So, at the end, NetworkingModule.sendRequest is called.

fetch --(whatwg-fetch)--> XMLHttpRequest -> RCTNetworking --(android)--> NetworkingModule.sendRequest

We need to go deeper

Inspecting NetworkingModule.sendRequest method we can see that before any real network request is constructed, it first iterates over mUriHandlers. It’s a list of objects conforming to UriHandler interface, which is very simple, and as the documentation says:

Allows to implement a custom fetching process for specific URIs. It is the handler’s job to fetch the URI and return the JS body payload.

So if we would like to support some custom URI scheme, we would add our own UriHandler to NetworkingModule, nice! 👌

In a basic React Native application (on Android), there is only one such custom handler, created by BlobModulelink. As we can see from the implementation, it runs only for requests that both:

  1. aren’t http:// or https:// and
  2. expect blob as response type.

For such requests, this handler is capable of returning a valid blob as expected. So why doesn’t it work in RN 0.57+?

This commit, which first landed in React Native 0.57 switched the default xhr.responseType from blob to text. Thus, BlobModule’s UriHandler doesn’t kick in, and since okhttp doesn’t recognize URL of file:// scheme, it throws an exception.

It’s important to note that this commit doesn’t introduce a bug. In fact, it fixes a blob leak that was occurring due to fetch requests not releasing underlying native blobs when they’re done with it!

Since we really need this blob, and can’t use fetch (there’s no way to pass an option that would instruct fetch to start with responseType = blob) we have to construct XMLHttpRequest ourselves.

The following snippet creates a very basic blob promise:

const blob = await new Promise((resolve, reject) => {
   const xhr = new XMLHttpRequest();
   xhr.onload = function() {
     resolve(xhr.response); // when BlobModule finishes reading, resolve with the blob
  };
  xhr.onerror = function() {
    reject(new TypeError('Network request failed')); // error occurred, rejecting
  };
  xhr.responseType = 'blob'; // use BlobModule's UriHandler
  xhr.open('GET', uri, true); // fetch the blob from uri in async mode
  xhr.send(null); // no initial data
});

In order not to leak blobs, we should also call blob.close() when we’re done sending it. That’s why the full snippet of asyncUpload should look like this:

async function uploadImageAsync(uri) {
  const blob = await new Promise((resolve, reject) => {
    // ... create and resolve with XMLHttpRequest blob
  });

  // do something with the blob, eg. upload it to firebase (API v5.6.0 below)
  const ref = firebase
    .storage()
    .ref()
    .child(uuid.v4());
  const snapshot = await ref.put(blob);
  const remoteUri = await snapshot.ref.getDownloadURL();

  // when we're done sending it, close and release the blob
  blob.close();

  // return the result, eg. remote URI to the image
  return remoteUri;
}

Blobs in RN need to be brought into compliance with JS specs with proper native implementations. I’m not convinced they should even be called Blob currently.

See: https://github.com/facebook/react-native/issues/22681

TL;DR: Replace fetch().blob() with:

  const blob = await new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest();
    xhr.onload = function() {
      resolve(xhr.response);
    };
    xhr.onerror = function() {
      reject(new TypeError('Network request failed'));
    };
    xhr.responseType = 'blob';
    xhr.open('GET', uri, true);
    xhr.send(null);
  });

Let me guide you through some code to understand the problem better: }

God Damn Hero

Also got this issue.

base64 firebase upload also broken on android