From 3f0de2167cedf11b8934116ee38298ff61dbb9cc Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 24 Sep 2026 00:36:10 -0500 Subject: [PATCH] feat(http): expand static node:http parity (#403) - Add typed HTTP header validation, outgoing header APIs, and lifecycle state. - Emit 100, 102, and string-valued 103 informational responses in C and LLVM. - Cover the behavior with differential fixtures and update compatibility data. --- .../node-v24-compatibility-meta.json | 2 +- .../src/generated/node-v24-compatibility.json | 184 ++++---- .../generated/node-v24-backlog.json | 424 ++++++++++-------- .../generated/node-v24-internal.json | 218 ++++++--- internal/compatibility/src/generate.mjs | 17 + internal/compatibility/static-support.json | 4 + .../ambient/scriptc-node-fallback.d.ts | 33 +- packages/compiler/src/backend/c/exprs.ts | 54 ++- .../compiler/src/backend/llvm/lib-network.ts | 3 +- .../compiler/src/backend/llvm/lib-shared.ts | 27 ++ .../src/frontend/lowering/lower-server.ts | 123 ++++- packages/compiler/src/ir/ir.ts | 30 ++ packages/compiler/src/ir/validate.ts | 25 +- packages/runtime/src/scr_http.c | 382 +++++++++++++++- packages/runtime/src/scr_net.c | 1 + packages/runtime/src/scr_runtime.h | 23 + tests/corpus/2953-http-header-validation.ts | 36 ++ tests/corpus/2954-http-outgoing-headers.ts | 49 ++ tests/corpus/2955-http-static-state.ts | 37 ++ .../corpus/2956-http-static-informational.ts | 58 +++ 20 files changed, 1380 insertions(+), 350 deletions(-) create mode 100644 tests/corpus/2953-http-header-validation.ts create mode 100644 tests/corpus/2954-http-outgoing-headers.ts create mode 100644 tests/corpus/2955-http-static-state.ts create mode 100644 tests/corpus/2956-http-static-informational.ts diff --git a/docs/src/generated/node-v24-compatibility-meta.json b/docs/src/generated/node-v24-compatibility-meta.json index d09fd990..0dd04f74 100644 --- a/docs/src/generated/node-v24-compatibility-meta.json +++ b/docs/src/generated/node-v24-compatibility-meta.json @@ -2,6 +2,6 @@ "schemaVersion": 3, "nodeVersion": "24.15.0", "nodeCommit": "848430679556aed0bd073f2bc263331ad84fa119", - "artifactVersion": "0ac7558fba31464469e7", + "artifactVersion": "a7e38d1b2b0e92cf8ec4", "rowCount": 3662 } diff --git a/docs/src/generated/node-v24-compatibility.json b/docs/src/generated/node-v24-compatibility.json index a3d2c993..d8842613 100644 --- a/docs/src/generated/node-v24-compatibility.json +++ b/docs/src/generated/node-v24-compatibility.json @@ -506,9 +506,9 @@ "apiEntries": 174, "static": { "supported": 0, - "partial": 40, + "partial": 70, "refused": 0, - "not-implemented": 134, + "not-implemented": 104, "by-design": 0, "unreviewed": 0, "not-applicable": 0 @@ -45933,9 +45933,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 outgoing header reads and mutations support the tested string-value shapes.", + "verification": "test-backed" }, "dynamic": { "status": "partial", @@ -45961,9 +45961,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 outgoing header reads and mutations support the tested string-value shapes.", + "verification": "test-backed" }, "dynamic": { "status": "not-implemented", @@ -45989,9 +45989,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 outgoing header reads and mutations support the tested string-value shapes.", + "verification": "test-backed" }, "dynamic": { "status": "not-implemented", @@ -46017,9 +46017,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 outgoing header reads and mutations support the tested string-value shapes.", + "verification": "test-backed" }, "dynamic": { "status": "not-implemented", @@ -46045,9 +46045,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 outgoing header reads and mutations support the tested string-value shapes.", + "verification": "test-backed" }, "dynamic": { "status": "not-implemented", @@ -46073,9 +46073,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 outgoing header reads and mutations support the tested string-value shapes.", + "verification": "test-backed" }, "dynamic": { "status": "partial", @@ -46101,9 +46101,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 outgoing header reads and mutations support the tested string-value shapes.", + "verification": "test-backed" }, "dynamic": { "status": "partial", @@ -46381,9 +46381,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 HTTP request metadata and message state support the tested lifecycle.", + "verification": "test-backed" }, "dynamic": { "status": "not-implemented", @@ -46409,9 +46409,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 HTTP request metadata and message state support the tested lifecycle.", + "verification": "test-backed" }, "dynamic": { "status": "not-implemented", @@ -46437,9 +46437,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 HTTP request metadata and message state support the tested lifecycle.", + "verification": "test-backed" }, "dynamic": { "status": "not-implemented", @@ -46465,9 +46465,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 HTTP request metadata and message state support the tested lifecycle.", + "verification": "test-backed" }, "dynamic": { "status": "not-implemented", @@ -46549,9 +46549,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 HTTP request metadata and message state support the tested lifecycle.", + "verification": "test-backed" }, "dynamic": { "status": "not-implemented", @@ -47109,9 +47109,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 HTTP request metadata and message state support the tested lifecycle.", + "verification": "test-backed" }, "dynamic": { "status": "not-implemented", @@ -47725,9 +47725,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 outgoing header reads and mutations support the tested string-value shapes.", + "verification": "test-backed" }, "dynamic": { "status": "not-implemented", @@ -47753,9 +47753,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 outgoing header reads and mutations support the tested string-value shapes.", + "verification": "test-backed" }, "dynamic": { "status": "not-implemented", @@ -47949,9 +47949,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 HTTP/1.1 informational responses support the tested no-callback forms; Early Hints accepts string-valued fields.", + "verification": "test-backed" }, "dynamic": { "status": "not-implemented", @@ -47977,9 +47977,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 HTTP/1.1 informational responses support the tested no-callback forms; Early Hints accepts string-valued fields.", + "verification": "test-backed" }, "dynamic": { "status": "not-implemented", @@ -48033,9 +48033,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 HTTP/1.1 informational responses support the tested no-callback forms; Early Hints accepts string-valued fields.", + "verification": "test-backed" }, "dynamic": { "status": "not-implemented", @@ -48117,9 +48117,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 HTTP request metadata and message state support the tested lifecycle.", + "verification": "test-backed" }, "dynamic": { "status": "not-implemented", @@ -48229,9 +48229,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 HTTP request metadata and message state support the tested lifecycle.", + "verification": "test-backed" }, "dynamic": { "status": "not-implemented", @@ -48313,9 +48313,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 HTTP request metadata and message state support the tested lifecycle.", + "verification": "test-backed" }, "dynamic": { "status": "not-implemented", @@ -48537,9 +48537,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 HTTP request metadata and message state support the tested lifecycle.", + "verification": "test-backed" }, "dynamic": { "status": "not-implemented", @@ -48649,9 +48649,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 HTTP request metadata and message state support the tested lifecycle.", + "verification": "test-backed" }, "dynamic": { "status": "not-implemented", @@ -48677,9 +48677,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 HTTP request metadata and message state support the tested lifecycle.", + "verification": "test-backed" }, "dynamic": { "status": "not-implemented", @@ -48789,9 +48789,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 HTTP request metadata and message state support the tested lifecycle.", + "verification": "test-backed" }, "dynamic": { "status": "not-implemented", @@ -48901,9 +48901,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 HTTP request metadata and message state support the tested lifecycle.", + "verification": "test-backed" }, "dynamic": { "status": "not-implemented", @@ -49517,9 +49517,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 HTTP request metadata and message state support the tested lifecycle.", + "verification": "test-backed" }, "dynamic": { "status": "not-implemented", @@ -49601,9 +49601,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 HTTP request metadata and message state support the tested lifecycle.", + "verification": "test-backed" }, "dynamic": { "status": "not-implemented", @@ -49993,9 +49993,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 header validation is implemented for the tested call shapes.", + "verification": "test-backed" }, "dynamic": { "status": "not-implemented", @@ -50021,9 +50021,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 header validation is implemented for the tested call shapes.", + "verification": "test-backed" }, "dynamic": { "status": "not-implemented", diff --git a/internal/compatibility/generated/node-v24-backlog.json b/internal/compatibility/generated/node-v24-backlog.json index 468224b7..327c5c5e 100644 --- a/internal/compatibility/generated/node-v24-backlog.json +++ b/internal/compatibility/generated/node-v24-backlog.json @@ -8,8 +8,8 @@ "tiers": { "static": { "replace-refusal": 46, - "verify-gap": 2672, - "audit-partial": 494, + "verify-gap": 2642, + "audit-partial": 524, "classify": 2, "implement": 81 }, @@ -47149,12 +47149,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.request.getHeader", + "tests": [ + "tests/corpus/2954-http-outgoing-headers.ts" + ] }, "dynamic": { "status": "partial", @@ -47184,12 +47186,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.request.getHeaderNames", + "tests": [ + "tests/corpus/2954-http-outgoing-headers.ts" + ] }, "dynamic": { "status": "not-implemented", @@ -47217,12 +47221,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.request.getHeaders", + "tests": [ + "tests/corpus/2954-http-outgoing-headers.ts" + ] }, "dynamic": { "status": "not-implemented", @@ -47250,12 +47256,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.request.getRawHeaderNames", + "tests": [ + "tests/corpus/2954-http-outgoing-headers.ts" + ] }, "dynamic": { "status": "not-implemented", @@ -47283,12 +47291,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.request.hasHeader", + "tests": [ + "tests/corpus/2954-http-outgoing-headers.ts" + ] }, "dynamic": { "status": "not-implemented", @@ -47316,12 +47326,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.request.removeHeader", + "tests": [ + "tests/corpus/2954-http-outgoing-headers.ts" + ] }, "dynamic": { "status": "partial", @@ -47351,12 +47363,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.request.setHeader", + "tests": [ + "tests/corpus/2954-http-outgoing-headers.ts" + ] }, "dynamic": { "status": "partial", @@ -47700,12 +47714,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.path", + "tests": [ + "tests/corpus/2955-http-static-state.ts" + ] }, "dynamic": { "status": "not-implemented", @@ -47733,12 +47749,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.method", + "tests": [ + "tests/corpus/2955-http-static-state.ts" + ] }, "dynamic": { "status": "not-implemented", @@ -47766,12 +47784,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.host", + "tests": [ + "tests/corpus/2955-http-static-state.ts" + ] }, "dynamic": { "status": "not-implemented", @@ -47799,12 +47819,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.protocol", + "tests": [ + "tests/corpus/2955-http-static-state.ts" + ] }, "dynamic": { "status": "not-implemented", @@ -47898,12 +47920,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.writableEnded", + "tests": [ + "tests/corpus/2955-http-static-state.ts" + ] }, "dynamic": { "status": "not-implemented", @@ -48568,12 +48592,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.listening", + "tests": [ + "tests/corpus/2955-http-static-state.ts" + ] }, "dynamic": { "status": "not-implemented", @@ -49333,12 +49359,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.getHeaderNames", + "tests": [ + "tests/corpus/2954-http-outgoing-headers.ts" + ] }, "dynamic": { "status": "not-implemented", @@ -49366,12 +49394,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.getHeaders", + "tests": [ + "tests/corpus/2954-http-outgoing-headers.ts" + ] }, "dynamic": { "status": "not-implemented", @@ -49618,12 +49648,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.writeContinue", + "tests": [ + "tests/corpus/2956-http-static-informational.ts" + ] }, "dynamic": { "status": "not-implemented", @@ -49651,12 +49683,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.writeEarlyHints", + "tests": [ + "tests/corpus/2956-http-static-informational.ts" + ] }, "dynamic": { "status": "not-implemented", @@ -49721,12 +49755,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.writeProcessing", + "tests": [ + "tests/corpus/2956-http-static-informational.ts" + ] }, "dynamic": { "status": "not-implemented", @@ -49820,12 +49856,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.headersSent", + "tests": [ + "tests/corpus/2955-http-static-state.ts" + ] }, "dynamic": { "status": "not-implemented", @@ -49952,12 +49990,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.statusCode", + "tests": [ + "tests/corpus/2955-http-static-state.ts" + ] }, "dynamic": { "status": "not-implemented", @@ -50051,12 +50091,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.writableEnded", + "tests": [ + "tests/corpus/2955-http-static-state.ts" + ] }, "dynamic": { "status": "not-implemented", @@ -50319,12 +50361,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.complete", + "tests": [ + "tests/corpus/2955-http-static-state.ts" + ] }, "dynamic": { "status": "not-implemented", @@ -50456,12 +50500,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.httpVersion", + "tests": [ + "tests/corpus/2955-http-static-state.ts" + ] }, "dynamic": { "status": "not-implemented", @@ -50489,12 +50535,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.method", + "tests": [ + "tests/corpus/2955-http-static-state.ts" + ] }, "dynamic": { "status": "not-implemented", @@ -50626,12 +50674,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.statusCode", + "tests": [ + "tests/corpus/2955-http-static-state.ts" + ] }, "dynamic": { "status": "not-implemented", @@ -50768,12 +50818,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.url", + "tests": [ + "tests/corpus/2955-http-static-state.ts" + ] }, "dynamic": { "status": "not-implemented", @@ -51514,12 +51566,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.headersSent", + "tests": [ + "tests/corpus/2955-http-static-state.ts" + ] }, "dynamic": { "status": "not-implemented", @@ -51618,12 +51672,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.writableEnded", + "tests": [ + "tests/corpus/2955-http-static-state.ts" + ] }, "dynamic": { "status": "not-implemented", @@ -52108,12 +52164,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.http.validateHeaderName", + "tests": [ + "tests/corpus/2953-http-header-validation.ts" + ] }, "dynamic": { "status": "not-implemented", @@ -52141,12 +52199,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.http.validateHeaderValue", + "tests": [ + "tests/corpus/2953-http-header-validation.ts" + ] }, "dynamic": { "status": "not-implemented", diff --git a/internal/compatibility/generated/node-v24-internal.json b/internal/compatibility/generated/node-v24-internal.json index cf973d15..5a671193 100644 --- a/internal/compatibility/generated/node-v24-internal.json +++ b/internal/compatibility/generated/node-v24-internal.json @@ -555,9 +555,9 @@ "apiEntries": 174, "static": { "supported": 0, - "partial": 40, + "partial": 70, "refused": 0, - "not-implemented": 134, + "not-implemented": 104, "by-design": 0, "unreviewed": 0, "not-applicable": 0 @@ -62161,6 +62161,10 @@ "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/2953-http-header-validation.ts", + "tests/corpus/2954-http-outgoing-headers.ts", + "tests/corpus/2955-http-static-state.ts", + "tests/corpus/2956-http-static-informational.ts", "tests/fixtures/server/cases/http-trailers/main.ts" ] }, @@ -62828,8 +62832,11 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:http" + "status": "partial", + "evidence": "compiler-feature:http.request.getHeader", + "tests": [ + "tests/corpus/2954-http-outgoing-headers.ts" + ] }, "dynamic": { "status": "partial", @@ -62858,8 +62865,11 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:http" + "status": "partial", + "evidence": "compiler-feature:http.request.getHeaderNames", + "tests": [ + "tests/corpus/2954-http-outgoing-headers.ts" + ] }, "dynamic": { "status": "not-implemented", @@ -62885,8 +62895,11 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:http" + "status": "partial", + "evidence": "compiler-feature:http.request.getHeaders", + "tests": [ + "tests/corpus/2954-http-outgoing-headers.ts" + ] }, "dynamic": { "status": "not-implemented", @@ -62912,8 +62925,11 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:http" + "status": "partial", + "evidence": "compiler-feature:http.request.getRawHeaderNames", + "tests": [ + "tests/corpus/2954-http-outgoing-headers.ts" + ] }, "dynamic": { "status": "not-implemented", @@ -62939,8 +62955,11 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:http" + "status": "partial", + "evidence": "compiler-feature:http.request.hasHeader", + "tests": [ + "tests/corpus/2954-http-outgoing-headers.ts" + ] }, "dynamic": { "status": "not-implemented", @@ -62966,8 +62985,11 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:http" + "status": "partial", + "evidence": "compiler-feature:http.request.removeHeader", + "tests": [ + "tests/corpus/2954-http-outgoing-headers.ts" + ] }, "dynamic": { "status": "partial", @@ -62996,8 +63018,11 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:http" + "status": "partial", + "evidence": "compiler-feature:http.request.setHeader", + "tests": [ + "tests/corpus/2954-http-outgoing-headers.ts" + ] }, "dynamic": { "status": "partial", @@ -63291,8 +63316,11 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:http" + "status": "partial", + "evidence": "compiler-feature:http.path", + "tests": [ + "tests/corpus/2955-http-static-state.ts" + ] }, "dynamic": { "status": "not-implemented", @@ -63318,8 +63346,11 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:http" + "status": "partial", + "evidence": "compiler-feature:http.method", + "tests": [ + "tests/corpus/2955-http-static-state.ts" + ] }, "dynamic": { "status": "not-implemented", @@ -63345,8 +63376,11 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:http" + "status": "partial", + "evidence": "compiler-feature:http.host", + "tests": [ + "tests/corpus/2955-http-static-state.ts" + ] }, "dynamic": { "status": "not-implemented", @@ -63372,8 +63406,11 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:http" + "status": "partial", + "evidence": "compiler-feature:http.protocol", + "tests": [ + "tests/corpus/2955-http-static-state.ts" + ] }, "dynamic": { "status": "not-implemented", @@ -63453,8 +63490,11 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:http" + "status": "partial", + "evidence": "compiler-feature:http.writableEnded", + "tests": [ + "tests/corpus/2955-http-static-state.ts" + ] }, "dynamic": { "status": "not-implemented", @@ -64006,8 +64046,11 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:http" + "status": "partial", + "evidence": "compiler-feature:http.listening", + "tests": [ + "tests/corpus/2955-http-static-state.ts" + ] }, "dynamic": { "status": "not-implemented", @@ -64648,8 +64691,11 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:http" + "status": "partial", + "evidence": "compiler-feature:http.response.getHeaderNames", + "tests": [ + "tests/corpus/2954-http-outgoing-headers.ts" + ] }, "dynamic": { "status": "not-implemented", @@ -64675,8 +64721,11 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:http" + "status": "partial", + "evidence": "compiler-feature:http.response.getHeaders", + "tests": [ + "tests/corpus/2954-http-outgoing-headers.ts" + ] }, "dynamic": { "status": "not-implemented", @@ -64890,8 +64939,11 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:http" + "status": "partial", + "evidence": "compiler-feature:http.response.writeContinue", + "tests": [ + "tests/corpus/2956-http-static-informational.ts" + ] }, "dynamic": { "status": "not-implemented", @@ -64917,8 +64969,11 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:http" + "status": "partial", + "evidence": "compiler-feature:http.response.writeEarlyHints", + "tests": [ + "tests/corpus/2956-http-static-informational.ts" + ] }, "dynamic": { "status": "not-implemented", @@ -64976,8 +65031,11 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:http" + "status": "partial", + "evidence": "compiler-feature:http.response.writeProcessing", + "tests": [ + "tests/corpus/2956-http-static-informational.ts" + ] }, "dynamic": { "status": "not-implemented", @@ -65057,8 +65115,11 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:http" + "status": "partial", + "evidence": "compiler-feature:http.headersSent", + "tests": [ + "tests/corpus/2955-http-static-state.ts" + ] }, "dynamic": { "status": "not-implemented", @@ -65165,8 +65226,11 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:http" + "status": "partial", + "evidence": "compiler-feature:http.statusCode", + "tests": [ + "tests/corpus/2955-http-static-state.ts" + ] }, "dynamic": { "status": "not-implemented", @@ -65246,8 +65310,11 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:http" + "status": "partial", + "evidence": "compiler-feature:http.writableEnded", + "tests": [ + "tests/corpus/2955-http-static-state.ts" + ] }, "dynamic": { "status": "not-implemented", @@ -65468,8 +65535,11 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:http" + "status": "partial", + "evidence": "compiler-feature:http.complete", + "tests": [ + "tests/corpus/2955-http-static-state.ts" + ] }, "dynamic": { "status": "not-implemented", @@ -65582,8 +65652,11 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:http" + "status": "partial", + "evidence": "compiler-feature:http.httpVersion", + "tests": [ + "tests/corpus/2955-http-static-state.ts" + ] }, "dynamic": { "status": "not-implemented", @@ -65609,8 +65682,11 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:http" + "status": "partial", + "evidence": "compiler-feature:http.method", + "tests": [ + "tests/corpus/2955-http-static-state.ts" + ] }, "dynamic": { "status": "not-implemented", @@ -65723,8 +65799,11 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:http" + "status": "partial", + "evidence": "compiler-feature:http.statusCode", + "tests": [ + "tests/corpus/2955-http-static-state.ts" + ] }, "dynamic": { "status": "not-implemented", @@ -65843,8 +65922,11 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:http" + "status": "partial", + "evidence": "compiler-feature:http.url", + "tests": [ + "tests/corpus/2955-http-static-state.ts" + ] }, "dynamic": { "status": "not-implemented", @@ -66461,8 +66543,11 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:http" + "status": "partial", + "evidence": "compiler-feature:http.headersSent", + "tests": [ + "tests/corpus/2955-http-static-state.ts" + ] }, "dynamic": { "status": "not-implemented", @@ -66548,8 +66633,11 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:http" + "status": "partial", + "evidence": "compiler-feature:http.writableEnded", + "tests": [ + "tests/corpus/2955-http-static-state.ts" + ] }, "dynamic": { "status": "not-implemented", @@ -66963,8 +67051,11 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:http" + "status": "partial", + "evidence": "compiler-feature:http.http.validateHeaderName", + "tests": [ + "tests/corpus/2953-http-header-validation.ts" + ] }, "dynamic": { "status": "not-implemented", @@ -66990,8 +67081,11 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:http" + "status": "partial", + "evidence": "compiler-feature:http.http.validateHeaderValue", + "tests": [ + "tests/corpus/2953-http-header-validation.ts" + ] }, "dynamic": { "status": "not-implemented", diff --git a/internal/compatibility/src/generate.mjs b/internal/compatibility/src/generate.mjs index af55c474..170fac04 100644 --- a/internal/compatibility/src/generate.mjs +++ b/internal/compatibility/src/generate.mjs @@ -599,6 +599,23 @@ function publicDetail(tier) { if (source.startsWith("compiler-feature:stream.") || source.startsWith("compiler-feature:webstreams.")) { return "Implemented for the documented static async-iteration subset."; } + if (source.startsWith("compiler-feature:http.http.validateHeader")) { + return "Static header validation is implemented for the tested call shapes."; + } + if (source.startsWith("compiler-feature:http.request.getHeader") || + source.startsWith("compiler-feature:http.request.getRawHeaderNames") || + source.startsWith("compiler-feature:http.request.hasHeader") || + source.startsWith("compiler-feature:http.request.removeHeader") || + source.startsWith("compiler-feature:http.request.setHeader") || + source.startsWith("compiler-feature:http.response.getHeader")) { + return "Static outgoing header reads and mutations support the tested string-value shapes."; + } + if (/^compiler-feature:http\.(?:path|method|host|protocol|listening|headersSent|writableEnded|complete|httpVersion|url|statusCode)$/.test(source)) { + return "Static HTTP request metadata and message state support the tested lifecycle."; + } + 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."; + } return "Implemented for the documented scriptc module-loader subset."; } if (source.startsWith("compiler-chapter-policy:")) { diff --git a/internal/compatibility/static-support.json b/internal/compatibility/static-support.json index c163557d..c61f3241 100644 --- a/internal/compatibility/static-support.json +++ b/internal/compatibility/static-support.json @@ -79,6 +79,10 @@ "stdlib.abort": ["tests/harness/fetch-conformance.test.ts"] }, "features": [ + { "chapter": "http", "signatures": ["`http.validateHeaderName(name[, label])`", "`http.validateHeaderValue(name, value)`"], "status": "partial", "evidence": ["tests/corpus/2953-http-header-validation.ts"] }, + { "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": ["`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" }, diff --git a/packages/compiler/ambient/scriptc-node-fallback.d.ts b/packages/compiler/ambient/scriptc-node-fallback.d.ts index f34cfc52..755800a8 100644 --- a/packages/compiler/ambient/scriptc-node-fallback.d.ts +++ b/packages/compiler/ambient/scriptc-node-fallback.d.ts @@ -2228,6 +2228,7 @@ declare module "net" { once(event: "session", listener: (session: Buffer) => void): void; } export interface Server { + readonly listening: boolean; /* Node answers the server itself (`return this` chaining). */ listen(port: number, callback?: () => void): Server; /* The positional bind address (the options form's host in argument @@ -2356,6 +2357,8 @@ declare module "http" { export interface IncomingMessage { readonly url: string; readonly method: string; + readonly httpVersion: string; + readonly complete: boolean; readonly statusCode: number | undefined; readonly statusMessage: string | undefined; readonly socket: Socket; @@ -2372,7 +2375,7 @@ declare module "http" { setEncoding(encoding: string): void; /* The proxy legs: the body streams into a ServerResponse, a * ClientRequest, or a raw Socket; natural end ends the destination. */ - pipe(destination: ServerResponse | ClientRequest | Socket): void; + pipe(destination: ServerResponse | import("http2").Http2ServerResponse | ClientRequest | Socket): void; on(event: "data", listener: (chunk: any) => void): void; on(event: "end" | "close", listener: () => void): void; on(event: "error", listener: (err: Error) => void): void; @@ -2386,18 +2389,25 @@ declare module "http" { } export interface ServerResponse { readonly headersSent: boolean; + readonly writableEnded: boolean; readonly writableCorked: number; /* Node's writable head properties: the implicit head reads them. */ statusCode: number; statusMessage: string; setHeader(name: string, value: string | number): void; getHeader(name: string): string | undefined; + getHeaderNames(): string[]; + getRawHeaderNames(): string[]; + getHeaders(): OutgoingHttpHeaders; hasHeader(name: string): boolean; removeHeader(name: string): void; /* Both overloads answer the response (`return this` chaining); the * headers argument also takes Node's flat [name, value, ...] array. */ writeHead(statusCode: number, headers?: OutgoingHttpHeaders | string[]): ServerResponse; writeHead(statusCode: number, statusMessage: string, headers?: OutgoingHttpHeaders | string[]): ServerResponse; + writeContinue(callback?: () => void): void; + writeProcessing(callback?: () => void): void; + writeEarlyHints(hints: Record, callback?: () => void): void; write(data: string | Uint8Array): void; flushHeaders(): void; cork(): void; @@ -2479,6 +2489,19 @@ declare module "http" { export interface ClientRequest { readonly destroyed: boolean; readonly writableCorked: number; + readonly method: string; + readonly path: string; + readonly host: string; + readonly protocol: string; + readonly headersSent: boolean; + readonly writableEnded: boolean; + setHeader(name: string, value: string): void; + getHeader(name: string): string | undefined; + hasHeader(name: string): boolean; + removeHeader(name: string): void; + getHeaderNames(): string[]; + getRawHeaderNames(): string[]; + getHeaders(): OutgoingHttpHeaders; flushHeaders(): void; addTrailers(headers: OutgoingHttpHeaders | ReadonlyArray<[string, string]>): void; cork(): void; @@ -2528,6 +2551,8 @@ declare module "http" { target: RequestOptions | string | URL, callback?: (res: IncomingMessage) => void, ): ClientRequest; + export function validateHeaderName(name: string, label?: string): void; + export function validateHeaderValue(name: string, value: unknown): void; } declare module "node:http" { export * from "http"; @@ -2700,6 +2725,7 @@ declare module "http2" { } export interface Http2ServerResponse { readonly headersSent: boolean; + readonly writableEnded: boolean; readonly writableCorked: number; /* The same lowered surface as http.ServerResponse — the allowHTTP1 * lowering serves every connection as HTTP/1.1, where the compat @@ -2708,10 +2734,15 @@ declare module "http2" { statusMessage: string; setHeader(name: string, value: string | number): void; getHeader(name: string): string | undefined; + getHeaderNames(): string[]; + getRawHeaderNames(): string[]; + getHeaders(): import("http").OutgoingHttpHeaders; hasHeader(name: string): boolean; removeHeader(name: string): void; writeHead(statusCode: number, headers?: import("http").OutgoingHttpHeaders | string[]): Http2ServerResponse; writeHead(statusCode: number, statusMessage: string, headers?: import("http").OutgoingHttpHeaders | string[]): Http2ServerResponse; + writeContinue(): void; + writeEarlyHints(hints: Record): void; write(data: string | Uint8Array): void; flushHeaders(): void; cork(): void; diff --git a/packages/compiler/src/backend/c/exprs.ts b/packages/compiler/src/backend/c/exprs.ts index 7f4481d4..0b48f15c 100644 --- a/packages/compiler/src/backend/c/exprs.ts +++ b/packages/compiler/src/backend/c/exprs.ts @@ -5928,6 +5928,8 @@ function emitNetworkLibCall(state: LibCallState): Temp { } case "net.serverPort": return finish(`scr_net_server_port(${arg(0)})`); + case "net.serverListening": + return finish(`scr_net_server_listening(${arg(0)})`); case "net.serverAddress": { // The AddressInfo record from the three runtime reads (the // dgram.address materialization; none of these throw). @@ -6458,10 +6460,22 @@ function emitHttpLibCall(state: LibCallState): Temp { case "http.resSetHeader": emitter.line(`scr_http_res_set_header(${arg(0)}, ${arg(1)}, ${arg(2)});${emitter.srcComment(e.loc)}`); return { name: "", type: e.type }; + case "http.validateHeaderName": + emitter.line(`scr_http_validate_header_name(${arg(0)}, ${arg(1)});${emitter.srcComment(e.loc)}`); + return { name: "", type: e.type }; + case "http.validateHeaderValue": + emitter.line(`scr_http_validate_header_value(${arg(0)}, ${arg(1)});${emitter.srcComment(e.loc)}`); + return { name: "", type: e.type }; case "http.resWriteHead": return finish(`scr_http_res_write_head(${arg(0)}, ${arg(1)})`); case "http.resWriteHeadN": return finish(`scr_http_res_write_head_n(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)})`); + case "http.resWriteContinue": + return finish(`scr_http_res_write_continue(${arg(0)})`); + case "http.resWriteProcessing": + return finish(`scr_http_res_write_processing(${arg(0)})`); + case "http.resWriteEarlyHints": + return finish(`scr_http_res_write_early_hints(${arg(0)}, ${arg(1)})`); case "http.resWrite": return finish(`scr_http_res_write_str(${arg(0)}, ${arg(1)})`); case "http.resWriteBytes": @@ -6478,6 +6492,8 @@ function emitHttpLibCall(state: LibCallState): Temp { return finish(`scr_http_res_end_dynv(${arg(0)}, ${arg(1)})`); case "http.resHeadersSent": return finish(`scr_http_res_headers_sent(${arg(0)})`); + case "http.resWritableEnded": + return finish(`scr_http_res_writable_ended(${arg(0)})`); case "http.resFlushHeaders": return finish(`scr_http_res_flush_headers(${arg(0)})`); case "http.resAddTrailers": @@ -6657,21 +6673,29 @@ function emitHttpLibCall(state: LibCallState): Temp { 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.resGetHeader": { + case "http.resGetHeader": + case "http.clientGetHeader": { // string|undefined, exactly the http.reqHeader emission. - if (e.type.kind !== "union") throw new InternalCompilerError("emitter bug: http.resGetHeader result is not a union"); + if (e.type.kind !== "union") throw new InternalCompilerError(`emitter bug: ${e.fn} 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.resGetHeader union lacks its arms"); } - const s = emitter.newTemp(STRING, `scr_http_res_get_header(${arg(0)}, ${arg(1)})`); + const entry = e.fn === "http.resGetHeader" ? "scr_http_res_get_header" : "scr_http_client_get_header"; + const s = emitter.newTemp(STRING, `${entry}(${arg(0)}, ${arg(1)})`); emitter.moveTemp(s); // moves into the box when present; NULL otherwise const present = `scr_union_new_ref(${strTag}, ${s.name}, &scr_str_retain_v, &scr_str_release_v, NULL)`; const absent = emitter.unitInstanceRef(e.type.unionId, undefTag); return emitter.newTemp(e.type, `${s.name} ? ${present} : ${absent}`); } + case "http.resGetHeaderNames": + return finish(`scr_http_res_get_header_names(${arg(0)})`); + case "http.resGetRawHeaderNames": + return finish(`scr_http_res_get_raw_header_names(${arg(0)})`); + case "http.resGetHeaders": + return finish(`scr_http_res_get_headers(${arg(0)})`); case "http.resHasHeader": return finish(`scr_http_res_has_header_named(${arg(0)}, ${arg(1)})`); case "http.resRemoveHeader": @@ -6869,6 +6893,18 @@ function emitHttpLibCall(state: LibCallState): Temp { } case "http.clientWrite": return finish(`scr_http_client_write_str(${arg(0)}, ${arg(1)})`); + case "http.clientSetHeader": + return finish(`scr_http_client_set_header(${arg(0)}, ${arg(1)}, ${arg(2)})`); + case "http.clientHasHeader": + return finish(`scr_http_client_has_header_named(${arg(0)}, ${arg(1)})`); + case "http.clientRemoveHeader": + return finish(`scr_http_client_remove_header(${arg(0)}, ${arg(1)})`); + case "http.clientGetHeaderNames": + return finish(`scr_http_client_get_header_names(${arg(0)})`); + case "http.clientGetRawHeaderNames": + return finish(`scr_http_client_get_raw_header_names(${arg(0)})`); + case "http.clientGetHeaders": + return finish(`scr_http_client_get_headers(${arg(0)})`); case "http.clientWriteBytes": return finish(`scr_http_client_write_bytes(${arg(0)}, ${arg(1)})`); case "http.clientEnd": @@ -6892,6 +6928,18 @@ function emitHttpLibCall(state: LibCallState): Temp { return finish(`scr_http_client_uncork(${arg(0)})`); case "http.clientWritableCorked": return finish(`scr_http_client_writable_corked(${arg(0)})`); + case "http.clientMethod": + return finish(`scr_http_client_method(${arg(0)})`); + case "http.clientPath": + return finish(`scr_http_client_path(${arg(0)})`); + case "http.clientHost": + return finish(`scr_http_client_host(${arg(0)})`); + case "http.clientProtocol": + return finish(`scr_http_client_protocol(${arg(0)})`); + case "http.clientHeadersSent": + return finish(`scr_http_client_headers_sent(${arg(0)})`); + case "http.clientWritableEnded": + return finish(`scr_http_client_writable_ended(${arg(0)})`); case "http.clientDestroy": emitter.line(`scr_http_client_destroy(${arg(0)});${emitter.srcComment(e.loc)}`); return { name: "", type: e.type }; diff --git a/packages/compiler/src/backend/llvm/lib-network.ts b/packages/compiler/src/backend/llvm/lib-network.ts index 4e6c1e47..d2c9a542 100644 --- a/packages/compiler/src/backend/llvm/lib-network.ts +++ b/packages/compiler/src/backend/llvm/lib-network.ts @@ -242,7 +242,7 @@ 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.reqStatusMessage") { + 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") { // 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); @@ -254,6 +254,7 @@ export function emitNetworkHttpLibCall(host: LlvmEmitterContext, e: LibCallExpr) "http.reqHeader": "scr_http_req_header", "http.reqTrailer": "scr_http_req_trailer", "http.resGetHeader": "scr_http_res_get_header", + "http.clientGetHeader": "scr_http_client_get_header", "http.reqStatusMessage": "scr_http_req_status_message", }[e.fn]!; const args = e.args.map((a) => host.emitExpr(a)); diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index 6d2dca0e..335d3651 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -579,6 +579,7 @@ export const LIB_FN_SYMS: Record = { // are may-throw (generic pending check). Listener registrations and // the client request forms have non-generic shapes — special-cased. "net.serverPort": "scr_net_server_port", + "net.serverListening": "scr_net_server_listening", "net.sockWrite": "scr_net_sock_write_str", "net.sockWriteBytes": "scr_net_sock_write_bytes", "net.sockEnd": "scr_net_sock_end", @@ -604,7 +605,14 @@ export const LIB_FN_SYMS: Record = { "net.getAutoSelTimeout": "scr_net_get_autosel_timeout", "net.setAutoSelTimeout": "scr_net_set_autosel_timeout", "http.reqUrl": "scr_http_req_url", + "http.validateHeaderName": "scr_http_validate_header_name", + "http.validateHeaderValue": "scr_http_validate_header_value", "http.reqMethod": "scr_http_req_method", + "http.reqHttpVersion": "scr_http_req_http_version", + "http.reqHttpVersionMajor": "scr_http_req_http_version_major", + "http.reqHttpVersionMinor": "scr_http_req_http_version_minor", + "http.reqAborted": "scr_http_req_aborted_flag", + "http.reqComplete": "scr_http_req_complete", "http.reqSocket": "scr_http_req_socket", "http.reqRawHeaders": "scr_http_req_raw_headers", "http.reqRawTrailers": "scr_http_req_raw_trailers", @@ -617,8 +625,14 @@ export const LIB_FN_SYMS: Record = { "http.reqDestroy": "scr_http_req_destroy", "http.reqSetEncoding": "scr_http_req_set_encoding", "http.resSetHeader": "scr_http_res_set_header", + "http.resGetHeaderNames": "scr_http_res_get_header_names", + "http.resGetRawHeaderNames": "scr_http_res_get_raw_header_names", + "http.resGetHeaders": "scr_http_res_get_headers", "http.resWriteHead": "scr_http_res_write_head", "http.resWriteHeadN": "scr_http_res_write_head_n", + "http.resWriteContinue": "scr_http_res_write_continue", + "http.resWriteProcessing": "scr_http_res_write_processing", + "http.resWriteEarlyHints": "scr_http_res_write_early_hints", "http.resWrite": "scr_http_res_write_str", "http.resWriteBytes": "scr_http_res_write_bytes", "http.resEnd": "scr_http_res_end", @@ -627,6 +641,7 @@ export const LIB_FN_SYMS: Record = { "http.resWriteDyn": "scr_http_res_write_dynv", "http.resEndDyn": "scr_http_res_end_dynv", "http.resHeadersSent": "scr_http_res_headers_sent", + "http.resWritableEnded": "scr_http_res_writable_ended", "http.resFlushHeaders": "scr_http_res_flush_headers", "http.resAddTrailers": "scr_http_res_add_trailers", "http.resCork": "scr_http_res_cork", @@ -647,6 +662,12 @@ export const LIB_FN_SYMS: Record = { "http.serverTimeoutSet": "scr_net_server_timeout_set", "http.serverTimeoutOptionSet": "scr_net_server_timeout_option_set", "http.clientWrite": "scr_http_client_write_str", + "http.clientSetHeader": "scr_http_client_set_header", + "http.clientHasHeader": "scr_http_client_has_header_named", + "http.clientRemoveHeader": "scr_http_client_remove_header", + "http.clientGetHeaderNames": "scr_http_client_get_header_names", + "http.clientGetRawHeaderNames": "scr_http_client_get_raw_header_names", + "http.clientGetHeaders": "scr_http_client_get_headers", "http.clientWriteBytes": "scr_http_client_write_bytes", "http.clientEnd": "scr_http_client_end", "http.clientEndStr": "scr_http_client_end_str", @@ -658,6 +679,12 @@ export const LIB_FN_SYMS: Record = { "http.clientCork": "scr_http_client_cork", "http.clientUncork": "scr_http_client_uncork", "http.clientWritableCorked": "scr_http_client_writable_corked", + "http.clientMethod": "scr_http_client_method", + "http.clientPath": "scr_http_client_path", + "http.clientHost": "scr_http_client_host", + "http.clientProtocol": "scr_http_client_protocol", + "http.clientHeadersSent": "scr_http_client_headers_sent", + "http.clientWritableEnded": "scr_http_client_writable_ended", "http.clientDestroy": "scr_http_client_destroy", "http.clientDestroyed": "scr_http_client_destroyed", "http2.streamUndefCall": "scr_http2_stream_undef_call", diff --git a/packages/compiler/src/frontend/lowering/lower-server.ts b/packages/compiler/src/frontend/lowering/lower-server.ts index 39c91c70..d54d825a 100644 --- a/packages/compiler/src/frontend/lowering/lower-server.ts +++ b/packages/compiler/src/frontend/lowering/lower-server.ts @@ -2057,6 +2057,10 @@ export function lowerServerProperty(lowerer: Lowerer, expr: ts.PropertyAccessExp // req.headers.NAME — the `string | undefined` union (envGet's type). const recvKind = lowerer.mapTypeOf(lowerer.typeOf(expr.expression))?.kind; if (recvKind === "netServer" && lowerer.isStdlibMember(expr)) { + if (expr.name.text === "listening") { + const receiver = coerceToHandle(lowerer, expr.expression, NETSERVER_T); + return { kind: "libCall", fn: "net.serverListening", args: [receiver], type: BOOL, loc }; + } const field = httpServerTimeoutField(expr.name.text); if (field !== null) { const receiver = coerceToHandle(lowerer, expr.expression, NETSERVER_T); @@ -2245,6 +2249,23 @@ export function lowerServerProperty(lowerer: Lowerer, expr: ts.PropertyAccessExp const receiver = coerceToHandle(lowerer, expr.expression, HTTPCLIENTREQ_T); return { kind: "libCall", fn: "http.clientWritableCorked", args: [receiver], type: F64, loc }; } + if (recvKind === "httpClientReq" && lowerer.isStdlibMember(expr)) { + const name = expr.name.text; + const strFn: IrLibFn | null = name === "method" ? "http.clientMethod" + : name === "path" ? "http.clientPath" + : name === "host" ? "http.clientHost" + : name === "protocol" ? "http.clientProtocol" : null; + if (strFn !== null) { + const receiver = coerceToHandle(lowerer, expr.expression, HTTPCLIENTREQ_T); + return { kind: "libCall", fn: strFn, args: [receiver], type: STRING, loc }; + } + const boolFn: IrLibFn | null = name === "headersSent" ? "http.clientHeadersSent" + : name === "writableEnded" ? "http.clientWritableEnded" : null; + if (boolFn !== null) { + const receiver = coerceToHandle(lowerer, expr.expression, HTTPCLIENTREQ_T); + return { kind: "libCall", fn: boolFn, args: [receiver], type: BOOL, loc }; + } + } if (httpReqFieldCollection(lowerer, expr.expression) !== null) { const collection = httpReqFieldCollection(lowerer, expr.expression)!; const receiver = coerceToHandle(lowerer, (expr.expression as ts.PropertyAccessExpression).expression, HTTPREQ_T); @@ -2259,6 +2280,10 @@ export function lowerServerProperty(lowerer: Lowerer, expr: ts.PropertyAccessExp const receiver = coerceToHandle(lowerer, expr.expression, HTTPRES_T); return { kind: "libCall", fn: "http.resHeadersSent", args: [receiver], type: BOOL, loc }; } + if (recvKind === "httpRes" && lowerer.isStdlibMember(expr) && expr.name.text === "writableEnded") { + const receiver = coerceToHandle(lowerer, expr.expression, HTTPRES_T); + return { kind: "libCall", fn: "http.resWritableEnded", args: [receiver], type: BOOL, loc }; + } if (recvKind === "httpRes" && lowerer.isStdlibMember(expr) && expr.name.text === "writableCorked") { const receiver = coerceToHandle(lowerer, expr.expression, HTTPRES_T); return { kind: "libCall", fn: "http.resWritableCorked", args: [receiver], type: F64, loc }; @@ -2706,6 +2731,26 @@ export function lowerHttpServerNew(lowerer: Lowerer, expr: ts.NewExpression): Ir function lowerHttpModuleCall(lowerer: Lowerer, expr: ts.CallExpression, bi: { module: string; member: string }, loc: SrcLoc,): IrExpr { + if (bi.member === "validateHeaderName") { + requireStatementPosition(lowerer, expr, "http.validateHeaderName(...)"); + if (expr.arguments.length < 1 || expr.arguments.length > 2 || expr.arguments.some(ts.isSpreadElement)) { + lowerer.noLowering("http.validateHeaderName arguments", expr, "pass a header name and an optional label"); + } + const name = lowerer.lowerExprExpecting(expr.arguments[0]!, STRING); + const label = expr.arguments.length === 2 + ? lowerer.lowerExprExpecting(expr.arguments[1]!, STRING) + : strLit("Header name", loc); + return { kind: "libCall", fn: "http.validateHeaderName", args: [name, label], type: VOID, loc }; + } + if (bi.member === "validateHeaderValue") { + requireStatementPosition(lowerer, expr, "http.validateHeaderValue(...)"); + if (expr.arguments.length !== 2 || expr.arguments.some(ts.isSpreadElement)) { + lowerer.noLowering("http.validateHeaderValue arguments", expr, "pass a header name and a value"); + } + const name = lowerer.lowerExprExpecting(expr.arguments[0]!, STRING); + const value = lowerer.lowerExprExpecting(expr.arguments[1]!, DYN); + return { kind: "libCall", fn: "http.validateHeaderValue", args: [name, value], type: VOID, loc }; + } if (bi.member === "createServer" || bi.member === "Server") { return lowerHttpCreateServerForms(lowerer, expr, `http.${bi.member}`, loc); } @@ -2715,7 +2760,7 @@ function lowerHttpModuleCall(lowerer: Lowerer, expr: ts.CallExpression, lowerer.noLowering( `http.${bi.member}`, expr, - "createServer, Server, request, and get are the lowered http module functions", + "createServer, Server, request, get, validateHeaderName, and validateHeaderValue are the lowered http module functions", lowerer.resolveValueSymbol(expr.expression as ts.Identifier), ); } @@ -4235,6 +4280,31 @@ function lowerHttpClientMethodCall(lowerer: Lowerer, call: ts.CallExpression, const name = access.name.text; const loc = locOf(call); const args = call.arguments; + if (name === "setHeader") { + requireStatementPosition(lowerer, call, "request.setHeader(...)"); + if (args.length !== 2) lowerer.noLowering("request.setHeader arguments", call, "pass a name and string value"); + const receiver = coerceToHandle(lowerer, access.expression, HTTPCLIENTREQ_T); + const header = lowerer.lowerExprExpecting(args[0]!, STRING); + const value = lowerer.lowerExprExpecting(args[1]!, STRING); + return { kind: "libCall", fn: "http.clientSetHeader", args: [receiver, header, value], type: VOID, loc }; + } + if (name === "getHeader" || name === "hasHeader" || name === "removeHeader") { + if (name === "removeHeader") requireStatementPosition(lowerer, call, "request.removeHeader(...)"); + if (args.length !== 1) lowerer.noLowering(`request.${name} arguments`, call, `pass one header name`); + const receiver = coerceToHandle(lowerer, access.expression, HTTPCLIENTREQ_T); + const header = lowerer.lowerExprExpecting(args[0]!, STRING); + const fn: IrLibFn = name === "getHeader" ? "http.clientGetHeader" + : name === "hasHeader" ? "http.clientHasHeader" : "http.clientRemoveHeader"; + const type = name === "getHeader" ? lowerer.envValueType() : name === "hasHeader" ? BOOL : VOID; + return { kind: "libCall", fn, args: [receiver, header], type, loc }; + } + if (name === "getHeaderNames" || name === "getRawHeaderNames" || name === "getHeaders") { + if (args.length !== 0) lowerer.noLowering(`request.${name} arguments`, call, `${name}() takes no arguments`); + const receiver = coerceToHandle(lowerer, access.expression, HTTPCLIENTREQ_T); + const fn: IrLibFn = name === "getHeaderNames" ? "http.clientGetHeaderNames" + : name === "getRawHeaderNames" ? "http.clientGetRawHeaderNames" : "http.clientGetHeaders"; + return { kind: "libCall", fn, args: [receiver], type: name === "getHeaders" ? DYN : arrayOf(STRING), loc }; + } if (name === "flushHeaders" || name === "cork" || name === "uncork") { requireStatementPosition(lowerer, call, `request.${name}()`); if (args.length !== 0) lowerer.noLowering(`request.${name} with arguments`, call, `${name}() takes no arguments`); @@ -4359,7 +4429,7 @@ function lowerHttpClientMethodCall(lowerer: Lowerer, call: ts.CallExpression, lowerer.noLowering( `ClientRequest.${name}`, call, - "write, end, destroy, destroyed, and on/once of response/error/timeout/close are the supported ClientRequest members", + "header reads/mutations, write, end, destroy, and on/once of response/error/timeout/close are the supported ClientRequest members", lowerer.checker.getSymbolAtLocation(access.name), ); } @@ -4564,6 +4634,53 @@ function lowerHttpResMethodCall(lowerer: Lowerer, call: ts.CallExpression, const name = access.name.text; const loc = locOf(call); const args = call.arguments; + if (name === "writeContinue" || name === "writeProcessing" || name === "writeEarlyHints") { + requireStatementPosition(lowerer, call, `res.${name}(...)`); + if (lowerer.typeOf(access.expression).getSymbol()?.name === "Http2ServerResponse") { + lowerer.noLowering(`Http2ServerResponse.${name}`, call, "HTTP/2 informational responses are not supported by the static runtime yet"); + } + const receiver = coerceToHandle(lowerer, access.expression, HTTPRES_T); + if (name !== "writeEarlyHints") { + if (args.length !== 0) lowerer.noLowering(`res.${name} callback`, call, `${name}() supports no callback here`); + return { kind: "libCall", fn: name === "writeContinue" ? "http.resWriteContinue" : "http.resWriteProcessing", args: [receiver], type: VOID, loc }; + } + if (args.length !== 1) lowerer.noLowering("res.writeEarlyHints arguments", call, "pass one header object or Record without a callback"); + const node = args[0]!; + let pairs: IrExpr; + if (ts.isObjectLiteralExpression(node)) { + const elems: IrExpr[] = []; + for (const prop of node.properties) { + const key = ts.isPropertyAssignment(prop) ? staticHeaderKeyOf(lowerer, prop) : null; + if (!ts.isPropertyAssignment(prop) || key === null) { + lowerer.noLowering("writeEarlyHints headers with dynamic keys, spreads, or shorthand entries", prop, "use literal header keys and string values"); + } + const value = lowerer.lowerExpr(prop.initializer); + if (value.type.kind !== "string") { + lowerer.noLowering(`writeEarlyHints value of type '${lowerer.fmt(value.type)}'`, prop.initializer, "header values must be strings here"); + } + elems.push({ kind: "strLit", value: key, type: STRING, loc }, value); + } + pairs = { kind: "arrayLit", elems, type: arrayOf(STRING), loc }; + } else { + const value = lowerer.lowerExpr(node); + if (value.type.kind !== "record") { + lowerer.noLowering(`writeEarlyHints headers of '${lowerer.fmt(value.type)}'`, node, "pass an object literal or a Record"); + } + const helper = lowerer.envToPairsHelper(value.type.shapeId, loc); + if (helper === null) { + lowerer.noLowering("writeEarlyHints header values", node, "use a Record with string values"); + } + pairs = { kind: "call", callee: helper, args: [value], type: arrayOf(STRING), loc }; + } + return { kind: "libCall", fn: "http.resWriteEarlyHints", args: [receiver, pairs], type: VOID, loc }; + } + if (name === "getHeaderNames" || name === "getRawHeaderNames" || name === "getHeaders") { + if (args.length !== 0) lowerer.noLowering(`res.${name} arguments`, call, `${name}() takes no arguments`); + const receiver = coerceToHandle(lowerer, access.expression, HTTPRES_T); + const fn: IrLibFn = name === "getHeaderNames" ? "http.resGetHeaderNames" + : name === "getRawHeaderNames" ? "http.resGetRawHeaderNames" : "http.resGetHeaders"; + return { kind: "libCall", fn, args: [receiver], type: name === "getHeaders" ? DYN : arrayOf(STRING), loc }; + } if (name === "flushHeaders" || name === "cork" || name === "uncork") { requireStatementPosition(lowerer, call, `res.${name}()`); if (args.length !== 0) lowerer.noLowering(`res.${name} with arguments`, call, `${name}() takes no arguments`); @@ -4857,7 +4974,7 @@ function lowerHttpResMethodCall(lowerer: Lowerer, call: ts.CallExpression, lowerer.noLowering( `ServerResponse.${name}`, call, - "setHeader, getHeader, hasHeader, removeHeader, writeHead, write, end, destroy, headersSent, statusCode, statusMessage, and on/once of close are the supported ServerResponse members", + "setHeader, getHeader, hasHeader, removeHeader, writeHead, writeContinue, writeProcessing, writeEarlyHints, write, end, destroy, headersSent, statusCode, statusMessage, and on/once of close are the supported ServerResponse members", lowerer.checker.getSymbolAtLocation(access.name), ); } diff --git a/packages/compiler/src/ir/ir.ts b/packages/compiler/src/ir/ir.ts index edaeae7f..963acebb 100644 --- a/packages/compiler/src/ir/ir.ts +++ b/packages/compiler/src/ir/ir.ts @@ -2582,6 +2582,7 @@ export type IrLibFn = | "net.listenOptsReusePort" | "net.listenOptsReusePortCb" | "net.serverPort" + | "net.serverListening" /** server.address() as the full AddressInfo record (the dgram.address * materialization pattern: the emitter builds the record from the three * runtime reads; the frontend pinned the shape). Never throws — before @@ -2740,6 +2741,8 @@ export type IrLibFn = * on("request") route; option helpers set parser behavior on a fresh * server before the constructor/factory result is returned. */ | "http.createServerEmpty" + | "http.validateHeaderName" + | "http.validateHeaderValue" | "http.serverJoinDupHeaders" | "http.serverAllowMissingHostHeader" /** The five writable numeric http.Server timeout fields use one @@ -2765,6 +2768,9 @@ export type IrLibFn = | "http.resStatusMsgGet" | "http.resStatusMsgSet" | "http.resGetHeader" + | "http.resGetHeaderNames" + | "http.resGetRawHeaderNames" + | "http.resGetHeaders" | "http.resHasHeader" | "http.resRemoveHeader" | "http.resOnFinish" @@ -2779,6 +2785,11 @@ export type IrLibFn = | "http.resSetHeader" | "http.resWriteHead" | "http.resWriteHeadN" + /** HTTP/1.1 informational heads; early hints takes flat string pairs, + * with a lowercase `link` key required to send anything. */ + | "http.resWriteContinue" + | "http.resWriteProcessing" + | "http.resWriteEarlyHints" | "http.resWrite" | "http.resWriteBytes" | "http.resEnd" @@ -2788,6 +2799,7 @@ export type IrLibFn = | "http.resWriteDyn" | "http.resEndDyn" | "http.resHeadersSent" + | "http.resWritableEnded" | "http.resFlushHeaders" | "http.resAddTrailers" | "http.resCork" @@ -3166,6 +3178,13 @@ export type IrLibFn = * idiom reads it through a cast). */ | "net.sockEncrypted" | "http.clientWrite" + | "http.clientSetHeader" + | "http.clientGetHeader" + | "http.clientHasHeader" + | "http.clientRemoveHeader" + | "http.clientGetHeaderNames" + | "http.clientGetRawHeaderNames" + | "http.clientGetHeaders" | "http.clientWriteBytes" | "http.clientEnd" | "http.clientEndStr" @@ -3178,6 +3197,12 @@ export type IrLibFn = | "http.clientCork" | "http.clientUncork" | "http.clientWritableCorked" + | "http.clientMethod" + | "http.clientPath" + | "http.clientHost" + | "http.clientProtocol" + | "http.clientHeadersSent" + | "http.clientWritableEnded" /** request/get with a URL-STRING first argument: the runtime parses it * (WHATWG) and dials — throws catchably on an unparsable input or a * non-http scheme. */ @@ -7358,6 +7383,11 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "dc.chanBindStore", "dc.chanRunStores", "http.resWriteHeadDyn", + "http.resWriteContinue", + "http.resWriteProcessing", + "http.resWriteEarlyHints", + "http.validateHeaderName", + "http.validateHeaderValue", "http.serverTimeoutGet", "http.serverTimeoutOptionSet", "net.sockSetEncoding", diff --git a/packages/compiler/src/ir/validate.ts b/packages/compiler/src/ir/validate.ts index 9296d6ff..265bea7c 100644 --- a/packages/compiler/src/ir/validate.ts +++ b/packages/compiler/src/ir/validate.ts @@ -428,6 +428,7 @@ export const LIB_FN_SIGS: Recordurl); } ScrStr *scr_http_req_method(ScrHttpReq *r) { return scr_str_retain(r->method); } -/* Case-insensitive equality of two lowercased-vs-any names. */ +/* Case-insensitive equality of HTTP field names. Incoming names happen to + * be lowercased already; outgoing names retain the spelling of the setter. */ static bool scr_http_name_eq(const ScrStr *lower, const ScrStr *name) { if (lower->len != name->len) return false; for (size_t j = 0; j < name->len; j++) { - if (lower->data[j] != (char)tolower((unsigned char)name->data[j])) return false; + if (tolower((unsigned char)lower->data[j]) != tolower((unsigned char)name->data[j])) return false; } return true; } @@ -682,6 +683,72 @@ static bool scr_http_token_char(unsigned char ch) { (ch >= 'a' && ch <= 'z') || (ch != 0 && strchr("!#$%&'*+-.^_`|~", ch) != NULL); } +/* node:http's public validators use the same token and field-vchar rules + * as OutgoingMessage. Strings are UTF-8 here, so the Latin-1 obs-text + * range accepted by Node's JS regex is C2/C3 plus one continuation byte. */ +void scr_http_validate_header_name(ScrStr *name /*borrowed*/, ScrStr *label /*borrowed*/) { + bool valid = name->len > 0; + for (size_t i = 0; i < name->len && valid; i++) + valid = scr_http_token_char((unsigned char)name->data[i]); + if (valid) return; + + ScrJsonBuf b; + scr_jb_init(&b); + ScrStr *shown = label->len ? label : NULL; + if (shown) { + for (size_t i = 0; i < shown->len; i++) scr_jb_putc(&b, shown->data[i]); + } else { + scr_jb_puts(&b, "Header name"); + } + scr_jb_puts(&b, " must be a valid HTTP token [\""); + for (size_t i = 0; i < name->len; i++) scr_jb_putc(&b, name->data[i]); + scr_jb_puts(&b, "\"]"); + ScrStr *msg = scr_jb_finish(&b); + scr_throw_error_msg_code(SCR_ERR_TYPE, msg->data, msg->len, "ERR_INVALID_HTTP_TOKEN"); + scr_str_release(msg); +} + +void scr_http_validate_header_value(ScrStr *name /*borrowed*/, const ScrDyn *value /*borrowed*/) { + if (value->kind == SCR_DYN_UNDEF) { + ScrJsonBuf b; + scr_jb_init(&b); + scr_jb_puts(&b, "Invalid value \"undefined\" for header \""); + for (size_t i = 0; i < name->len; i++) scr_jb_putc(&b, name->data[i]); + scr_jb_putc(&b, '"'); + ScrStr *msg = scr_jb_finish(&b); + scr_throw_error_msg_code(SCR_ERR_TYPE, msg->data, msg->len, "ERR_HTTP_INVALID_HEADER_VALUE"); + scr_str_release(msg); + return; + } + + ScrStr *s = scr_dyn_string_coerce_js(value); + if (!s) return; /* object coercion threw */ + bool invalid = false; + for (size_t i = 0; i < s->len; i++) { + unsigned char ch = (unsigned char)s->data[i]; + if (ch == '\t' || (ch >= 0x20 && ch <= 0x7e)) continue; + if ((ch == 0xc2 || ch == 0xc3) && i + 1 < s->len && + (unsigned char)s->data[i + 1] >= 0x80 && + (unsigned char)s->data[i + 1] <= 0xbf) { + i++; + continue; + } + invalid = true; + break; + } + scr_str_release(s); + if (!invalid) return; + + ScrJsonBuf b; + scr_jb_init(&b); + scr_jb_puts(&b, "Invalid character in header content [\""); + for (size_t i = 0; i < name->len; i++) scr_jb_putc(&b, name->data[i]); + scr_jb_puts(&b, "\"]"); + ScrStr *msg = scr_jb_finish(&b); + scr_throw_error_msg_code(SCR_ERR_TYPE, msg->data, msg->len, "ERR_INVALID_CHAR"); + scr_str_release(msg); +} + static bool scr_http_trailers_replace(ScrHttpTrailers *t, ScrArr *pairs /*borrowed*/) { scr_http_trailers_clear(t); size_t n = (size_t)scr_arr_len(pairs); @@ -803,6 +870,7 @@ void *scr_http_res_retain_v(void *p) { return scr_http_res_retain((ScrHttpRes *) 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; } /* res.statusCode: 200 until assigned (Node's fresh-response default); * assignment after the head went out is inert (Node throws on the WRITE @@ -845,6 +913,50 @@ ScrStr *scr_http_res_get_header(ScrHttpRes *r, ScrStr *name) { return NULL; } +/* Outgoing header snapshots omit the automatically serialized fields. The + * names API lowercases keys while the raw variant preserves their casing. */ +static ScrArr *scr_http_out_header_names(ScrStr **names, size_t count, bool raw) { + ScrArr *out = scr_arr_new(SCR_ELEM_STR, count); + for (size_t i = 0; i < count; i++) { + bool seen = false; + for (size_t j = 0; j < i && !seen; j++) seen = scr_http_name_eq(names[j], names[i]); + if (seen) continue; + if (raw) { + scr_arr_push_ref(out, scr_str_retain(names[i])); + } else { + ScrStr *lower = scr_str_new(names[i]->data, names[i]->len); + for (size_t j = 0; j < lower->len; j++) + lower->data[j] = (char)tolower((unsigned char)lower->data[j]); + scr_arr_push_ref(out, lower); + } + } + return out; +} + +static ScrDyn *scr_http_out_headers(ScrStr **names, ScrStr **values, size_t count) { + ScrDyn *out = scr_dyn_new_obj(); + for (size_t i = 0; i < count; i++) { + ScrStr *lower = scr_str_new(names[i]->data, names[i]->len); + for (size_t j = 0; j < lower->len; j++) + lower->data[j] = (char)tolower((unsigned char)lower->data[j]); + scr_dyn_obj_set(out, lower->data, lower->len, scr_dyn_new_str(values[i])); + scr_str_release(lower); + } + return out; +} + +ScrArr *scr_http_res_get_header_names(ScrHttpRes *r) { + return scr_http_out_header_names(r->hnames, r->nheaders, false); +} + +ScrArr *scr_http_res_get_raw_header_names(ScrHttpRes *r) { + return scr_http_out_header_names(r->hnames, r->nheaders, true); +} + +ScrDyn *scr_http_res_get_headers(ScrHttpRes *r) { + return scr_http_out_headers(r->hnames, r->hvalues, r->nheaders); +} + bool scr_http_res_has_header_named(ScrHttpRes *r, ScrStr *name) { ScrStr *v = scr_http_res_get_header(r, name); if (v) scr_str_release(v); @@ -914,6 +1026,8 @@ void scr_http_res_set_header(ScrHttpRes *r, ScrStr *name /*borrowed*/, ScrStr *v if (tolower((unsigned char)r->hnames[i]->data[j]) != tolower((unsigned char)name->data[j])) eq = false; } if (eq) { + scr_str_release(r->hnames[i]); + r->hnames[i] = scr_str_retain(name); scr_str_release(r->hvalues[i]); r->hvalues[i] = scr_str_retain(value); return; @@ -951,6 +1065,144 @@ static void scr_http_buf_append(ScrHttpBuf *b, const char *s, size_t n) { static void scr_http_buf_str(ScrHttpBuf *b, const char *s) { scr_http_buf_append(b, s, strlen(s)); } +/* Informational response heads are written with Node's `ascii` encoding. + * The header validator allows Latin-1 obs-text; collapse its UTF-8 C2/C3 + * spelling back to one wire byte. */ +static void scr_http_buf_header_value(ScrHttpBuf *b, const ScrStr *s) { + for (size_t i = 0; i < s->len; i++) { + unsigned char ch = (unsigned char)s->data[i]; + if ((ch == 0xc2 || ch == 0xc3) && i + 1 < s->len) { + unsigned char next = (unsigned char)s->data[++i]; + char byte = (char)(((ch & 1) << 6) | (next & 0x3f)); + scr_http_buf_append(b, &byte, 1); + } else { + scr_http_buf_append(b, s->data + i, 1); + } + } +} + +static bool scr_http_res_informational_h1(ScrHttpRes *r, const char *method) { + if (r->h2_stream == NULL) return true; + char msg[128]; + int n = snprintf(msg, sizeof msg, "Http2ServerResponse.%s is not supported by the static runtime yet", method); + scr_throw_error_msg(SCR_ERR_ERROR, msg, (size_t)n); + return false; +} + +void scr_http_res_write_continue(ScrHttpRes *r) { + if (!scr_http_res_informational_h1(r, "writeContinue")) return; + if (r->sock) scr_net_sock_write_native(r->sock, "HTTP/1.1 100 Continue\r\n\r\n", 25); +} + +void scr_http_res_write_processing(ScrHttpRes *r) { + if (!scr_http_res_informational_h1(r, "writeProcessing")) return; + if (r->sock) scr_net_sock_write_native(r->sock, "HTTP/1.1 102 Processing\r\n\r\n", 27); +} + +/* Node's linkValueRegExp from internal/validators: `<...>` followed by + * optional semicolon parameters. Its string arm refuses an empty or + * malformed link before the general header-content check runs. */ +static bool scr_http_early_link_format(const ScrStr *link) { + if (link->len < 2 || link->data[0] != '<') return false; + size_t i = 1; + while (i < link->len && link->data[i] != '>') { + if (link->data[i] == '\r' || link->data[i] == '\n') return false; + i++; + } + if (i == link->len) return false; + i++; + while (i < link->len) { + while (i < link->len && isspace((unsigned char)link->data[i])) i++; + if (i == link->len || link->data[i++] != ';') return false; + while (i < link->len && isspace((unsigned char)link->data[i])) i++; + size_t start = i; + while (i < link->len && link->data[i] != ';' && link->data[i] != '"' && + !isspace((unsigned char)link->data[i])) i++; + if (i == start) return false; + if (i < link->len && link->data[i] == '"') { + if (link->data[i - 1] != '=') return false; + i++; + while (i < link->len && link->data[i] != ';' && link->data[i] != '"' && + !isspace((unsigned char)link->data[i])) i++; + if (i == link->len || link->data[i++] != '"') return false; + } + } + return true; +} + +/* A lowercase `link` controls whether Node sends a 103 at all; when + * present it is always the first field, followed by the other object + * properties in insertion order. This static entry accepts string-valued + * fields. */ +void scr_http_res_write_early_hints(ScrHttpRes *r, ScrArr *pairs /*borrowed*/) { + if (!scr_http_res_informational_h1(r, "writeEarlyHints")) return; + size_t count = (size_t)scr_arr_len(pairs); + ScrStr *link = NULL; + for (size_t i = 0; i + 1 < count; i += 2) { + ScrStr *name = (ScrStr *)scr_arr_get_ref(pairs, (double)i); + if (name->len == 4 && memcmp(name->data, "link", 4) == 0) { + scr_str_release(link); + link = (ScrStr *)scr_arr_get_ref(pairs, (double)(i + 1)); + } + scr_str_release(name); + } + if (!link) return; + if (!scr_http_early_link_format(link)) { + ScrDyn *received = scr_dyn_new_str(link); + scr_dyn_arg_value_fail("hints", "must be an array or string of format \"; rel=preload; as=style\"", received); + scr_dyn_release(received); + scr_str_release(link); + return; + } + ScrStr *link_name = scr_str_new("Link", 4); + ScrDyn *link_value = scr_dyn_new_str(link); + scr_http_validate_header_value(link_name, link_value); + scr_dyn_release(link_value); + scr_str_release(link_name); + if (scr_exc_pending()) { + scr_str_release(link); + return; + } + ScrHttpBuf b = {NULL, 0, 0}; + scr_http_buf_str(&b, "HTTP/1.1 103 Early Hints\r\nLink: "); + scr_http_buf_header_value(&b, link); + scr_http_buf_str(&b, "\r\n"); + scr_str_release(link); + ScrStr *empty_label = scr_str_new("", 0); + for (size_t i = 0; i + 1 < count; i += 2) { + ScrStr *name = (ScrStr *)scr_arr_get_ref(pairs, (double)i); + ScrStr *value = (ScrStr *)scr_arr_get_ref(pairs, (double)(i + 1)); + if (name->len == 4 && memcmp(name->data, "link", 4) == 0) { + scr_str_release(name); + scr_str_release(value); + continue; + } + scr_http_validate_header_name(name, empty_label); + if (!scr_exc_pending()) { + ScrDyn *wrapped = scr_dyn_new_str(value); + scr_http_validate_header_value(name, wrapped); + scr_dyn_release(wrapped); + } + if (scr_exc_pending()) { + scr_str_release(name); + scr_str_release(value); + scr_str_release(empty_label); + free(b.data); + return; + } + scr_http_buf_append(&b, name->data, name->len); + scr_http_buf_str(&b, ": "); + scr_http_buf_header_value(&b, value); + scr_http_buf_str(&b, "\r\n"); + scr_str_release(name); + scr_str_release(value); + } + scr_str_release(empty_label); + scr_http_buf_str(&b, "\r\n"); + if (r->sock) scr_net_sock_write_native(r->sock, b.data, b.len); + free(b.data); +} + static void scr_http_send_chunk_end(ScrNetSocket *sock, const ScrHttpTrailers *t) { ScrHttpBuf b = {NULL, 0, 0}; scr_http_buf_str(&b, "0\r\n"); @@ -2684,6 +2936,8 @@ struct ScrHttpClientReq { ScrStr **hnames; /* user headers, verbatim case */ ScrStr **hvalues; size_t nheaders; + bool removed_host; + bool removed_connection; ScrHttpTrailers trailers; bool user_cl; /* caller set content-length/transfer-encoding */ bool head_sent; @@ -2822,6 +3076,106 @@ static bool scr_http_client_has_header(ScrHttpClientReq *c, const char *name) { return false; } +static bool scr_http_header_name_is(ScrStr *name, const char *text) { + size_t len = strlen(text); + if (name->len != len) return false; + for (size_t i = 0; i < len; i++) + if (tolower((unsigned char)name->data[i]) != text[i]) return false; + return true; +} + +ScrStr *scr_http_client_get_header(ScrHttpClientReq *c, ScrStr *name) { + for (size_t i = 0; i < c->nheaders; i++) + if (scr_http_name_eq(c->hnames[i], name)) return scr_str_retain(c->hvalues[i]); + return NULL; +} + +bool scr_http_client_has_header_named(ScrHttpClientReq *c, ScrStr *name) { + for (size_t i = 0; i < c->nheaders; i++) + if (scr_http_name_eq(c->hnames[i], name)) return true; + return false; +} + +ScrArr *scr_http_client_get_header_names(ScrHttpClientReq *c) { + return scr_http_out_header_names(c->hnames, c->nheaders, false); +} + +ScrArr *scr_http_client_get_raw_header_names(ScrHttpClientReq *c) { + return scr_http_out_header_names(c->hnames, c->nheaders, true); +} + +ScrDyn *scr_http_client_get_headers(ScrHttpClientReq *c) { + return scr_http_out_headers(c->hnames, c->hvalues, c->nheaders); +} + +ScrStr *scr_http_client_method(ScrHttpClientReq *c) { return scr_str_retain(c->method); } +ScrStr *scr_http_client_path(ScrHttpClientReq *c) { return scr_str_retain(c->path); } +ScrStr *scr_http_client_host(ScrHttpClientReq *c) { return scr_str_retain(c->host); } +ScrStr *scr_http_client_protocol(ScrHttpClientReq *c) { + return c->default_port == 443 ? scr_str_new("https:", 6) : scr_str_new("http:", 5); +} +bool scr_http_client_headers_sent(ScrHttpClientReq *c) { return c->head_sent; } +bool scr_http_client_writable_ended(ScrHttpClientReq *c) { return c->ended; } + +static bool scr_http_client_header_mutable(ScrHttpClientReq *c, const char *op) { + if (!c->head_sent) return true; + char msg[80]; + int len = snprintf(msg, sizeof msg, "Cannot %s headers after they are sent to the client", op); + scr_throw_error_msg_code(SCR_ERR_ERROR, msg, (size_t)len, "ERR_HTTP_HEADERS_SENT"); + return false; +} + +void scr_http_client_set_header(ScrHttpClientReq *c, ScrStr *name, ScrStr *value) { + if (!scr_http_client_header_mutable(c, "set")) return; + ScrStr *label = scr_str_new("Header name", 11); + scr_http_validate_header_name(name, label); + scr_str_release(label); + if (scr_exc_pending()) return; + ScrDyn *wrapped = scr_dyn_new_str(value); + scr_http_validate_header_value(name, wrapped); + scr_dyn_release(wrapped); + if (scr_exc_pending()) return; + for (size_t i = 0; i < c->nheaders; i++) { + if (!scr_http_name_eq(c->hnames[i], name)) continue; + scr_str_release(c->hnames[i]); + scr_str_release(c->hvalues[i]); + c->hnames[i] = scr_str_retain(name); + c->hvalues[i] = scr_str_retain(value); + goto updated; + } + c->hnames = realloc(c->hnames, (c->nheaders + 1) * sizeof *c->hnames); + c->hvalues = realloc(c->hvalues, (c->nheaders + 1) * sizeof *c->hvalues); + if (!c->hnames || !c->hvalues) scr_http_oom(); + c->hnames[c->nheaders] = scr_str_retain(name); + c->hvalues[c->nheaders] = scr_str_retain(value); + c->nheaders++; +updated: + if (scr_http_header_name_is(name, "host")) c->removed_host = false; + if (scr_http_header_name_is(name, "connection")) c->removed_connection = false; + c->user_cl = scr_http_client_has_header(c, "content-length") || + scr_http_client_has_header(c, "transfer-encoding"); +} + +void scr_http_client_remove_header(ScrHttpClientReq *c, ScrStr *name) { + if (!scr_http_client_header_mutable(c, "remove")) return; + size_t w = 0; + for (size_t i = 0; i < c->nheaders; i++) { + if (scr_http_name_eq(c->hnames[i], name)) { + scr_str_release(c->hnames[i]); + scr_str_release(c->hvalues[i]); + } else { + c->hnames[w] = c->hnames[i]; + c->hvalues[w] = c->hvalues[i]; + w++; + } + } + c->nheaders = w; + if (scr_http_header_name_is(name, "host")) c->removed_host = true; + if (scr_http_header_name_is(name, "connection")) c->removed_connection = true; + c->user_cl = scr_http_client_has_header(c, "content-length") || + scr_http_client_has_header(c, "transfer-encoding"); +} + /* Serializes and sends the head. `body_len` >= 0 fixes Content-Length * (the end(data) path — and Node's Content-Length: 0 for empty POST/PUT/ * PATCH bodies); -1 means STREAMING: chunked unless the caller set the @@ -2845,7 +3199,7 @@ static void scr_http_client_send_head(ScrHttpClientReq *c, long long body_len) { } if (transfer && scr_http_header_has_token(c->hvalues[i], "chunked")) c->chunked = true; } - if (!scr_http_client_has_header(c, "host")) { + if (!c->removed_host && !scr_http_client_has_header(c, "host")) { /* Host: name[:port] — the scheme's default port omitted (80 http, * 443 https), IPv6 literals bracketed (Node) */ bool v6 = memchr(c->host->data, ':', c->host->len) != NULL; @@ -2860,7 +3214,7 @@ static void scr_http_client_send_head(ScrHttpClientReq *c, long long body_len) { } scr_http_buf_str(&b, "\r\n"); } - if (!scr_http_client_has_header(c, "connection")) { + if (!c->removed_connection && !scr_http_client_has_header(c, "connection")) { scr_http_buf_str(&b, "Connection: keep-alive\r\n"); } if (!c->user_cl) { @@ -3437,6 +3791,26 @@ static ScrHttpClientReq *scr_http_request_impl(ScrStr *host /*borrowed*/, double } c->user_cl = scr_http_client_has_header(c, "content-length") || scr_http_client_has_header(c, "transfer-encoding"); + if (!scr_http_client_has_header(c, "host")) { + /* Node queues the implicit Host header at construction, so all three + * outgoing header readers see it before end()/flushHeaders(). */ + ScrHttpBuf hostbuf = {NULL, 0, 0}; + bool v6 = memchr(c->host->data, ':', c->host->len) != NULL; + if (v6) scr_http_buf_str(&hostbuf, "["); + scr_http_buf_append(&hostbuf, c->host->data, c->host->len); + if (v6) scr_http_buf_str(&hostbuf, "]"); + if (c->port != c->default_port) { + char portbuf[16]; + snprintf(portbuf, sizeof portbuf, ":%d", c->port); + scr_http_buf_str(&hostbuf, portbuf); + } + ScrStr *host_name = scr_str_new("Host", 4); + ScrStr *host_value = scr_str_new(hostbuf.data, hostbuf.len); + free(hostbuf.data); + scr_http_client_set_header(c, host_name, host_value); + scr_str_release(host_name); + scr_str_release(host_value); + } if (cb) scr_net_ls_add(&c->resp_ls, cb, (void *)fn, true); /* dial — or take the caller's pre-made socket (createConnection); dial diff --git a/packages/runtime/src/scr_net.c b/packages/runtime/src/scr_net.c index d34ec2ea..e7330251 100644 --- a/packages/runtime/src/scr_net.c +++ b/packages/runtime/src/scr_net.c @@ -1525,6 +1525,7 @@ void scr_net_listen_opts_reuse_port(ScrNetServer *s, double port, ScrStr *host / } double scr_net_server_port(ScrNetServer *s) { return (double)s->port; } +bool scr_net_server_listening(ScrNetServer *s) { return s->listening; } double scr_net_server_timeout_get(ScrNetServer *s, double field) { static const char *const names[] = { diff --git a/packages/runtime/src/scr_runtime.h b/packages/runtime/src/scr_runtime.h index ce3cf3ff..9f91c823 100644 --- a/packages/runtime/src/scr_runtime.h +++ b/packages/runtime/src/scr_runtime.h @@ -5649,6 +5649,7 @@ void scr_net_listen_opts_reuse_port(ScrNetServer *s, double port, ScrStr *host / bool ipv6_only, bool reuse_port, ScrClosure *cb /*moves, nullable*/); double scr_net_server_port(ScrNetServer *s); /* address().port */ +bool scr_net_server_listening(ScrNetServer *s); /* Writable http.Server timeout property storage. `field` is the compiler * ABI selector: timeout, keepAliveTimeout, headersTimeout, requestTimeout, * keepAliveTimeoutBuffer. Typed reads validate that no dynamic write left @@ -6058,6 +6059,8 @@ bool scr_http_req_complete(ScrHttpReq *r); * SCR_DYNH_HTTP_RES) — emitted main() calls this exactly when the http * unit is linked (the scr_net_dyn_install story). */ void scr_http_dyn_install(void); +void scr_http_validate_header_name(ScrStr *name /*borrowed*/, ScrStr *label /*borrowed*/); +void scr_http_validate_header_value(ScrStr *name /*borrowed*/, const ScrDyn *value /*borrowed*/); ScrNetServer *scr_http_create_server(ScrClosure *handler /*moves, nullable*/, ScrHttpReqFn fn); /* +1 */ /* The unguarded h2-only stream call: throws Node's exact catchable @@ -6092,6 +6095,9 @@ void scr_http_req_on_end(ScrHttpReq *r, ScrClosure *cb /*moves*/, bool once); void scr_http_res_set_header(ScrHttpRes *r, ScrStr *name /*borrowed*/, ScrStr *value /*borrowed*/); void scr_http_res_write_head(ScrHttpRes *r, double status); void scr_http_res_write_head_n(ScrHttpRes *r, double status, ScrArr *names /*borrowed*/, ScrArr *values /*borrowed*/); +void scr_http_res_write_continue(ScrHttpRes *r); +void scr_http_res_write_processing(ScrHttpRes *r); +void scr_http_res_write_early_hints(ScrHttpRes *r, ScrArr *pairs /*borrowed: [name, value, ...] */); /* writeHead with a CHECKED-DYNAMIC headers object (borrowed): OBJ * entries setHeader in insertion order (string/number values), then the * head goes out; undefined/null = the plain head; may throw. */ @@ -6109,6 +6115,7 @@ void scr_http_res_add_trailers(ScrHttpRes *r, ScrArr *pairs /*borrowed: [name, v 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); /* 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 @@ -6118,6 +6125,9 @@ void scr_http_res_status_set(ScrHttpRes *r, double status); ScrStr *scr_http_res_status_msg_get(ScrHttpRes *r); /* +1 */ void scr_http_res_status_msg_set(ScrHttpRes *r, ScrStr *msg /*borrowed*/); ScrStr *scr_http_res_get_header(ScrHttpRes *r, ScrStr *name /*borrowed*/); /* +1 or NULL */ +ScrArr *scr_http_res_get_header_names(ScrHttpRes *r); /* +1 */ +ScrArr *scr_http_res_get_raw_header_names(ScrHttpRes *r); /* +1 */ +ScrDyn *scr_http_res_get_headers(ScrHttpRes *r); /* +1 */ bool scr_http_res_has_header_named(ScrHttpRes *r, ScrStr *name /*borrowed*/); void scr_http_res_remove_header(ScrHttpRes *r, ScrStr *name /*borrowed*/); void scr_http_res_on_finish(ScrHttpRes *r, ScrClosure *cb /*moves*/); @@ -6200,6 +6210,19 @@ void scr_http_client_end(ScrHttpClientReq *c); void scr_http_client_end_str(ScrHttpClientReq *c, ScrStr *data /*borrowed*/); void scr_http_client_end_bytes(ScrHttpClientReq *c, ScrBytes *data /*borrowed*/); void scr_http_client_flush_headers(ScrHttpClientReq *c); +void scr_http_client_set_header(ScrHttpClientReq *c, ScrStr *name /*borrowed*/, ScrStr *value /*borrowed*/); +ScrStr *scr_http_client_get_header(ScrHttpClientReq *c, ScrStr *name /*borrowed*/); /* +1 or NULL */ +bool scr_http_client_has_header_named(ScrHttpClientReq *c, ScrStr *name /*borrowed*/); +void scr_http_client_remove_header(ScrHttpClientReq *c, ScrStr *name /*borrowed*/); +ScrArr *scr_http_client_get_header_names(ScrHttpClientReq *c); /* +1 */ +ScrArr *scr_http_client_get_raw_header_names(ScrHttpClientReq *c); /* +1 */ +ScrDyn *scr_http_client_get_headers(ScrHttpClientReq *c); /* +1 */ +ScrStr *scr_http_client_method(ScrHttpClientReq *c); /* +1 */ +ScrStr *scr_http_client_path(ScrHttpClientReq *c); /* +1 */ +ScrStr *scr_http_client_host(ScrHttpClientReq *c); /* +1 */ +ScrStr *scr_http_client_protocol(ScrHttpClientReq *c); /* +1 */ +bool scr_http_client_headers_sent(ScrHttpClientReq *c); +bool scr_http_client_writable_ended(ScrHttpClientReq *c); void scr_http_client_add_trailers(ScrHttpClientReq *c, ScrArr *pairs /*borrowed*/); void scr_http_client_cork(ScrHttpClientReq *c); void scr_http_client_uncork(ScrHttpClientReq *c); diff --git a/tests/corpus/2953-http-header-validation.ts b/tests/corpus/2953-http-header-validation.ts new file mode 100644 index 00000000..c0f4b4b6 --- /dev/null +++ b/tests/corpus/2953-http-header-validation.ts @@ -0,0 +1,36 @@ +import * as http from "node:http"; +import { validateHeaderName, validateHeaderValue } from "node:http"; + +function result(fn: () => void): string { + try { + fn(); + return "ok"; + } catch (error) { + if (error instanceof Error) { + return `${error.name}:${(error as NodeJS.ErrnoException).code}:${error.message}`; + } + return "unexpected error"; + } +} + +const dynamicName = "X-Valid_09"; +console.log("name", result(() => validateHeaderName(dynamicName))); +console.log("token", result(() => http.validateHeaderName("!#$%&'*+-.^_`|~"))); +console.log("empty-name", result(() => validateHeaderName(""))); +console.log("space-name", result(() => http.validateHeaderName("Bad Name", "Field name"))); +console.log("colon-name", result(() => validateHeaderName("Bad:Name"))); +console.log("unicode-name", result(() => validateHeaderName("Café"))); +console.log("empty-label", result(() => validateHeaderName("bad name", ""))); + +console.log("empty-value", result(() => validateHeaderValue("x-test", ""))); +console.log("tab-value", result(() => http.validateHeaderValue("x-test", "a\tb"))); +console.log("latin1-value", result(() => validateHeaderValue("x-test", "café\u00ff"))); +console.log("number-value", result(() => validateHeaderValue("x-test", 42))); +console.log("boolean-value", result(() => validateHeaderValue("x-test", true))); +console.log("null-value", result(() => validateHeaderValue("x-test", null))); +console.log("array-value", result(() => validateHeaderValue("x-test", [1, 2]))); +console.log("undefined-value", result(() => validateHeaderValue("x-test", undefined))); +console.log("newline-value", result(() => validateHeaderValue("x-test", "a\nb"))); +console.log("del-value", result(() => validateHeaderValue("x-test", "a\u007fb"))); +console.log("wide-value", result(() => validateHeaderValue("x-test", "a\u0100b"))); +console.log("unchecked-name", result(() => validateHeaderValue("bad name", "valid"))); diff --git a/tests/corpus/2954-http-outgoing-headers.ts b/tests/corpus/2954-http-outgoing-headers.ts new file mode 100644 index 00000000..59c4d9ea --- /dev/null +++ b/tests/corpus/2954-http-outgoing-headers.ts @@ -0,0 +1,49 @@ +import { createServer, request } from "node:http"; + +function outcome(fn: () => void): string { + try { + fn(); + return "ok"; + } catch (error) { + if (error instanceof Error) return `${error.name}:${(error as NodeJS.ErrnoException).code}:${error.message}`; + return "unexpected error"; + } +} + +const server = createServer((incoming, outgoing) => { + console.log("wire", incoming.headers["x-initial"], incoming.headers["x-new"], incoming.headers["x-drop"] === undefined); + outgoing.setHeader("X-First", "one"); + const names = outgoing.getHeaderNames(); + const snapshot = outgoing.getHeaders(); + outgoing.setHeader("x-first", "two"); + outgoing.setHeader("X-Drop", "gone"); + outgoing.removeHeader("x-drop"); + console.log("server names", names.join(","), outgoing.getHeaderNames().join(","), outgoing.getRawHeaderNames().join(",")); + console.log("server headers", JSON.stringify(snapshot), JSON.stringify(outgoing.getHeaders()), outgoing.getHeader("X-FIRST")); + outgoing.end("done"); +}); + +server.listen(0, () => { + const port = server.address().port; + const outbound = request({ hostname: "127.0.0.1", port, headers: { "X-Initial": "one" } }, (response) => { + console.log("reply", response.statusCode, response.headers["x-first"]); + response.on("data", () => {}); + response.on("end", () => server.close()); + }); + outbound.on("error", (error) => console.log("request error", error.message)); + const initial = outbound.getHeaders(); + const initialNames = outbound.getHeaderNames(); + console.log("client initial", initialNames.join(","), outbound.getRawHeaderNames().join(","), initial["x-initial"], outbound.getHeader("HOST") === `127.0.0.1:${port}`); + outbound.setHeader("x-initial", "two"); + outbound.setHeader("X-Drop", "gone"); + outbound.setHeader("X-New", "yes"); + console.log("client changed", outbound.getHeader("X-INITIAL"), outbound.hasHeader("x-drop"), initial["x-initial"], outbound.getRawHeaderNames().join(",")); + outbound.removeHeader("x-drop"); + console.log("client removed", outbound.getHeader("X-Drop") === undefined, outbound.hasHeader("X-DROP"), JSON.stringify(outbound.getHeaderNames())); + console.log("bad name", outcome(() => outbound.setHeader("bad name", "value"))); + console.log("bad value", outcome(() => outbound.setHeader("X-Bad", "line\nbreak"))); + outbound.flushHeaders(); + console.log("late set", outcome(() => outbound.setHeader("X-Late", "no"))); + console.log("late remove", outcome(() => outbound.removeHeader("X-New"))); + outbound.end(); +}); diff --git a/tests/corpus/2955-http-static-state.ts b/tests/corpus/2955-http-static-state.ts new file mode 100644 index 00000000..f9d30bf2 --- /dev/null +++ b/tests/corpus/2955-http-static-state.ts @@ -0,0 +1,37 @@ +import { createServer, request } from "node:http"; + +const server = createServer((incoming, outgoing) => { + console.log("incoming", incoming.method, incoming.url, incoming.httpVersion); + incoming.on("data", () => {}); + incoming.on("end", () => { + console.log("incoming complete", incoming.complete); + outgoing.end("done"); + console.log("response ended", outgoing.headersSent, outgoing.writableEnded); + }); + console.log("response initial", outgoing.statusCode, outgoing.headersSent, outgoing.writableEnded); + outgoing.setHeader("Content-Type", "text/plain"); + outgoing.flushHeaders(); + console.log("response flushed", outgoing.headersSent, outgoing.writableEnded); +}); + +console.log("server initial", server.listening); +server.listen(0, () => { + console.log("server listening", server.listening); + const port = server.address().port; + const outbound = request({ hostname: "127.0.0.1", port, path: "/state?q=1", method: "POST" }, (incoming) => { + console.log("reply", incoming.statusCode, incoming.httpVersion); + incoming.on("data", () => {}); + incoming.on("end", () => { + console.log("reply complete", incoming.complete); + server.close(() => console.log("server closed", server.listening)); + console.log("server closing", server.listening); + }); + }); + outbound.on("error", (error) => console.log("client error", error.message)); + console.log("client metadata", outbound.method, outbound.path, outbound.host, outbound.protocol); + console.log("client initial", outbound.headersSent, outbound.writableEnded); + outbound.flushHeaders(); + console.log("client flushed", outbound.headersSent, outbound.writableEnded); + outbound.end("hi"); + console.log("client ended", outbound.headersSent, outbound.writableEnded); +}); diff --git a/tests/corpus/2956-http-static-informational.ts b/tests/corpus/2956-http-static-informational.ts new file mode 100644 index 00000000..ec4f3697 --- /dev/null +++ b/tests/corpus/2956-http-static-informational.ts @@ -0,0 +1,58 @@ +import { createServer } from "node:http"; +import { connect } from "node:net"; + +const server = createServer((_request, response) => { + response.writeEarlyHints({ "X-Ignored": "no link" }); + try { + response.writeEarlyHints({ link: "" }); + } catch (error) { + const caught = error as NodeJS.ErrnoException; + console.log("empty link", caught.name, caught.code, caught.message); + } + response.writeContinue(); + response.writeProcessing(); + + try { + response.writeEarlyHints({ link: "bad" }); + } catch (error) { + const caught = error as NodeJS.ErrnoException; + console.log("bad link", caught.name, caught.code, caught.message); + } + try { + response.writeEarlyHints({ link: "; rel=preload", "Bad Name": "value" }); + } catch (error) { + const caught = error as NodeJS.ErrnoException; + console.log("bad name", caught.name, caught.code, caught.message); + } + try { + response.writeEarlyHints({ link: "; rel=preload", "X-Bad": "bad\rvalue" }); + } catch (error) { + const caught = error as NodeJS.ErrnoException; + console.log("bad value", caught.name, caught.code, caught.message); + } + + const hints: Record = { + "X-Before": "first", + link: "; rel=preload; as=style", + "X-After": "last", + }; + response.writeEarlyHints(hints); + console.log("final state", response.headersSent, response.writableEnded); + response.end("ok"); +}); + +server.listen(0, () => { + const socket = connect(server.address().port, "127.0.0.1"); + let wire = ""; + socket.on("connect", () => socket.write("GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n")); + socket.on("data", (chunk: Buffer) => { wire += chunk.toString("utf8"); }); + socket.on("end", () => { + const parts = wire.split("\r\n\r\n"); + for (let i = 0; i < parts.length - 1; i++) { + console.log("head", parts[i]!.split("\r\n")[0]); + } + console.log("hints", parts[2]!.split("\r\n").slice(1).join("|")); + console.log("body", parts[parts.length - 1]); + server.close(); + }); +});