UNPKG

@whatwg-node/server

Version:

Fetch API compliant HTTP Server adapter

491 lines (485 loc) • 19.6 kB
import { Request, Response } from '@whatwg-node/fetch'; export { Response } from '@whatwg-node/fetch'; function isAsyncIterable(body) { return body != null && typeof body === 'object' && typeof body[Symbol.asyncIterator] === 'function'; } function getPort(nodeRequest) { var _a, _b, _c, _d, _e; if ((_a = nodeRequest.socket) === null || _a === void 0 ? void 0 : _a.localPort) { return (_b = nodeRequest.socket) === null || _b === void 0 ? void 0 : _b.localPort; } const portInHeader = (_e = (_d = (_c = nodeRequest.headers) === null || _c === void 0 ? void 0 : _c.host) === null || _d === void 0 ? void 0 : _d.split(':')) === null || _e === void 0 ? void 0 : _e[1]; if (portInHeader) { return portInHeader; } return 80; } function getHostnameWithPort(nodeRequest) { var _a, _b, _c; if ((_a = nodeRequest.headers) === null || _a === void 0 ? void 0 : _a.host) { return (_b = nodeRequest.headers) === null || _b === void 0 ? void 0 : _b.host; } const port = getPort(nodeRequest); if (nodeRequest.hostname) { return nodeRequest.hostname + ':' + port; } const localIp = (_c = nodeRequest.socket) === null || _c === void 0 ? void 0 : _c.localAddress; if (localIp && !(localIp === null || localIp === void 0 ? void 0 : localIp.includes('::')) && !(localIp === null || localIp === void 0 ? void 0 : localIp.includes('ffff'))) { return `${localIp}:${port}`; } return 'localhost'; } function buildFullUrl(nodeRequest) { const hostnameWithPort = getHostnameWithPort(nodeRequest); const protocol = nodeRequest.protocol || 'http'; const endpoint = nodeRequest.originalUrl || nodeRequest.url || '/graphql'; return `${protocol}://${hostnameWithPort}${endpoint}`; } function configureSocket(rawRequest) { var _a, _b, _c, _d, _e, _f; (_b = (_a = rawRequest === null || rawRequest === void 0 ? void 0 : rawRequest.socket) === null || _a === void 0 ? void 0 : _a.setTimeout) === null || _b === void 0 ? void 0 : _b.call(_a, 0); (_d = (_c = rawRequest === null || rawRequest === void 0 ? void 0 : rawRequest.socket) === null || _c === void 0 ? void 0 : _c.setNoDelay) === null || _d === void 0 ? void 0 : _d.call(_c, true); (_f = (_e = rawRequest === null || rawRequest === void 0 ? void 0 : rawRequest.socket) === null || _e === void 0 ? void 0 : _e.setKeepAlive) === null || _f === void 0 ? void 0 : _f.call(_e, true); } function isRequestBody(body) { const stringTag = body[Symbol.toStringTag]; if (typeof body === 'string' || stringTag === 'Uint8Array' || stringTag === 'Blob' || stringTag === 'FormData' || stringTag === 'URLSearchParams' || isAsyncIterable(body)) { return true; } return false; } function normalizeNodeRequest(nodeRequest, RequestCtor) { var _a; const rawRequest = nodeRequest.raw || nodeRequest.req || nodeRequest; configureSocket(rawRequest); let fullUrl = buildFullUrl(rawRequest); if (nodeRequest.query) { const urlObj = new URL(fullUrl); for (const queryName in nodeRequest.query) { const queryValue = nodeRequest.query[queryName]; urlObj.searchParams.set(queryName, queryValue); } fullUrl = urlObj.toString(); } const baseRequestInit = { method: nodeRequest.method, headers: nodeRequest.headers, }; if (nodeRequest.method === 'GET' || nodeRequest.method === 'HEAD') { return new RequestCtor(fullUrl, baseRequestInit); } /** * Some Node server frameworks like Serverless Express sends a dummy object with body but as a Buffer not string * so we do those checks to see is there something we can use directly as BodyInit * because the presence of body means the request stream is already consumed and, * rawRequest cannot be used as BodyInit/ReadableStream by Fetch API in this case. */ const maybeParsedBody = nodeRequest.body; if (maybeParsedBody != null && Object.keys(maybeParsedBody).length > 0) { if (isRequestBody(maybeParsedBody)) { return new RequestCtor(fullUrl, { ...baseRequestInit, body: maybeParsedBody, }); } const request = new RequestCtor(fullUrl, { ...baseRequestInit, }); if (!((_a = request.headers.get('content-type')) === null || _a === void 0 ? void 0 : _a.includes('json'))) { request.headers.set('content-type', 'application/json'); } return new Proxy(request, { get: (target, prop, receiver) => { switch (prop) { case 'json': return async () => maybeParsedBody; case 'text': return async () => JSON.stringify(maybeParsedBody); default: return Reflect.get(target, prop, receiver); } }, }); } return new RequestCtor(fullUrl, { headers: nodeRequest.headers, method: nodeRequest.method, body: rawRequest, }); } function isReadable(stream) { return stream.read != null; } function isNodeRequest(request) { return isReadable(request); } function isServerResponse(stream) { // Check all used functions are defined return (stream != null && stream.setHeader != null && stream.end != null && stream.once != null && stream.write != null); } function isReadableStream(stream) { return stream != null && stream.getReader != null; } function isFetchEvent(event) { return event != null && event.request != null && event.respondWith != null; } async function sendNodeResponse({ headers, status, statusText, body }, serverResponse) { headers.forEach((value, name) => { serverResponse.setHeader(name, value); }); serverResponse.statusCode = status; serverResponse.statusMessage = statusText; // eslint-disable-next-line no-async-promise-executor return new Promise(async (resolve) => { if (body == null) { serverResponse.end(resolve); } else if (body[Symbol.toStringTag] === 'Uint8Array') { serverResponse // @ts-expect-error http and http2 writes are actually compatible .write(body); serverResponse.end(resolve); } else if (isReadable(body)) { serverResponse.once('close', () => { body.destroy(); resolve(); }); body.pipe(serverResponse); } else if (isAsyncIterable(body)) { for await (const chunk of body) { if (!serverResponse // @ts-expect-error http and http2 writes are actually compatible .write(chunk)) { break; } } serverResponse.end(resolve); } }); } function isRequestInit(val) { return (val != null && typeof val === 'object' && ('body' in val || 'cache' in val || 'credentials' in val || 'headers' in val || 'integrity' in val || 'keepalive' in val || 'method' in val || 'mode' in val || 'redirect' in val || 'referrer' in val || 'referrerPolicy' in val || 'signal' in val || 'window' in val)); } async function handleWaitUntils(waitUntilPromises) { const waitUntils = await Promise.allSettled(waitUntilPromises); waitUntils.forEach(waitUntil => { if (waitUntil.status === 'rejected') { console.error(waitUntil.reason); } }); } function createServerAdapter(serverAdapterBaseObject, /** * WHATWG Fetch spec compliant `Request` constructor. */ RequestCtor = Request) { const handleRequest = typeof serverAdapterBaseObject === 'function' ? serverAdapterBaseObject : serverAdapterBaseObject.handle; function handleNodeRequest(nodeRequest, ...ctx) { const serverContext = ctx.length > 1 ? Object.assign({}, ...ctx) : ctx[0]; const request = normalizeNodeRequest(nodeRequest, RequestCtor); return handleRequest(request, serverContext); } async function requestListener(nodeRequest, serverResponse, ...ctx) { const waitUntilPromises = []; const defaultServerContext = { req: nodeRequest, res: serverResponse, waitUntil(p) { waitUntilPromises.push(p); }, }; const response = await handleNodeRequest(nodeRequest, defaultServerContext, ...ctx); if (response) { await sendNodeResponse(response, serverResponse); } else { await new Promise(resolve => { serverResponse.statusCode = 404; serverResponse.end(resolve); }); } if (waitUntilPromises.length > 0) { await handleWaitUntils(waitUntilPromises); } } function handleEvent(event, ...ctx) { if (!event.respondWith || !event.request) { throw new TypeError(`Expected FetchEvent, got ${event}`); } const serverContext = ctx.length > 0 ? Object.assign({}, event, ...ctx) : event; const response$ = handleRequest(event.request, serverContext); event.respondWith(response$); } function handleRequestWithWaitUntil(request, ...ctx) { const serverContext = ctx.length > 1 ? Object.assign({}, ...ctx) : ctx[0] || {}; if (!('waitUntil' in serverContext)) { const waitUntilPromises = []; const response$ = handleRequest(request, { ...serverContext, waitUntil(p) { waitUntilPromises.push(p); }, }); if (waitUntilPromises.length > 0) { return handleWaitUntils(waitUntilPromises).then(() => response$); } return response$; } return handleRequest(request, serverContext); } const fetchFn = (input, ...maybeCtx) => { if (typeof input === 'string' || input instanceof URL) { const [initOrCtx, ...restOfCtx] = maybeCtx; if (isRequestInit(initOrCtx)) { return handleRequestWithWaitUntil(new RequestCtor(input, initOrCtx), ...restOfCtx); } return handleRequestWithWaitUntil(new RequestCtor(input), ...maybeCtx); } return handleRequestWithWaitUntil(input, ...maybeCtx); }; const genericRequestHandler = (input, ...maybeCtx) => { // If it is a Node request const [initOrCtxOrRes, ...restOfCtx] = maybeCtx; if (isNodeRequest(input)) { if (!isServerResponse(initOrCtxOrRes)) { throw new TypeError(`Expected ServerResponse, got ${initOrCtxOrRes}`); } return requestListener(input, initOrCtxOrRes, ...restOfCtx); } if (isServerResponse(initOrCtxOrRes)) { throw new TypeError('Got Node response without Node request'); } // Is input a container object over Request? if (typeof input === 'object' && 'request' in input) { // Is it FetchEvent? if (isFetchEvent(input)) { return handleEvent(input, ...maybeCtx); } // In this input is also the context return handleRequestWithWaitUntil(input.request, input, ...maybeCtx); } // Or is it Request itself? // Then ctx is present and it is the context return fetchFn(input, ...maybeCtx); }; const adapterObj = { handleRequest, fetch: fetchFn, handleNodeRequest, requestListener, handleEvent, handle: genericRequestHandler, }; return new Proxy(genericRequestHandler, { // It should have all the attributes of the handler function and the server instance has: (_, prop) => { return (prop in adapterObj || prop in genericRequestHandler || (serverAdapterBaseObject && prop in serverAdapterBaseObject)); }, get: (_, prop) => { const adapterProp = adapterObj[prop]; if (adapterProp) { if (adapterProp.bind) { return adapterProp.bind(adapterObj); } return adapterProp; } const handleProp = genericRequestHandler[prop]; if (handleProp) { if (handleProp.bind) { return handleProp.bind(genericRequestHandler); } return handleProp; } if (serverAdapterBaseObject) { const serverAdapterBaseObjectProp = serverAdapterBaseObject[prop]; if (serverAdapterBaseObjectProp) { if (serverAdapterBaseObjectProp.bind) { return serverAdapterBaseObjectProp.bind(serverAdapterBaseObject); } return serverAdapterBaseObjectProp; } } }, apply(_, __, args) { return genericRequestHandler(...args); }, }); // 😡 } function getCORSHeadersByRequestAndOptions(request, corsOptions) { var _a, _b; const headers = {}; if (corsOptions === false) { return headers; } // If defined origins have '*' or undefined by any means, we should allow all origins if (corsOptions.origin == null || corsOptions.origin.length === 0 || corsOptions.origin.includes('*')) { const currentOrigin = request.headers.get('origin'); // If origin is available in the headers, use it if (currentOrigin != null) { headers['Access-Control-Allow-Origin'] = currentOrigin; // Vary by origin because there are multiple origins headers['Vary'] = 'Origin'; } else { headers['Access-Control-Allow-Origin'] = '*'; } } else if (typeof corsOptions.origin === 'string') { // If there is one specific origin is specified, use it directly headers['Access-Control-Allow-Origin'] = corsOptions.origin; } else if (Array.isArray(corsOptions.origin)) { // If there is only one origin defined in the array, consider it as a single one if (corsOptions.origin.length === 1) { headers['Access-Control-Allow-Origin'] = corsOptions.origin[0]; } else { const currentOrigin = request.headers.get('origin'); if (currentOrigin != null && corsOptions.origin.includes(currentOrigin)) { // If origin is available in the headers, use it headers['Access-Control-Allow-Origin'] = currentOrigin; // Vary by origin because there are multiple origins headers['Vary'] = 'Origin'; } else { // There is no origin found in the headers, so we should return null headers['Access-Control-Allow-Origin'] = 'null'; } } } if ((_a = corsOptions.methods) === null || _a === void 0 ? void 0 : _a.length) { headers['Access-Control-Allow-Methods'] = corsOptions.methods.join(', '); } else { const requestMethod = request.headers.get('access-control-request-method'); if (requestMethod) { headers['Access-Control-Allow-Methods'] = requestMethod; } } if ((_b = corsOptions.allowedHeaders) === null || _b === void 0 ? void 0 : _b.length) { headers['Access-Control-Allow-Headers'] = corsOptions.allowedHeaders.join(', '); } else { const requestHeaders = request.headers.get('access-control-request-headers'); if (requestHeaders) { headers['Access-Control-Allow-Headers'] = requestHeaders; if (headers['Vary']) { headers['Vary'] += ', Access-Control-Request-Headers'; } headers['Vary'] = 'Access-Control-Request-Headers'; } } if (corsOptions.credentials != null) { if (corsOptions.credentials === true) { headers['Access-Control-Allow-Credentials'] = 'true'; } } else if (headers['Access-Control-Allow-Origin'] !== '*') { headers['Access-Control-Allow-Credentials'] = 'true'; } if (corsOptions.exposedHeaders) { headers['Access-Control-Expose-Headers'] = corsOptions.exposedHeaders.join(', '); } if (corsOptions.maxAge) { headers['Access-Control-Max-Age'] = corsOptions.maxAge.toString(); } return headers; } async function getCORSResponseHeaders(request, corsOptionsFactory, serverContext) { const corsOptions = await corsOptionsFactory(request, serverContext); return getCORSHeadersByRequestAndOptions(request, corsOptions); } function withCORS(obj, options, ResponseCtor = Response) { let corsOptionsFactory = () => ({}); if (options != null) { if (typeof options === 'function') { corsOptionsFactory = options; } else if (typeof options === 'object') { const corsOptions = { ...options, }; corsOptionsFactory = () => corsOptions; } else if (options === false) { corsOptionsFactory = () => false; } } async function handleWithCORS(request, serverContext) { let response; if (request.method.toUpperCase() === 'OPTIONS') { response = new ResponseCtor(null, { status: 204, }); } else { response = await obj.handle(request, serverContext); } if (response != null) { const headers = await getCORSResponseHeaders(request, corsOptionsFactory, serverContext); for (const headerName in headers) { response.headers.set(headerName, headers[headerName]); } return response; } } return new Proxy(obj, { get(_, prop, receiver) { if (prop === 'handle') { return handleWithCORS; } return Reflect.get(obj, prop, receiver); }, }); } function createDefaultErrorHandler(ResponseCtor = Response) { return function defaultErrorHandler(e) { return new ResponseCtor(e.stack || e.message || e.toString(), { status: e.statusCode || e.status || 500, statusText: e.statusText || 'Internal Server Error', }); }; } function withErrorHandling(obj, onError = createDefaultErrorHandler()) { async function handleWithErrorHandling(request, ctx) { try { const res = await obj.handle(request, ctx); return res; } catch (e) { return onError(e, request, ctx); } } return new Proxy(obj, { get(obj, prop, receiver) { if (prop === 'handle') { return handleWithErrorHandling; } return Reflect.get(obj, prop, receiver); }, }); } export { createDefaultErrorHandler, createServerAdapter, getCORSHeadersByRequestAndOptions, isAsyncIterable, isFetchEvent, isNodeRequest, isReadable, isReadableStream, isRequestInit, isServerResponse, normalizeNodeRequest, sendNodeResponse, withCORS, withErrorHandling };