Mar
20

Apollo Server 4 Serverless GraphQL Upload

posted on 20 March 2023 in programming

Apollo Server 4 changed the way we integrate with Serverless functions, the apollo-server-lambda project is no longer part of the core package, being replaced with @as-integrations/aws-lambda. With this change it’s no longer obvious how we can use Express middleware like graphql-upload for serverless functions, but it is still possible using the @vendia/serverless-express project. This article will focus on AWS Lambda integrations, but the solution should be just as relevant for any of the cloud providers (using the appropriate apollo-server-integrations package).

Use a signed URL upload instead

We’ll start with an obligatory statement that you probably shouldn’t be trying to upload files / images through a GraphQL mutation. The Apollo team have written up a great blog post on Apollo Server File Upload Best Practices, in summary, when possible;

  1. Use signed URL uploads to upload a file directly to a storage provider rather than through the GraphQL server, or;
  2. Use a dedicated image service. Finally, it’s not recommended but if you really want to;
  3. Upload the file with a Multipart GraphQL mutation (e.g. using graphql-upload)

With that aside, if you have a good reason for wanting to do this, then I have a solution for you.

Using graphql-upload with Apollo Server 4

In Apollo Server 3, the apollo-server-lambda package exposed an interface for using Express middleware that allowed us to use the graphql-upload package, e.g.

export const handler = server.createHandler({
  expressAppFromMiddleware(middleware) {
    const app = Express()
    app.use(graphqlUploadExpress())
    app.use(middleware)
    return app
  },
  expressGetMiddlewareOptions: {
    cors: {
      origin: "*",
      credentials: false,
    },
    bodyParserConfig: { limit: "50mb" },
  },
})

This has been replaced with a cleaner and leaner adapter from ` @as-integrations/aws-lambda`.

export const handler = startServerAndCreateLambdaHandler(
  server,
  handlers.createAPIGatewayProxyEventV2RequestHandler(),
);

But how can we use graphqlUploadExpress with this?! Well it appears that Apollo Server 3 used an adapter to convert their Express app integration to a lambda handler, so plugging in Express middleware was easy. It looks like we can revert back to this method by using the @vendia/serverless-express project that exposes an Express web app API for serverless functions running on AWS Lambda or Azure Functions.

For this solution, we’ll use the Async setup Lambda handler for @vendia/serverless-express because we need to await for the Apollo Server server.start() function. We’ll also use the expressMiddleware function from Apollo Server. Here’s the full example:

const app = express()

const server = new ApolloServer({
  typeDefs,
  resolvers,
})

let serverlessExpressInstance

async function setup(event, context) {
  await server.start()
  app.use(
    "/graphql",
    cors(),
    bodyParser.json({ limit: "50mb" }),
    graphqlUploadExpress({ maxFileSize: 50000000, maxFiles: 10 }),
    expressMiddleware(server, {
      context: async () => { /* Your context function */ },
    })
  )
  serverlessExpressInstance = serverlessExpress({ app })
  return serverlessExpressInstance(event, context)
}

exports.handler = (event, context) => {
  if (serverlessExpressInstance)
    return serverlessExpressInstance(event, context)

  return setup(event, context)
}

Preventing CSRF errors

The Apollo team have taken care to ensure that Apollo Server is secure by default, and this includes disabling “simple” HTTP operations that don’t require a CORS preflight check and may be vulnerable to CSRF attacks. You can read about Preventing Cross-Site Request Forgery (CSRF) here.

What this means is that when using the graphql-upload package you’re likely to run into the following error:

{
  "errors": [
    {
      "message": "This operation has been blocked as a potential Cross-Site Request Forgery (CSRF). Please either specify a 'content-type' header (with a type that is not one of application/x-www-form-urlencoded, multipart/form-data, text/plain) or provide a non-empty value for one of the following headers: x-apollo-operation-name, apollo-require-preflight\n",
      "extensions": {
        "code": "BAD_REQUEST",

The message is pretty self-explanatory, but essentially to enable uploads you will need to either;

  1. Send an Apollo-Require-Preflight or X-Apollo-Operation-Name HTTP header with the request, or;
  2. Disable CSRF protection with new ApolloServer({ csrfPrevention: false }) (but this is a bad idea so don’t do this)

The Apollo docs callout how to do this with the apollo-upload-client package:

For example, if you use the apollo-upload-client package with Apollo Client Web, pass {headers: {'Apollo-Require-Preflight': 'true'}} to createUploadLink.