feat(http): expand static ServerResponse parity

- Expose response state, socket access, and timeout calls to static programs.
- Enforce strict response body lengths with Node compatible errors.
- Add differential evidence and scoped compatibility claims.
This commit is contained in:
Chris Tate
2026-09-24 01:51:34 -05:00
parent 82b2417e53
commit 5a5e2a47eb
17 changed files with 574 additions and 135 deletions
@@ -2,6 +2,6 @@
"schemaVersion": 3,
"nodeVersion": "24.15.0",
"nodeCommit": "848430679556aed0bd073f2bc263331ad84fa119",
"artifactVersion": "a7e38d1b2b0e92cf8ec4",
"artifactVersion": "b92c655b71259eb9c1f8",
"rowCount": 3662
}
+26 -26
View File
@@ -506,9 +506,9 @@
"apiEntries": 174,
"static": {
"supported": 0,
"partial": 70,
"partial": 78,
"refused": 0,
"not-implemented": 104,
"not-implemented": 96,
"by-design": 0,
"unreviewed": 0,
"not-applicable": 0
@@ -47865,9 +47865,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Static ServerResponse state, socket access, timeouts, and strict body length checks support the tested HTTP/1.1 forms.",
"verification": "test-backed"
},
"dynamic": {
"status": "not-implemented",
@@ -48061,9 +48061,9 @@
"inherited": false
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Static ServerResponse state, socket access, timeouts, and strict body length checks support the tested HTTP/1.1 forms.",
"verification": "test-backed"
},
"dynamic": {
"status": "not-implemented",
@@ -48145,9 +48145,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Static ServerResponse state, socket access, timeouts, and strict body length checks support the tested HTTP/1.1 forms.",
"verification": "test-backed"
},
"dynamic": {
"status": "not-implemented",
@@ -48173,9 +48173,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Static ServerResponse state, socket access, timeouts, and strict body length checks support the tested HTTP/1.1 forms.",
"verification": "test-backed"
},
"dynamic": {
"status": "not-implemented",
@@ -48201,9 +48201,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Static ServerResponse state, socket access, timeouts, and strict body length checks support the tested HTTP/1.1 forms.",
"verification": "test-backed"
},
"dynamic": {
"status": "not-implemented",
@@ -48257,9 +48257,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Static ServerResponse state, socket access, timeouts, and strict body length checks support the tested HTTP/1.1 forms.",
"verification": "test-backed"
},
"dynamic": {
"status": "not-implemented",
@@ -48285,9 +48285,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Static ServerResponse state, socket access, timeouts, and strict body length checks support the tested HTTP/1.1 forms.",
"verification": "test-backed"
},
"dynamic": {
"status": "not-implemented",
@@ -48341,9 +48341,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Static ServerResponse state, socket access, timeouts, and strict body length checks support the tested HTTP/1.1 forms.",
"verification": "test-backed"
},
"dynamic": {
"status": "not-implemented",
@@ -8,8 +8,8 @@
"tiers": {
"static": {
"replace-refusal": 46,
"verify-gap": 2642,
"audit-partial": 524,
"verify-gap": 2634,
"audit-partial": 532,
"classify": 2,
"implement": 81
},
@@ -49540,12 +49540,14 @@
"priority": "high",
"tiers": {
"static": {
"status": "not-implemented",
"action": "verify-gap",
"verification": "registry-gap",
"confidence": "low",
"source": "compiler-unmatched:http",
"tests": []
"status": "partial",
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "compiler-feature:http.response.setTimeout",
"tests": [
"tests/corpus/2957-http-static-response-controls.ts"
]
},
"dynamic": {
"status": "not-implemented",
@@ -49790,12 +49792,14 @@
"priority": "low",
"tiers": {
"static": {
"status": "not-implemented",
"action": "verify-gap",
"verification": "registry-gap",
"confidence": "low",
"source": "compiler-unmatched:http",
"tests": []
"status": "partial",
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "compiler-feature:http.connection",
"tests": [
"tests/corpus/2957-http-static-response-controls.ts"
]
},
"dynamic": {
"status": "not-implemented",
@@ -49891,12 +49895,14 @@
"priority": "high",
"tiers": {
"static": {
"status": "not-implemented",
"action": "verify-gap",
"verification": "registry-gap",
"confidence": "low",
"source": "compiler-unmatched:http",
"tests": []
"status": "partial",
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "compiler-feature:http.req",
"tests": [
"tests/corpus/2957-http-static-response-controls.ts"
]
},
"dynamic": {
"status": "not-implemented",
@@ -49924,12 +49930,14 @@
"priority": "high",
"tiers": {
"static": {
"status": "not-implemented",
"action": "verify-gap",
"verification": "registry-gap",
"confidence": "low",
"source": "compiler-unmatched:http",
"tests": []
"status": "partial",
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "compiler-feature:http.sendDate",
"tests": [
"tests/corpus/2957-http-static-response-controls.ts"
]
},
"dynamic": {
"status": "not-implemented",
@@ -49957,12 +49965,14 @@
"priority": "high",
"tiers": {
"static": {
"status": "not-implemented",
"action": "verify-gap",
"verification": "registry-gap",
"confidence": "low",
"source": "compiler-unmatched:http",
"tests": []
"status": "partial",
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "compiler-feature:http.socket",
"tests": [
"tests/corpus/2957-http-static-response-controls.ts"
]
},
"dynamic": {
"status": "not-implemented",
@@ -50025,12 +50035,14 @@
"priority": "high",
"tiers": {
"static": {
"status": "not-implemented",
"action": "verify-gap",
"verification": "registry-gap",
"confidence": "low",
"source": "compiler-unmatched:http",
"tests": []
"status": "partial",
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "compiler-feature:http.statusMessage",
"tests": [
"tests/corpus/2957-http-static-response-controls.ts"
]
},
"dynamic": {
"status": "not-implemented",
@@ -50058,12 +50070,14 @@
"priority": "high",
"tiers": {
"static": {
"status": "not-implemented",
"action": "verify-gap",
"verification": "registry-gap",
"confidence": "low",
"source": "compiler-unmatched:http",
"tests": []
"status": "partial",
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "compiler-feature:http.strictContentLength",
"tests": [
"tests/corpus/2957-http-static-response-controls.ts"
]
},
"dynamic": {
"status": "not-implemented",
@@ -50126,12 +50140,14 @@
"priority": "high",
"tiers": {
"static": {
"status": "not-implemented",
"action": "verify-gap",
"verification": "registry-gap",
"confidence": "low",
"source": "compiler-unmatched:http",
"tests": []
"status": "partial",
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "compiler-feature:http.writableFinished",
"tests": [
"tests/corpus/2957-http-static-response-controls.ts"
]
},
"dynamic": {
"status": "not-implemented",
@@ -555,9 +555,9 @@
"apiEntries": 174,
"static": {
"supported": 0,
"partial": 70,
"partial": 78,
"refused": 0,
"not-implemented": 104,
"not-implemented": 96,
"by-design": 0,
"unreviewed": 0,
"not-applicable": 0
@@ -62165,6 +62165,7 @@
"tests/corpus/2954-http-outgoing-headers.ts",
"tests/corpus/2955-http-static-state.ts",
"tests/corpus/2956-http-static-informational.ts",
"tests/corpus/2957-http-static-response-controls.ts",
"tests/fixtures/server/cases/http-trailers/main.ts"
]
},
@@ -64847,8 +64848,11 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:http"
"status": "partial",
"evidence": "compiler-feature:http.response.setTimeout",
"tests": [
"tests/corpus/2957-http-static-response-controls.ts"
]
},
"dynamic": {
"status": "not-implemented",
@@ -65061,8 +65065,11 @@
"inherited": false
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:http"
"status": "partial",
"evidence": "compiler-feature:http.connection",
"tests": [
"tests/corpus/2957-http-static-response-controls.ts"
]
},
"dynamic": {
"status": "not-implemented",
@@ -65145,8 +65152,11 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:http"
"status": "partial",
"evidence": "compiler-feature:http.req",
"tests": [
"tests/corpus/2957-http-static-response-controls.ts"
]
},
"dynamic": {
"status": "not-implemented",
@@ -65172,8 +65182,11 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:http"
"status": "partial",
"evidence": "compiler-feature:http.sendDate",
"tests": [
"tests/corpus/2957-http-static-response-controls.ts"
]
},
"dynamic": {
"status": "not-implemented",
@@ -65199,8 +65212,11 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:http"
"status": "partial",
"evidence": "compiler-feature:http.socket",
"tests": [
"tests/corpus/2957-http-static-response-controls.ts"
]
},
"dynamic": {
"status": "not-implemented",
@@ -65256,8 +65272,11 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:http"
"status": "partial",
"evidence": "compiler-feature:http.statusMessage",
"tests": [
"tests/corpus/2957-http-static-response-controls.ts"
]
},
"dynamic": {
"status": "not-implemented",
@@ -65283,8 +65302,11 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:http"
"status": "partial",
"evidence": "compiler-feature:http.strictContentLength",
"tests": [
"tests/corpus/2957-http-static-response-controls.ts"
]
},
"dynamic": {
"status": "not-implemented",
@@ -65340,8 +65362,11 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:http"
"status": "partial",
"evidence": "compiler-feature:http.writableFinished",
"tests": [
"tests/corpus/2957-http-static-response-controls.ts"
]
},
"dynamic": {
"status": "not-implemented",
+22 -10
View File
@@ -186,9 +186,10 @@ function featureEntries(compat) {
return compat.features ?? [];
}
function featureOf(row, entries) {
function featureOf(row, entries, parentSignature) {
return entries.find((entry) =>
entry.chapter === row.chapter &&
(entry.parent === undefined || entry.parent === parentSignature) &&
((entry.symbols ?? []).includes(row.apiSymbol) || (entry.signatures ?? []).includes(row.signature))
);
}
@@ -320,10 +321,10 @@ function classificationContext() {
};
}
function classifyStatic(row, chapter, ctx) {
function classifyStatic(row, chapter, ctx, parentSignature) {
if (row.depth === 0) return tier("unreviewed", "", { source: "chapter-summary" });
if (row.scope === "documentation" || row.scope === "configuration") return tier("not-applicable", "", { source: row.scope });
const feature = featureOf(row, ctx.staticFeatures);
const feature = featureOf(row, ctx.staticFeatures, parentSignature);
if (feature) return tier(feature.status, "", { source: `compiler-feature:${row.chapter}.${row.apiSymbol}`, tests: feature.evidence });
for (const candidate of symbolCandidates(row)) {
// Dedicated lowering paths are authoritative when an older generic
@@ -354,10 +355,10 @@ function classifyStatic(row, chapter, ctx) {
return tier("unreviewed", "");
}
function classifyDynamic(row, chapter, ctx) {
function classifyDynamic(row, chapter, ctx, parentSignature) {
if (row.depth === 0) return tier("unreviewed", "", { source: "chapter-summary" });
if (row.scope === "documentation" || row.scope === "configuration") return tier("not-applicable", "", { source: row.scope });
const feature = featureOf(row, ctx.dynamicFeatures);
const feature = featureOf(row, ctx.dynamicFeatures, parentSignature);
if (feature) return tier(feature.status, "", { source: `island-feature:${row.chapter}.${row.apiSymbol}`, tests: feature.evidence });
if (row.chapter === "globals") {
@@ -445,8 +446,9 @@ function flattenChapter(rootNode, chapter, ctx) {
nodeStability: stability,
};
row.scope = scopeOf(row);
row.static = classifyStatic(row, chapter, ctx);
row.dynamic = classifyDynamic(row, chapter, ctx);
const parentSignature = parents.at(-1)?.textRaw ?? parents.at(-1)?.name;
row.static = classifyStatic(row, chapter, ctx, parentSignature);
row.dynamic = classifyDynamic(row, chapter, ctx, parentSignature);
rows.push(row);
for (const field of CHILD_FIELDS) {
for (const child of node[field] ?? []) walk(child, [...parents, node], depth + 1, stability);
@@ -616,6 +618,9 @@ function publicDetail(tier) {
if (/^compiler-feature:http\.response\.write(?:Continue|Processing|EarlyHints)$/.test(source)) {
return "Static HTTP/1.1 informational responses support the tested no-callback forms; Early Hints accepts string-valued fields.";
}
if (/^compiler-feature:http\.(?:response\.setTimeout|connection|req|sendDate|socket|statusMessage|strictContentLength|writableFinished)$/.test(source)) {
return "Static ServerResponse state, socket access, timeouts, and strict body length checks support the tested HTTP/1.1 forms.";
}
return "Implemented for the documented scriptc module-loader subset.";
}
if (source.startsWith("compiler-chapter-policy:")) {
@@ -893,11 +898,18 @@ function checkLocal() {
}
const ctx = classificationContext();
if (snapshot.chapters.length !== 62) throw new Error(`expected 62 pinned Node API chapters, found ${snapshot.chapters.length}`);
const ancestors = [];
for (const row of snapshot.rows) {
if (row.depth === 0) continue;
if (row.depth === 0) {
ancestors.length = 1;
ancestors[0] = row;
continue;
}
const chapter = snapshot.chapters.find((item) => item.slug === row.chapter);
const expectedStatic = classifyStatic(row, chapter, ctx);
const expectedDynamic = classifyDynamic(row, chapter, ctx);
const parentSignature = ancestors[row.depth - 1]?.signature;
const expectedStatic = classifyStatic(row, chapter, ctx, parentSignature);
const expectedDynamic = classifyDynamic(row, chapter, ctx, parentSignature);
ancestors[row.depth] = row;
if (JSON.stringify(row.static) !== JSON.stringify(expectedStatic) || JSON.stringify(row.dynamic) !== JSON.stringify(expectedDynamic)) {
throw new Error(`generated classification is stale at ${row.id} (${row.signature}); run 'pnpm node-compat'`);
}
+3 -1
View File
@@ -49,7 +49,7 @@
"node-builtin.fs.fstatSync": ["tests/corpus/2950-fs-descriptor-publication.ts"],
"node-builtin.fs.fsyncSync": ["tests/corpus/2950-fs-descriptor-publication.ts"],
"node-builtin.fs.linkSync": ["tests/corpus/2950-fs-descriptor-publication.ts"],
"node-builtin.http": ["tests/corpus/1780-http-res-surface.ts", "tests/corpus/2672-http-request-response-callback.ts", "tests/corpus/2700-http-static-trailers.ts", "tests/corpus/2701-http-static-trailer-wire.ts", "tests/corpus/2702-http-static-trailer-end.ts", "tests/fixtures/server/cases/http-trailers/main.ts"],
"node-builtin.http": ["tests/corpus/1780-http-res-surface.ts", "tests/corpus/2672-http-request-response-callback.ts", "tests/corpus/2700-http-static-trailers.ts", "tests/corpus/2701-http-static-trailer-wire.ts", "tests/corpus/2702-http-static-trailer-end.ts", "tests/corpus/2957-http-static-response-controls.ts", "tests/fixtures/server/cases/http-trailers/main.ts"],
"node-builtin.http2": ["tests/corpus/2260-http2-constants.cjs"],
"node-builtin.https": ["tests/corpus/2645-https-client-url-argument.ts", "tests/corpus/2690-https-server-timeout-option.ts"],
"node-builtin.module": ["tests/corpus/1598-cjs-builtin-require/main.js"],
@@ -83,6 +83,8 @@
{ "chapter": "http", "signatures": ["`request.getHeader(name)`", "`request.getHeaderNames()`", "`request.getHeaders()`", "`request.getRawHeaderNames()`", "`request.hasHeader(name)`", "`request.removeHeader(name)`", "`request.setHeader(name, value)`", "`response.getHeaderNames()`", "`response.getHeaders()`"], "status": "partial", "evidence": ["tests/corpus/2954-http-outgoing-headers.ts"] },
{ "chapter": "http", "signatures": ["`path` Type: {string} The request path.", "`method` Type: {string} The request method.", "`host` Type: {string} The request host.", "`protocol` Type: {string} The request protocol.", "`listening` Type: {boolean} Indicates whether or not the server is listening for connections.", "`headersSent` Type: {boolean}", "`writableEnded` Type: {boolean}", "`complete` Type: {boolean}", "`httpVersion` Type: {string}", "`method` Type: {string}", "`url` Type: {string}", "`statusCode` Type: {number}", "`statusCode` Type: {number} **Default:** `200`"], "status": "partial", "evidence": ["tests/corpus/2955-http-static-state.ts"] },
{ "chapter": "http", "signatures": ["`response.writeContinue()`", "`response.writeProcessing()`", "`response.writeEarlyHints(hints[, callback])`"], "status": "partial", "evidence": ["tests/corpus/2956-http-static-informational.ts"] },
{ "chapter": "http", "signatures": ["`response.setTimeout(msecs[, callback])`"], "status": "partial", "evidence": ["tests/corpus/2957-http-static-response-controls.ts"] },
{ "chapter": "http", "parent": "Class: `http.ServerResponse`", "signatures": ["`req` Type: {http.IncomingMessage}", "`sendDate` Type: {boolean}", "`strictContentLength` Type: {boolean} **Default:** `false`", "`statusMessage` Type: {string}", "`socket` Type: {stream.Duplex}", "`connection` Type: {stream.Duplex}", "`writableFinished` Type: {boolean}"], "status": "partial", "evidence": ["tests/corpus/2957-http-static-response-controls.ts"] },
{ "chapter": "http", "signatures": ["`request.cork()`", "`request.flushHeaders()`", "`request.uncork()`", "`response.addTrailers(headers)`", "`response.cork()`", "`response.flushHeaders()`", "`response.uncork()`", "`headersDistinct` Type: {Object}", "`rawTrailers` Type: {string\\[]}", "`trailers` Type: {Object}", "`trailersDistinct` Type: {Object}", "`outgoingMessage.addTrailers(headers)`", "`outgoingMessage.cork()`", "`outgoingMessage.flushHeaders()`", "`outgoingMessage.uncork()`", "`writableCorked` Type: {number}"], "status": "partial", "evidence": ["tests/corpus/2700-http-static-trailers.ts", "tests/corpus/2701-http-static-trailer-wire.ts", "tests/corpus/2702-http-static-trailer-end.ts", "tests/fixtures/server/cases/http-trailers/main.ts"] },
{ "chapter": "buffer", "signatures": ["`buf.readBigInt64BE([offset])`", "`buf.readBigInt64LE([offset])`", "`buf.readBigUInt64BE([offset])`", "`buf.readBigUInt64LE([offset])`", "`buf.writeBigInt64BE(value[, offset])`", "`buf.writeBigInt64LE(value[, offset])`", "`buf.writeBigUInt64BE(value[, offset])`", "`buf.writeBigUInt64LE(value[, offset])`"], "status": "supported", "evidence": ["tests/corpus/2902-bigint-bytes.ts", "tests/corpus/2904-bigint-errors.ts"] },
{ "chapter": "environment_variables", "symbols": ["export"], "status": "not-applicable" },
+8 -1
View File
@@ -2390,10 +2390,16 @@ declare module "http" {
export interface ServerResponse {
readonly headersSent: boolean;
readonly writableEnded: boolean;
readonly writableFinished: boolean;
readonly writableCorked: number;
readonly req: IncomingMessage;
readonly socket: Socket | null;
readonly connection: Socket | null;
sendDate: boolean;
strictContentLength: boolean;
/* Node's writable head properties: the implicit head reads them. */
statusCode: number;
statusMessage: string;
statusMessage: string | undefined;
setHeader(name: string, value: string | number): void;
getHeader(name: string): string | undefined;
getHeaderNames(): string[];
@@ -2413,6 +2419,7 @@ declare module "http" {
cork(): void;
uncork(): void;
addTrailers(headers: OutgoingHttpHeaders | ReadonlyArray<[string, string]>): void;
setTimeout(msecs: number, callback?: () => void): this;
/* end's callback forms fire once the body went out (the 'finish'
* emit, deferred past the handler's synchronous tail). */
end(data?: string | Uint8Array, callback?: () => void): void;
+46 -3
View File
@@ -3,7 +3,7 @@ import { InternalCompilerError } from "../../errors.js";
* expression lands in a fresh C temp, with RC ownership tracked on the
* emitter's frames (see the discipline comment in emitter core). */
import type { CEmitter, Temp } from "./c-emitter.js";
import { arrayOf, BOOL, BYTES_U8, bytesOf, canMarshalFuncIntoIsland, CHILDSTREAM_T, CHILDWRITER_T, DYN, F64, IrExpr, IrLibFn, IrRecordShape, IrType, islandPromisePayloadTag, isClassOwnEnumerableFieldName, isDynTypedRefType, isFfiCallbackParam, isFfiContextParam, isFfiReleaseParam, isRefCounted, isUnitType, MAY_THROW_LIB_FNS, RUNTIME_ERROR_CLASSES, STRING, typeEquals, typeKey } from "../../ir/ir.js";
import { arrayOf, BOOL, BYTES_U8, bytesOf, canMarshalFuncIntoIsland, CHILDSTREAM_T, CHILDWRITER_T, DYN, F64, IrExpr, IrLibFn, IrRecordShape, IrType, islandPromisePayloadTag, isClassOwnEnumerableFieldName, isDynTypedRefType, isFfiCallbackParam, isFfiContextParam, isFfiReleaseParam, isRefCounted, isUnitType, MAY_THROW_LIB_FNS, NETSOCKET_T, RUNTIME_ERROR_CLASSES, STRING, typeEquals, typeKey } from "../../ir/ir.js";
import { boxAccess, BYTES_NUM_KIND_C, BYTES_NUM_VAR_C, bytesElemKindC, cDecl, cFnPtrCast, cNumberLiteral, cStringLiteral, cType, DV_GET_KIND_C, DV_SET_KIND_C, elemAccess, mapKeyAccess, mapKeyKindC, mapValKindC, releaseCallC, retainCallC, vAdapters } from "./types.js";
import { mangleClassNew, mangleClassRetain, mangleClassStruct, mangleField, mangleFnClosure, mangleFunction, mangleGlobal, mangleLocal, mangleRecordClone, mangleRecordNew, mangleRecordStruct, mangleVtStruct } from "../mangle.js";
import { OVERFLOW_MEMBER } from "./shapes.js";
@@ -6672,11 +6672,54 @@ function emitHttpLibCall(state: LibCallState): Temp {
case "http.resStatusSet":
emitter.line(`scr_http_res_status_set(${arg(0)}, ${arg(1)});${emitter.srcComment(e.loc)}`);
return { name: "", type: e.type };
case "http.resStatusMsgGet":
return finish(`scr_http_res_status_msg_get(${arg(0)})`);
case "http.resStatusMsgGet": {
if (e.type.kind !== "union") throw new InternalCompilerError("emitter bug: http.resStatusMsgGet result is not a union");
const def = emitter.unionsById.get(e.type.unionId);
const strTag = def ? def.arms.findIndex((a) => a.kind === "string") : -1;
const undefTag = undefinedArmTag(e.type, emitter.unionsById);
if (strTag < 0 || undefTag < 0) throw new InternalCompilerError("emitter bug: http.resStatusMsgGet union lacks its arms");
const msg = emitter.newTemp(STRING, `scr_http_res_status_msg_get(${arg(0)})`);
emitter.moveTemp(msg);
const present = `scr_union_new_ref(${strTag}, ${msg.name}, &scr_str_retain_v, &scr_str_release_v, NULL)`;
return emitter.newTemp(e.type, `${msg.name} ? ${present} : ${emitter.unitInstanceRef(e.type.unionId, undefTag)}`);
}
case "http.resStatusMsgSet":
emitter.line(`scr_http_res_status_msg_set(${arg(0)}, ${arg(1)});${emitter.srcComment(e.loc)}`);
return { name: "", type: e.type };
case "http.resRequest":
return finish(`scr_http_res_request(${arg(0)})`);
case "http.resSocket": {
if (e.type.kind !== "union") throw new InternalCompilerError("emitter bug: http.resSocket result is not a union");
const def = emitter.unionsById.get(e.type.unionId);
const socketTag = def ? def.arms.findIndex((a) => a.kind === "netSocket") : -1;
const nullTag = def ? def.arms.findIndex((a) => a.kind === "nullT") : -1;
if (socketTag < 0 || nullTag < 0) throw new InternalCompilerError("emitter bug: http.resSocket union lacks its arms");
const sock = emitter.newTemp(NETSOCKET_T, `scr_http_res_socket(${arg(0)})`);
emitter.moveTemp(sock);
const present = `scr_union_new_ref(${socketTag}, ${sock.name}, &scr_net_sock_retain_v, &scr_net_sock_release_v, NULL)`;
return emitter.newTemp(e.type, `${sock.name} ? ${present} : ${emitter.unitInstanceRef(e.type.unionId, nullTag)}`);
}
case "http.resWritableFinished":
return finish(`scr_http_res_writable_finished(${arg(0)})`);
case "http.resSendDateGet":
return finish(`scr_http_res_send_date(${arg(0)})`);
case "http.resSendDateSet":
emitter.line(`scr_http_res_set_send_date(${arg(0)}, ${arg(1)});${emitter.srcComment(e.loc)}`);
return { name: "", type: e.type };
case "http.resStrictContentLengthGet":
return finish(`scr_http_res_strict_content_length(${arg(0)})`);
case "http.resStrictContentLengthSet":
emitter.line(`scr_http_res_set_strict_content_length(${arg(0)}, ${arg(1)});${emitter.srcComment(e.loc)}`);
return { name: "", type: e.type };
case "http.resSetTimeout":
emitter.line(`scr_http_res_set_timeout_plain(${arg(0)}, ${arg(1)});${emitter.srcComment(e.loc)}`);
return { name: "", type: e.type };
case "http.resSetTimeoutCb": {
const cb = args[2]!;
emitter.moveTemp(cb);
emitter.line(`scr_http_res_set_timeout(${arg(0)}, ${arg(1)}, ${cb.name});${emitter.srcComment(e.loc)}`);
return { name: "", type: e.type };
}
case "http.resGetHeader":
case "http.clientGetHeader": {
// string|undefined, exactly the http.reqHeader emission.
@@ -1,7 +1,7 @@
/* Focused LLVM library-call emission extracted from emitter.ts. */
import { InternalCompilerError } from "../../errors.js";
import { undefinedArmTag } from "../../ir/analysis.js";
import { arrayOf, STRING } from "../../ir/ir.js";
import { arrayOf, NETSOCKET_T, STRING } from "../../ir/ir.js";
import { mangleRecordNew, mangleRecordStruct } from "../mangle.js";
import type { LlvmEmitterContext, LibCallExpr, LlValue } from "./expr-context.js";
import { f64Lit } from "./common.js";
@@ -242,7 +242,19 @@ export function emitNetworkHttpLibCall(host: LlvmEmitterContext, e: LibCallExpr)
B.line(`${raw} = call ptr @scr_net_sock_read_bytes(ptr ${args[0]!.name}, double ${args[1]!.name}) ; +1 or NULL`);
return host.wrapNullable(raw, raw, def!.arms[bytesTag]!, bytesTag, e.type, nullTag);
}
if (e.fn === "net.sockRemoteAddress" || e.fn === "http.reqHeader" || e.fn === "http.reqTrailer" || e.fn === "http.resGetHeader" || e.fn === "http.clientGetHeader" || e.fn === "http.reqStatusMessage") {
if (e.fn === "http.resSocket") {
if (e.type.kind !== "union") throw new InternalCompilerError("llvm emitter bug: http.resSocket result is not a union");
const def = host.unionsById.get(e.type.unionId);
const socketTag = def ? def.arms.findIndex((a) => a.kind === "netSocket") : -1;
const nullTag = def ? def.arms.findIndex((a) => a.kind === "nullT") : -1;
if (socketTag < 0 || nullTag < 0) throw new InternalCompilerError("llvm emitter bug: http.resSocket union lacks its arms");
const args = e.args.map((a) => host.emitExpr(a));
host.declare(`declare ptr @scr_http_res_socket(ptr)`);
const raw = B.tmp();
B.line(`${raw} = call ptr @scr_http_res_socket(ptr ${args[0]!.name}) ; +1 or NULL`);
return host.wrapNullable(raw, raw, NETSOCKET_T, socketTag, e.type, nullTag);
}
if (e.fn === "net.sockRemoteAddress" || e.fn === "http.reqHeader" || e.fn === "http.reqTrailer" || e.fn === "http.resGetHeader" || e.fn === "http.clientGetHeader" || e.fn === "http.reqStatusMessage" || e.fn === "http.resStatusMsgGet") {
// string | undefined: +1 or NULL, NULL takes the undefined arm.
if (e.type.kind !== "union") throw new InternalCompilerError(`llvm emitter bug: ${e.fn} result is not a union`);
const def = host.unionsById.get(e.type.unionId);
@@ -256,6 +268,7 @@ export function emitNetworkHttpLibCall(host: LlvmEmitterContext, e: LibCallExpr)
"http.resGetHeader": "scr_http_res_get_header",
"http.clientGetHeader": "scr_http_client_get_header",
"http.reqStatusMessage": "scr_http_req_status_message",
"http.resStatusMsgGet": "scr_http_res_status_msg_get",
}[e.fn]!;
const args = e.args.map((a) => host.emitExpr(a));
const argList = args.map((a) => `${host.llType(a.type)} ${a.name}`).join(", ");
@@ -418,6 +431,13 @@ export function emitNetworkHttpLibCall(host: LlvmEmitterContext, e: LibCallExpr)
B.line(`call void @scr_http_res_on_finish(ptr ${args[0]!.name}, ptr ${args[1]!.name})`);
return { name: "", type: e.type };
}
if (e.fn === "http.resSetTimeoutCb") {
const args = e.args.map((a) => host.emitExpr(a));
host.moveTemp(args[2]!);
host.declare(`declare void @scr_http_res_set_timeout(ptr, double, ptr)`);
B.line(`call void @scr_http_res_set_timeout(ptr ${args[0]!.name}, double ${args[1]!.name}, ptr ${args[2]!.name})`);
return { name: "", type: e.type };
}
if (e.fn === "net.sockOnFinish") {
const args = e.args.map((a) => host.emitExpr(a));
host.moveTemp(args[1]!);
@@ -652,6 +652,13 @@ export const LIB_FN_SYMS: Record<string, string> = {
"http.resStatusSet": "scr_http_res_status_set",
"http.resStatusMsgGet": "scr_http_res_status_msg_get",
"http.resStatusMsgSet": "scr_http_res_status_msg_set",
"http.resRequest": "scr_http_res_request",
"http.resWritableFinished": "scr_http_res_writable_finished",
"http.resSendDateGet": "scr_http_res_send_date",
"http.resSendDateSet": "scr_http_res_set_send_date",
"http.resStrictContentLengthGet": "scr_http_res_strict_content_length",
"http.resStrictContentLengthSet": "scr_http_res_set_strict_content_length",
"http.resSetTimeout": "scr_http_res_set_timeout_plain",
"http.resHasHeader": "scr_http_res_has_header_named",
"http.resRemoveHeader": "scr_http_res_remove_header",
"http.resWriteHeadPairs": "scr_http_res_write_head_pairs",
@@ -976,22 +976,23 @@ export function lowerServerCloseOverrideAssignment(lowerer: Lowerer, left: ts.Ex
};
}
/** `res.statusCode = 404` / `res.statusMessage = "Nope"` — Node's
* writable ServerResponse properties (the implicit head reads them):
* routed from lower-stmts' property-assignment path beside the
* close-override hook. Null when the target isn't one of the two. */
/** Writable ServerResponse properties consulted by the implicit head and
* the strict body-length check. Null when the target isn't one of them. */
export function lowerHttpResPropertyAssignment(lowerer: Lowerer, left: ts.Expression,
right: ts.Expression, loc: SrcLoc,): IrStmt | null {
if (!ts.isPropertyAccessExpression(left) || left.questionDotToken) return null;
const name = left.name.text;
if (name !== "statusCode" && name !== "statusMessage") return null;
if (name !== "statusCode" && name !== "statusMessage" &&
name !== "sendDate" && name !== "strictContentLength") return null;
if (lowerer.mapTypeOf(lowerer.typeOf(left.expression))?.kind !== "httpRes") return null;
if (!lowerer.isStdlibMember(left)) return null;
const receiver = lowerer.lowerExpr(left.expression);
const receiver = coerceToHandle(lowerer, left.expression, HTTPRES_T);
const value = name === "statusCode"
? lowerer.lowerExprExpecting(right, F64)
: lowerer.lowerExprExpecting(right, STRING);
const fn: IrLibFn = name === "statusCode" ? "http.resStatusSet" : "http.resStatusMsgSet";
: lowerer.lowerExprExpecting(right, name === "statusMessage" ? STRING : BOOL);
const fn: IrLibFn = name === "statusCode" ? "http.resStatusSet"
: name === "statusMessage" ? "http.resStatusMsgSet"
: name === "sendDate" ? "http.resSendDateSet" : "http.resStrictContentLengthSet";
return {
kind: "exprStmt",
expr: { kind: "libCall", fn, args: [receiver, value], type: VOID, loc },
@@ -2295,11 +2296,30 @@ export function lowerServerProperty(lowerer: Lowerer, expr: ts.PropertyAccessExp
return { kind: "libCall", fn: "http.resStatusGet", args: [receiver], type: F64, loc };
}
if (recvKind === "httpRes" && lowerer.isStdlibMember(expr) && expr.name.text === "statusMessage") {
// The assigned reason phrase, or the current status code's default
// when none was set (Node answers undefined until the head goes out
// — divergence: this surface is string-typed, the checker's shape).
// Unset until the head is sent; then the assigned or default reason.
const receiver = coerceToHandle(lowerer, expr.expression, HTTPRES_T);
return { kind: "libCall", fn: "http.resStatusMsgGet", args: [receiver], type: STRING, loc };
return { kind: "libCall", fn: "http.resStatusMsgGet", args: [receiver], type: lowerer.withUndefinedArm(STRING), loc };
}
if (recvKind === "httpRes" && lowerer.isStdlibMember(expr)) {
const name = expr.name.text;
if (name === "req") {
const receiver = coerceToHandle(lowerer, expr.expression, HTTPRES_T);
return { kind: "libCall", fn: "http.resRequest", args: [receiver], type: HTTPREQ_T, loc };
}
if (name === "socket" || name === "connection") {
const receiver = coerceToHandle(lowerer, expr.expression, HTTPRES_T);
const type: IrType = { kind: "union", unionId: lowerer.unions.intern([NETSOCKET_T, NULL_T]) };
return { kind: "libCall", fn: "http.resSocket", args: [receiver], type, loc };
}
if (name === "writableFinished") {
const receiver = coerceToHandle(lowerer, expr.expression, HTTPRES_T);
return { kind: "libCall", fn: "http.resWritableFinished", args: [receiver], type: BOOL, loc };
}
if (name === "sendDate" || name === "strictContentLength") {
const receiver = coerceToHandle(lowerer, expr.expression, HTTPRES_T);
const fn: IrLibFn = name === "sendDate" ? "http.resSendDateGet" : "http.resStrictContentLengthGet";
return { kind: "libCall", fn, args: [receiver], type: BOOL, loc };
}
}
if (recvKind === "http2Session" && lowerer.isStdlibMember(expr)) {
const m = expr.name.text;
@@ -4634,6 +4654,20 @@ function lowerHttpResMethodCall(lowerer: Lowerer, call: ts.CallExpression,
const name = access.name.text;
const loc = locOf(call);
const args = call.arguments;
if (name === "setTimeout") {
if (args.length < 1 || args.length > 2) {
lowerer.noLowering(`res.setTimeout with ${args.length} arguments`, call, "use setTimeout(milliseconds[, callback])");
}
const receiver = coerceToHandle(lowerer, access.expression, HTTPRES_T);
const ms = lowerer.lowerExprExpecting(args[0]!, F64);
const cb = args.length === 2
? lowerCallbackArg(lowerer, args[1]!, "response timeout callbacks", 0, () => false, "use ()", []).cb
: null;
const fn: IrLibFn = cb === null ? "http.resSetTimeout" : "http.resSetTimeoutCb";
const callArgs = cb === null ? [receiver, ms] : [receiver, ms, cb];
if (resultIsDiscarded(call)) return { kind: "libCall", fn, args: callArgs, type: VOID, loc };
return receiverReturningCall(lowerer, fn, callArgs, HTTPRES_T, loc);
}
if (name === "writeContinue" || name === "writeProcessing" || name === "writeEarlyHints") {
requireStatementPosition(lowerer, call, `res.${name}(...)`);
if (lowerer.typeOf(access.expression).getSymbol()?.name === "Http2ServerResponse") {
@@ -4974,7 +5008,7 @@ function lowerHttpResMethodCall(lowerer: Lowerer, call: ts.CallExpression,
lowerer.noLowering(
`ServerResponse.${name}`,
call,
"setHeader, getHeader, hasHeader, removeHeader, writeHead, writeContinue, writeProcessing, writeEarlyHints, write, end, destroy, headersSent, statusCode, statusMessage, and on/once of close are the supported ServerResponse members",
"setHeader, getHeader, hasHeader, removeHeader, writeHead, writeContinue, writeProcessing, writeEarlyHints, setTimeout, write, end, destroy, response state reads, and on/once of close are the supported ServerResponse members",
lowerer.checker.getSymbolAtLocation(access.name),
);
}
+9
View File
@@ -2767,6 +2767,15 @@ export type IrLibFn =
| "http.resStatusSet"
| "http.resStatusMsgGet"
| "http.resStatusMsgSet"
| "http.resRequest"
| "http.resSocket"
| "http.resWritableFinished"
| "http.resSendDateGet"
| "http.resSendDateSet"
| "http.resStrictContentLengthGet"
| "http.resStrictContentLengthSet"
| "http.resSetTimeout"
| "http.resSetTimeoutCb"
| "http.resGetHeader"
| "http.resGetHeaderNames"
| "http.resGetRawHeaderNames"
+20 -3
View File
@@ -514,8 +514,17 @@ export const LIB_FN_SIGS: Record<IrLibFn, { argTypes: (IrType | null)[]; result:
"net.serverOnListening": { argTypes: [NETSERVER_T, { kind: "func", params: [], ret: VOID }, BOOL], result: VOID },
"http.resStatusGet": { argTypes: [HTTPRES_T], result: F64 },
"http.resStatusSet": { argTypes: [HTTPRES_T, F64], result: VOID },
"http.resStatusMsgGet": { argTypes: [HTTPRES_T], result: STRING },
"http.resStatusMsgGet": { argTypes: [HTTPRES_T], result: VOID },
"http.resStatusMsgSet": { argTypes: [HTTPRES_T, STRING], result: VOID },
"http.resRequest": { argTypes: [HTTPRES_T], result: HTTPREQ_T },
"http.resSocket": { argTypes: [HTTPRES_T], result: VOID },
"http.resWritableFinished": { argTypes: [HTTPRES_T], result: BOOL },
"http.resSendDateGet": { argTypes: [HTTPRES_T], result: BOOL },
"http.resSendDateSet": { argTypes: [HTTPRES_T, BOOL], result: VOID },
"http.resStrictContentLengthGet": { argTypes: [HTTPRES_T], result: BOOL },
"http.resStrictContentLengthSet": { argTypes: [HTTPRES_T, BOOL], result: VOID },
"http.resSetTimeout": { argTypes: [HTTPRES_T, F64], result: VOID },
"http.resSetTimeoutCb": { argTypes: [HTTPRES_T, F64, { kind: "func", params: [], ret: VOID }], result: VOID },
// resGetHeader answers the interned `string | undefined` union — the
// reqHeader/envGet sentinel pattern (VOID here, checked specially).
"http.resGetHeader": { argTypes: [HTTPRES_T, STRING], result: VOID },
@@ -4240,7 +4249,7 @@ function validateFunction(
}
break;
}
if (e.fn === "http.reqStatusMessage") {
if (e.fn === "http.reqStatusMessage" || e.fn === "http.resStatusMsgGet") {
// Result is the interned `string | undefined` union (reqHeader's).
const def = e.type.kind === "union" ? unions.get(e.type.unionId) : undefined;
const ok =
@@ -4249,10 +4258,18 @@ function validateFunction(
def.arms.some((a) => a.kind === "string") &&
def.arms.some((a) => a.kind === "undefinedT");
if (!ok) {
err(`libCall http.reqStatusMessage must return the 'string | undefined' union`, e.loc);
err(`libCall ${e.fn} must return the 'string | undefined' union`, e.loc);
}
break;
}
if (e.fn === "http.resSocket") {
const def = e.type.kind === "union" ? unions.get(e.type.unionId) : undefined;
const ok = def && def.arms.length === 2 &&
def.arms.some((a) => a.kind === "netSocket") &&
def.arms.some((a) => a.kind === "nullT");
if (!ok) err(`libCall http.resSocket must return the 'Socket | null' union`, e.loc);
break;
}
if (e.fn === "http.reqH2Stream") {
const def = e.type.kind === "union" ? unions.get(e.type.unionId) : undefined;
const ok = def && def.arms.length === 2 &&
@@ -6277,6 +6277,12 @@
],
"diags": []
},
"<repo>/tests/corpus/2900-float64array-static.ts": {
"order": [
"<repo>/tests/corpus/2900-float64array-static.ts"
],
"diags": []
},
"<repo>/tests/corpus/2900-void-conditional-arrow.ts": {
"order": [
"<repo>/tests/corpus/2900-void-conditional-arrow.ts"
@@ -6576,6 +6582,48 @@
],
"diags": []
},
"<repo>/tests/corpus/2951-math-static-analytic.ts": {
"order": [
"<repo>/tests/corpus/2951-math-static-analytic.ts"
],
"diags": []
},
"<repo>/tests/corpus/2952-math-static-remaining.ts": {
"order": [
"<repo>/tests/corpus/2952-math-static-remaining.ts"
],
"diags": []
},
"<repo>/tests/corpus/2953-http-header-validation.ts": {
"order": [
"<repo>/tests/corpus/2953-http-header-validation.ts"
],
"diags": []
},
"<repo>/tests/corpus/2954-http-outgoing-headers.ts": {
"order": [
"<repo>/tests/corpus/2954-http-outgoing-headers.ts"
],
"diags": []
},
"<repo>/tests/corpus/2955-http-static-state.ts": {
"order": [
"<repo>/tests/corpus/2955-http-static-state.ts"
],
"diags": []
},
"<repo>/tests/corpus/2956-http-static-informational.ts": {
"order": [
"<repo>/tests/corpus/2956-http-static-informational.ts"
],
"diags": []
},
"<repo>/tests/corpus/2957-http-static-response-controls.ts": {
"order": [
"<repo>/tests/corpus/2957-http-static-response-controls.ts"
],
"diags": []
},
"<repo>/tests/corpus/2961-emitter-computed-names.cjs": {
"order": [
"<repo>/tests/corpus/2961-emitter-computed-names.cjs"
@@ -6594,6 +6642,30 @@
],
"diags": []
},
"<repo>/tests/corpus/2964-indexed-compound.ts": {
"order": [
"<repo>/tests/corpus/2964-indexed-compound.ts"
],
"diags": []
},
"<repo>/tests/corpus/2966-numeric-array-read-fusion.ts": {
"order": [
"<repo>/tests/corpus/2966-numeric-array-read-fusion.ts"
],
"diags": []
},
"<repo>/tests/corpus/2967-numeric-array-read-lifetime.ts": {
"order": [
"<repo>/tests/corpus/2967-numeric-array-read-lifetime.ts"
],
"diags": []
},
"<repo>/tests/corpus/2968-numeric-array-borrow.ts": {
"order": [
"<repo>/tests/corpus/2968-numeric-array-borrow.ts"
],
"diags": []
},
"<repo>/tests/corpus/300-if-else.ts": {
"order": [
"<repo>/tests/corpus/300-if-else.ts"
+97 -7
View File
@@ -813,8 +813,11 @@ struct ScrHttpRes {
bool chunked;
bool finished;
bool no_date; /* res.sendDate = false: suppress the implicit Date header */
bool strict_content_length;
size_t strict_bytes_written; /* Node counts UTF-8 bytes, not JS characters */
bool keep_alive; /* the REQUEST's verdict; Connection: close overrides */
bool destroyed; /* res.destroy()/teardown — res.destroyed (true in 'close') */
bool socket_detached; /* finish/close removes the response's socket view */
/* cork()/uncork(): corked counts the nesting (res.writableCorked);
* writes while corked coalesce in cork_buf and flush as ONE write when
* the count reaches zero (or at end()) — Node's coalescing, and the
@@ -871,6 +874,51 @@ void scr_http_res_release_v(void *p) { scr_http_res_release((ScrHttpRes *)p); }
bool scr_http_res_headers_sent(ScrHttpRes *r) { return r->head_sent; }
bool scr_http_res_writable_ended(ScrHttpRes *r) { return r->finished; }
bool scr_http_res_send_date(ScrHttpRes *r) { return !r->no_date; }
void scr_http_res_set_send_date(ScrHttpRes *r, bool value) { r->no_date = !value; }
bool scr_http_res_strict_content_length(ScrHttpRes *r) { return r->strict_content_length; }
void scr_http_res_set_strict_content_length(ScrHttpRes *r, bool value) { r->strict_content_length = value; }
ScrHttpReq *scr_http_res_request(ScrHttpRes *r) { return scr_http_req_retain(r->req_ref); }
ScrNetSocket *scr_http_res_socket(ScrHttpRes *r) {
return r->sock && !r->socket_detached ? scr_net_sock_retain(r->sock) : NULL;
}
bool scr_http_res_writable_finished(ScrHttpRes *r) { return r->finished; }
static bool scr_http_res_has_header(ScrHttpRes *r, const char *name);
/* Node checks declared body length only while strictContentLength is true,
* with no transfer encoding and a body-bearing response. The stored header
* string has already passed setHeader's validation; strtod matches Node's
* numeric conversion for the ordinary decimal Content-Length values. */
static bool scr_http_res_expected_length(ScrHttpRes *r, double *expected) {
if (!r->strict_content_length || r->h2_stream || r->chunked ||
(r->status >= 100 && r->status < 200) || r->status == 204 || r->status == 304 ||
scr_http_res_has_header(r, "transfer-encoding")) return false;
if (r->req_ref && r->req_ref->method && r->req_ref->method->len == 4 &&
memcmp(r->req_ref->method->data, "HEAD", 4) == 0) return false;
for (size_t i = 0; i < r->nheaders; i++) {
if (r->hnames[i]->len != 14) continue;
bool match = true;
for (size_t j = 0; j < 14; j++) {
if (tolower((unsigned char)r->hnames[i]->data[j]) != "content-length"[j]) {
match = false;
break;
}
}
if (!match) continue;
*expected = strtod(r->hvalues[i]->data, NULL);
return true;
}
return false;
}
static void scr_http_res_length_mismatch(ScrHttpRes *r, double expected) {
char msg[256];
int n = snprintf(msg, sizeof msg,
"Response body's content-length of %zu byte(s) does not match the content-length of %.15g byte(s) set in header",
r->strict_bytes_written, expected);
scr_throw_error_msg_code(SCR_ERR_ERROR, msg, (size_t)n, "ERR_HTTP_CONTENT_LENGTH_MISMATCH");
}
/* res.statusCode: 200 until assigned (Node's fresh-response default);
* assignment after the head went out is inert (Node throws on the WRITE
@@ -882,11 +930,11 @@ void scr_http_res_status_set(ScrHttpRes *r, double status) {
r->status = (int)status;
}
/* res.statusMessage: the assigned reason phrase, or the current status
* code's default once none was set (Node answers undefined before the
* head goes out — divergence: this surface is string-typed). */
/* An unset statusMessage is undefined until the head chooses the status
* code's default reason phrase. */
ScrStr *scr_http_res_status_msg_get(ScrHttpRes *r) {
if (r->status_msg) return scr_str_retain(r->status_msg);
if (!r->head_sent) return NULL;
const char *reason = scr_http_reason(r->status > 0 ? r->status : 200);
return scr_str_new(reason, strlen(reason));
}
@@ -1373,10 +1421,28 @@ static void scr_http_res_write_raw(ScrHttpRes *r, const char *data, size_t len)
}
void scr_http_res_write_str(ScrHttpRes *r, ScrStr *data /*borrowed*/) {
double expected;
if (r->head_sent && scr_http_res_expected_length(r, &expected) &&
(double)(r->strict_bytes_written + data->len) > expected) {
r->strict_bytes_written += data->len;
scr_http_res_length_mismatch(r, expected);
r->strict_bytes_written -= data->len;
return;
}
if (r->strict_content_length) r->strict_bytes_written += data->len;
scr_http_res_write_raw(r, data->data, data->len);
}
void scr_http_res_write_bytes(ScrHttpRes *r, ScrBytes *data /*borrowed*/) {
double expected;
if (r->head_sent && scr_http_res_expected_length(r, &expected) &&
(double)(r->strict_bytes_written + data->len) > expected) {
r->strict_bytes_written += data->len;
scr_http_res_length_mismatch(r, expected);
r->strict_bytes_written -= data->len;
return;
}
if (r->strict_content_length) r->strict_bytes_written += data->len;
scr_http_res_write_raw(r, (const char *)data->data, data->len);
}
@@ -1420,6 +1486,10 @@ void scr_http_res_set_timeout(ScrHttpRes *r, double ms, ScrClosure *cb /*moves,
if (cb) scr_net_sock_on_timeout(r->sock, cb, true);
}
void scr_http_res_set_timeout_plain(ScrHttpRes *r, double ms) {
scr_http_res_set_timeout(r, ms, NULL);
}
static void scr_http_conn_response_finished(struct ScrHttpConn *conn, bool keep_alive);
static void scr_http_queue_res_finish(ScrHttpRes *res);
@@ -1437,6 +1507,15 @@ static void scr_http_res_cork_flush(ScrHttpRes *r) {
static void scr_http_res_end_raw(ScrHttpRes *r, const char *data, size_t len) {
if (r->finished) return;
double expected;
if (len > 0 && r->head_sent && scr_http_res_expected_length(r, &expected) &&
(double)(r->strict_bytes_written + len) != expected) {
r->strict_bytes_written += len;
scr_http_res_length_mismatch(r, expected);
r->strict_bytes_written -= len;
return;
}
if (r->strict_content_length) r->strict_bytes_written += len;
if (r->corked > 0 || r->cork_len > 0) {
/* end() flushes every cork level (Node) — the body streamed, so the
* framing below takes the already-committed streaming path */
@@ -1449,7 +1528,7 @@ static void scr_http_res_end_raw(ScrHttpRes *r, const char *data, size_t len) {
if (!r->head_sent) scr_http_res_send_head(r, (long long)len);
scr_http_h2_ops->end(r->h2_stream, data, len);
r->finished = true;
if (r->finish_ls.n > 0) scr_http_queue_res_finish(r);
scr_http_queue_res_finish(r);
return;
}
if (!r->head_sent) {
@@ -1466,8 +1545,13 @@ static void scr_http_res_end_raw(ScrHttpRes *r, const char *data, size_t len) {
scr_http_res_write_raw(r, data, len);
if (r->chunked && r->sock) scr_http_send_chunk_end(r->sock, &r->trailers);
}
if (scr_http_res_expected_length(r, &expected) &&
(double)r->strict_bytes_written != expected) {
scr_http_res_length_mismatch(r, expected);
return;
}
r->finished = true;
if (r->finish_ls.n > 0) scr_http_queue_res_finish(r);
scr_http_queue_res_finish(r);
if (r->conn) scr_http_conn_response_finished(r->conn, r->keep_alive);
}
@@ -1734,6 +1818,7 @@ static void scr_http_proto_sweep(void) {
if (!res->close_emitted) {
res->close_emitted = true;
res->destroyed = true; /* Node: destroyed reads true inside 'close' */
res->socket_detached = true;
scr_net_fire0_this(&res->close_ls, res, SCR_DYNH_HTTP_RES);
scr_net_ls_drop(&res->close_ls);
}
@@ -1792,6 +1877,7 @@ static void scr_http_proto_sweep(void) {
case SCR_HTTP_EMIT_RES_FINISH: {
ScrHttpRes *res = (ScrHttpRes *)e.h;
res->finish_queued = false; /* a later end(cb) on the finished res re-queues */
res->socket_detached = true;
scr_net_fire0_this(&res->finish_ls, res, SCR_DYNH_HTTP_RES);
break;
}
@@ -4910,6 +4996,7 @@ static ScrDyn *scr_http_dynh_res_get(void *h, const char *key, size_t key_len) {
if (strcmp(key, "statusCode") == 0) return scr_dyn_new_num(scr_http_res_status_get(r));
if (strcmp(key, "statusMessage") == 0) {
ScrStr *s = scr_http_res_status_msg_get(r);
if (!s) return scr_dyn_retain(scr_dyn_undefined());
ScrDyn *d = scr_dyn_new_str(s);
scr_str_release(s);
return d;
@@ -4921,8 +5008,11 @@ static ScrDyn *scr_http_dynh_res_get(void *h, const char *key, size_t key_len) {
return scr_dyn_new_bool(r->finished);
}
if (strcmp(key, "socket") == 0 || strcmp(key, "connection") == 0) {
if (!r->sock) return scr_dyn_new_null(); /* destroyed: Node nulls it */
return scr_dyn_new_handle(r->sock, SCR_DYNH_NET_SOCKET);
ScrNetSocket *sock = scr_http_res_socket(r);
if (!sock) return scr_dyn_new_null();
ScrDyn *view = scr_dyn_new_handle(sock, SCR_DYNH_NET_SOCKET);
scr_net_sock_release(sock);
return view;
}
if (strcmp(key, "req") == 0) {
if (r->req_cleared || r->req_ref == NULL) return scr_dyn_new_null();
+8
View File
@@ -6116,6 +6116,13 @@ void scr_http_res_write_dynv(ScrHttpRes *r, const ScrDyn *d /*borrowed*/);
void scr_http_res_end_dynv(ScrHttpRes *r, const ScrDyn *d /*borrowed*/);
bool scr_http_res_headers_sent(ScrHttpRes *r);
bool scr_http_res_writable_ended(ScrHttpRes *r);
bool scr_http_res_send_date(ScrHttpRes *r);
void scr_http_res_set_send_date(ScrHttpRes *r, bool value);
bool scr_http_res_strict_content_length(ScrHttpRes *r);
void scr_http_res_set_strict_content_length(ScrHttpRes *r, bool value);
ScrHttpReq *scr_http_res_request(ScrHttpRes *r); /* +1 */
ScrNetSocket *scr_http_res_socket(ScrHttpRes *r); /* +1 or NULL after finish */
bool scr_http_res_writable_finished(ScrHttpRes *r);
/* The res member surface: statusCode (200 until assigned; inert once the
* head went out), statusMessage (the reason phrase — assigned value, or
* the code's default), the header CRUD trio, and end(cb)'s finish slot
@@ -6177,6 +6184,7 @@ double scr_http_res_writable_corked(ScrHttpRes *r);
bool scr_http_res_destroyed_flag(ScrHttpRes *r);
void scr_http_res_set_req(ScrHttpRes *r, ScrHttpReq *req /*borrowed, nullable*/);
void scr_http_res_set_timeout(ScrHttpRes *r, double ms, ScrClosure *cb /*moves, nullable*/);
void scr_http_res_set_timeout_plain(ScrHttpRes *r, double ms);
void scr_http_res_on_write_flush(ScrHttpRes *r, ScrClosure *cb /*moves*/);
/* req.setEncoding(enc) — the socket twin's contract; may throw. */
void scr_http_req_set_encoding(ScrHttpReq *r, ScrStr *enc /*borrowed*/);
@@ -0,0 +1,77 @@
import { createServer, get } from "node:http";
const server = createServer((req, res) => {
console.log("paired", res.req.method, res.req.url);
console.log("defaults", res.sendDate, res.strictContentLength, res.statusMessage === undefined, res.writableFinished);
const socket = res.socket;
console.log("socket", socket !== null, res.connection !== null, socket !== null && socket.remoteAddress === req.socket.remoteAddress);
if (req.url === "/no-date") {
res.sendDate = false;
res.strictContentLength = true;
res.setHeader("Content-Length", "2");
console.log("configured", res.sendDate, res.strictContentLength);
res.setTimeout(1000).end("ok", () => {
console.log("finished", res.statusMessage, res.writableFinished, res.socket === null, res.connection === null);
});
console.log("after end", res.statusMessage, res.writableFinished, res.socket !== null);
return;
}
if (req.url === "/recover") {
res.strictContentLength = true;
res.setHeader("Content-Length", "2");
try {
res.end("a");
} catch (error) {
if (error instanceof Error) {
console.log("mismatch", (error as NodeJS.ErrnoException).code, error.message);
}
res.end("a");
}
return;
}
if (req.url === "/overflow") {
res.strictContentLength = true;
res.setHeader("Content-Length", "2");
res.write("a");
try {
res.write("bc");
} catch (error) {
if (error instanceof Error) {
console.log("overflow", (error as NodeJS.ErrnoException).code, error.message);
}
}
res.end("d");
return;
}
if (req.url === "/unicode") {
res.strictContentLength = true;
res.setHeader("Content-Length", "3");
res.end("é!");
return;
}
res.setTimeout(25, () => {
console.log("response timeout");
res.end("late");
});
});
server.listen(0, () => {
const port = server.address().port;
const paths = ["/no-date", "/recover", "/overflow", "/unicode", "/slow"];
const visit = (index: number): void => {
if (index === paths.length) {
server.close(() => console.log("closed"));
return;
}
const path = paths[index]!;
get({ hostname: "127.0.0.1", port, path }, (response) => {
let body = "";
response.on("data", (chunk: Buffer) => { body += chunk.toString("utf8"); });
response.on("end", () => {
console.log("reply", path, response.statusCode, response.headers.date !== undefined, body);
visit(index + 1);
});
}).on("error", (error) => console.log("client error", error.message));
};
visit(0);
});