UNPKG

@whatwg-node/server

Version:

Fetch API compliant HTTP Server adapter

57 lines (56 loc) 3.2 kB
import { handleMaybePromise } from '@whatwg-node/promise-helpers'; import { abortSignalAny } from '../abortSignalAny.js'; /** * A plugin that allows you to set a deadline for each request. If the request takes longer than * the specified timeout, the plugin will produce a response using the provided response factory function. * * This plugin is useful to prevent requests from taking too long and to provide a better experience * for the clients by sending a timely response when the server is under heavy load or when there are * long-running requests. * * This plugin also aborts the request signal when the deadline is reached. This means that if the * request handler, or any of the plugins hooks, are listening to the request signal, they will be able * to react to the deadline being reached and perform any necessary cleanup operations. */ export function useRequestDeadline(opts) { return { onRequest({ request, requestHandler, setRequestHandler }) { // AbortSignal.timeout() creates an internal timer with no cancellation handle, so it keeps // the event loop alive and holds memory for the full timeout duration even when the request // finishes early. using AbortController + setTimeout gives us a timer handle we can clearTimeout const deadlineCtrl = new AbortController(); const deadlineSignal = deadlineCtrl.signal; const composedSignal = abortSignalAny([request.signal, deadlineSignal]); // overwrite the request signal with the composed signal. we intentionally // dont create a new request because that comes with a performance penalty Object.defineProperty(request, 'signal', { value: composedSignal }); setRequestHandler(function handlerWithDeadline(req, ctx) { if (deadlineSignal.aborted) { return opts.response(req, ctx); } return new Promise((resolve, reject) => { const timer = setTimeout(() => { deadlineCtrl.abort(); resolve(opts.response(req, ctx)); }, opts.timeout); function onDeadlineAbort() { clearTimeout(timer); resolve(opts.response(req, ctx)); } // handle the case where the deadline signal is aborted externally (e.g. via composedSignal) // before the timer fires - we still need to clear the timer deadlineSignal.addEventListener('abort', onDeadlineAbort, { once: true }); return handleMaybePromise(() => requestHandler(req, ctx), result => { clearTimeout(timer); deadlineSignal.removeEventListener('abort', onDeadlineAbort); resolve(result); }, err => { clearTimeout(timer); deadlineSignal.removeEventListener('abort', onDeadlineAbort); reject(err); }); }); }); }, }; }