Handle file uploads using a GraphQL middleware

GraphQL is a very interesting implementation so that the fronts of your web applications communicate efficiently with your different backends sources (and aggregate if necessary) and also retrieves the information that will be useful to the application only. Based on a JSON request system (query or mutation), the entry of the HTTP request to the GraphQL server is of the type application/json.

GraphQL Go Upload

On client side, I needed to upload (via a mutation) information about media content in order to manage a media library, a media consisting of a title and a source image. This is where it gets stuck: we're going to need to make an HTTP POST request of type multipart/form-data containing our meta-data and our source file. My case and for this article, the GraphQL server is developed here in Go via the library GraphQL-Go but this article can be used for all languages.

There are therefore two steps to be carried out. There are two steps:

  • Add a new scalar type (to be called Upload) at the input of our mutation,
  • Develop middleware to intercept the HTTP POST multipart/form-data request and re-map it for GraphQL.

Declaration of a new scalar type: Upload

The first step is therefore to declare our mutation which will look like this in our diagram:

scalar Upload

type Mutation {
    myUploadMutation(file: Upload!, title: String!): Boolean
}

We therefore declare here a new scalar type "Upload" and use it under the "file" entry in the mutation below. This type must also be declared in our Go code, so we will declare a new structure in our code:

type GraphQLUpload struct {
    Filename string `json:"filename"`
    MIMEType string `json:"mimetype"`
    Filepath string `json:"filepath"`
}

Thus, when we upload a media, three informations will be uploaded: the file name, the MIME file type (application/json, image/png, ...) as well as the path to the file temporarily stored on the GraphQL server. GraphQL-Go asks us to implement an interface Unmarshaler defining two methods ImplementsGraphQLType(name string) to define the name of our scalar type (Upload in our case) and UnmarshalGraphQL(input interface{}) to transform the received input into a GraphQLUpload structure in our case:

func (u GraphQLUpload) ImplementsGraphQLType(name string) bool {
    return name == "Upload"
}

func (u *GraphQLUpload) UnmarshalGraphQL(input interface{}) error {
    switch input := input.(type) {
        case map[string]interface{}:
            data, err := json.Marshal(input)
            if err != nil {
                u = &GraphQLUpload{}
            } else {
                json.Unmarshal(data, u)
            }
        
            return nil

        default:
            return errors.New("Cannot unmarshal received type as a GraphQLUpload type")
    }
}

We are now ready to receive our input file, let's move to middleware on the server side.

HTTP Middleware to re-map GraphQL data

This step consists in retrieving the mutlipart/form-data data and transforming the received POST files and data into a GraphQL input so that the request runs correctly. I'm not going to detail the middleware code here but simply describe how it works.

An HTTP request to send a file with additional data (here a title) will be made in the following form:

$ curl http://localhost:8000/graphql \
-F operations='{ "query": "mutation DoUpload($file: Upload!, $title: String!) { upload(file: $file, title: $title) }",
"variables": { "file": null, "title": null } }' \
-F map='{ "file": ["variables.file"], "title": ["variables.title"] }' \
-F file=@myfile.txt \
-F title="My content title"

The HTTP request (type multipart/form-data) on our GraphQL server receives several informations:

  • operations: this is the content of the GraphQL query: we call here the mutation upload(file: Upload!, title: String!) declared at the beginning of this article,
  • map: this entry is very important, it will allow us to know which data received corresponds to which variable in the GraphQL query,
  • file and title: our two data.

All the magic here is to re-map the form data received and specified in the entry map in the received operations and more particularly by using the variables. Thus, once these operations have been performed, the middleware re-formulates the HTTP request with the correct parameters and sends it back to the GraphQL server (in JSON):

graphqlParams := graphqlParams{
    Variables: operations["variables"],
    Query: operations["query"],
    Operations: operations,
    Map: mapEntries,
}

body, err := json.Marshal(graphqlParams)
if err == nil {
    r.Body = ioutil.NopCloser(bytes.NewReader(body))
    w.Header().Set("Content-Type", "application/json")
}

next.ServeHTTP(w, r)

Implement the middleware library graphql-go-upload

The middleware code is available on GitHub here: https://github.com/eko/graphql-go-upload. In order to declare the middleware upstream of your GraphQL server, you only have to import the library and define the middleware as follows:

import (
    "github.com/eko/graphql-go-upload"
)

// ...
h := handler.GraphQL{
    Schema: graphql.MustParseSchema(schema.String(), root, graphql.MaxParallelism(maxParallelism),
    graphql.MaxDepth(maxDepth)),
    Handler: handler.NewHandler(conf, &m),
}

mux := mux.NewRouter()
mux.Handle("/graphql", upload.Handler(h)) // Add the middleware here (wrap the original handler)

s := &http.Server{
    Addr: ":8000",
    Handler: mux,
}

Conclusion

It is finally simple to allow your GraphQL server to intercept multipart/form-data requests in order to be able to send files and/or form data. This implementation is also available on NodeJS via Apollo server.

Feel free to contact me if you would like more details on the implementation or use of this library.