Skip to content

Commit e8b21f0

Browse files
feat: don't read full file if Range header is present
1 parent 0d55268 commit e8b21f0

6 files changed

+355
-269
lines changed

package-lock.json

+18-18
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/middleware.js

+145-47
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,42 @@
11
import path from "path";
22

33
import mime from "mime-types";
4+
import parseRange from "range-parser";
45

56
import getFilenameFromUrl from "./utils/getFilenameFromUrl";
6-
import handleRangeHeaders from "./utils/handleRangeHeaders";
7+
import {
8+
getHeaderNames,
9+
getHeaderFromRequest,
10+
getHeaderFromResponse,
11+
setHeaderForResponse,
12+
setStatusCode,
13+
send,
14+
} from "./utils/compatibleAPI";
715
import ready from "./utils/ready";
816

17+
function getValueContentRangeHeader(type, size, range) {
18+
return `${type} ${range ? `${range.start}-${range.end}` : "*"}/${size}`;
19+
}
20+
21+
function createHtmlDocument(title, body) {
22+
return (
23+
`${
24+
"<!DOCTYPE html>\n" +
25+
'<html lang="en">\n' +
26+
"<head>\n" +
27+
'<meta charset="utf-8">\n' +
28+
"<title>"
29+
}${title}</title>\n` +
30+
`</head>\n` +
31+
`<body>\n` +
32+
`<pre>${body}</pre>\n` +
33+
`</body>\n` +
34+
`</html>\n`
35+
);
36+
}
37+
38+
const BYTES_RANGE_REGEXP = /^ *bytes/i;
39+
940
export default function wrapper(context) {
1041
return async function middleware(req, res, next) {
1142
const acceptedMethods = context.options.methods || ["GET", "HEAD"];
@@ -16,6 +47,7 @@ export default function wrapper(context) {
1647

1748
if (!acceptedMethods.includes(req.method)) {
1849
await goNext();
50+
1951
return;
2052
}
2153

@@ -42,80 +74,146 @@ export default function wrapper(context) {
4274

4375
async function processRequest() {
4476
const filename = getFilenameFromUrl(context, req.url);
45-
let { headers } = context.options;
46-
47-
if (typeof headers === "function") {
48-
headers = headers(req, res, context);
49-
}
50-
51-
let content;
5277

5378
if (!filename) {
5479
await goNext();
80+
5581
return;
5682
}
5783

58-
try {
59-
content = context.outputFileSystem.readFileSync(filename);
60-
} catch (_ignoreError) {
61-
await goNext();
62-
return;
84+
let { headers } = context.options;
85+
86+
if (typeof headers === "function") {
87+
headers = headers(req, res, context);
6388
}
6489

65-
const contentTypeHeader = res.get
66-
? res.get("Content-Type")
67-
: res.getHeader("Content-Type");
90+
if (headers) {
91+
const names = Object.keys(headers);
92+
93+
for (const name of names) {
94+
setHeaderForResponse(res, name, headers[name]);
95+
}
96+
}
6897

69-
if (!contentTypeHeader) {
98+
if (!getHeaderFromResponse(res, "Content-Type")) {
7099
// content-type name(like application/javascript; charset=utf-8) or false
71100
const contentType = mime.contentType(path.extname(filename));
72101

73102
// Only set content-type header if media type is known
74103
// https://tools.ietf.org/html/rfc7231#section-3.1.1.5
75104
if (contentType) {
76-
// Express API
77-
if (res.set) {
78-
res.set("Content-Type", contentType);
79-
}
80-
// Node.js API
81-
else {
82-
res.setHeader("Content-Type", contentType);
83-
}
105+
setHeaderForResponse(res, "Content-Type", contentType);
84106
}
85107
}
86108

87-
if (headers) {
88-
const names = Object.keys(headers);
109+
if (!getHeaderFromResponse(res, "Accept-Ranges")) {
110+
setHeaderForResponse(res, "Accept-Ranges", "bytes");
111+
}
89112

90-
for (const name of names) {
91-
// Express API
92-
if (res.set) {
93-
res.set(name, headers[name]);
94-
}
95-
// Node.js API
96-
else {
97-
res.setHeader(name, headers[name]);
113+
const rangeHeader = getHeaderFromRequest(req, "range");
114+
115+
let start;
116+
let end;
117+
118+
if (rangeHeader && BYTES_RANGE_REGEXP.test(rangeHeader)) {
119+
const size = await new Promise((resolve) => {
120+
context.outputFileSystem.lstat(filename, (error, stats) => {
121+
if (error) {
122+
context.logger.error(error);
123+
124+
return;
125+
}
126+
127+
resolve(stats.size);
128+
});
129+
});
130+
131+
const parsedRanges = parseRange(size, rangeHeader, {
132+
combine: true,
133+
});
134+
135+
if (parsedRanges === -1) {
136+
const message = "Unsatisfiable range for 'Range' header.";
137+
138+
context.logger.error(message);
139+
140+
const existingHeaders = getHeaderNames(res);
141+
142+
for (let i = 0; i < existingHeaders.length; i++) {
143+
res.removeHeader(existingHeaders[i]);
98144
}
145+
146+
setStatusCode(res, 416);
147+
setHeaderForResponse(
148+
res,
149+
"Content-Range",
150+
getValueContentRangeHeader("bytes", size)
151+
);
152+
setHeaderForResponse(res, "Content-Type", "text/html; charset=utf-8");
153+
154+
const document = createHtmlDocument(416, `Error: ${message}`);
155+
const byteLength = Buffer.byteLength(document);
156+
157+
setHeaderForResponse(
158+
res,
159+
"Content-Length",
160+
Buffer.byteLength(document)
161+
);
162+
163+
send(req, res, document, byteLength);
164+
165+
return;
166+
} else if (parsedRanges === -2) {
167+
context.logger.error(
168+
"A malformed 'Range' header was provided. A regular response will be sent for this request."
169+
);
170+
} else if (parsedRanges.length > 1) {
171+
context.logger.error(
172+
"A 'Range' header with multiple ranges was provided. Multiple ranges are not supported, so a regular response will be sent for this request."
173+
);
99174
}
100-
}
101175

102-
// Buffer
103-
content = handleRangeHeaders(context, content, req, res);
176+
if (parsedRanges !== -2 && parsedRanges.length === 1) {
177+
// Content-Range
178+
setStatusCode(res, 206);
179+
setHeaderForResponse(
180+
res,
181+
"Content-Range",
182+
getValueContentRangeHeader("bytes", size, parsedRanges[0])
183+
);
104184

105-
// Express API
106-
if (res.send) {
107-
res.send(content);
185+
[{ start, end }] = parsedRanges;
186+
}
108187
}
109-
// Node.js API
110-
else {
111-
res.setHeader("Content-Length", content.length);
112188

113-
if (req.method === "HEAD") {
114-
res.end();
189+
const isFsSupportsStream =
190+
typeof context.outputFileSystem.createReadStream === "function";
191+
192+
let bufferOtStream;
193+
let byteLength;
194+
195+
try {
196+
if (
197+
typeof start !== "undefined" &&
198+
typeof end !== "undefined" &&
199+
isFsSupportsStream
200+
) {
201+
bufferOtStream = context.outputFileSystem.createReadStream(filename, {
202+
start,
203+
end,
204+
});
205+
byteLength = end - start + 1;
115206
} else {
116-
res.end(content);
207+
bufferOtStream = context.outputFileSystem.readFileSync(filename);
208+
byteLength = Buffer.byteLength(bufferOtStream);
117209
}
210+
} catch (_ignoreError) {
211+
await goNext();
212+
213+
return;
118214
}
215+
216+
send(req, res, bufferOtStream, byteLength);
119217
}
120218
};
121219
}

0 commit comments

Comments
 (0)