jaydenseric/apollo-upload-client

Blob data not included in multipart request in React Native

agj32mrgibbits opened this issue · 3 comments

I've been able to get apollo-upload-client setup and sending multipart requests for uploads but I cannot get it to actually send the payload along with the request.

Versions:

"@apollo/client": "^3.0.1",
"apollo-upload-client": "^17.0.0",
"graphql": "^15.3.0",
"graphql.macro": "^1.4.2",
"react": "16.13.1",
"react-native": "0.63.2",

Client setup:

const cache = new InMemoryCache();
const httpLink = createUploadLink({
  uri: Config.API_URL,
});

const authLink = setContext((_, {headers}) =>
  getItem('@clientToken')
    .then((token) => {
      if (!token) {
        return;
      }

      return {
        headers: {
          ...headers,
          'X-Auth-Token': `${token}`,
        },
      };
    })
    .catch((err) => console.error(err)),
);

const errorLink = some error handling

const defaultOptions = {
  query: {
    fetchPolicy: 'no-cache',
    errorPolicy: 'all',
  },
};

const link = ApolloLink.from([authLink, errorLink, httpLink]);

export const client = new ApolloClient({
  link,
  cache,
  defaultOptions,
});

Mutation:

mutation SaveTicketSignature(
    $TicketID: Int!
    $FullName: String!
    $Signature: Upload!
) {
    SaveTicketSignature(TicketID: $TicketID, FullName: $FullName, Signature: $Signature)
}

Action to send Mutation:

const TICKET_SAVE_SIGNATURE = loader('../../../gql/mutations/SaveTicketSignature.gql');

export const Actions = (update) => ({
  ...other actions...
  saveTicketSignature: (TicketID, FullName, Signature) => {
    console.info(`saving ticket signature for ticket ${TicketID}`);
    client
      .mutate({
        mutation: TICKET_SAVE_SIGNATURE,
        variables: {
          TicketID: TicketID,
          FullName: FullName,
          Signature: Signature,
        },
      })
      .then((result) => {
        console.log('result', result);
      })
      .catch((err) => {
        console.log('error',err);
      });
  },
});

Save Handler:

const handleSave = (sigResult) => {
    console.log('sig saved');
    const upload = new Blob([Buffer.from(sigResult.encoded,'base64')],{type: 'image/png'});
    console.log('upload:',upload);
    actions.saveTicketSignature(
      ticketID,
      fullName,
      upload,
    );
  };

Console log:

LOG      sig saved
LOG      upload: {"_data": {"__collector": {}, "blobId": "330bffa9-ba34-4633-93ae-6f1375564e3f", "lastModified": undefined, "offset": 0, "size": 13029, "type": "image/png"}}
INFO     saving ticket signature for ticket 18803
ERROR    [Network error]: TypeError: Network request failed
LOG      error [Error: Network request failed]

Server log:

[DEBUG] REQUEST:
POST /graphql HTTP/1.1
Host: x
Accept: */*
Accept-Encoding: gzip
Connection: Keep-Alive
Content-Length: 592
Content-Type: multipart/form-data; boundary=3326165f-d882-43df-817e-acd3fa604ae8
User-Agent: okhttp/3.12.1
X-Auth-Token: x

--3326165f-d882-43df-817e-acd3fa604ae8
content-disposition: form-data; name="operations"
Content-Length: 297

{"operationName":"SaveTicketSignature","variables":{"TicketID":18803,"FullName":"test","Signature":null},"query":"mutation SaveTicketSignature($TicketID: Int!, $FullName: String!, $Signature: Upload!) {\n  SaveTicketSignature(TicketID: $TicketID, FullName: $FullName, Signature: $Signature)\n}\n"}
--3326165f-d882-43df-817e-acd3fa604ae8
content-disposition: form-data; name="map"
Content-Length: 29

{"1":["variables.Signature"]}
--3326165f-d882-43df-817e-acd3fa604ae8--

[DEBUG] status=422 body={"errors":[{"message":"failed to get key 1 from form"}],"data":null}

As you can see, it constructs the multipart request and variable map but it doesn't include the payload. Any ideas?

I'm rusty about the state of React Native and their non-standard FormData implementation (it doesn't properly use the File and Blob globals), but are you sure you shouldn't be using the class ReactNativeFile?

You might have to use a special scheme in the uri to inline the blob, idk.

Note that in future versions I'll be removing out of the box hacks and workarounds for React Native; I only intend to support runtime environments that support web standards moving forwards.

Using ReactNativeFile with a base64 uri gives [Network error]: TypeError: Network request failed and the request doesn't reach the server.

const upload = new ReactNativeFile({
  uri: `data:image/png;base64,${sigResult.encoded}`,
  type: 'image/png',
  name: 'signature.png',
});

I've already disabled flipper in case that was messing with things.

apollo-upload-client v18 has been published, which removes out of the box support for React Native. As per the release notes, you should be able to configure it to get things working in React Native but it won't be something I will be explicitly endorsing or supporting moving forwards.

It’s the responsibility of Facebook to adhere to web standards and implement spec-complaint Blob, File, and FormData globals in the React Native environment.

If you manage to figure out the solution to your React Native issue, feel free to leave it here in a comment. Likewise, anyone else feel free to contribute suggestions.