@octanejs/app-core 0.0.56 → 0.0.58
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/package.json +3 -3
- package/src/server/node-http.js +45 -7
- package/src/server/server-entry.js +4 -4
- package/types/node.d.ts +7 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@octanejs/app-core",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.58",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -79,10 +79,10 @@
|
|
|
79
79
|
"esbuild": "^0.28.1"
|
|
80
80
|
},
|
|
81
81
|
"peerDependencies": {
|
|
82
|
-
"octane": "^0.
|
|
82
|
+
"octane": "^0.7.0"
|
|
83
83
|
},
|
|
84
84
|
"devDependencies": {
|
|
85
85
|
"@types/node": "^24.13.3",
|
|
86
|
-
"octane": "0.
|
|
86
|
+
"octane": "0.7.0"
|
|
87
87
|
}
|
|
88
88
|
}
|
package/src/server/node-http.js
CHANGED
|
@@ -212,7 +212,13 @@ export function nodeRequestToWebRequest(nodeRequest, nodeResponse) {
|
|
|
212
212
|
|
|
213
213
|
/**
|
|
214
214
|
* Pipe a Web Response to a Node.js ServerResponse. Streams chunk-by-chunk so a
|
|
215
|
-
* streaming SSR body flushes as it renders (no buffering).
|
|
215
|
+
* streaming SSR body flushes as it renders (no buffering). A HEAD response ends
|
|
216
|
+
* with its headers and cancels the body.
|
|
217
|
+
*
|
|
218
|
+
* If the body fails, the returned promise rejects. Once headers were sent, the
|
|
219
|
+
* response has already been destroyed so the client observes an interrupted
|
|
220
|
+
* transfer; the caller must only report the error. Before that, the response's
|
|
221
|
+
* headers have been removed and the caller may send its own error response.
|
|
216
222
|
*
|
|
217
223
|
* @param {import('node:http').ServerResponse} nodeResponse
|
|
218
224
|
* @param {Response} webResponse
|
|
@@ -237,6 +243,9 @@ async function sendWebResponseForRequest(nodeResponse, webResponse, nodeRequest)
|
|
|
237
243
|
if (nodeRequest && body && shouldGzip(nodeRequest, webResponse.status, headers, true)) {
|
|
238
244
|
headers.set('Content-Encoding', 'gzip');
|
|
239
245
|
headers.delete('Content-Length');
|
|
246
|
+
const etag = headers.get('ETag');
|
|
247
|
+
// A strong validator for the identity bytes cannot identify their gzip form.
|
|
248
|
+
if (etag !== null && !etag.startsWith('W/')) headers.delete('ETag');
|
|
240
249
|
// Sync-flush each input chunk so an SSR shell stays progressively
|
|
241
250
|
// observable instead of waiting for the final segment to close gzip.
|
|
242
251
|
const gzip = Duplex.toWeb(createGzip({ flush: zlibConstants.Z_SYNC_FLUSH }));
|
|
@@ -247,10 +256,24 @@ async function sendWebResponseForRequest(nodeResponse, webResponse, nodeRequest)
|
|
|
247
256
|
|
|
248
257
|
nodeResponse.statusCode = webResponse.status;
|
|
249
258
|
if (webResponse.statusText) nodeResponse.statusMessage = webResponse.statusText;
|
|
259
|
+
// `Headers` yields each Set-Cookie as its own entry, and `setHeader`
|
|
260
|
+
// replaces rather than appends, so cookies go over as one array: Node writes
|
|
261
|
+
// one header line per element. Commas cannot join them because `Expires`
|
|
262
|
+
// contains one.
|
|
250
263
|
headers.forEach((value, key) => {
|
|
251
|
-
nodeResponse.setHeader(key, value);
|
|
264
|
+
if (key !== 'set-cookie') nodeResponse.setHeader(key, value);
|
|
252
265
|
});
|
|
266
|
+
const cookies = headers.getSetCookie();
|
|
267
|
+
if (cookies.length) nodeResponse.setHeader('set-cookie', cookies);
|
|
253
268
|
if (body) {
|
|
269
|
+
// HEAD transfers metadata without content, and Node discards body writes.
|
|
270
|
+
// End now and release the producer instead of draining a body that may
|
|
271
|
+
// stay open indefinitely.
|
|
272
|
+
if (nodeResponse.req?.method === 'HEAD') {
|
|
273
|
+
if (!nodeResponse.destroyed) nodeResponse.end();
|
|
274
|
+
await body.cancel().catch(() => {});
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
254
277
|
const reader = body.getReader();
|
|
255
278
|
let disconnected = nodeResponse.destroyed;
|
|
256
279
|
let disconnectReason = new Error('The client disconnected while streaming the response.');
|
|
@@ -280,7 +303,21 @@ async function sendWebResponseForRequest(nodeResponse, webResponse, nodeRequest)
|
|
|
280
303
|
if (!accepted && !disconnected) await waitForDrain(nodeResponse);
|
|
281
304
|
}
|
|
282
305
|
} catch (error) {
|
|
283
|
-
if (!disconnected)
|
|
306
|
+
if (!disconnected) {
|
|
307
|
+
if (nodeResponse.headersSent) {
|
|
308
|
+
// The status is committed, so an interrupted transfer is the only
|
|
309
|
+
// failure signal left; completing it would pass the partial body off
|
|
310
|
+
// as whole. Destroy without the error, which Node would otherwise
|
|
311
|
+
// report as a clientError: the caller reports it.
|
|
312
|
+
nodeResponse.destroy();
|
|
313
|
+
} else {
|
|
314
|
+
// Nothing is on the wire yet. Drop this representation's metadata so
|
|
315
|
+
// the caller's error response does not inherit its framing or encoding.
|
|
316
|
+
for (const name of headers.keys()) nodeResponse.removeHeader(name);
|
|
317
|
+
}
|
|
318
|
+
void cancelReader(error);
|
|
319
|
+
throw error;
|
|
320
|
+
}
|
|
284
321
|
} finally {
|
|
285
322
|
nodeResponse.off('close', onClose);
|
|
286
323
|
nodeResponse.off('error', onError);
|
|
@@ -490,10 +527,11 @@ export function createNodeServer(handler, options = {}) {
|
|
|
490
527
|
await sendWebResponseForRequest(res, response, req);
|
|
491
528
|
})().catch((error) => {
|
|
492
529
|
console.error('[octane] Request error:', error);
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
530
|
+
// Once headers are sent the sender has aborted the transfer. Appending
|
|
531
|
+
// here would complete it with bytes the application never produced.
|
|
532
|
+
if (res.headersSent) return;
|
|
533
|
+
res.statusCode = 500;
|
|
534
|
+
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
|
497
535
|
res.end('Internal Server Error');
|
|
498
536
|
});
|
|
499
537
|
});
|
|
@@ -475,10 +475,10 @@ export async function nodeHandler(req, res) {
|
|
|
475
475
|
await sendWebResponse(res, response);
|
|
476
476
|
} catch (error) {
|
|
477
477
|
console.error('[octane] Request error:', error);
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
478
|
+
// sendWebResponse already aborted a response whose headers were sent.
|
|
479
|
+
if (res.headersSent) return;
|
|
480
|
+
res.statusCode = 500;
|
|
481
|
+
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
|
482
482
|
res.end('Internal Server Error');
|
|
483
483
|
}
|
|
484
484
|
}
|
package/types/node.d.ts
CHANGED
|
@@ -13,7 +13,13 @@ export function nodeRequestToWebRequest(
|
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* Pipe a Web Response to a Node.js ServerResponse, streaming chunk-by-chunk
|
|
16
|
-
* (a streaming SSR body flushes as it renders).
|
|
16
|
+
* (a streaming SSR body flushes as it renders). A HEAD response ends with its
|
|
17
|
+
* headers and cancels the body.
|
|
18
|
+
*
|
|
19
|
+
* Rejects when the body fails. If headers were already sent, the response has
|
|
20
|
+
* been destroyed so the client sees an interrupted transfer; only report the
|
|
21
|
+
* error. Otherwise the Web Response's headers were removed and the caller may
|
|
22
|
+
* send its own error response.
|
|
17
23
|
*/
|
|
18
24
|
export function sendWebResponse(nodeResponse: ServerResponse, webResponse: Response): Promise<void>;
|
|
19
25
|
|