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

Missing multipart field 'operations' #164

Closed adriaanbalt closed 5 years ago

adriaanbalt commented 5 years ago

Firstly, thanks for all this great work; I appreciate it.

I've been trying to send files with GraphQL over Firebase HTTP Cloud Functions and upload them to Firebase Storage as well as update Firebase Firestore DB; ideally using a Firebase Transaction. For some reason I keep getting the following error: BadRequestError: Missing multipart field ‘operations’ (https://github.com/jaydenseric/graphql-multipart-request-spec).

I've tried a bunch of things using Busboy, Rawbody and Express-Multipart-File-Parser; you can see a conversation I've been having with snippets of my code in this "issue" here. Note: code snippets are also copied at the bottom

Even with the above setup (using uploads property when using ApolloServer, etc) I am still getting the BadRequestError. My headers look like this:

Screen Shot 2019-10-14 at 5 37 07 PM

And here is a curl:

curl 'http://localhost:5000/fairplay-app/us-central1/api' 
    -H 'accept: */*' -H 'Referer: http://localhost:3000/add' 
    -H 'Origin: http://localhost:3000' 
    -H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36' 
    -H 'Sec-Fetch-Mode: cors' 
    -H 'Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryQPiCZ99VZAAqVJQY' --data-binary $'------WebKitFormBoundaryQPiCZ99VZAAqVJQY\r\nContent-Disposition: form-data; name="operations"\r\n\r\n{"operationName":"SingleUpload","variables":{"file":null},"query":"mutation SingleUpload($file: Upload\u0021) {\\n  singleUpload(file: $file) {\\n    filename\\n    mimetype\\n    encoding\\n    __typename\\n  }\\n}\\n"}\r\n------WebKitFormBoundaryQPiCZ99VZAAqVJQY\r\nContent-Disposition: form-data; name="map"\r\n\r\n{"1":["variables.file"]}\r\n------WebKitFormBoundaryQPiCZ99VZAAqVJQY\r\nContent-Disposition: form-data; name="1"; filename="cooktop-scratches.jpg"\r\nContent-Type: image/jpeg\r\n\r\n\r\n------WebKitFormBoundaryQPiCZ99VZAAqVJQY--\r\n' --compressed

It appears that I am sending operations in Form Data. What am I missing with my server setup?

Server:

const express = require('express')
const cors = require('cors');
const { ApolloServer } = require('apollo-server-express')
const fileParser = require('express-multipart-file-parser')
const schema = require('./schema')
const resolvers = require('./resolvers')

const app = express();
// cors allows our server to accept requests from different origins
app.use(cors());
app.options('*', cors());
app.use(fileParser) // supposedly this will fix the issue but doesn't seem to work
// setup server
const server = new ApolloServer({
    typeDefs: schema,
    resolvers,
    introspection: true, // so we can access the playground in production reference: https://www.apollographql.com/docs/apollo-server/api/apollo-server/#constructor-options-lt-ApolloServer-gt
    playground: true,
    uploads: {
        // Limits here should be stricter than config for surrounding
        // infrastructure such as Nginx so errors can be handled elegantly by
        // graphql-upload:
        // https://github.com/jaydenseric/graphql-upload#type-processrequestoptions
        maxFileSize: 10000000, // 10 MB
        maxFiles: 1
    },
})
server.applyMiddleware({ app, path: '/', cors: true })

React Front End:

import React from 'react'
import ReactDOM from 'react-dom'
import { ApolloProvider } from 'react-apollo'
import { ApolloClient } from 'apollo-client'
import { InMemoryCache } from 'apollo-cache-inmemory'
import { createUploadLink } from 'apollo-upload-client'
import { Provider } from 'react-redux'
import store, { history } from './Store'
import { ConnectedRouter } from 'react-router-redux'
import App from './App'
import registerServiceWorker from './lib/serviceWorker'
import './index.scss'
import { GRAPHQL_URL} from './constants/graphql'

const uploadLink = createUploadLink({
    uri: GRAPHQL_URL, // Apollo Server is served from port 4000
    headers: {
        "keep-alive": "true"
    }
})
const apolloCache = new InMemoryCache()

const client = new ApolloClient({
    cache: apolloCache,
    link: uploadLink,
    uri: GRAPHQL_URL,
});

ReactDOM.render(
    <ApolloProvider client={client}>
        <Provider store={store}>
            <ConnectedRouter history={history}>
                <App />
            </ConnectedRouter>
        </Provider>
    </ApolloProvider>,
    document.getElementById('root')
)
registerServiceWorker()
jaydenseric commented 5 years ago

graphql-upload is implemented under the hood by Apollo Server (if you have an up to date version), so you don't need to set it up manually.

You definitely don't want the app.use(fileParser) bit, it could interfere.

I don't know much about Firebase, but if you check the issues here you will see that some cloud environments (particularly serverless ones) don't support standard multipart requests, or they do, but they do all the parsing for you and pass the result on from memory.

mike-marcacci commented 5 years ago

As @jaydenseric mentioned, you definitely can't use express-multipart-file-parser to consume and parse the incoming request stream, and then expect graphql-upload to do the same, as it's already been consumed.

I think it's safe to say that you can close your other issue at least as it relates to its use in conjunction with this library.

I also see that you're running this in Google Cloud Functions. The HTTP emulator for cloud functions deviates from node's standards in some important ways. You can make this work, but it's not optimized for that use case. If you know that you'll just be dealing with small files, this isn't a serious concern. However, if you expect large files or serious traffic, you may want to start an API-compatible version of this package that plays more nicely with Google Could Functions.

I may be missing something about your use case, so I'll leave this open for now, but I suspect that this can be closed in favor of #129.

adriaanbalt commented 5 years ago

@mike-marcacci thanks for these details! To do #129 I will have to fork and link the NPM to test the implementation. I'm only sending images and eventually videos.

I was thinking to avoid using graphql to upload, go directly to my storage containers. What are the pros vs cons with these two options? Thanks!

mike-marcacci commented 5 years ago

@adriaanbalt if you are set on using cloud functions for your API, that may be a good workaround. They really aren't designed to do any sort of heavy lifting, and they have hard timeouts that will make uploads of large videos impossible.

IIRC Google Cloud Storage has pre-signed URLs, so it may be possible to:

  1. create a GraphQL mutation for generating and returning a pre-signed URL
  2. have the client upload the file to the pre-signed URL
  3. use Google Pub-Sub to listen for completion of an upload to that URL
  4. process the upload

From an API simplicity perspective this is not ideal, but from an implementation perspective it may do the trick.

I'm going to go ahead and close this issue, as I don't think it's a bug here :)

adriaanbalt commented 5 years ago

Thanks @mike-marcacci Just so you have a sense of what I'm trying to create:

I'm using cloud functions to "host" the GraphQL API like this

Originally I was going to setup my app like this:

  1. User creates a post of text and imagery (possibly video 1 day)
  2. When the user submits this new post, the app first uploads the image/video to Storage and separately creates a GraphQL mutation to update the DB with the path to the image in storage as well as the text.

Then I came across graphql-upload and thats when I became curious as to the potential to also send a file across the mutation. Unfortunately Cloud Functions http don't process multipart/form-data without busboy, which is under the hood of express-multipart-file-parser.

So it looks like I'm back to my original approach. Unless you can suggest an alternative? Thanks for all your help and guidance; loving and learning so much when using these utils!

mike-marcacci commented 5 years ago

@adriaanbalt, yes graphql-upload would be a very elegant solution, but isn't a great fit for cloud functions. (Large file uploads in general aren't a great fit for cloud functions...)

I think what you are describing is a perfectly fine strategy, especially if you're in control of both the server and client. I will mention, though, that you will want to be able to control who has access to upload files to prevent potentially costly abuse of your systems (hence my reference to pre-signed URLs).

stevewirig commented 4 years ago

@adriaanbalt I have been headed down the exact same path as you with Firebase Functions. I have all of my file uploads working on local nodejs server, but when I run via the cloud functions I too am seeing the Bad Request Error. Before I keep digging and investigating my own potential solution and reverting back to interfacing directly with Firebase Storage, I wanted to get your thoughts on where you ended up landing with GraphQL FileUploads via firebase functions...

Aslemammad commented 3 years ago

For folks that encountered this issue, if you have middlewares like express-fileupload or fileParser, remove them; they won't let graphql-upload consume the data. That's how I solved it.

ghost commented 3 years ago

I solve it, by adding uploads: false: const server = new ApolloServer({ uploads:false, schema, playground: true });

namadaza commented 3 years ago

@Enmanuellperez98 +1 to this solution, was the fix for me as well. Not sure the root cause of the error, but I'll take it 😄

ghost commented 3 years ago

Hey @namadaza, It seems that in previous versions of Apollo Server, they had a built-in integration with an old version of graphql-upload. So you have to indicate with uploads: false, that you are integrating it yourself.

Take a look at this implementation of the repo author: https://github.com/jaydenseric/apollo-upload-examples/blob/master/api/server.mjs

new ApolloServer ({
   // Disable the built in file upload implementation that uses an outdated
   // `graphql-upload` version, see:
   // https://github.com/apollographql/apollo-server/issues/3508#issuecomment-662371289
   uploads: false,
   schema,
}). applyMiddleware ({app}); 
Arideno commented 3 years ago

Hi, a have this code in NestJS app module

GraphQLModule.forRoot({
      autoSchemaFile: true,
      sortSchema: true,
      uploads: false,
}),

but I also get "Missing multipart field 'operations'". Anybody knows how to figure this out?

omar-dulaimi commented 3 years ago

This can happen when using apollo server express to host a graphql and a rest api. The failing request was multipart. So to fix it, I changed it to application/x-www-form-urlencoded from the frontend.

Hope this helps someone.

tsirolnik commented 3 years ago

Had the same issue with "graphql-upload": "^12.0.0" and "apollo-server-express": "^2.2.3" adding uploads: false, solved it

atomoc commented 2 years ago

Hi, a have this code in NestJS app module

GraphQLModule.forRoot({
      autoSchemaFile: true,
      sortSchema: true,
      uploads: false,
}),

but I also get "Missing multipart field 'operations'". Anybody knows how to figure this out?

did you manage to solve the problem?

tkssharma commented 2 years ago

Guys any help here, i am getting the same error {"correlationId":"6999c306-2480-4632-b8dd-954cf9e67bc0","level":"error","message":"[Fri May 27 12:44:48 2022] [error] Missing multipart field ‘operations’ (https://github.com/jaydenseric/graphql-multipart-request-spec)."}