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.
This commit is contained in:
Chris Tate
2026-09-24 00:36:10 -05:00
committed by GitHub
parent 6242053fc9
commit 3f0de2167c
20 changed files with 1380 additions and 350 deletions
@@ -2,6 +2,6 @@
"schemaVersion": 3,
"nodeVersion": "24.15.0",
"nodeCommit": "848430679556aed0bd073f2bc263331ad84fa119",
"artifactVersion": "0ac7558fba31464469e7",
"artifactVersion": "a7e38d1b2b0e92cf8ec4",
"rowCount": 3662
}
+92 -92
View File
@@ -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",
@@ -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",
@@ -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",
+17
View File
@@ -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:")) {
@@ -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" },
+32 -1
View File
@@ -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<string, string | string[]>, 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<string, string | string[]>): void;
write(data: string | Uint8Array): void;
flushHeaders(): void;
cork(): void;
+51 -3
View File
@@ -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 };
@@ -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));
@@ -579,6 +579,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
// 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<string, string> = {
"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<string, string> = {
"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<string, string> = {
"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<string, string> = {
"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<string, string> = {
"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",
@@ -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<string, string> 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<string, string>");
}
const helper = lowerer.envToPairsHelper(value.type.shapeId, loc);
if (helper === null) {
lowerer.noLowering("writeEarlyHints header values", node, "use a Record<string, string> 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),
);
}
+30
View File
@@ -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<IrLibFn> = new Set([
"dc.chanBindStore",
"dc.chanRunStores",
"http.resWriteHeadDyn",
"http.resWriteContinue",
"http.resWriteProcessing",
"http.resWriteEarlyHints",
"http.validateHeaderName",
"http.validateHeaderValue",
"http.serverTimeoutGet",
"http.serverTimeoutOptionSet",
"net.sockSetEncoding",
+24 -1
View File
@@ -428,6 +428,7 @@ export const LIB_FN_SIGS: Record<IrLibFn, { argTypes: (IrType | null)[]; result:
// the callback follows the reusePort BOOL in this additive ABI.
"net.listenOptsReusePortCb": { argTypes: [NETSERVER_T, F64, STRING, BOOL, BOOL, null], result: VOID },
"net.serverPort": { argTypes: [NETSERVER_T], result: F64 },
"net.serverListening": { argTypes: [NETSERVER_T], result: BOOL },
// net.serverAddress's record result is shape-checked in the libCall case
// (the dgram.address sentinel pattern).
"net.serverAddress": { argTypes: [NETSERVER_T], result: VOID },
@@ -503,6 +504,8 @@ export const LIB_FN_SIGS: Record<IrLibFn, { argTypes: (IrType | null)[]; result:
// union, checked like process.envGet.
"http.createServer": { argTypes: [null], result: NETSERVER_T },
"http.createServerEmpty": { argTypes: [], result: NETSERVER_T },
"http.validateHeaderName": { argTypes: [STRING, STRING], result: VOID },
"http.validateHeaderValue": { argTypes: [STRING, DYN], result: VOID },
"http.serverJoinDupHeaders": { argTypes: [NETSERVER_T], result: VOID },
"http.serverAllowMissingHostHeader": { argTypes: [NETSERVER_T], result: VOID },
"http.serverTimeoutGet": { argTypes: [NETSERVER_T, F64], result: F64 },
@@ -516,6 +519,9 @@ export const LIB_FN_SIGS: Record<IrLibFn, { argTypes: (IrType | null)[]; result:
// resGetHeader answers the interned `string | undefined` union — the
// reqHeader/envGet sentinel pattern (VOID here, checked specially).
"http.resGetHeader": { argTypes: [HTTPRES_T, STRING], result: VOID },
"http.resGetHeaderNames": { argTypes: [HTTPRES_T], result: arrayOf(STRING) },
"http.resGetRawHeaderNames": { argTypes: [HTTPRES_T], result: arrayOf(STRING) },
"http.resGetHeaders": { argTypes: [HTTPRES_T], result: DYN },
"http.resHasHeader": { argTypes: [HTTPRES_T, STRING], result: BOOL },
"http.resRemoveHeader": { argTypes: [HTTPRES_T, STRING], result: VOID },
"http.resOnFinish": { argTypes: [HTTPRES_T, { kind: "func", params: [], ret: VOID }], result: VOID },
@@ -530,6 +536,9 @@ export const LIB_FN_SIGS: Record<IrLibFn, { argTypes: (IrType | null)[]; result:
"http.resSetHeader": { argTypes: [HTTPRES_T, STRING, STRING], result: VOID },
"http.resWriteHead": { argTypes: [HTTPRES_T, F64], result: VOID },
"http.resWriteHeadN": { argTypes: [HTTPRES_T, F64, arrayOf(STRING), arrayOf(STRING)], result: VOID },
"http.resWriteContinue": { argTypes: [HTTPRES_T], result: VOID },
"http.resWriteProcessing": { argTypes: [HTTPRES_T], result: VOID },
"http.resWriteEarlyHints": { argTypes: [HTTPRES_T, arrayOf(STRING)], result: VOID },
"http.resWrite": { argTypes: [HTTPRES_T, STRING], result: VOID },
"http.resWriteBytes": { argTypes: [HTTPRES_T, BYTES_U8], result: VOID },
"http.resEnd": { argTypes: [HTTPRES_T], result: VOID },
@@ -538,6 +547,7 @@ export const LIB_FN_SIGS: Record<IrLibFn, { argTypes: (IrType | null)[]; result:
"http.resWriteDyn": { argTypes: [HTTPRES_T, DYN], result: VOID },
"http.resEndDyn": { argTypes: [HTTPRES_T, DYN], result: VOID },
"http.resHeadersSent": { argTypes: [HTTPRES_T], result: BOOL },
"http.resWritableEnded": { argTypes: [HTTPRES_T], result: BOOL },
"http.resFlushHeaders": { argTypes: [HTTPRES_T], result: VOID },
"http.resAddTrailers": { argTypes: [HTTPRES_T, arrayOf(STRING)], result: VOID },
"http.resCork": { argTypes: [HTTPRES_T], result: VOID },
@@ -742,6 +752,13 @@ export const LIB_FN_SIGS: Record<IrLibFn, { argTypes: (IrType | null)[]; result:
"https.requestFn": { argTypes: [BOOL, STRING, F64, STRING, STRING, F64, arrayOf(STRING), BOOL, BOOL, null], result: HTTPCLIENTREQ_T },
"https.requestFnCb": { argTypes: [BOOL, STRING, F64, STRING, STRING, F64, arrayOf(STRING), BOOL, BOOL, null, null], result: HTTPCLIENTREQ_T },
"http.clientWrite": { argTypes: [HTTPCLIENTREQ_T, STRING], result: VOID },
"http.clientSetHeader": { argTypes: [HTTPCLIENTREQ_T, STRING, STRING], result: VOID },
"http.clientGetHeader": { argTypes: [HTTPCLIENTREQ_T, STRING], result: VOID },
"http.clientHasHeader": { argTypes: [HTTPCLIENTREQ_T, STRING], result: BOOL },
"http.clientRemoveHeader": { argTypes: [HTTPCLIENTREQ_T, STRING], result: VOID },
"http.clientGetHeaderNames": { argTypes: [HTTPCLIENTREQ_T], result: arrayOf(STRING) },
"http.clientGetRawHeaderNames": { argTypes: [HTTPCLIENTREQ_T], result: arrayOf(STRING) },
"http.clientGetHeaders": { argTypes: [HTTPCLIENTREQ_T], result: DYN },
"http.clientWriteBytes": { argTypes: [HTTPCLIENTREQ_T, BYTES_U8], result: VOID },
"http.clientEnd": { argTypes: [HTTPCLIENTREQ_T], result: VOID },
"http.clientEndStr": { argTypes: [HTTPCLIENTREQ_T, STRING], result: VOID },
@@ -753,6 +770,12 @@ export const LIB_FN_SIGS: Record<IrLibFn, { argTypes: (IrType | null)[]; result:
"http.clientCork": { argTypes: [HTTPCLIENTREQ_T], result: VOID },
"http.clientUncork": { argTypes: [HTTPCLIENTREQ_T], result: VOID },
"http.clientWritableCorked": { argTypes: [HTTPCLIENTREQ_T], result: F64 },
"http.clientMethod": { argTypes: [HTTPCLIENTREQ_T], result: STRING },
"http.clientPath": { argTypes: [HTTPCLIENTREQ_T], result: STRING },
"http.clientHost": { argTypes: [HTTPCLIENTREQ_T], result: STRING },
"http.clientProtocol": { argTypes: [HTTPCLIENTREQ_T], result: STRING },
"http.clientHeadersSent": { argTypes: [HTTPCLIENTREQ_T], result: BOOL },
"http.clientWritableEnded": { argTypes: [HTTPCLIENTREQ_T], result: BOOL },
"http.clientDestroy": { argTypes: [HTTPCLIENTREQ_T], result: VOID },
"http.clientDestroyed": { argTypes: [HTTPCLIENTREQ_T], result: BOOL },
"http.clientOnResponse": { argTypes: [HTTPCLIENTREQ_T, null, BOOL], result: VOID },
@@ -4238,7 +4261,7 @@ function validateFunction(
if (!ok) err(`libCall http.reqH2Stream must return the 'Http2Stream | undefined' union`, e.loc);
break;
}
if (e.fn === "http.reqHeader" || e.fn === "http.reqTrailer" || e.fn === "http.resGetHeader") {
if (e.fn === "http.reqHeader" || e.fn === "http.reqTrailer" || e.fn === "http.resGetHeader" || e.fn === "http.clientGetHeader") {
// Result is the interned `string | undefined` union (envGet's).
const def = e.type.kind === "union" ? unions.get(e.type.unionId) : undefined;
const ok =
+378 -4
View File
@@ -267,11 +267,12 @@ void scr_http_req_release_v(void *p) { scr_http_req_release((ScrHttpReq *)p); }
ScrStr *scr_http_req_url(ScrHttpReq *r) { return scr_str_retain(r->url); }
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 \"</styles.css>; 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
+1
View File
@@ -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[] = {
+23
View File
@@ -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);
@@ -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")));
@@ -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();
});
+37
View File
@@ -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);
});
@@ -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: "</good.css>; 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: "</good.css>; 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<string, string> = {
"X-Before": "first",
link: "</main.css>; 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();
});
});