middleware.js 23 KB

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