import type { WithRequired } from '@apollo/utils.withrequired'; import type express from 'express'; import type { ApolloServer, BaseContext, ContextFunction, HTTPGraphQLRequest, } from '@apollo/server'; import { parse as urlParse } from 'url'; import { HeaderMap } from '@apollo/server'; export interface ExpressContextFunctionArgument { req: express.Request; res: express.Response; } export interface ExpressMiddlewareOptions { context?: ContextFunction<[ExpressContextFunctionArgument], TContext>; } export function expressMiddleware( server: ApolloServer, options?: ExpressMiddlewareOptions, ): express.RequestHandler; export function expressMiddleware( server: ApolloServer, options: WithRequired, 'context'>, ): express.RequestHandler; export function expressMiddleware( server: ApolloServer, options?: ExpressMiddlewareOptions, ): express.RequestHandler { server.assertStarted('expressMiddleware()'); // This `any` is safe because the overload above shows that context can // only be left out if you're using BaseContext as your context, and {} is a // valid BaseContext. const defaultContext: ContextFunction< [ExpressContextFunctionArgument], // eslint-disable-next-line @typescript-eslint/no-explicit-any any // eslint-disable-next-line @typescript-eslint/require-await > = async () => ({}); const context: ContextFunction<[ExpressContextFunctionArgument], TContext> = options?.context ?? defaultContext; return (req, res, next) => { if (!req.body) { // The json body-parser *always* sets req.body to {} if it's unset (even // if the Content-Type doesn't match), so if it isn't set, you probably // forgot to set up body-parser. (Note that this may change in the future // body-parser@2.) res.status(500); res.send( '`req.body` is not set; this probably means you forgot to set up the ' + '`json` middleware before the Apollo Server middleware.', ); return; } const headers = new HeaderMap(); for (const [key, value] of Object.entries(req.headers)) { if (value !== undefined) { // Node/Express headers can be an array or a single value. We join // multi-valued headers with `, ` just like the Fetch API's `Headers` // does. We assume that keys are already lower-cased (as per the Node // docs on IncomingMessage.headers) and so we don't bother to lower-case // them or combine across multiple keys that would lower-case to the // same value. headers.set(key, Array.isArray(value) ? value.join(', ') : value); } } const httpGraphQLRequest: HTTPGraphQLRequest = { method: req.method.toUpperCase(), headers, search: urlParse(req.url).search ?? '', body: req.body, }; server .executeHTTPGraphQLRequest({ httpGraphQLRequest, context: () => context({ req, res }), }) .then(async (httpGraphQLResponse) => { for (const [key, value] of httpGraphQLResponse.headers) { res.setHeader(key, value); } res.statusCode = httpGraphQLResponse.status || 200; if (httpGraphQLResponse.body.kind === 'complete') { res.send(httpGraphQLResponse.body.string); return; } for await (const chunk of httpGraphQLResponse.body.asyncIterator) { res.write(chunk); // Express/Node doesn't define a way of saying "it's time to send this // data over the wire"... but the popular `compression` middleware // (which implements `accept-encoding: gzip` and friends) does, by // monkey-patching a `flush` method onto the response. So we call it // if it's there. // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access if (typeof (res as any).flush === 'function') { // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access (res as any).flush(); } } res.end(); }) .catch(next); }; }