middleware.js 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  1. "use strict";
  2. const path = require("path");
  3. const mime = require("mime-types");
  4. const getFilenameFromUrl = require("./utils/getFilenameFromUrl");
  5. const {
  6. getHeaderNames,
  7. getHeaderFromRequest,
  8. getHeaderFromResponse,
  9. setHeaderForResponse,
  10. setStatusCode,
  11. send
  12. } = require("./utils/compatibleAPI");
  13. const ready = require("./utils/ready");
  14. /** @typedef {import("./index.js").NextFunction} NextFunction */
  15. /** @typedef {import("./index.js").IncomingMessage} IncomingMessage */
  16. /** @typedef {import("./index.js").ServerResponse} ServerResponse */
  17. /**
  18. * @param {string} type
  19. * @param {number} size
  20. * @param {import("range-parser").Range} [range]
  21. * @returns {string}
  22. */
  23. function getValueContentRangeHeader(type, size, range) {
  24. return `${type} ${range ? `${range.start}-${range.end}` : "*"}/${size}`;
  25. }
  26. /**
  27. * @param {string | number} title
  28. * @param {string} body
  29. * @returns {string}
  30. */
  31. function createHtmlDocument(title, body) {
  32. return `${"<!DOCTYPE html>\n" + '<html lang="en">\n' + "<head>\n" + '<meta charset="utf-8">\n' + "<title>"}${title}</title>\n` + `</head>\n` + `<body>\n` + `<pre>${body}</pre>\n` + `</body>\n` + `</html>\n`;
  33. }
  34. const BYTES_RANGE_REGEXP = /^ *bytes/i;
  35. /**
  36. * @template {IncomingMessage} Request
  37. * @template {ServerResponse} Response
  38. * @param {import("./index.js").Context<Request, Response>} context
  39. * @return {import("./index.js").Middleware<Request, Response>}
  40. */
  41. function wrapper(context) {
  42. return async function middleware(req, res, next) {
  43. const acceptedMethods = context.options.methods || ["GET", "HEAD"];
  44. // fixes #282. credit @cexoso. in certain edge situations res.locals is undefined.
  45. // eslint-disable-next-line no-param-reassign
  46. res.locals = res.locals || {};
  47. if (req.method && !acceptedMethods.includes(req.method)) {
  48. await goNext();
  49. return;
  50. }
  51. ready(context, processRequest, req);
  52. async function goNext() {
  53. if (!context.options.serverSideRender) {
  54. return next();
  55. }
  56. return new Promise(resolve => {
  57. ready(context, () => {
  58. /** @type {any} */
  59. // eslint-disable-next-line no-param-reassign
  60. res.locals.webpack = {
  61. devMiddleware: context
  62. };
  63. resolve(next());
  64. }, req);
  65. });
  66. }
  67. async function processRequest() {
  68. const filename = getFilenameFromUrl(context, /** @type {string} */req.url);
  69. if (!filename) {
  70. await goNext();
  71. return;
  72. }
  73. let {
  74. headers
  75. } = context.options;
  76. if (typeof headers === "function") {
  77. // @ts-ignore
  78. headers = headers(req, res, context);
  79. }
  80. /**
  81. * @type {{key: string, value: string | number}[]}
  82. */
  83. const allHeaders = [];
  84. if (typeof headers !== "undefined") {
  85. if (!Array.isArray(headers)) {
  86. // eslint-disable-next-line guard-for-in
  87. for (const name in headers) {
  88. // @ts-ignore
  89. allHeaders.push({
  90. key: name,
  91. value: headers[name]
  92. });
  93. }
  94. headers = allHeaders;
  95. }
  96. headers.forEach(
  97. /**
  98. * @param {{key: string, value: any}} header
  99. */
  100. header => {
  101. setHeaderForResponse(res, header.key, header.value);
  102. });
  103. }
  104. if (!getHeaderFromResponse(res, "Content-Type")) {
  105. // content-type name(like application/javascript; charset=utf-8) or false
  106. const contentType = mime.contentType(path.extname(filename));
  107. // Only set content-type header if media type is known
  108. // https://tools.ietf.org/html/rfc7231#section-3.1.1.5
  109. if (contentType) {
  110. setHeaderForResponse(res, "Content-Type", contentType);
  111. } else if (context.options.mimeTypeDefault) {
  112. setHeaderForResponse(res, "Content-Type", context.options.mimeTypeDefault);
  113. }
  114. }
  115. if (!getHeaderFromResponse(res, "Accept-Ranges")) {
  116. setHeaderForResponse(res, "Accept-Ranges", "bytes");
  117. }
  118. const rangeHeader = getHeaderFromRequest(req, "range");
  119. let start;
  120. let end;
  121. if (rangeHeader && BYTES_RANGE_REGEXP.test(rangeHeader)) {
  122. const size = await new Promise(resolve => {
  123. /** @type {import("fs").lstat} */
  124. context.outputFileSystem.lstat(filename, (error, stats) => {
  125. if (error) {
  126. context.logger.error(error);
  127. return;
  128. }
  129. resolve(stats.size);
  130. });
  131. });
  132. // eslint-disable-next-line global-require
  133. const parsedRanges = require("range-parser")(size, rangeHeader, {
  134. combine: true
  135. });
  136. if (parsedRanges === -1) {
  137. const message = "Unsatisfiable range for 'Range' header.";
  138. context.logger.error(message);
  139. const existingHeaders = getHeaderNames(res);
  140. for (let i = 0; i < existingHeaders.length; i++) {
  141. res.removeHeader(existingHeaders[i]);
  142. }
  143. setStatusCode(res, 416);
  144. setHeaderForResponse(res, "Content-Range", getValueContentRangeHeader("bytes", size));
  145. setHeaderForResponse(res, "Content-Type", "text/html; charset=utf-8");
  146. /** @type {string | Buffer | import("fs").ReadStream} */
  147. let document = createHtmlDocument(416, `Error: ${message}`);
  148. let byteLength = Buffer.byteLength(document);
  149. setHeaderForResponse(res, "Content-Length", Buffer.byteLength(document));
  150. if (context.options.modifyResponseData) {
  151. ({
  152. data: document,
  153. byteLength
  154. } = context.options.modifyResponseData(req, res, document, byteLength));
  155. }
  156. send(req, res, document, byteLength);
  157. return;
  158. } else if (parsedRanges === -2) {
  159. context.logger.error("A malformed 'Range' header was provided. A regular response will be sent for this request.");
  160. } else if (parsedRanges.length > 1) {
  161. 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.");
  162. }
  163. if (parsedRanges !== -2 && parsedRanges.length === 1) {
  164. // Content-Range
  165. setStatusCode(res, 206);
  166. setHeaderForResponse(res, "Content-Range", getValueContentRangeHeader("bytes", size, /** @type {import("range-parser").Ranges} */parsedRanges[0]));
  167. [{
  168. start,
  169. end
  170. }] = parsedRanges;
  171. }
  172. }
  173. const isFsSupportsStream = typeof context.outputFileSystem.createReadStream === "function";
  174. let bufferOrStream;
  175. let byteLength;
  176. try {
  177. if (typeof start !== "undefined" && typeof end !== "undefined" && isFsSupportsStream) {
  178. bufferOrStream = /** @type {import("fs").createReadStream} */
  179. context.outputFileSystem.createReadStream(filename, {
  180. start,
  181. end
  182. });
  183. byteLength = end - start + 1;
  184. } else {
  185. bufferOrStream = /** @type {import("fs").readFileSync} */context.outputFileSystem.readFileSync(filename);
  186. ({
  187. byteLength
  188. } = bufferOrStream);
  189. }
  190. } catch (_ignoreError) {
  191. await goNext();
  192. return;
  193. }
  194. if (context.options.modifyResponseData) {
  195. ({
  196. data: bufferOrStream,
  197. byteLength
  198. } = context.options.modifyResponseData(req, res, bufferOrStream, byteLength));
  199. }
  200. send(req, res, bufferOrStream, byteLength);
  201. }
  202. };
  203. }
  204. module.exports = wrapper;