@whatwg-node/server
Version:
Fetch API compliant HTTP Server adapter
57 lines (56 loc) • 3.2 kB
JavaScript
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);
});
});
});
},
};
}