@whatwg-node/server
Version:
Fetch API compliant HTTP Server adapter
368 lines (367 loc) • 15.9 kB
JavaScript
import { chain, getInstrumented } from '@envelop/instrumentation';
import { AsyncDisposableStack, DisposableSymbols } from '@whatwg-node/disposablestack';
import * as DefaultFetchAPI from '@whatwg-node/fetch';
import { handleMaybePromise, unfakePromise } from '@whatwg-node/promise-helpers';
import { completeAssign, createCustomAbortControllerSignal, ensureDisposableStackRegisteredForTerminateEvents, handleAbortSignalAndPromiseResponse, handleErrorFromRequestHandler, isFetchEvent, isNodeRequest, isolateObject, isPromise, isRequestInit, isServerResponse, iterateAsyncVoid, normalizeNodeRequest, sendNodeResponse, } from './utils.js';
import { fakePromise, getRequestFromUWSRequest, isUWSResponse, sendResponseToUwsOpts, } from './uwebsockets.js';
// Required for envs like nextjs edge runtime
function isRequestAccessible(serverContext) {
try {
return !!serverContext?.request;
}
catch {
return false;
}
}
const EMPTY_OBJECT = {};
function createServerAdapter(serverAdapterBaseObject, options) {
const useSingleWriteHead = options?.__useSingleWriteHead == null ? true : options.__useSingleWriteHead;
const fetchAPI = {
...DefaultFetchAPI,
...options?.fetchAPI,
};
const useCustomAbortCtrl = options?.__useCustomAbortCtrl == null
? fetchAPI.Request !== globalThis.Request
: options.__useCustomAbortCtrl;
const givenHandleRequest = typeof serverAdapterBaseObject === 'function'
? serverAdapterBaseObject
: serverAdapterBaseObject.handle;
const onRequestHooks = [];
const onResponseHooks = [];
let instrumentation;
const waitUntilPromises = new Set();
let _disposableStack;
function ensureDisposableStack() {
if (!_disposableStack) {
_disposableStack = new AsyncDisposableStack();
if (options?.disposeOnProcessTerminate) {
ensureDisposableStackRegisteredForTerminateEvents(_disposableStack);
}
_disposableStack.defer(() => {
if (waitUntilPromises.size > 0) {
return Promise.allSettled(waitUntilPromises).then(() => {
waitUntilPromises.clear();
}, () => {
waitUntilPromises.clear();
});
}
});
}
return _disposableStack;
}
function waitUntil(maybePromise) {
// Ensure that the disposable stack is created
if (isPromise(maybePromise)) {
ensureDisposableStack();
waitUntilPromises.add(maybePromise);
maybePromise.then(() => {
waitUntilPromises.delete(maybePromise);
}, err => {
console.error(`Unexpected error while waiting: ${err.message || err}`);
waitUntilPromises.delete(maybePromise);
});
}
}
if (options?.plugins != null) {
for (const plugin of options.plugins) {
if (plugin.instrumentation) {
instrumentation = instrumentation
? chain(instrumentation, plugin.instrumentation)
: plugin.instrumentation;
}
if (plugin.onRequest) {
onRequestHooks.push(plugin.onRequest);
}
if (plugin.onResponse) {
onResponseHooks.push(plugin.onResponse);
}
const disposeFn = plugin[DisposableSymbols.dispose];
if (disposeFn) {
ensureDisposableStack().defer(disposeFn);
}
const asyncDisposeFn = plugin[DisposableSymbols.asyncDispose];
if (asyncDisposeFn) {
ensureDisposableStack().defer(asyncDisposeFn);
}
if (plugin.onDispose) {
ensureDisposableStack().defer(plugin.onDispose);
}
}
}
let handleRequest = onRequestHooks.length > 0 || onResponseHooks.length > 0
? function handleRequest(request, serverContext) {
let requestHandler = givenHandleRequest;
let response;
if (onRequestHooks.length === 0) {
return handleEarlyResponse();
}
let url = request['parsedUrl'] ||
new Proxy(EMPTY_OBJECT, {
get(_target, prop, _receiver) {
url = new fetchAPI.URL(request.url, 'http://localhost');
return Reflect.get(url, prop, url);
},
});
function handleResponse(response) {
if (onResponseHooks.length === 0) {
return response;
}
return handleMaybePromise(() => iterateAsyncVoid(onResponseHooks, onResponseHook => onResponseHook({
request,
response,
serverContext,
setResponse(newResponse) {
response = newResponse;
},
fetchAPI,
})), () => response);
}
function handleEarlyResponse() {
if (!response) {
return handleMaybePromise(() => requestHandler(request, serverContext), handleResponse);
}
return handleResponse(response);
}
return handleMaybePromise(() => iterateAsyncVoid(onRequestHooks, (onRequestHook, stopEarly) => onRequestHook({
request,
setRequest(newRequest) {
request = newRequest;
},
serverContext,
fetchAPI,
url,
requestHandler,
setRequestHandler(newRequestHandler) {
requestHandler = newRequestHandler;
},
endResponse(newResponse) {
response = newResponse;
if (newResponse) {
stopEarly();
}
},
})), handleEarlyResponse);
}
: givenHandleRequest;
if (instrumentation?.request) {
const originalRequestHandler = handleRequest;
handleRequest = (request, initialContext) => {
return getInstrumented({ request }).asyncFn(instrumentation.request, originalRequestHandler)(request, initialContext);
};
}
// TODO: Remove this on the next major version
function handleNodeRequest(nodeRequest, ...ctx) {
const serverContext = ctx.length > 1 ? completeAssign(...ctx) : ctx[0] || {};
// Ensure `waitUntil` is available in the server context
if (!serverContext.waitUntil) {
serverContext.waitUntil = waitUntil;
}
const request = normalizeNodeRequest(nodeRequest, fetchAPI, undefined, useCustomAbortCtrl);
return handleRequest(request, serverContext);
}
function handleNodeRequestAndResponse(nodeRequest, nodeResponseOrContainer, ...ctx) {
const nodeResponse = nodeResponseOrContainer.raw || nodeResponseOrContainer;
const serverContext = ctx.length > 1 ? completeAssign(...ctx) : ctx[0] || {};
// Ensure `waitUntil` is available in the server context
if (!serverContext.waitUntil) {
serverContext.waitUntil = waitUntil;
}
const request = normalizeNodeRequest(nodeRequest, fetchAPI, nodeResponse, useCustomAbortCtrl);
return handleRequest(request, serverContext);
}
function requestListener(nodeRequest, nodeResponse, ...ctx) {
const defaultServerContext = {
req: nodeRequest,
res: nodeResponse,
waitUntil,
};
return unfakePromise(fakePromise()
.then(() => handleNodeRequestAndResponse(nodeRequest, nodeResponse, defaultServerContext, ...ctx))
.catch(err => handleErrorFromRequestHandler(err, fetchAPI.Response))
.then(response => sendNodeResponse(response, nodeResponse, nodeRequest, useSingleWriteHead))
.catch(err => console.error(`Unexpected error while handling request: ${err.message || err}`)));
}
function handleUWS(res, req, ...ctx) {
const defaultServerContext = {
res,
req,
waitUntil,
};
const filteredCtxParts = ctx.filter(partCtx => partCtx != null);
const serverContext = filteredCtxParts.length > 0
? completeAssign(defaultServerContext, ...ctx)
: defaultServerContext;
const controller = useCustomAbortCtrl
? createCustomAbortControllerSignal()
: new AbortController();
const originalResEnd = res.end.bind(res);
let resEnded = false;
res.end = function (data) {
resEnded = true;
return originalResEnd(data);
};
const originalOnAborted = res.onAborted.bind(res);
originalOnAborted(function () {
controller.abort();
});
res.onAborted = function (cb) {
controller.signal.addEventListener('abort', cb, { once: true });
};
const request = getRequestFromUWSRequest({
req,
res,
fetchAPI,
controller,
});
return handleMaybePromise(() => handleMaybePromise(() => handleRequest(request, serverContext), response => response, err => handleErrorFromRequestHandler(err, fetchAPI.Response)), response => {
if (!controller.signal.aborted && !resEnded) {
return handleMaybePromise(() => sendResponseToUwsOpts(res, response, controller, fetchAPI), r => r, err => {
console.error(`Unexpected error while handling request: ${err.message || err}`);
});
}
});
}
function handleEvent(event, ...ctx) {
if (!event.respondWith || !event.request) {
throw new TypeError(`Expected FetchEvent, got ${event}`);
}
const filteredCtxParts = ctx.filter(partCtx => partCtx != null);
const serverContext = filteredCtxParts.length > 0
? completeAssign({}, event, ...filteredCtxParts)
: isolateObject(event);
const response$ = handleRequest(event.request, serverContext);
event.respondWith(response$);
}
function handleRequestWithWaitUntil(request, ...ctx) {
const filteredCtxParts = ctx.filter(partCtx => partCtx != null);
const serverContext = filteredCtxParts.length > 1
? completeAssign({}, ...filteredCtxParts)
: isolateObject(filteredCtxParts[0], filteredCtxParts[0] == null || filteredCtxParts[0].waitUntil == null
? waitUntil
: undefined);
return handleRequest(request, serverContext);
}
const fetchFn = (input, ...maybeCtx) => {
if (typeof input === 'string' || 'href' in input) {
const [initOrCtx, ...restOfCtx] = maybeCtx;
if (isRequestInit(initOrCtx)) {
const request = new fetchAPI.Request(input, initOrCtx);
const res$ = handleRequestWithWaitUntil(request, ...restOfCtx);
const signal = initOrCtx.signal;
if (signal) {
return handleAbortSignalAndPromiseResponse(res$, signal);
}
return res$;
}
const request = new fetchAPI.Request(input);
return handleRequestWithWaitUntil(request, ...maybeCtx);
}
const res$ = handleRequestWithWaitUntil(input, ...maybeCtx);
return handleAbortSignalAndPromiseResponse(res$, input.signal);
};
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 (isUWSResponse(input)) {
return handleUWS(input, initOrCtxOrRes, ...restOfCtx);
}
if (isServerResponse(initOrCtxOrRes)) {
throw new TypeError('Got Node response without Node request');
}
// Is input a container object over Request?
if (isRequestAccessible(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: handleRequestWithWaitUntil,
fetch: fetchFn,
handleNodeRequest,
handleNodeRequestAndResponse,
requestListener,
handleEvent,
handleUWS,
handle: genericRequestHandler,
get disposableStack() {
return ensureDisposableStack();
},
[DisposableSymbols.asyncDispose]() {
if (_disposableStack && !_disposableStack.disposed) {
return _disposableStack.disposeAsync();
}
return fakePromise();
},
dispose() {
if (_disposableStack && !_disposableStack.disposed) {
return _disposableStack.disposeAsync();
}
return fakePromise();
},
waitUntil,
};
const serverAdapter = 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) => {
// Somehow Deno and Node 24 don't like bound dispose functions
if (globalThis.Deno || prop === Symbol.asyncDispose || prop === Symbol.dispose) {
const adapterProp = Reflect.get(adapterObj, prop, adapterObj);
if (adapterProp) {
return adapterProp;
}
}
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 function (...args) {
const returnedVal = serverAdapterBaseObject[prop](...args);
if (returnedVal === serverAdapterBaseObject) {
return serverAdapter;
}
return returnedVal;
};
}
return serverAdapterBaseObjectProp;
}
}
},
apply(_, __, args) {
return genericRequestHandler(...args);
},
});
return serverAdapter;
}
export { createServerAdapter };