jaydenseric / graphql-upload

Middleware and a scalar Upload to add support for GraphQL multipart requests (file uploads via queries and mutations) to various Node.js GraphQL servers.
https://npm.im/graphql-upload
MIT License
1.43k stars 132 forks source link

400 error, however I added logging and data shows as valid #233

Closed toscanov closed 3 years ago

toscanov commented 3 years ago

I know this is a common issues, and I've made sure the app is sending the right data. I've even added logging to show that the fields are being populated correctly.

The one thing I noticed is the console.log I have inside parser.once('finish' () => {}) wasn't being called, which is where the "Missing multipart field" error should be coming from.  Any insight would be appreciated.

progressRequest - operations called {
  variables: { file: null, name: 'test' },
  query: 'mutation ($file: Upload!, $name: String!) {\n' +
    '  createDocument(file: $file, name: $name) {\n' +
    '    id\n' +
    '    name\n' +
    '    __typename\n' +
    '  }\n' +
    '}\n'
}
processRequest - map called { '1': [ 'variables.file' ] }
processRequest - map entries [ [ '1', [ 'variables.file' ] ] ]
processRequest - map complete Map(1) {
  '1' => Upload {
    resolve: [Function (anonymous)],
    reject: [Function (anonymous)],
    promise: Promise { <pending> }
  }
}
processRequest - release Map(1) {
  '1' => Upload {
    resolve: [Function (anonymous)],
    reject: [Function (anonymous)],
    promise: Promise { [Object] },
    file: {
      filename: 'file.docx',
      mimetype: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
      encoding: '7bit',
      createReadStream: [Function: createReadStream]
    }
  }
}
BadRequestError: Missing multipart field ‘operations’ (https://github.com/jaydenseric/graphql-multipart-request-spec).

Client post has the following:

------WebKitFormBoundaryVcAmZ4u9BOEmAIVW
Content-Disposition: form-data; name="operations"

{"variables":{"file":null,"name":"test"},"query":"mutation ($file: Upload!, $name: String!) {\n  createDocument(file: $file, name: $name) {\n    id\n    name\n    __typename\n  }\n}\n"}
------WebKitFormBoundaryVcAmZ4u9BOEmAIVW
Content-Disposition: form-data; name="map"

{"1":["variables.file"]}
------WebKitFormBoundaryVcAmZ4u9BOEmAIVW
Content-Disposition: form-data; name="1"; filename="file.docx"
Content-Type: application/vnd.openxmlformats-officedocument.wordprocessingml.document

------WebKitFormBoundaryVcAmZ4u9BOEmAIVW--  

Server Typescript:

import { FileUpload, GraphQLUpload } from "graphql-upload";
import { Mutation, Arg, Resolver } from "type-graphql";
import { Document } from "../Document";

@Resolver(Document)
export class DocumentResolver {

    @Mutation(() => Document)
    async createDocument(@Arg("name") name: string, @Arg("file", () => GraphQLUpload) file: FileUpload): Promise<Document> {
        console.warn(name, file);        
        return null; // dont care about getting anything back as long as I see my console message
    };
}

Client Typescript:

import { Component } from "@angular/core";
import { Apollo, gql } from "apollo-angular";

@Component({
  selector: 'test-page',
  templateUrl: './test.html'
})
export class TestPageComponent {

  constructor(private apollo: Apollo) { }

  public onFileSelected(e: any): void {
    if (e.target.validity.valid) {
      this.serverResponse = {};
      this.serverResponseErr = {};

      let file = e.target.files[0];
      this.loadFileApollo(file);
    }
  }

  public loadFileApollo(file: File): void {
    const GRAPHQL_CREATE_TEMPLATE = `
    mutation($file: Upload!, $name: String!) {
      createDocument(file: $file, name: $name) {
        id,
        name
      }
    }`;
    // 
    //var ti = { file: file };
    this.apollo.mutate<any>({
      mutation: gql(GRAPHQL_CREATE_TEMPLATE),
      variables: { file: file, name: "test" },
      context: {
        useMultipart: true
      }
    }).subscribe(x=> console.warn(x));
  }
}

HTML:

  <div>
    Click this button to select a file and immediately upload it to the server
    <input class="btn btn-primary" type="file" (change)="onFileSelected($event)"/>
  </div>
toscanov commented 3 years ago

I may have some additional information.

Running apollo-server, added graphql-upload supposedly this is already included, there's no documentation on how to add this via typescript only if you are using a graphql file. I did finally find the one throwing the error that was inside graphql-upload under apollo-server-core.

app.use(
    graphqlUploadExpress()
);
jaydenseric commented 3 years ago

The multipart request the client is sending looks ok to me at first glance, so chances are your problem is not the client.

Unfortunately Apollo Server's built in implementation of graphql-upload uses a very outdated major version of graphql-upload; there are a lot of issues in their repo about the frustrations this causes. They refuse to update the dependency because they are not yet ready to market an Apollo Server v3. Please completely disable their implementation of file uploads (you can find instructions in the Apollo Server issues) and do a fresh manual setup of the latest graphql-upload version. If you have not already done this properly, you will for sure experience issues. Issues from the old graphql-version that have since been fixed (e.g. support for recent Node.js versions), and issues from having multiple instances of graphql-upload being used at once and conflicting with each other (if you attempted a manual installation without undoing the Apollo Server integration first).

If you manage to isolate a graphql-upload bug in a correct setup, we can reopen and investigate further.

toscanov commented 3 years ago

Thanks, I'll look into disabling their implementation. I'm testing my theory that they don't export the graphqlUploadExpress function which might be causing some problems as that's the only way to add the scalar upload when you're autogenerating the file.

jaydenseric commented 3 years ago

they don't export the graphqlUploadExpress function which might be causing some problems as that's the only way to add the scalar upload when you're autogenerating the file

This confuses me. What file are you "autogenerating", the schema? The Express middleware is to process an incoming GraphQL multipart request; you don't need it if all you are doing is introspecting the schema to get types or something.

The actual Upload scalar is available on it's own:

https://github.com/jaydenseric/graphql-upload#class-graphqlupload

toscanov commented 3 years ago

Sorry, all the documentation basically says add "scalar Upload" to the schema, but you are correct I'm autogenerating the schema which is exhausting because of the utter lack of information out there. Without adding the middleware I haven't been able to determine how to add the scalar Upload to the schema being generated. I think once I get my code back to normal and disable their upload I might be okay.

And thank you for all this help, I appreciate the time you've given me.

toscanov commented 3 years ago

Thank you again, It worked once I disabled their upload via the apollo config settings "uploads: false"