@hono/node-server 2.0.8 → 2.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -60,6 +60,54 @@ const newHeadersFromIncoming = (incoming) => {
60
60
  return new Headers(headerRecord);
61
61
  };
62
62
  const wrapBodyStream = Symbol("wrapBodyStream");
63
+ const byteExactEncodings = new Set([
64
+ "latin1",
65
+ "binary",
66
+ "hex",
67
+ "base64",
68
+ "base64url"
69
+ ]);
70
+ const isByteExactEncoding = (encoding) => encoding === null || byteExactEncodings.has(encoding);
71
+ const bodyBufferedBeforeDisconnectKey = Symbol("bodyBufferedBeforeDisconnect");
72
+ const bodyBufferedLengthBeforeDisconnectKey = Symbol("bodyBufferedLengthBeforeDisconnect");
73
+ const toBufferChunk = (chunk, encoding) => Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, encoding ?? "utf8");
74
+ const isRecoverableDisconnectedIncoming = (incoming) => !(incoming instanceof node_http2.Http2ServerRequest) && !!incoming.complete && !!incoming.readableAborted && typeof incoming.read === "function" && isByteExactEncoding(incoming.readableEncoding);
75
+ const recordBodyBufferedBeforeDisconnect = (incoming) => {
76
+ if (incoming.readableDidRead || !isRecoverableDisconnectedIncoming(incoming)) return;
77
+ const incomingWithRecovery = incoming;
78
+ incomingWithRecovery[bodyBufferedLengthBeforeDisconnectKey] ??= incoming.readableLength;
79
+ };
80
+ const readBodyBufferedBeforeDisconnect = (incoming, chunks) => {
81
+ if (incoming.readableDidRead && !chunks || !isRecoverableDisconnectedIncoming(incoming)) return;
82
+ const incomingWithRecovery = incoming;
83
+ if (incomingWithRecovery[bodyBufferedBeforeDisconnectKey] !== void 0) return incomingWithRecovery[bodyBufferedBeforeDisconnectKey];
84
+ let result;
85
+ const errored = incoming.errored;
86
+ if (errored && errored.code !== "ECONNRESET") result = errored;
87
+ else if (incomingWithRecovery[bodyBufferedLengthBeforeDisconnectKey] !== void 0 && incoming.readableLength !== incomingWithRecovery[bodyBufferedLengthBeforeDisconnectKey]) result = newBodyUnusableError();
88
+ else {
89
+ const bodyChunks = chunks ?? [];
90
+ const chunk = incoming.read();
91
+ if (chunk !== null) bodyChunks.push(toBufferChunk(chunk, incoming.readableEncoding));
92
+ const buffer = bodyChunks.length === 1 ? bodyChunks[0] : Buffer.concat(bodyChunks);
93
+ result = buffer;
94
+ const contentLength = incoming.headers["content-length"];
95
+ if (typeof contentLength === "string" && /^\d+$/.test(contentLength)) {
96
+ const expectedLength = Number(contentLength);
97
+ if (Number.isSafeInteger(expectedLength) && buffer.length !== expectedLength) result = newBodyUnusableError();
98
+ }
99
+ }
100
+ incomingWithRecovery[bodyBufferedBeforeDisconnectKey] = result;
101
+ return result;
102
+ };
103
+ const enqueueBufferedBody = (controller, buffered) => {
104
+ if (buffered instanceof Error) {
105
+ controller.error(buffered);
106
+ return;
107
+ }
108
+ if (buffered.length > 0) controller.enqueue(buffered);
109
+ controller.close();
110
+ };
63
111
  const newRequestFromIncoming = (method, url, headers, incoming, abortController) => {
64
112
  const init = {
65
113
  method,
@@ -82,6 +130,13 @@ const newRequestFromIncoming = (method, url, headers, incoming, abortController)
82
130
  let reader;
83
131
  init.body = new ReadableStream({ async pull(controller) {
84
132
  try {
133
+ if (!reader) {
134
+ const buffered = readBodyBufferedBeforeDisconnect(incoming);
135
+ if (buffered !== void 0) {
136
+ enqueueBufferedBody(controller, buffered);
137
+ return;
138
+ }
139
+ }
85
140
  reader ||= node_stream.Readable.toWeb(incoming).getReader();
86
141
  const { done, value } = await reader.read();
87
142
  if (done) controller.close();
@@ -90,7 +145,13 @@ const newRequestFromIncoming = (method, url, headers, incoming, abortController)
90
145
  controller.error(error);
91
146
  }
92
147
  } });
93
- } else init.body = node_stream.Readable.toWeb(incoming);
148
+ } else {
149
+ const buffered = readBodyBufferedBeforeDisconnect(incoming);
150
+ if (buffered !== void 0) init.body = new ReadableStream({ start(controller) {
151
+ enqueueBufferedBody(controller, buffered);
152
+ } });
153
+ else init.body = node_stream.Readable.toWeb(incoming);
154
+ }
94
155
  return new Request$1(url, init);
95
156
  };
96
157
  const getRequestCache = Symbol("getRequestCache");
@@ -178,11 +239,23 @@ const readRawBodyIfAvailable = (request) => {
178
239
  const incoming = request[incomingKey];
179
240
  if ("rawBody" in incoming && incoming.rawBody instanceof Buffer) return incoming.rawBody;
180
241
  };
242
+ const normalizeAbortError = (request, incoming) => {
243
+ if (incoming.errored) return incoming.errored;
244
+ const reason = request[abortReasonKey];
245
+ if (reason !== void 0) return reason instanceof Error ? reason : new Error(String(reason));
246
+ return /* @__PURE__ */ new Error("Client connection prematurely closed.");
247
+ };
181
248
  const readBodyDirect = (request) => {
182
249
  if (request[bodyBufferKey]) return Promise.resolve(request[bodyBufferKey]);
183
250
  if (request[bodyReadPromiseKey]) return request[bodyReadPromiseKey];
184
251
  const incoming = request[incomingKey];
185
- if (node_stream.Readable.isDisturbed(incoming)) return rejectBodyUnusable();
252
+ if (incoming.readableDidRead) return rejectBodyUnusable();
253
+ const buffered = readBodyBufferedBeforeDisconnect(incoming);
254
+ if (buffered !== void 0) {
255
+ if (buffered instanceof Error) return Promise.reject(buffered);
256
+ request[bodyBufferKey] = buffered;
257
+ return Promise.resolve(buffered);
258
+ }
186
259
  const promise = new Promise((resolve, reject) => {
187
260
  const chunks = [];
188
261
  let settled = false;
@@ -192,8 +265,22 @@ const readBodyDirect = (request) => {
192
265
  cleanup();
193
266
  callback();
194
267
  };
268
+ const recoverCompleteBodyAfterDisconnect = (error) => {
269
+ const streamError = incoming.errored ?? error;
270
+ if (!isRecoverableDisconnectedIncoming(incoming) || streamError && streamError.code !== "ECONNRESET") return false;
271
+ finish(() => {
272
+ const recovered = readBodyBufferedBeforeDisconnect(incoming, chunks);
273
+ if (recovered instanceof Error) reject(recovered);
274
+ else if (recovered === void 0) reject(error ?? normalizeAbortError(request, incoming));
275
+ else {
276
+ request[bodyBufferKey] = recovered;
277
+ resolve(recovered);
278
+ }
279
+ });
280
+ return true;
281
+ };
195
282
  const onData = (chunk) => {
196
- chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
283
+ chunks.push(toBufferChunk(chunk, incoming.readableEncoding));
197
284
  };
198
285
  const onEnd = () => {
199
286
  finish(() => {
@@ -203,6 +290,7 @@ const readBodyDirect = (request) => {
203
290
  });
204
291
  };
205
292
  const onError = (error) => {
293
+ if (recoverCompleteBodyAfterDisconnect(error)) return;
206
294
  finish(() => {
207
295
  reject(error);
208
296
  });
@@ -212,17 +300,9 @@ const readBodyDirect = (request) => {
212
300
  onEnd();
213
301
  return;
214
302
  }
303
+ if (recoverCompleteBodyAfterDisconnect()) return;
215
304
  finish(() => {
216
- if (incoming.errored) {
217
- reject(incoming.errored);
218
- return;
219
- }
220
- const reason = request[abortReasonKey];
221
- if (reason !== void 0) {
222
- reject(reason instanceof Error ? reason : new Error(String(reason)));
223
- return;
224
- }
225
- reject(/* @__PURE__ */ new Error("Client connection prematurely closed."));
305
+ reject(normalizeAbortError(request, incoming));
226
306
  });
227
307
  };
228
308
  const cleanup = () => {
@@ -617,8 +697,13 @@ const drainIncoming = (incoming) => {
617
697
  incoming.resume();
618
698
  };
619
699
  const makeCloseHandler = (req, incoming, outgoing, needsBodyCleanup) => () => {
620
- if (incoming.errored) req[abortRequest](incoming.errored.toString());
621
- else if (!outgoing.writableFinished) req[abortRequest]("Client connection prematurely closed.");
700
+ if (incoming.errored) {
701
+ recordBodyBufferedBeforeDisconnect(incoming);
702
+ req[abortRequest](incoming.errored.toString());
703
+ } else if (!outgoing.writableFinished) {
704
+ recordBodyBufferedBeforeDisconnect(incoming);
705
+ req[abortRequest]("Client connection prematurely closed.");
706
+ }
622
707
  if (needsBodyCleanup && !incoming.readableEnded) setTimeout(() => {
623
708
  if (!incoming.readableEnded) setTimeout(() => {
624
709
  drainIncoming(incoming);
@@ -839,6 +924,32 @@ const CloseEvent = globalThis.CloseEvent ?? class extends Event {
839
924
  return this.#eventInitDict.reason ?? "";
840
925
  }
841
926
  };
927
+ /**
928
+ * Node.js has no global `ErrorEvent`, unlike `Event`/`MessageEvent`/`CloseEvent`.
929
+ * @link https://developer.mozilla.org/en-US/docs/Web/API/ErrorEvent
930
+ */
931
+ const ErrorEvent = globalThis.ErrorEvent ?? class extends Event {
932
+ #eventInitDict;
933
+ constructor(type, eventInitDict = {}) {
934
+ super(type, eventInitDict);
935
+ this.#eventInitDict = eventInitDict;
936
+ }
937
+ get message() {
938
+ return this.#eventInitDict.message ?? "";
939
+ }
940
+ get filename() {
941
+ return this.#eventInitDict.filename ?? "";
942
+ }
943
+ get lineno() {
944
+ return this.#eventInitDict.lineno ?? 0;
945
+ }
946
+ get colno() {
947
+ return this.#eventInitDict.colno ?? 0;
948
+ }
949
+ get error() {
950
+ return this.#eventInitDict.error ?? null;
951
+ }
952
+ };
842
953
  const generateConnectionSymbol = () => Symbol("connection");
843
954
  const CONNECTION_SYMBOL_KEY = Symbol("CONNECTION_SYMBOL_KEY");
844
955
  const WAIT_FOR_WEBSOCKET_SYMBOL = Symbol("WAIT_FOR_WEBSOCKET_SYMBOL");
package/dist/index.mjs CHANGED
@@ -59,6 +59,54 @@ const newHeadersFromIncoming = (incoming) => {
59
59
  return new Headers(headerRecord);
60
60
  };
61
61
  const wrapBodyStream = Symbol("wrapBodyStream");
62
+ const byteExactEncodings = new Set([
63
+ "latin1",
64
+ "binary",
65
+ "hex",
66
+ "base64",
67
+ "base64url"
68
+ ]);
69
+ const isByteExactEncoding = (encoding) => encoding === null || byteExactEncodings.has(encoding);
70
+ const bodyBufferedBeforeDisconnectKey = Symbol("bodyBufferedBeforeDisconnect");
71
+ const bodyBufferedLengthBeforeDisconnectKey = Symbol("bodyBufferedLengthBeforeDisconnect");
72
+ const toBufferChunk = (chunk, encoding) => Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, encoding ?? "utf8");
73
+ const isRecoverableDisconnectedIncoming = (incoming) => !(incoming instanceof Http2ServerRequest) && !!incoming.complete && !!incoming.readableAborted && typeof incoming.read === "function" && isByteExactEncoding(incoming.readableEncoding);
74
+ const recordBodyBufferedBeforeDisconnect = (incoming) => {
75
+ if (incoming.readableDidRead || !isRecoverableDisconnectedIncoming(incoming)) return;
76
+ const incomingWithRecovery = incoming;
77
+ incomingWithRecovery[bodyBufferedLengthBeforeDisconnectKey] ??= incoming.readableLength;
78
+ };
79
+ const readBodyBufferedBeforeDisconnect = (incoming, chunks) => {
80
+ if (incoming.readableDidRead && !chunks || !isRecoverableDisconnectedIncoming(incoming)) return;
81
+ const incomingWithRecovery = incoming;
82
+ if (incomingWithRecovery[bodyBufferedBeforeDisconnectKey] !== void 0) return incomingWithRecovery[bodyBufferedBeforeDisconnectKey];
83
+ let result;
84
+ const errored = incoming.errored;
85
+ if (errored && errored.code !== "ECONNRESET") result = errored;
86
+ else if (incomingWithRecovery[bodyBufferedLengthBeforeDisconnectKey] !== void 0 && incoming.readableLength !== incomingWithRecovery[bodyBufferedLengthBeforeDisconnectKey]) result = newBodyUnusableError();
87
+ else {
88
+ const bodyChunks = chunks ?? [];
89
+ const chunk = incoming.read();
90
+ if (chunk !== null) bodyChunks.push(toBufferChunk(chunk, incoming.readableEncoding));
91
+ const buffer = bodyChunks.length === 1 ? bodyChunks[0] : Buffer.concat(bodyChunks);
92
+ result = buffer;
93
+ const contentLength = incoming.headers["content-length"];
94
+ if (typeof contentLength === "string" && /^\d+$/.test(contentLength)) {
95
+ const expectedLength = Number(contentLength);
96
+ if (Number.isSafeInteger(expectedLength) && buffer.length !== expectedLength) result = newBodyUnusableError();
97
+ }
98
+ }
99
+ incomingWithRecovery[bodyBufferedBeforeDisconnectKey] = result;
100
+ return result;
101
+ };
102
+ const enqueueBufferedBody = (controller, buffered) => {
103
+ if (buffered instanceof Error) {
104
+ controller.error(buffered);
105
+ return;
106
+ }
107
+ if (buffered.length > 0) controller.enqueue(buffered);
108
+ controller.close();
109
+ };
62
110
  const newRequestFromIncoming = (method, url, headers, incoming, abortController) => {
63
111
  const init = {
64
112
  method,
@@ -81,6 +129,13 @@ const newRequestFromIncoming = (method, url, headers, incoming, abortController)
81
129
  let reader;
82
130
  init.body = new ReadableStream({ async pull(controller) {
83
131
  try {
132
+ if (!reader) {
133
+ const buffered = readBodyBufferedBeforeDisconnect(incoming);
134
+ if (buffered !== void 0) {
135
+ enqueueBufferedBody(controller, buffered);
136
+ return;
137
+ }
138
+ }
84
139
  reader ||= Readable.toWeb(incoming).getReader();
85
140
  const { done, value } = await reader.read();
86
141
  if (done) controller.close();
@@ -89,7 +144,13 @@ const newRequestFromIncoming = (method, url, headers, incoming, abortController)
89
144
  controller.error(error);
90
145
  }
91
146
  } });
92
- } else init.body = Readable.toWeb(incoming);
147
+ } else {
148
+ const buffered = readBodyBufferedBeforeDisconnect(incoming);
149
+ if (buffered !== void 0) init.body = new ReadableStream({ start(controller) {
150
+ enqueueBufferedBody(controller, buffered);
151
+ } });
152
+ else init.body = Readable.toWeb(incoming);
153
+ }
93
154
  return new Request$1(url, init);
94
155
  };
95
156
  const getRequestCache = Symbol("getRequestCache");
@@ -177,11 +238,23 @@ const readRawBodyIfAvailable = (request) => {
177
238
  const incoming = request[incomingKey];
178
239
  if ("rawBody" in incoming && incoming.rawBody instanceof Buffer) return incoming.rawBody;
179
240
  };
241
+ const normalizeAbortError = (request, incoming) => {
242
+ if (incoming.errored) return incoming.errored;
243
+ const reason = request[abortReasonKey];
244
+ if (reason !== void 0) return reason instanceof Error ? reason : new Error(String(reason));
245
+ return /* @__PURE__ */ new Error("Client connection prematurely closed.");
246
+ };
180
247
  const readBodyDirect = (request) => {
181
248
  if (request[bodyBufferKey]) return Promise.resolve(request[bodyBufferKey]);
182
249
  if (request[bodyReadPromiseKey]) return request[bodyReadPromiseKey];
183
250
  const incoming = request[incomingKey];
184
- if (Readable.isDisturbed(incoming)) return rejectBodyUnusable();
251
+ if (incoming.readableDidRead) return rejectBodyUnusable();
252
+ const buffered = readBodyBufferedBeforeDisconnect(incoming);
253
+ if (buffered !== void 0) {
254
+ if (buffered instanceof Error) return Promise.reject(buffered);
255
+ request[bodyBufferKey] = buffered;
256
+ return Promise.resolve(buffered);
257
+ }
185
258
  const promise = new Promise((resolve, reject) => {
186
259
  const chunks = [];
187
260
  let settled = false;
@@ -191,8 +264,22 @@ const readBodyDirect = (request) => {
191
264
  cleanup();
192
265
  callback();
193
266
  };
267
+ const recoverCompleteBodyAfterDisconnect = (error) => {
268
+ const streamError = incoming.errored ?? error;
269
+ if (!isRecoverableDisconnectedIncoming(incoming) || streamError && streamError.code !== "ECONNRESET") return false;
270
+ finish(() => {
271
+ const recovered = readBodyBufferedBeforeDisconnect(incoming, chunks);
272
+ if (recovered instanceof Error) reject(recovered);
273
+ else if (recovered === void 0) reject(error ?? normalizeAbortError(request, incoming));
274
+ else {
275
+ request[bodyBufferKey] = recovered;
276
+ resolve(recovered);
277
+ }
278
+ });
279
+ return true;
280
+ };
194
281
  const onData = (chunk) => {
195
- chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
282
+ chunks.push(toBufferChunk(chunk, incoming.readableEncoding));
196
283
  };
197
284
  const onEnd = () => {
198
285
  finish(() => {
@@ -202,6 +289,7 @@ const readBodyDirect = (request) => {
202
289
  });
203
290
  };
204
291
  const onError = (error) => {
292
+ if (recoverCompleteBodyAfterDisconnect(error)) return;
205
293
  finish(() => {
206
294
  reject(error);
207
295
  });
@@ -211,17 +299,9 @@ const readBodyDirect = (request) => {
211
299
  onEnd();
212
300
  return;
213
301
  }
302
+ if (recoverCompleteBodyAfterDisconnect()) return;
214
303
  finish(() => {
215
- if (incoming.errored) {
216
- reject(incoming.errored);
217
- return;
218
- }
219
- const reason = request[abortReasonKey];
220
- if (reason !== void 0) {
221
- reject(reason instanceof Error ? reason : new Error(String(reason)));
222
- return;
223
- }
224
- reject(/* @__PURE__ */ new Error("Client connection prematurely closed."));
304
+ reject(normalizeAbortError(request, incoming));
225
305
  });
226
306
  };
227
307
  const cleanup = () => {
@@ -616,8 +696,13 @@ const drainIncoming = (incoming) => {
616
696
  incoming.resume();
617
697
  };
618
698
  const makeCloseHandler = (req, incoming, outgoing, needsBodyCleanup) => () => {
619
- if (incoming.errored) req[abortRequest](incoming.errored.toString());
620
- else if (!outgoing.writableFinished) req[abortRequest]("Client connection prematurely closed.");
699
+ if (incoming.errored) {
700
+ recordBodyBufferedBeforeDisconnect(incoming);
701
+ req[abortRequest](incoming.errored.toString());
702
+ } else if (!outgoing.writableFinished) {
703
+ recordBodyBufferedBeforeDisconnect(incoming);
704
+ req[abortRequest]("Client connection prematurely closed.");
705
+ }
621
706
  if (needsBodyCleanup && !incoming.readableEnded) setTimeout(() => {
622
707
  if (!incoming.readableEnded) setTimeout(() => {
623
708
  drainIncoming(incoming);
@@ -838,6 +923,32 @@ const CloseEvent = globalThis.CloseEvent ?? class extends Event {
838
923
  return this.#eventInitDict.reason ?? "";
839
924
  }
840
925
  };
926
+ /**
927
+ * Node.js has no global `ErrorEvent`, unlike `Event`/`MessageEvent`/`CloseEvent`.
928
+ * @link https://developer.mozilla.org/en-US/docs/Web/API/ErrorEvent
929
+ */
930
+ const ErrorEvent = globalThis.ErrorEvent ?? class extends Event {
931
+ #eventInitDict;
932
+ constructor(type, eventInitDict = {}) {
933
+ super(type, eventInitDict);
934
+ this.#eventInitDict = eventInitDict;
935
+ }
936
+ get message() {
937
+ return this.#eventInitDict.message ?? "";
938
+ }
939
+ get filename() {
940
+ return this.#eventInitDict.filename ?? "";
941
+ }
942
+ get lineno() {
943
+ return this.#eventInitDict.lineno ?? 0;
944
+ }
945
+ get colno() {
946
+ return this.#eventInitDict.colno ?? 0;
947
+ }
948
+ get error() {
949
+ return this.#eventInitDict.error ?? null;
950
+ }
951
+ };
841
952
  const generateConnectionSymbol = () => Symbol("connection");
842
953
  const CONNECTION_SYMBOL_KEY = Symbol("CONNECTION_SYMBOL_KEY");
843
954
  const WAIT_FOR_WEBSOCKET_SYMBOL = Symbol("WAIT_FOR_WEBSOCKET_SYMBOL");
@@ -19,6 +19,41 @@ const getStats = (path) => {
19
19
  } catch {}
20
20
  return stats;
21
21
  };
22
+ const BYTE_RANGE_PATTERN = /^(?:bytes=)?(?!-$)(\d*)-(\d*)$/;
23
+ const parseByteRange = (range) => {
24
+ const match = range.match(BYTE_RANGE_PATTERN);
25
+ if (!match) return;
26
+ const [, start, end] = match;
27
+ if (start === "") return {
28
+ type: "suffix",
29
+ length: Number(end)
30
+ };
31
+ if (end === "") return {
32
+ type: "open-ended",
33
+ start: Number(start)
34
+ };
35
+ return {
36
+ type: "bounded",
37
+ start: Number(start),
38
+ end: Number(end)
39
+ };
40
+ };
41
+ const resolveByteRange = (spec, size) => {
42
+ if (size === 0) return;
43
+ if (spec.type === "suffix") {
44
+ if (spec.length === 0) return;
45
+ return {
46
+ start: Math.max(size - spec.length, 0),
47
+ end: size - 1
48
+ };
49
+ }
50
+ const end = spec.type === "bounded" ? Math.min(spec.end, size - 1) : size - 1;
51
+ if (spec.start >= size || spec.start > end) return;
52
+ return {
53
+ start: spec.start,
54
+ end
55
+ };
56
+ };
22
57
  const tryDecode = (str, decoder) => {
23
58
  try {
24
59
  return decoder(str);
@@ -88,18 +123,24 @@ const serveStatic = (options = { root: "" }) => {
88
123
  result = c.body(require_utils_stream.createStreamBody((0, node_fs.createReadStream)(path)), 200);
89
124
  } else {
90
125
  c.header("Accept-Ranges", "bytes");
91
- const parts = range.replace(/bytes=/, "").split("-", 2);
92
- const start = parseInt(parts[0], 10) || 0;
93
- let end = parseInt(parts[1], 10) || size - 1;
94
- if (size < end - start + 1) end = size - 1;
95
- const chunksize = end - start + 1;
96
- const stream = (0, node_fs.createReadStream)(path, {
97
- start,
98
- end
99
- });
100
- c.header("Content-Length", chunksize.toString());
101
- c.header("Content-Range", `bytes ${start}-${end}/${stats.size}`);
102
- result = c.body(require_utils_stream.createStreamBody(stream), 206);
126
+ const resolvedRange = resolveByteRange(parseByteRange(range) ?? {
127
+ type: "open-ended",
128
+ start: 0
129
+ }, size);
130
+ if (!resolvedRange) {
131
+ c.header("Content-Range", `bytes */${size}`);
132
+ result = c.body(null, 416);
133
+ } else {
134
+ const { start, end } = resolvedRange;
135
+ const chunkSize = end - start + 1;
136
+ const stream = (0, node_fs.createReadStream)(path, {
137
+ start,
138
+ end
139
+ });
140
+ c.header("Content-Length", chunkSize.toString());
141
+ c.header("Content-Range", `bytes ${start}-${end}/${size}`);
142
+ result = c.body(require_utils_stream.createStreamBody(stream), 206);
143
+ }
103
144
  }
104
145
  await options.onFound?.(path, c);
105
146
  return result;
@@ -18,6 +18,41 @@ const getStats = (path) => {
18
18
  } catch {}
19
19
  return stats;
20
20
  };
21
+ const BYTE_RANGE_PATTERN = /^(?:bytes=)?(?!-$)(\d*)-(\d*)$/;
22
+ const parseByteRange = (range) => {
23
+ const match = range.match(BYTE_RANGE_PATTERN);
24
+ if (!match) return;
25
+ const [, start, end] = match;
26
+ if (start === "") return {
27
+ type: "suffix",
28
+ length: Number(end)
29
+ };
30
+ if (end === "") return {
31
+ type: "open-ended",
32
+ start: Number(start)
33
+ };
34
+ return {
35
+ type: "bounded",
36
+ start: Number(start),
37
+ end: Number(end)
38
+ };
39
+ };
40
+ const resolveByteRange = (spec, size) => {
41
+ if (size === 0) return;
42
+ if (spec.type === "suffix") {
43
+ if (spec.length === 0) return;
44
+ return {
45
+ start: Math.max(size - spec.length, 0),
46
+ end: size - 1
47
+ };
48
+ }
49
+ const end = spec.type === "bounded" ? Math.min(spec.end, size - 1) : size - 1;
50
+ if (spec.start >= size || spec.start > end) return;
51
+ return {
52
+ start: spec.start,
53
+ end
54
+ };
55
+ };
21
56
  const tryDecode = (str, decoder) => {
22
57
  try {
23
58
  return decoder(str);
@@ -87,18 +122,24 @@ const serveStatic = (options = { root: "" }) => {
87
122
  result = c.body(createStreamBody(createReadStream(path)), 200);
88
123
  } else {
89
124
  c.header("Accept-Ranges", "bytes");
90
- const parts = range.replace(/bytes=/, "").split("-", 2);
91
- const start = parseInt(parts[0], 10) || 0;
92
- let end = parseInt(parts[1], 10) || size - 1;
93
- if (size < end - start + 1) end = size - 1;
94
- const chunksize = end - start + 1;
95
- const stream = createReadStream(path, {
96
- start,
97
- end
98
- });
99
- c.header("Content-Length", chunksize.toString());
100
- c.header("Content-Range", `bytes ${start}-${end}/${stats.size}`);
101
- result = c.body(createStreamBody(stream), 206);
125
+ const resolvedRange = resolveByteRange(parseByteRange(range) ?? {
126
+ type: "open-ended",
127
+ start: 0
128
+ }, size);
129
+ if (!resolvedRange) {
130
+ c.header("Content-Range", `bytes */${size}`);
131
+ result = c.body(null, 416);
132
+ } else {
133
+ const { start, end } = resolvedRange;
134
+ const chunkSize = end - start + 1;
135
+ const stream = createReadStream(path, {
136
+ start,
137
+ end
138
+ });
139
+ c.header("Content-Length", chunkSize.toString());
140
+ c.header("Content-Range", `bytes ${start}-${end}/${size}`);
141
+ result = c.body(createStreamBody(stream), 206);
142
+ }
102
143
  }
103
144
  await options.onFound?.(path, c);
104
145
  return result;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hono/node-server",
3
- "version": "2.0.8",
3
+ "version": "2.0.9",
4
4
  "description": "Node.js Adapter for Hono",
5
5
  "main": "dist/index.mjs",
6
6
  "type": "module",