6

I'm using superagent to upload files from my React Native app. It works perfectly fine on iOS, but on Android it gives this error:

Possible causes: the network is offline, Origin is not allowed by Access-Control-Allow-Origin, the page is being unloaded, etc.

I've created a minimal example here https://snack.expo.io/@twohill/upload-example and copied the code below in case the snack goes away:

import React, { Component } from 'react';
import { Text, View, StyleSheet, TouchableOpacity } from 'react-native';
import Constants from 'expo-constants';
import * as DocumentPicker from 'expo-document-picker';
import superagent from 'superagent';

import AssetExample from './components/AssetExample';
import { Card } from 'react-native-paper';

const upload = (file, setMessage) => {
  const { name } = file;

  superagent.post('https://example.org/uploads') // <-- change this URL to your server that accepts uploads and is CORS enabled
    .set('Authorization', 'BEARER xxxyyy') // <-- JWT token here
    .attach('uri', file, name)
    .then(
      result => setMessage(JSON.stringify({result})),
      error => setMessage(JSON.stringify({error}))
      );
};

const pickDocuments = async (setMessage) => {
  const file = await DocumentPicker.getDocumentAsync({ copyToCacheDirectory: true });
  if (file.type === "success") {
    upload(file, setMessage);
  }
}

export default class App extends Component {
  state = {
    message: null,
  }
  render() {
    const { message } = this.state;
    return (

      <View style={styles.container}>
       <TouchableOpacity onPress={() => pickDocuments(message => this.setState({message}))}>
        <Text style={styles.paragraph}>
          Tap to upload a file
        </Text>
        <Card>
          <AssetExample />
        </Card>
        <Card><Text>{message}</Text></Card>
         </TouchableOpacity>
      </View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    paddingTop: Constants.statusBarHeight,
    backgroundColor: '#ecf0f1',
    padding: 8,
  },
  paragraph: {
    margin: 24,
    fontSize: 18,
    fontWeight: 'bold',
    textAlign: 'center',
  },
});

If I console.log the error it gives the following:

Request has been terminated
Possible causes: the network is offline, Origin is not allowed by Access-Control-Allow-Origin, the page is being unloaded, etc.
* http://192.168.1.3:19001/node_modules%5Cexpo%5CAppEntry.bundle?platform=android&dev=true&minify=false&hot=false:282311:24 in crossDomainError
- node_modules\@sentry\utils\dist\instrument.js:224:24 in <anonymous>
- node_modules\event-target-shim\dist\event-target-shim.js:818:39 in EventTarget.prototype.dispatchEvent
- node_modules\react-native\Libraries\Network\XMLHttpRequest.js:566:23 in setReadyState
- node_modules\react-native\Libraries\Network\XMLHttpRequest.js:388:25 in __didCompleteResponse
- node_modules\react-native\Libraries\vendor\emitter\EventEmitter.js:190:12 in emit
- node_modules\react-native\Libraries\BatchedBridge\MessageQueue.js:436:47 in __callFunction
- node_modules\react-native\Libraries\BatchedBridge\MessageQueue.js:111:26 in __guard$argument_0
- node_modules\react-native\Libraries\BatchedBridge\MessageQueue.js:384:10 in __guard
- node_modules\react-native\Libraries\BatchedBridge\MessageQueue.js:110:17 in __guard$argument_0
* [native code]:null in callFunctionReturnFlushedQueue

As far as I can tell, on Android the app never tries an upload. My server runs express and has the cors middleware enabled with the default configuration

{
  "origin": "*",
  "methods": "GET,HEAD,PUT,PATCH,POST,DELETE",
  "preflightContinue": false,
  "optionsSuccessStatus": 204
}

Any ideas what to do here? I get the feeling that the Android is baulking at the "*" origin, but have no idea what to put in place for a mobile app.

Or am I barking up the wrong tree completely?

sideshowbarker
  • 81,827
  • 26
  • 193
  • 197
alt
  • 2,356
  • 2
  • 21
  • 28
  • From https://stackoverflow.com/a/46966838/441757 I see superagent is the source of that error which mentions *“Origin is not allowed by Access-Control-Allow-Origin”* — specifically the code at https://github.com/visionmedia/superagent/blob/master/src/client.js#L671-L682. But that *“Origin is not allowed by Access-Control-Allow-Origin”* part of the message is misleading, because Node doesn’t enforce the same-origin policy; therefore CORS and the Access-Control-Allow-Origin header aren’t relevant. So the actual cause of the error is something else, not CORS. – sideshowbarker Feb 05 '20 at 05:02
  • The answers at https://stackoverflow.com/a/39454628/441757 and https://stackoverflow.com/a/32570827/441757 may or may not be relevant here: *“The cause is ANY button click getting treated as a submit and the form not having a submit action target. One solution is to add a preventDefault event handler.”* – sideshowbarker Feb 05 '20 at 05:52

2 Answers2

1

Thanks for everyone's help. With the confirmation that the error isn't likely to be CORS I did some digging with the debugger and found the actual error: Binary FormData part needs a content-type header

screenshot of XMLHttpRequest

From there, I was able to do even more digging, and found that I was being led astray (at least on Android) by the documentation https://visionmedia.github.io/superagent/#attaching-files as the options map I was sending was being ignored

debugger showing underlying FormData append call

With the help of the react-native-mime-types package the fixed code looks like this:

  //snip
  const fileWithMime = { ...file, type: mime.lookup(name) };

  request.post(`${SERVER_URL}/uploads`)
    .set('Authorization', cookie)
    .withCredentials()
    .attach('uri', fileWithMime)
alt
  • 2,356
  • 2
  • 21
  • 28
-1

http client in react-native is basically native http client with JS bridge, it has no CORS restriction like the web, so you don't need to allow CORS in your express server.

In terms of superagent, it seems you need some tweaking to use it in react-native, checkout this issue

duan
  • 8,515
  • 3
  • 48
  • 70
  • I had assumed that CORS wasn't required, however I also serve web clients so it's needed for that. Also I'm not sure if the issue link applies any more - otherwise wouldn't it be broken on iOS as well? – alt Feb 05 '20 at 03:53