middleware.js 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677
  1. "use strict";
  2. const path = require("path");
  3. const mime = require("mime-types");
  4. const onFinishedStream = require("on-finished");
  5. const getFilenameFromUrl = require("./utils/getFilenameFromUrl");
  6. const {
  7. setStatusCode,
  8. getStatusCode,
  9. getRequestHeader,
  10. getRequestMethod,
  11. getRequestURL,
  12. getResponseHeader,
  13. setResponseHeader,
  14. removeResponseHeader,
  15. getResponseHeaders,
  16. getHeadersSent,
  17. send,
  18. finish,
  19. pipe,
  20. createReadStreamOrReadFileSync,
  21. getOutgoing,
  22. initState,
  23. setState,
  24. getReadyReadableStreamState
  25. } = require("./utils/compatibleAPI");
  26. const ready = require("./utils/ready");
  27. const parseTokenList = require("./utils/parseTokenList");
  28. const memorize = require("./utils/memorize");
  29. /** @typedef {import("./index.js").NextFunction} NextFunction */
  30. /** @typedef {import("./index.js").IncomingMessage} IncomingMessage */
  31. /** @typedef {import("./index.js").ServerResponse} ServerResponse */
  32. /** @typedef {import("./index.js").NormalizedHeaders} NormalizedHeaders */
  33. /** @typedef {import("fs").ReadStream} ReadStream */
  34. const BYTES_RANGE_REGEXP = /^ *bytes/i;
  35. /**
  36. * @param {string} type
  37. * @param {number} size
  38. * @param {import("range-parser").Range} [range]
  39. * @returns {string}
  40. */
  41. function getValueContentRangeHeader(type, size, range) {
  42. return `${type} ${range ? `${range.start}-${range.end}` : "*"}/${size}`;
  43. }
  44. /**
  45. * Parse an HTTP Date into a number.
  46. *
  47. * @param {string} date
  48. * @returns {number}
  49. */
  50. function parseHttpDate(date) {
  51. const timestamp = date && Date.parse(date);
  52. // istanbul ignore next: guard against date.js Date.parse patching
  53. return typeof timestamp === "number" ? timestamp : NaN;
  54. }
  55. const CACHE_CONTROL_NO_CACHE_REGEXP = /(?:^|,)\s*?no-cache\s*?(?:,|$)/;
  56. /**
  57. * @param {import("fs").ReadStream} stream stream
  58. * @param {boolean} suppress do need suppress?
  59. * @returns {void}
  60. */
  61. function destroyStream(stream, suppress) {
  62. if (typeof stream.destroy === "function") {
  63. stream.destroy();
  64. }
  65. if (typeof stream.close === "function") {
  66. // Node.js core bug workaround
  67. stream.on("open",
  68. /**
  69. * @this {import("fs").ReadStream}
  70. */
  71. function onOpenClose() {
  72. // @ts-ignore
  73. if (typeof this.fd === "number") {
  74. // actually close down the fd
  75. this.close();
  76. }
  77. });
  78. }
  79. if (typeof stream.addListener === "function" && suppress) {
  80. stream.removeAllListeners("error");
  81. stream.addListener("error", () => {});
  82. }
  83. }
  84. /** @type {Record<number, string>} */
  85. const statuses = {
  86. 400: "Bad Request",
  87. 403: "Forbidden",
  88. 404: "Not Found",
  89. 416: "Range Not Satisfiable",
  90. 500: "Internal Server Error"
  91. };
  92. const parseRangeHeaders = memorize(
  93. /**
  94. * @param {string} value
  95. * @returns {import("range-parser").Result | import("range-parser").Ranges}
  96. */
  97. value => {
  98. const [len, rangeHeader] = value.split("|");
  99. // eslint-disable-next-line global-require
  100. return require("range-parser")(Number(len), rangeHeader, {
  101. combine: true
  102. });
  103. });
  104. const MAX_MAX_AGE = 31536000000;
  105. /**
  106. * @template {IncomingMessage} Request
  107. * @template {ServerResponse} Response
  108. * @typedef {Object} SendErrorOptions send error options
  109. * @property {Record<string, number | string | string[] | undefined>=} headers headers
  110. * @property {import("./index").ModifyResponseData<Request, Response>=} modifyResponseData modify response data callback
  111. */
  112. /**
  113. * @template {IncomingMessage} Request
  114. * @template {ServerResponse} Response
  115. * @param {import("./index.js").FilledContext<Request, Response>} context
  116. * @return {import("./index.js").Middleware<Request, Response>}
  117. */
  118. function wrapper(context) {
  119. return async function middleware(req, res, next) {
  120. const acceptedMethods = context.options.methods || ["GET", "HEAD"];
  121. initState(res);
  122. async function goNext() {
  123. if (!context.options.serverSideRender) {
  124. return next();
  125. }
  126. return new Promise(resolve => {
  127. ready(context, () => {
  128. setState(res, "webpack", {
  129. devMiddleware: context
  130. });
  131. resolve(next());
  132. }, req);
  133. });
  134. }
  135. const method = getRequestMethod(req);
  136. if (method && !acceptedMethods.includes(method)) {
  137. await goNext();
  138. return;
  139. }
  140. /**
  141. * @param {number} status status
  142. * @param {Partial<SendErrorOptions<Request, Response>>=} options options
  143. * @returns {void}
  144. */
  145. function sendError(status, options) {
  146. // eslint-disable-next-line global-require
  147. const escapeHtml = require("./utils/escapeHtml");
  148. const content = statuses[status] || String(status);
  149. let document = Buffer.from(`<!DOCTYPE html>
  150. <html lang="en">
  151. <head>
  152. <meta charset="utf-8">
  153. <title>Error</title>
  154. </head>
  155. <body>
  156. <pre>${escapeHtml(content)}</pre>
  157. </body>
  158. </html>`, "utf-8");
  159. // Clear existing headers
  160. const headers = getResponseHeaders(res);
  161. for (let i = 0; i < headers.length; i++) {
  162. removeResponseHeader(res, headers[i]);
  163. }
  164. if (options && options.headers) {
  165. const keys = Object.keys(options.headers);
  166. for (let i = 0; i < keys.length; i++) {
  167. const key = keys[i];
  168. const value = options.headers[key];
  169. if (typeof value !== "undefined") {
  170. setResponseHeader(res, key, value);
  171. }
  172. }
  173. }
  174. // Send basic response
  175. setStatusCode(res, status);
  176. setResponseHeader(res, "Content-Type", "text/html; charset=utf-8");
  177. setResponseHeader(res, "Content-Security-Policy", "default-src 'none'");
  178. setResponseHeader(res, "X-Content-Type-Options", "nosniff");
  179. let byteLength = Buffer.byteLength(document);
  180. if (options && options.modifyResponseData) {
  181. ({
  182. data: document,
  183. byteLength
  184. } = /** @type {{ data: Buffer, byteLength: number }} */
  185. options.modifyResponseData(req, res, document, byteLength));
  186. }
  187. setResponseHeader(res, "Content-Length", byteLength);
  188. finish(res, document);
  189. }
  190. /**
  191. * @param {NodeJS.ErrnoException} error
  192. */
  193. function errorHandler(error) {
  194. switch (error.code) {
  195. case "ENAMETOOLONG":
  196. case "ENOENT":
  197. case "ENOTDIR":
  198. sendError(404, {
  199. modifyResponseData: context.options.modifyResponseData
  200. });
  201. break;
  202. default:
  203. sendError(500, {
  204. modifyResponseData: context.options.modifyResponseData
  205. });
  206. break;
  207. }
  208. }
  209. function isConditionalGET() {
  210. return getRequestHeader(req, "if-match") || getRequestHeader(req, "if-unmodified-since") || getRequestHeader(req, "if-none-match") || getRequestHeader(req, "if-modified-since");
  211. }
  212. function isPreconditionFailure() {
  213. // if-match
  214. const ifMatch = /** @type {string} */getRequestHeader(req, "if-match");
  215. // A recipient MUST ignore If-Unmodified-Since if the request contains
  216. // an If-Match header field; the condition in If-Match is considered to
  217. // be a more accurate replacement for the condition in
  218. // If-Unmodified-Since, and the two are only combined for the sake of
  219. // interoperating with older intermediaries that might not implement If-Match.
  220. if (ifMatch) {
  221. const etag = getResponseHeader(res, "ETag");
  222. return !etag || ifMatch !== "*" && parseTokenList(ifMatch).every(match => match !== etag && match !== `W/${etag}` && `W/${match}` !== etag);
  223. }
  224. // if-unmodified-since
  225. const ifUnmodifiedSince = /** @type {string} */
  226. getRequestHeader(req, "if-unmodified-since");
  227. if (ifUnmodifiedSince) {
  228. const unmodifiedSince = parseHttpDate(ifUnmodifiedSince);
  229. // A recipient MUST ignore the If-Unmodified-Since header field if the
  230. // received field-value is not a valid HTTP-date.
  231. if (!isNaN(unmodifiedSince)) {
  232. const lastModified = parseHttpDate( /** @type {string} */getResponseHeader(res, "Last-Modified"));
  233. return isNaN(lastModified) || lastModified > unmodifiedSince;
  234. }
  235. }
  236. return false;
  237. }
  238. /**
  239. * @returns {boolean} is cachable
  240. */
  241. function isCachable() {
  242. const statusCode = getStatusCode(res);
  243. return statusCode >= 200 && statusCode < 300 || statusCode === 304 ||
  244. // For Koa and Hono, because by default status code is 404, but we already found a file
  245. statusCode === 404;
  246. }
  247. /**
  248. * @param {import("http").OutgoingHttpHeaders} resHeaders
  249. * @returns {boolean}
  250. */
  251. function isFresh(resHeaders) {
  252. // Always return stale when Cache-Control: no-cache to support end-to-end reload requests
  253. // https://tools.ietf.org/html/rfc2616#section-14.9.4
  254. const cacheControl = /** @type {string} */
  255. getRequestHeader(req, "cache-control");
  256. if (cacheControl && CACHE_CONTROL_NO_CACHE_REGEXP.test(cacheControl)) {
  257. return false;
  258. }
  259. // fields
  260. const noneMatch = /** @type {string} */
  261. getRequestHeader(req, "if-none-match");
  262. const modifiedSince = /** @type {string} */
  263. getRequestHeader(req, "if-modified-since");
  264. // unconditional request
  265. if (!noneMatch && !modifiedSince) {
  266. return false;
  267. }
  268. // if-none-match
  269. if (noneMatch && noneMatch !== "*") {
  270. if (!resHeaders.etag) {
  271. return false;
  272. }
  273. const matches = parseTokenList(noneMatch);
  274. let etagStale = true;
  275. for (let i = 0; i < matches.length; i++) {
  276. const match = matches[i];
  277. if (match === resHeaders.etag || match === `W/${resHeaders.etag}` || `W/${match}` === resHeaders.etag) {
  278. etagStale = false;
  279. break;
  280. }
  281. }
  282. if (etagStale) {
  283. return false;
  284. }
  285. }
  286. // A recipient MUST ignore If-Modified-Since if the request contains an If-None-Match header field;
  287. // the condition in If-None-Match is considered to be a more accurate replacement for the condition in If-Modified-Since,
  288. // and the two are only combined for the sake of interoperating with older intermediaries that might not implement If-None-Match.
  289. if (noneMatch) {
  290. return true;
  291. }
  292. // if-modified-since
  293. if (modifiedSince) {
  294. const lastModified = resHeaders["last-modified"];
  295. // A recipient MUST ignore the If-Modified-Since header field if the
  296. // received field-value is not a valid HTTP-date, or if the request
  297. // method is neither GET nor HEAD.
  298. const modifiedStale = !lastModified || !(parseHttpDate(lastModified) <= parseHttpDate(modifiedSince));
  299. if (modifiedStale) {
  300. return false;
  301. }
  302. }
  303. return true;
  304. }
  305. function isRangeFresh() {
  306. const ifRange = /** @type {string | undefined} */
  307. getRequestHeader(req, "if-range");
  308. if (!ifRange) {
  309. return true;
  310. }
  311. // if-range as etag
  312. if (ifRange.indexOf('"') !== -1) {
  313. const etag = /** @type {string | undefined} */
  314. getResponseHeader(res, "ETag");
  315. if (!etag) {
  316. return true;
  317. }
  318. return Boolean(etag && ifRange.indexOf(etag) !== -1);
  319. }
  320. // if-range as modified date
  321. const lastModified = /** @type {string | undefined} */
  322. getResponseHeader(res, "Last-Modified");
  323. if (!lastModified) {
  324. return true;
  325. }
  326. return parseHttpDate(lastModified) <= parseHttpDate(ifRange);
  327. }
  328. /**
  329. * @returns {string | undefined}
  330. */
  331. function getRangeHeader() {
  332. const range = /** @type {string} */getRequestHeader(req, "range");
  333. if (range && BYTES_RANGE_REGEXP.test(range)) {
  334. return range;
  335. }
  336. // eslint-disable-next-line no-undefined
  337. return undefined;
  338. }
  339. /**
  340. * @param {import("range-parser").Range} range
  341. * @returns {[number, number]}
  342. */
  343. function getOffsetAndLenFromRange(range) {
  344. const offset = range.start;
  345. const len = range.end - range.start + 1;
  346. return [offset, len];
  347. }
  348. /**
  349. * @param {number} offset
  350. * @param {number} len
  351. * @returns {[number, number]}
  352. */
  353. function calcStartAndEnd(offset, len) {
  354. const start = offset;
  355. const end = Math.max(offset, offset + len - 1);
  356. return [start, end];
  357. }
  358. async function processRequest() {
  359. // Pipe and SendFile
  360. /** @type {import("./utils/getFilenameFromUrl").Extra} */
  361. const extra = {};
  362. const filename = getFilenameFromUrl(context, /** @type {string} */getRequestURL(req), extra);
  363. if (extra.errorCode) {
  364. if (extra.errorCode === 403) {
  365. context.logger.error(`Malicious path "${filename}".`);
  366. }
  367. sendError(extra.errorCode, {
  368. modifyResponseData: context.options.modifyResponseData
  369. });
  370. await goNext();
  371. return;
  372. }
  373. if (!filename) {
  374. await goNext();
  375. return;
  376. }
  377. if (getHeadersSent(res)) {
  378. await goNext();
  379. return;
  380. }
  381. const {
  382. size
  383. } = /** @type {import("fs").Stats} */extra.stats;
  384. let len = size;
  385. let offset = 0;
  386. // Send logic
  387. if (context.options.headers) {
  388. let {
  389. headers
  390. } = context.options;
  391. if (typeof headers === "function") {
  392. headers = /** @type {NormalizedHeaders} */
  393. headers(req, res, context);
  394. }
  395. /**
  396. * @type {{key: string, value: string | number}[]}
  397. */
  398. const allHeaders = [];
  399. if (typeof headers !== "undefined") {
  400. if (!Array.isArray(headers)) {
  401. // eslint-disable-next-line guard-for-in
  402. for (const name in headers) {
  403. allHeaders.push({
  404. key: name,
  405. value: headers[name]
  406. });
  407. }
  408. headers = allHeaders;
  409. }
  410. for (const {
  411. key,
  412. value
  413. } of headers) {
  414. setResponseHeader(res, key, value);
  415. }
  416. }
  417. }
  418. if (!getResponseHeader(res, "Accept-Ranges")) {
  419. setResponseHeader(res, "Accept-Ranges", "bytes");
  420. }
  421. if (!getResponseHeader(res, "Cache-Control")) {
  422. // TODO enable the `cacheImmutable` by default for the next major release
  423. const cacheControl = context.options.cacheImmutable && extra.immutable ? {
  424. immutable: true
  425. } : context.options.cacheControl;
  426. if (cacheControl) {
  427. let cacheControlValue;
  428. if (typeof cacheControl === "boolean") {
  429. cacheControlValue = "public, max-age=31536000";
  430. } else if (typeof cacheControl === "number") {
  431. const maxAge = Math.floor(Math.min(Math.max(0, cacheControl), MAX_MAX_AGE) / 1000);
  432. cacheControlValue = `public, max-age=${maxAge}`;
  433. } else if (typeof cacheControl === "string") {
  434. cacheControlValue = cacheControl;
  435. } else {
  436. const maxAge = cacheControl.maxAge ? Math.floor(Math.min(Math.max(0, cacheControl.maxAge), MAX_MAX_AGE) / 1000) : MAX_MAX_AGE / 1000;
  437. cacheControlValue = `public, max-age=${maxAge}`;
  438. if (cacheControl.immutable) {
  439. cacheControlValue += ", immutable";
  440. }
  441. }
  442. setResponseHeader(res, "Cache-Control", cacheControlValue);
  443. }
  444. }
  445. if (context.options.lastModified && !getResponseHeader(res, "Last-Modified")) {
  446. const modified = /** @type {import("fs").Stats} */
  447. extra.stats.mtime.toUTCString();
  448. setResponseHeader(res, "Last-Modified", modified);
  449. }
  450. /** @type {number} */
  451. let start;
  452. /** @type {number} */
  453. let end;
  454. /** @type {undefined | Buffer | ReadStream} */
  455. let bufferOrStream;
  456. /** @type {number | undefined} */
  457. let byteLength;
  458. const rangeHeader = getRangeHeader();
  459. if (context.options.etag && !getResponseHeader(res, "ETag")) {
  460. /** @type {import("fs").Stats | Buffer | ReadStream | undefined} */
  461. let value;
  462. // TODO cache etag generation?
  463. if (context.options.etag === "weak") {
  464. value = /** @type {import("fs").Stats} */extra.stats;
  465. } else {
  466. if (rangeHeader) {
  467. const parsedRanges = /** @type {import("range-parser").Ranges | import("range-parser").Result} */
  468. parseRangeHeaders(`${size}|${rangeHeader}`);
  469. if (parsedRanges !== -2 && parsedRanges !== -1 && parsedRanges.length === 1) {
  470. [offset, len] = getOffsetAndLenFromRange(parsedRanges[0]);
  471. }
  472. }
  473. [start, end] = calcStartAndEnd(offset, len);
  474. try {
  475. const result = createReadStreamOrReadFileSync(filename, context.outputFileSystem, start, end);
  476. value = result.bufferOrStream;
  477. ({
  478. bufferOrStream,
  479. byteLength
  480. } = result);
  481. } catch (error) {
  482. errorHandler( /** @type {NodeJS.ErrnoException} */error);
  483. await goNext();
  484. return;
  485. }
  486. }
  487. if (value) {
  488. // eslint-disable-next-line global-require
  489. const result = await require("./utils/etag")(value);
  490. // Because we already read stream, we can cache buffer to avoid extra read from fs
  491. if (result.buffer) {
  492. bufferOrStream = result.buffer;
  493. }
  494. setResponseHeader(res, "ETag", result.hash);
  495. }
  496. }
  497. if (!getResponseHeader(res, "Content-Type") || getStatusCode(res) === 404) {
  498. removeResponseHeader(res, "Content-Type");
  499. // content-type name(like application/javascript; charset=utf-8) or false
  500. const contentType = mime.contentType(path.extname(filename));
  501. // Only set content-type header if media type is known
  502. // https://tools.ietf.org/html/rfc7231#section-3.1.1.5
  503. if (contentType) {
  504. setResponseHeader(res, "Content-Type", contentType);
  505. } else if (context.options.mimeTypeDefault) {
  506. setResponseHeader(res, "Content-Type", context.options.mimeTypeDefault);
  507. }
  508. }
  509. // Conditional GET support
  510. if (isConditionalGET()) {
  511. if (isPreconditionFailure()) {
  512. sendError(412, {
  513. modifyResponseData: context.options.modifyResponseData
  514. });
  515. await goNext();
  516. return;
  517. }
  518. if (isCachable() && isFresh({
  519. etag: ( /** @type {string | undefined} */
  520. getResponseHeader(res, "ETag")),
  521. "last-modified": ( /** @type {string | undefined} */
  522. getResponseHeader(res, "Last-Modified"))
  523. })) {
  524. setStatusCode(res, 304);
  525. // Remove content header fields
  526. removeResponseHeader(res, "Content-Encoding");
  527. removeResponseHeader(res, "Content-Language");
  528. removeResponseHeader(res, "Content-Length");
  529. removeResponseHeader(res, "Content-Range");
  530. removeResponseHeader(res, "Content-Type");
  531. finish(res);
  532. await goNext();
  533. return;
  534. }
  535. }
  536. let isPartialContent = false;
  537. if (rangeHeader) {
  538. let parsedRanges = /** @type {import("range-parser").Ranges | import("range-parser").Result | []} */
  539. parseRangeHeaders(`${size}|${rangeHeader}`);
  540. // If-Range support
  541. if (!isRangeFresh()) {
  542. parsedRanges = [];
  543. }
  544. if (parsedRanges === -1) {
  545. context.logger.error("Unsatisfiable range for 'Range' header.");
  546. setResponseHeader(res, "Content-Range", getValueContentRangeHeader("bytes", size));
  547. sendError(416, {
  548. headers: {
  549. "Content-Range": getResponseHeader(res, "Content-Range")
  550. },
  551. modifyResponseData: context.options.modifyResponseData
  552. });
  553. await goNext();
  554. return;
  555. } else if (parsedRanges === -2) {
  556. context.logger.error("A malformed 'Range' header was provided. A regular response will be sent for this request.");
  557. } else if (parsedRanges.length > 1) {
  558. context.logger.error("A 'Range' header with multiple ranges was provided. Multiple ranges are not supported, so a regular response will be sent for this request.");
  559. }
  560. if (parsedRanges !== -2 && parsedRanges.length === 1) {
  561. // Content-Range
  562. setStatusCode(res, 206);
  563. setResponseHeader(res, "Content-Range", getValueContentRangeHeader("bytes", size, /** @type {import("range-parser").Ranges} */parsedRanges[0]));
  564. isPartialContent = true;
  565. [offset, len] = getOffsetAndLenFromRange(parsedRanges[0]);
  566. }
  567. }
  568. // When strong Etag generation is enabled we already read file, so we can skip extra fs call
  569. if (!bufferOrStream) {
  570. [start, end] = calcStartAndEnd(offset, len);
  571. try {
  572. ({
  573. bufferOrStream,
  574. byteLength
  575. } = createReadStreamOrReadFileSync(filename, context.outputFileSystem, start, end));
  576. } catch (error) {
  577. errorHandler( /** @type {NodeJS.ErrnoException} */error);
  578. await goNext();
  579. return;
  580. }
  581. }
  582. if (context.options.modifyResponseData) {
  583. ({
  584. data: bufferOrStream,
  585. byteLength
  586. } = context.options.modifyResponseData(req, res, bufferOrStream, /** @type {number} */
  587. byteLength));
  588. }
  589. setResponseHeader(res, "Content-Length", /** @type {number} */
  590. byteLength);
  591. if (method === "HEAD") {
  592. if (!isPartialContent) {
  593. setStatusCode(res, 200);
  594. }
  595. finish(res);
  596. await goNext();
  597. return;
  598. }
  599. if (!isPartialContent) {
  600. setStatusCode(res, 200);
  601. }
  602. const isPipeSupports = typeof ( /** @type {import("fs").ReadStream} */bufferOrStream.pipe) === "function";
  603. if (!isPipeSupports) {
  604. send(res, /** @type {Buffer} */bufferOrStream);
  605. await goNext();
  606. return;
  607. }
  608. // Cleanup
  609. const cleanup = () => {
  610. destroyStream( /** @type {import("fs").ReadStream} */bufferOrStream, true);
  611. };
  612. // Error handling
  613. /** @type {import("fs").ReadStream} */
  614. bufferOrStream.on("error", error => {
  615. // clean up stream early
  616. cleanup();
  617. errorHandler(error);
  618. goNext();
  619. }).on(getReadyReadableStreamState(res), () => {
  620. goNext();
  621. });
  622. pipe(res, /** @type {ReadStream} */bufferOrStream);
  623. const outgoing = getOutgoing(res);
  624. if (outgoing) {
  625. // Response finished, cleanup
  626. onFinishedStream(outgoing, cleanup);
  627. }
  628. }
  629. ready(context, processRequest, req);
  630. };
  631. }
  632. module.exports = wrapper;