Search code examples
node.jsgraphqlaxiosapollo-serverfastify

How to upload images to fastify + graphql backend with axios?


When sending images via axios I found I have to use formdata. I add my images here but when sending the formdata my entire backend just freezes, just says "pending".

Ive been following this

And my attempt so far:

backend:

Apollo:

import { ApolloServer, makeExecutableSchema } from 'apollo-server-fastify';

const schema = makeExecutableSchema({ typeDefs, resolvers });

const apolloServer = new ApolloServer({
  schema,
  uploads: {
    maxFileSize: 10000000,
    maxFiles: 5,
  },
});

(async function() {
  app.register(apolloServer.createHandler({ path: '/api' }));
})();

schema:

  scalar DateTime
  scalar Upload

  input addUser {
    Email: String!
    Password: String
    FirstName: String!
    LastName: String!
    Age: DateTime!
    JobTitle: String!
    File: Upload
  }

  type Mutation {
    register(input: addUser!): Boolean
  }

resolver:

  Mutation: {
    register: async (obj, args, context, info) => {
        // how to get the formData?
      },
  }

FrontEnd:

I build the request like this:

const getMutation = (mutate: MutationNames, returParams?: any): any => {
  const mutation = {
    login: print(
      gql`
        mutation($email: String!, $password: String!) {
          login(email: $email, password: $password) {
            token
            refreshToken
          }
        }
      `
    ),
    register: print(
      gql`
        mutation(
          $firstName: String!
          $email: String!
          $lastName: String!
          $age: DateTime!
          $jobTitle: String!
          $file: Upload
        ) {
          register(
            input: {
              FirstName: $firstName
              LastName: $lastName
              Email: $email
              Age: $age
              JobTitle: $jobTitle
              File: $file
            }
          )
        }
      `
    ),

  }[mutate];

  if (!mutation) return {};

  return mutation;
};

In this case im using the register mutation.

I have a few hooks on how I handle the data fetching so Im not going to include it since it is alot of code. The data is fetched correctly in the front end and before posting to the backend im putting everything to a formData object:

  const submitForm: SubmitForm = (obj: SendObject) => {
    const Fdata = new FormData();

    Fdata.append('0', fileImp.file);

    Fdata.append('operations', JSON.stringify(obj.data));

    const map = {
      '0': ['variables.file'],
    };
    Fdata.append('map', JSON.stringify(map));

    callAxiosFn(
      {
        method,
        url: 'http://localhost:4000/api',
        data: Fdata,
        // headers: obj.headers,
      },
      qlType.toString()
    );
  };

gets called like this:

  const response = await axios({
    headers: {
      Accept: 'application/json',
      'x-token': localStorage.getItem('token'),
      'x-refresh-token': localStorage.getItem('refreshToken'),
      ...(config.headers || {}),
    },
    ...config,
  });

config is AxiosRequestConfig

What Im sending:

enter image description here

I dont exactly understand How the formdata will hit my resolver endpoint and for that reason im doing something wrong since the backend returns:

(node:748) UnhandledPromiseRejectionWarning: [object Array] (node:748) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). (rejection id: 1) (node:748) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.

I realize this is alot but Im at the end of my vits here, been at this the entire day. Any help is deeply appreciated.

EDIT:

Since my backend was questioned I thought I would just show that when sending data without appending Formdata like I do above then I get it working:

  const submitForm: SubmitForm = (obj: SendObject) => {

    callAxiosFn(
      {
        method,
        url: 'http://localhost:4000/api',
        data: obj.data,
      },
      qlType.toString()
    );
  };

obj.data is:

{query: "mutation ($firstName: String!, $email: String!, $l… Age: $age, JobTitle: $jobTitle, File: $file})↵}↵", variables: {…}}
query: "mutation ($firstName: String!, $email: String!, $lastName: String!, $age: DateTime!, $jobTitle: String!, $file: Upload) {↵  register(input: {FirstName: $firstName, LastName: $lastName, Email: $email, Age: $age, JobTitle: $jobTitle, File: $file})↵}↵"
variables:
age: "1977-04-04"
email: "[email protected]"
file: File {name: "something.jpg", lastModified: 1589557760497, lastModifiedDate: Fri May 15 2020 17:49:20 GMT+0200 (centraleuropeisk sommartid), webkitRelativePath: "", size: 32355, …}
firstName: "Jhon"
jobTitle: "SomethingCool"
lastName: "Doe"
password: "CoolPassword!"123"
__proto__: Object
__proto__: Object

query getting sent in the browser:

enter image description here

Backend reciving the data but the image is not included: enter image description here

EDIT:

Recently found that my fastify backend might have issues with reading formData. tried installing

fastify-multipart

but got errors when registering it:

FST_ERR_CTP_ALREADY_PRESENT(contentType) ^ FastifyError [FST_ERR_CTP_ALREADY_PRESENT]:

After that I tried:

npm uninstall fastify-file-upload

Error remained.


Solution

  • This took some time and usally when you take something for granted it takes time to find the mistake.

    For anyone having the same problem please remember that the order you add something MATTERS!

    What I did:

    const Fdata = new FormData();
    
    Fdata.append('0', fileImp.file);  // NOTICE THIS
    
    Fdata.append('operations', JSON.stringify(obj.data));
    
    const map = { // NOTICE THIS
      '0': ['variables.file'],
    };
    Fdata.append('map', JSON.stringify(map));
    

    Problem: Remember when I said order of appending things matter? Well the case here was that the mapping was added after the file was added.

    The correct way:

    const Fdata = new FormData();
    
    Fdata.append('operations', JSON.stringify(obj.data));
    
    const map = { // NOTICE THIS
      '0': ['variables.file'],
    };
    Fdata.append('map', JSON.stringify(map));
    Fdata.append('0', fileImp.file);  // NOTICE THIS
    

    Also note that in my qestion I missed setting the file itself to null in the variables:

      variables: {
        file: null,
      },
    

    This has to be done.

    For more info read here