Search code examples
file-uploaderror-handlinguploadgraphqlexpress-graphql

Rejecting & error handling of GraphQL file upload


I have an express GraphQL endpoint with a resolver that accepts a single file. The resolver includes a simple validation of the file being received.

The problem is that when the validation fails, there is no way to immediately return the error to the front end, as throwing an error will not force the uploading request to be interrupted.

An over-simplified example:

fileUpload: async (parent, { file, otherdata }) => {
    const isValid = (some logic here)
    if(!isValid)
        throw new ApolloError('upload parameters not valid')

    //No need to get this far,
    //we could have rejected the request already by examining otherdata,
    //or the stream could be already created for max time utilization

    const { createReadStream, filename, mimetype, encoding } = await file;
    const readStream = await createReadStream()
    ...
 }

Expected behavior: The resolver returns the usual {errors:[], data:null} object - or the error itself - depending on the error-policy option.

Actual behavior: The error is thrown in the backend but the request remains pending in the frontend.

I have already unsuccessfully tried the following:

  • Initializing the readStream anyway and calling .destroy() on it. Result is that the read stream stops and the request remains pending for ever.
  • Including the request object (tried the response object as well) in the resolver context and calling .destroy() on it. This ends the request but results in a network error in the front end, leaving no opportunity for error specific handling.

Some clarifications:

  • Apparently there is client side verifications as well, but that does not make server side verification unnecessary.
  • Waiting for the upload to be completed and then throwing the error obviously works, but that is not really an option, time and bandwidth wise.

I understand that uploading files using GraphQL is borderline supported functionality, but in this case we are talking about a rather basic operation.

I would appreciate any suggestions!


Solution

  • It turns out that this is default express behavior and has absolutely nothing to do with the GraphQL interface. The pure express version of the solution can be found here.

    The request and response objects should be passed to the resolver context:

    const apollo = new ApolloServer({
        typeDefs,
        resolvers,
        context: ({ req, res }) => {
            return {
                req,
                res
            };
        }
    });
    

    Then in the resolver, according to the example:

    fileUpload: async (parent, { file, otherdata }, {req, res}) => {
        const isValid = (some logic here)
        if(!isValid){
            res.send(403).send("Some message")
            // Throwing ApolloError could also work,
            // in which case response object would not be required, but not tested.
            // throw new ApolloError('upload parameters not valid')
            return req.destroy()
        }
            
    
        //No need to get this far,
        //we could have rejected the request already by examining otherdata,
        //or the stream could be already created for max time utilization
    
        const { createReadStream, filename, mimetype, encoding } = await file;
        const readStream = await createReadStream()
        ...
     }