UNPKG

@whatwg-node/server

Version:

Fetch API compliant HTTP Server adapter

349 lines (343 loc) • 13.7 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); const fetch = require('@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 isServerResponse(stream) { // Check all used functions are defined return stream.setHeader != null && stream.end != null && stream.once != null && stream.write != 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.end(body, resolve); } else if (isReadable(body)) { serverResponse.once('close', () => { body.destroy(); }); body.pipe(serverResponse); } else if (isAsyncIterable(body)) { for await (const chunk of body) { if (!serverResponse.write(chunk)) { resolve(); return; } } 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)); } /// <reference lib="webworker" /> 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 = fetch.Request) { const handleRequest = typeof serverAdapterBaseObject === 'function' ? serverAdapterBaseObject : serverAdapterBaseObject.handle; function handleNodeRequest(nodeRequest, ctx) { const request = normalizeNodeRequest(nodeRequest, RequestCtor); return handleRequest(request, ctx); } async function requestListener(nodeRequest, serverResponse, ...ctx) { const waitUntilPromises = []; let serverContext = {}; if ((ctx === null || ctx === void 0 ? void 0 : ctx.length) > 0) { serverContext = Object.assign({}, serverContext, ...ctx); } const response = await handleNodeRequest(nodeRequest, { ...serverContext, req: nodeRequest, res: serverResponse, waitUntil(p) { waitUntilPromises.push(p); }, }); if (response) { await sendNodeResponse(response, serverResponse); } else { await new Promise(resolve => { serverResponse.statusCode = 404; serverResponse.end(resolve); }); } if (waitUntilPromises.length > 0) { await handleWaitUntils(waitUntilPromises); } return response; } function handleEvent(event, ...ctx) { if (!event.respondWith || !event.request) { throw new TypeError(`Expected FetchEvent, got ${event}`); } let serverContext = {}; if ((ctx === null || ctx === void 0 ? void 0 : ctx.length) > 0) { serverContext = Object.assign({}, serverContext, ...ctx); } const response$ = handleRequest(event.request, serverContext); event.respondWith(response$); return response$; } function handleRequestWithWaitUntil(request, ctx) { var _a; const extendedCtx = ctx; if ('process' in globalThis && ((_a = process.versions) === null || _a === void 0 ? void 0 : _a['bun']) != null) { // This is required for bun request.text(); } if (!extendedCtx.waitUntil) { const waitUntilPromises = []; extendedCtx.waitUntil = (p) => { waitUntilPromises.push(p); }; const response$ = handleRequest(request, { ...extendedCtx, waitUntil(p) { waitUntilPromises.push(p); }, }); if (waitUntilPromises.length > 0) { return handleWaitUntils(waitUntilPromises).then(() => response$); } return response$; } return handleRequest(request, extendedCtx); } const fetchFn = (input, initOrCtx, ...ctx) => { let init; let serverContext = {}; if (isRequestInit(initOrCtx)) { init = initOrCtx; } else { init = {}; serverContext = Object.assign({}, serverContext, initOrCtx); } if ((ctx === null || ctx === void 0 ? void 0 : ctx.length) > 0) { serverContext = Object.assign({}, serverContext, ...ctx); } if (typeof input === 'string' || input instanceof URL) { return handleRequestWithWaitUntil(new RequestCtor(input, init), serverContext); } return handleRequestWithWaitUntil(input, serverContext); }; const genericRequestHandler = (input, initOrCtxOrRes, ...ctx) => { // If it is a Node request if (isReadable(input) && isServerResponse(initOrCtxOrRes)) { return requestListener(input, initOrCtxOrRes, ...ctx); } if (isServerResponse(initOrCtxOrRes)) { throw new Error('Got Node response without Node request'); } // Is input a container object over Request? if (typeof input === 'object' && 'request' in input) { // Is it FetchEvent? if ('respondWith' in input) { return handleEvent(input, isRequestInit(initOrCtxOrRes) ? {} : initOrCtxOrRes, ...ctx); } // In this input is also the context return fetchFn( // @ts-expect-error input can indeed be a Request input.request, initOrCtxOrRes, ...ctx); } // Or is it Request itself? // Then ctx is present and it is the context return fetchFn( // @ts-expect-error input can indeed string | Request | URL input, initOrCtxOrRes, ...ctx); }; 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(_, __, [input, ctx]) { return genericRequestHandler(input, ctx); }, }); // 😡 } exports.createServerAdapter = createServerAdapter;