@hono/node-server 2.0.8 → 2.0.10
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 +145 -18
- package/dist/index.mjs +145 -18
- package/dist/serve-static.cjs +53 -12
- package/dist/serve-static.mjs +53 -12
- package/package.json +1 -1
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
|
|
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 (
|
|
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(
|
|
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
|
-
|
|
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)
|
|
621
|
-
|
|
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");
|
|
@@ -890,10 +1001,18 @@ const setupWebSocket = (options) => {
|
|
|
890
1001
|
waiterMap.delete(request);
|
|
891
1002
|
}
|
|
892
1003
|
});
|
|
1004
|
+
const rejectWaiter = (request) => {
|
|
1005
|
+
const waiter = waiterMap.get(request);
|
|
1006
|
+
if (waiter) {
|
|
1007
|
+
waiterMap.delete(request);
|
|
1008
|
+
waiter.reject(/* @__PURE__ */ new Error("WebSocket handshake aborted"));
|
|
1009
|
+
}
|
|
1010
|
+
};
|
|
893
1011
|
const waitForWebSocket = (request, connectionSymbol) => {
|
|
894
|
-
return new Promise((resolve) => {
|
|
1012
|
+
return new Promise((resolve, reject) => {
|
|
895
1013
|
waiterMap.set(request, {
|
|
896
1014
|
resolve,
|
|
1015
|
+
reject,
|
|
897
1016
|
connectionSymbol
|
|
898
1017
|
});
|
|
899
1018
|
});
|
|
@@ -920,16 +1039,19 @@ const setupWebSocket = (options) => {
|
|
|
920
1039
|
}
|
|
921
1040
|
const waiter = waiterMap.get(request);
|
|
922
1041
|
if (!waiter || waiter.connectionSymbol !== env[CONNECTION_SYMBOL_KEY]) {
|
|
923
|
-
|
|
1042
|
+
rejectWaiter(request);
|
|
924
1043
|
if (server.listenerCount("upgrade") === 1) rejectUpgradeRequest(socket, status, responseHeaders);
|
|
925
1044
|
return;
|
|
926
1045
|
}
|
|
927
1046
|
const addResponseHeaders = (headers) => {
|
|
928
1047
|
appendResponseHeaders(headers, responseHeaders);
|
|
929
1048
|
};
|
|
1049
|
+
const reclaimWaiterOnClose = () => rejectWaiter(request);
|
|
1050
|
+
socket.once("close", reclaimWaiterOnClose);
|
|
930
1051
|
wss.on("headers", addResponseHeaders);
|
|
931
1052
|
try {
|
|
932
1053
|
wss.handleUpgrade(request, socket, head, (ws) => {
|
|
1054
|
+
socket.off("close", reclaimWaiterOnClose);
|
|
933
1055
|
wss.emit("connection", ws, request);
|
|
934
1056
|
});
|
|
935
1057
|
} finally {
|
|
@@ -948,7 +1070,12 @@ const upgradeWebSocket = (0, hono_ws.defineWebSocketHelper)(async (c, events, op
|
|
|
948
1070
|
const connectionSymbol = generateConnectionSymbol();
|
|
949
1071
|
env[CONNECTION_SYMBOL_KEY] = connectionSymbol;
|
|
950
1072
|
(async () => {
|
|
951
|
-
|
|
1073
|
+
let ws;
|
|
1074
|
+
try {
|
|
1075
|
+
ws = await waitForWebSocket(env.incoming, connectionSymbol);
|
|
1076
|
+
} catch {
|
|
1077
|
+
return;
|
|
1078
|
+
}
|
|
952
1079
|
const messagesReceivedInStarting = [];
|
|
953
1080
|
const bufferMessage = (data, isBinary) => {
|
|
954
1081
|
messagesReceivedInStarting.push([data, isBinary]);
|
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
|
|
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 (
|
|
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(
|
|
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
|
-
|
|
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)
|
|
620
|
-
|
|
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");
|
|
@@ -889,10 +1000,18 @@ const setupWebSocket = (options) => {
|
|
|
889
1000
|
waiterMap.delete(request);
|
|
890
1001
|
}
|
|
891
1002
|
});
|
|
1003
|
+
const rejectWaiter = (request) => {
|
|
1004
|
+
const waiter = waiterMap.get(request);
|
|
1005
|
+
if (waiter) {
|
|
1006
|
+
waiterMap.delete(request);
|
|
1007
|
+
waiter.reject(/* @__PURE__ */ new Error("WebSocket handshake aborted"));
|
|
1008
|
+
}
|
|
1009
|
+
};
|
|
892
1010
|
const waitForWebSocket = (request, connectionSymbol) => {
|
|
893
|
-
return new Promise((resolve) => {
|
|
1011
|
+
return new Promise((resolve, reject) => {
|
|
894
1012
|
waiterMap.set(request, {
|
|
895
1013
|
resolve,
|
|
1014
|
+
reject,
|
|
896
1015
|
connectionSymbol
|
|
897
1016
|
});
|
|
898
1017
|
});
|
|
@@ -919,16 +1038,19 @@ const setupWebSocket = (options) => {
|
|
|
919
1038
|
}
|
|
920
1039
|
const waiter = waiterMap.get(request);
|
|
921
1040
|
if (!waiter || waiter.connectionSymbol !== env[CONNECTION_SYMBOL_KEY]) {
|
|
922
|
-
|
|
1041
|
+
rejectWaiter(request);
|
|
923
1042
|
if (server.listenerCount("upgrade") === 1) rejectUpgradeRequest(socket, status, responseHeaders);
|
|
924
1043
|
return;
|
|
925
1044
|
}
|
|
926
1045
|
const addResponseHeaders = (headers) => {
|
|
927
1046
|
appendResponseHeaders(headers, responseHeaders);
|
|
928
1047
|
};
|
|
1048
|
+
const reclaimWaiterOnClose = () => rejectWaiter(request);
|
|
1049
|
+
socket.once("close", reclaimWaiterOnClose);
|
|
929
1050
|
wss.on("headers", addResponseHeaders);
|
|
930
1051
|
try {
|
|
931
1052
|
wss.handleUpgrade(request, socket, head, (ws) => {
|
|
1053
|
+
socket.off("close", reclaimWaiterOnClose);
|
|
932
1054
|
wss.emit("connection", ws, request);
|
|
933
1055
|
});
|
|
934
1056
|
} finally {
|
|
@@ -947,7 +1069,12 @@ const upgradeWebSocket = defineWebSocketHelper(async (c, events, options) => {
|
|
|
947
1069
|
const connectionSymbol = generateConnectionSymbol();
|
|
948
1070
|
env[CONNECTION_SYMBOL_KEY] = connectionSymbol;
|
|
949
1071
|
(async () => {
|
|
950
|
-
|
|
1072
|
+
let ws;
|
|
1073
|
+
try {
|
|
1074
|
+
ws = await waitForWebSocket(env.incoming, connectionSymbol);
|
|
1075
|
+
} catch {
|
|
1076
|
+
return;
|
|
1077
|
+
}
|
|
951
1078
|
const messagesReceivedInStarting = [];
|
|
952
1079
|
const bufferMessage = (data, isBinary) => {
|
|
953
1080
|
messagesReceivedInStarting.push([data, isBinary]);
|
package/dist/serve-static.cjs
CHANGED
|
@@ -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
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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;
|
package/dist/serve-static.mjs
CHANGED
|
@@ -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
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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;
|