Apollo Server 4 Serverless GraphQL Upload
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;
- Use signed URL uploads to upload a file directly to a storage provider rather than through the GraphQL server, or;
- Use a dedicated image service. Finally, it’s not recommended but if you really want to;
- 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;
- Send an
Apollo-Require-Preflight
orX-Apollo-Operation-Name
HTTP header with the request, or; - 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'}}
tocreateUploadLink
.