Edd Chang
Edd Chang

Reputation: 1013

GraphQL: Error when resolving promises during file upload

So I've been working with GraphQL uploads, and before stating my problem here's an overview for the tech stack that I am using:

Backend: Mongoose, Express, Apollo, GraphQL

Frontend: VueJS, Apollo, GraphQL

I'm using Apollo Upload Client to send the Upload files to the server side from the client. Since I am sending a list of files type scalar Upload from the client, I am receiving a list of promises that need to be resolved. On using Promise.all() I am getting the following error (which, weirdly, I wasn't getting before and I don't know why). If I upload more than one file, the first file just gets lost somewhere and the second file uploads.... But this isn't all the time. Sometimes it doesn't happen. Maybe I am not resolving or catering to the promises properly. Note that I also have to save the file name in MongoDB through Mongoose

    { BadRequestError: Request disconnected during file upload stream parsing.
     at IncomingMessage.request.once (F:\repos\pushbox\node_modules\graphql-upload\lib\processRequest.js:245:35)
     at Object.onceWrapper (events.js:285:13)
     at IncomingMessage.emit (events.js:197:13)
     at resOnFinish (_http_server.js:583:7)
     at ServerResponse.emit (events.js:202:15)
     at onFinish (_http_outgoing.js:683:10)
     at processTicksAndRejections (internal/process/next_tick.js:74:9)
   message: 'Request disconnected during file upload stream parsing.',
   expose: true,
   statusCode: 499,
   status: 499 }

I have an HTML file input tag that takes multiple files and the mutation I use is:

async uploadFiles() {
  // Check if input tag is empty
  if (this.files.length === 0) {
    this.uploadErrorAlert = true;
    return;
  }

  // Mutation
  this.isUploading = true;
  await this.$apollo.mutate({
    mutation: UPLOAD_FILES,
    variables: {
      files: this.files,
      id: this.selectedCard.id,
    },
  })
    .then(() => {
    // clear files from the input tag
      this.files = '';
      this.$refs.selectedFiles.value = '';
      this.isUploading = false;
    })
    .catch((err) => {
      console.error(err);
    });
},

And finally, the resolver on the server is:

/**
 * Uploads files sent on disk and saves
 * the file names in the DB
 *
 * @param {Object} attachments - List of files for a card
 *
 * @return {Boolean} - true if upload is
 * successful
 */
uploadFiles: async (_, attachments, { controllers }) => {
  Promise.all(attachments.files.map(async (file) => {
    const { createReadStream, filename } = await file;
    const stream = createReadStream();

    /**
     * We need unique names for every file being uploaded,
     * so we use the ID generated by MongoDB and concat it
     * to the filename sent by the user.
     *
     * Therefore we instantiate an attachment object to get an ID
     */
    const attachment = await controllers.attachment.add({ id: attachments.id, file: '' });
    const newFileName = `${attachment.id}_${filename}`;
    const path = `${process.env.UPLOAD_DIR}/${newFileName}`;

    await controllers.attachment.update({
      id: attachment.id,
      file: newFileName,
    });

    console.log(`reached for ${path}`);
    // Attempting to save file in server
    return new Promise((resolve, reject) => stream
      .pipe(createWriteStream(path))
      .on('finish', () => resolve())
      .on('error', (error) => {
        console.log('dude?');
        if (stream.truncated) {
          // Delete the truncated file
          unlinkSync(path);
        }
        reject(error);
      }));
  })).then(() => {
    pubsub.publish(ATTACHMENTS_ADDED, { attachmentsChanged: controllers.attachment.getAll() });
  }).catch((err) => {
    console.log(err);
  });
},

Any help would be appreciated!

Upvotes: 4

Views: 4202

Answers (1)

Edd Chang
Edd Chang

Reputation: 1013

Okay so I don't know how I missed this issue here, but this right there is the solution! The issue is on the module's, that I am using, github issue forum.

So the problem is solved by using await before the Promise.all() function. So now the code inside the uploadFiles resolver looks like:

await Promise.all(attachments.files.map(async (file) => {
    const { createReadStream, filename } = await file;
    const stream = createReadStream();

    /**
     * We need unique names for every file being uploaded,
     * so we use the ID generated by MongoDB and concat it
     * to the filename sent by the user.
     *
     * Therefore we instantiate an attachment object to get an ID
     */
    const attachment = await controllers.attachment.add({ id: attachments.id, file: '' });
    const newFileName = `${attachment.id}_${filename}`;
    const path = `${process.env.UPLOAD_DIR}/${newFileName}`;

    await controllers.attachment.update({
      id: attachment.id,
      file: newFileName,
    });

    console.log(`reached for ${path}`);
    // Attempting to save file in server
    return new Promise((resolve, reject) => stream
      .pipe(createWriteStream(path))
      .on('finish', () => resolve())
      .on('error', (error) => {
        console.log('dude?');
        if (stream.truncated) {
          // Delete the truncated file
          unlinkSync(path);
        }
        reject(error);
      }));
  })).then(() => {
    pubsub.publish(ATTACHMENTS_ADDED, { attachmentsChanged: controllers.attachment.getAll() });
  }).catch((err) => {
    console.log(err);
  });

Upvotes: 3

Related Questions