Close Node 24 gaps in util.types keys, HTTP header limits and unread requests (#1021)

This commit is contained in:
Chris Tate
2026-10-15 09:29:01 -05:00
committed by GitHub
15 changed files with 481 additions and 52 deletions
+2
View File
@@ -158,6 +158,8 @@ title: "Limitations"
<include>../limitations/buffer-construction.mdx</include>
Every Buffer owns an ArrayBuffer of its own length. Node allocates small Buffers from `Buffer.from`, `Buffer.allocUnsafe`, and `Buffer.concat` out of a shared 64 KiB pool, so for those Buffers `buf.buffer.byteLength` is the pool size in Node and the Buffer's length here, and `buf.byteOffset` is `0` here. Reading `buf.buffer` through `buf.byteOffset` and `buf.length` returns the same bytes in both. `Buffer.poolSize` is unsupported.
### JSON callbacks
<include>../limitations/json-callbacks.mdx</include>
@@ -52,7 +52,13 @@ import { lowerProtocolMethodCall } from "./class-protocol-dispatch.js";
import { isJsSourceFile, locOf, requireSpecOf } from "../program.js";
import { isGenericCallableMemberType, typeKey } from "../type-mapper.js";
import { dynFallbackType, jsFuncNameOf, newFnCtx, staticImportNamespaceType } from "./lowerer.js";
import { NARROW_FIRST, STRING_INDEX_METHODS, STR_METHODS, stdlibGlobalNameOf } from "./surfaces.js";
import {
NARROW_FIRST,
STRING_INDEX_METHODS,
STR_METHODS,
UTIL_TYPES_KEYS,
stdlibGlobalNameOf,
} from "./surfaces.js";
import { dynStringReceiver } from "./containers/dynamic-receivers.js";
import {
lowerArrayConstructor,
@@ -7327,6 +7333,37 @@ function lowerObjectStaticCall(
if (member !== "keys" && member !== "values" && member !== "entries") return null;
if (call.arguments.length !== 1 || ts.isSpreadElement(call.arguments[0]!)) return null;
const argNode = call.arguments[0]!;
// util.types and node:util/types list Node's pinned key census
// (UTIL_TYPES_KEYS): the object's own keys in Node's order, or the module
// namespace's export names sorted with "default". values/entries would
// include members that do not compile yet.
const probedModule = tryLowerExpression(lowerer, argNode);
if (
lowerer.builtinNamespaceModuleOf(argNode) === "util/types" ||
(probedModule?.type.kind === "moduleNs" && probedModule.type.moduleId === "builtin:util/types")
) {
if (member !== "keys") {
lowerer.noLowering(
`Object.${member} over util.types`,
call,
"call the util.types predicates by name",
);
}
const symbol = ts.isIdentifier(argNode)
? lowerer.checker.getSymbolAtLocation(argNode)
: undefined;
const namespace = symbol
? lowerer.checker.declarationsOf(symbol).some((decl) => ts.isNamespaceImport(decl))
: false;
const names = namespace ? [...UTIL_TYPES_KEYS, "default"].sort() : UTIL_TYPES_KEYS;
const loc = locOf(call);
return {
kind: "arrayLit",
elems: names.map((name) => ({ kind: "strLit", value: name, type: STRING, loc })),
type: arrayOf(STRING),
loc,
};
}
// A compiled PROGRAM module namespace is a nominal token whose members
// remain live bindings, but its enumerable key set is static and Node
// sorts it in code-unit order. Materialize exactly those value-export
@@ -1015,6 +1015,55 @@ const PATH_WIN32_MODULE_FNS: Record<string, BuiltinModuleFn | undefined> = {
format: { fn: "path.win32Format", params: [], result: STRING },
};
/** util.types's own keys in Node's order (Node 24.22.0). The table is the
* key census for Object.keys over util.types and node:util/types, including
* the members that do not compile yet; the corpus checks it against Node. */
export const UTIL_TYPES_KEYS: readonly string[] = [
"isArgumentsObject",
"isArrayBuffer",
"isAsyncFunction",
"isBigIntObject",
"isBooleanObject",
"isDate",
"isExternal",
"isGeneratorFunction",
"isGeneratorObject",
"isMap",
"isMapIterator",
"isModuleNamespaceObject",
"isNativeError",
"isNumberObject",
"isPromise",
"isProxy",
"isRegExp",
"isSet",
"isSetIterator",
"isSharedArrayBuffer",
"isStringObject",
"isSymbolObject",
"isWeakMap",
"isWeakSet",
"isAnyArrayBuffer",
"isBoxedPrimitive",
"isArrayBufferView",
"isDataView",
"isTypedArray",
"isUint8Array",
"isUint8ClampedArray",
"isUint16Array",
"isUint32Array",
"isInt8Array",
"isInt16Array",
"isInt32Array",
"isFloat16Array",
"isFloat32Array",
"isFloat64Array",
"isBigInt64Array",
"isBigUint64Array",
"isKeyObject",
"isCryptoKey",
];
export const BUILTIN_MODULE_FNS: Record<
string,
Record<string, BuiltinModuleFn | undefined> | undefined
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5405-buffer-arraybuffer-window.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5406-util-types-keys.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5407-http-header-field-limit.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5408-http-unread-request-end.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5409-http-held-request-body.ts"
],
"diags": []
}
+159 -49
View File
@@ -57,7 +57,9 @@
/* The HTTP/1 parser has no configurable maxHeaderSize surface yet. Keep its
* head and trailer block within the existing 64 KiB parser bound, and cap
* fields too: each stored field owns three strings and three growing arrays. */
* fields per block at Node's default of 1000 (server.maxHeadersCount unset):
* each stored field owns three strings and three growing arrays. A server
* answers 431 past either bound, like Node. */
#define SCR_HTTP_MAX_HEADER_BYTES 65536u
#define SCR_HTTP_MAX_HEADER_FIELDS 1000u
@@ -243,6 +245,14 @@ struct ScrHttpReq {
bool drain_queued;
char *pend;
size_t pend_len, pend_cap;
/* HTTP/1 server requests start paused like Node's IncomingMessage: body
* bytes and the completed body's 'end' are held in pend/end_pending until
* a consumer starts the flow (a 'data' listener, pipe() or resume()), or
* until the response finishes and the unread request is dumped. */
bool gated; /* holds until flowing */
bool flowing; /* a consumer or the dump started the flow */
bool received; /* the whole body arrived (req.complete) */
bool body_seen; /* body bytes arrived */
bool destroyed; /* req.destroy()/the teardown ran — req.destroyed */
ScrNetLs aborted_ls; /* req.on('aborted') — the h2 compat event */
};
@@ -521,6 +531,8 @@ ScrStr *scr_http_req_header(ScrHttpReq *r, ScrStr *name) {
* backpressure — divergence 54's stream model); the body's natural end
* ends the destination, Node's pipe default. A body that already ended
* ends the destination NOW (nothing more will flow). */
static void scr_http_req_consume(ScrHttpReq *r);
void scr_http_req_pipe_res(ScrHttpReq *r, ScrHttpRes *dst /*borrowed*/) {
if (r->ended) {
scr_http_res_end(dst);
@@ -528,6 +540,7 @@ void scr_http_req_pipe_res(ScrHttpReq *r, ScrHttpRes *dst /*borrowed*/) {
}
if (r->pipe_res) scr_http_res_release(r->pipe_res);
r->pipe_res = scr_http_res_retain(dst);
scr_http_req_consume(r);
}
/* socket.pipe(res) — the extended-CONNECT bridge leg: a native reader on
@@ -576,6 +589,7 @@ void scr_http_req_pipe_client(ScrHttpReq *r, ScrHttpClientReq *dst /*borrowed*/)
}
if (r->pipe_client) scr_http_client_release(r->pipe_client);
r->pipe_client = scr_http_client_retain(dst);
scr_http_req_consume(r);
}
void scr_http_req_pipe_sock(ScrHttpReq *r, ScrNetSocket *dst /*borrowed*/) {
@@ -585,6 +599,7 @@ void scr_http_req_pipe_sock(ScrHttpReq *r, ScrNetSocket *dst /*borrowed*/) {
}
if (r->pipe_sock) scr_net_sock_release(r->pipe_sock);
r->pipe_sock = scr_net_sock_retain(dst);
scr_http_req_consume(r);
}
void scr_http_req_on_data(ScrHttpReq *r, ScrClosure *cb /*moves*/, ScrNetDataFn fn, bool once) {
@@ -593,6 +608,7 @@ void scr_http_req_on_data(ScrHttpReq *r, ScrClosure *cb /*moves*/, ScrNetDataFn
return;
}
scr_net_ls_add(&r->data_ls, cb, (void *)fn, once);
scr_http_req_consume(r);
}
void scr_http_req_on_end(ScrHttpReq *r, ScrClosure *cb /*moves*/, bool once) {
@@ -619,17 +635,48 @@ static void scr_http_req_deliver(ScrHttpReq *r, const char *data, size_t n);
static void scr_http_res_cork_flush(ScrHttpRes *r);
#define SCR_HTTP_EMIT_REQ_DRAIN_K 6
/* resume(): a flow-control no-op unless pause() held delivery — then the
* buffered bytes (and a deferred 'end') drain through the emit queue,
* never the resuming stack. This parser always consumes body bytes
* (SEMANTICS.md; Node requires the stream to flow). */
void scr_http_req_resume(ScrHttpReq *r) {
if (!r->paused) return;
static bool scr_http_req_holding(const ScrHttpReq *r) {
return r->paused || (r->gated && !r->flowing);
}
static void scr_http_req_queue_drain(ScrHttpReq *r) {
if (r->drain_queued) return;
r->drain_queued = true;
scr_http_emit_push(SCR_HTTP_EMIT_REQ_DRAIN_K, scr_http_req_retain(r));
}
/* Starts (or restarts) the flow: held bytes and a deferred 'end' drain
* through the emit queue, never the resuming stack. */
static void scr_http_req_flow(ScrHttpReq *r) {
r->paused = false;
if ((r->pend_len > 0 || r->end_pending) && !r->drain_queued) {
r->drain_queued = true;
scr_http_emit_push(SCR_HTTP_EMIT_REQ_DRAIN_K, scr_http_req_retain(r));
}
r->flowing = true;
if (r->pend_len > 0 || r->end_pending) scr_http_req_queue_drain(r);
}
/* A 'data' listener or pipe() starts the flow unless pause() stopped it,
* like Node's `if (state.flowing !== false) this.resume()`. */
static void scr_http_req_consume(ScrHttpReq *r) {
if (r->gated && !r->paused && !r->flowing) scr_http_req_flow(r);
}
/* resume(): releases what pause() or the initial paused state held. This
* parser always consumes body bytes from the socket (SEMANTICS.md); held
* bytes buffer in memory. */
void scr_http_req_resume(ScrHttpReq *r) {
if (!scr_http_req_holding(r)) return;
scr_http_req_flow(r);
}
/* Node's resOnFinish: a request nothing consumed is dumped. Its 'data'
* listeners are removed, held bytes are discarded and the stream flows,
* so 'end' fires once the body is complete. */
static void scr_http_req_dump(ScrHttpReq *r) {
scr_net_ls_drop(&r->data_ls);
free(r->pend);
r->pend = NULL;
r->pend_len = r->pend_cap = 0;
r->paused = false;
r->flowing = true;
}
/* pause(): delivery holds until resume() (scr_http_req_deliver buffers;
@@ -707,8 +754,8 @@ void scr_http_req_on_aborted(ScrHttpReq *r, ScrClosure *cb /*moves*/, bool once)
* req cannot cycle past the body). */
static void scr_http_req_finish(ScrHttpReq *r, bool fire) {
if (r->ended) return;
if (fire && r->paused) {
/* pause() holds 'end' too — resume()'s drain finishes the body */
if (fire && scr_http_req_holding(r)) {
/* a held body holds 'end' too — the drain finishes the body */
r->end_pending = true;
return;
}
@@ -2201,12 +2248,15 @@ typedef struct ScrHttpConn {
size_t len, cap;
ScrHttpParseState state;
size_t body_remaining;
size_t head_bytes, head_fields;
size_t head_bytes;
size_t trailer_bytes, trailer_fields;
ScrHttpReq *req; /* the in-flight request (server) or response (client),
* +1; NULL between requests / before the head */
ScrHttpRes *res; /* +1; server mode only */
bool close_after; /* the response said Connection: close */
bool pumping; /* inside scr_http_conn_pump */
bool res_close_deferred; /* the response finished inside the pump while
* the request body was still arriving */
bool client_mode; /* RESPONSE parsing: status line, EOF bodies */
bool client_resp_started; /* the status line parsed (conn-level twin of
* the client's response_started — the pump
@@ -2219,8 +2269,9 @@ typedef struct ScrHttpConn {
static bool scr_http_client_parse_head(ScrHttpConn *conn, size_t head_len);
static void scr_http_client_head_overflow(ScrHttpConn *conn);
/* The trailer budget includes the request/response head and every trailer
* line on the wire, including the terminating blank line. Counters live on
/* The trailer byte budget includes the request/response head and every
* trailer line on the wire, including the terminating blank line; the field
* count covers the trailer block alone, like Node's. Counters live on
* the connection because an early response can release conn->req while the
* parser must continue consuming and validating its incoming chunk stream. */
static bool scr_http_conn_trailer_fits(const ScrHttpConn *conn, size_t bytes, bool field) {
@@ -2228,10 +2279,7 @@ static bool scr_http_conn_trailer_fits(const ScrHttpConn *conn, size_t bytes, bo
conn->trailer_bytes > SCR_HTTP_MAX_HEADER_BYTES - conn->head_bytes) return false;
size_t remaining = SCR_HTTP_MAX_HEADER_BYTES - conn->head_bytes - conn->trailer_bytes;
if (bytes > remaining) return false;
if (!field) return true;
if (conn->head_fields > SCR_HTTP_MAX_HEADER_FIELDS ||
conn->trailer_fields >= SCR_HTTP_MAX_HEADER_FIELDS - conn->head_fields) return false;
return true;
return !field || conn->trailer_fields < SCR_HTTP_MAX_HEADER_FIELDS;
}
/* The server-side ctx: the 'request' listener list, shared by every
@@ -2343,17 +2391,53 @@ static void scr_http_conn_drop_request(ScrHttpConn *conn, bool fire_end) {
}
}
/* Ends a request whose response already finished and detaches the
* exchange; the queued emits hold their own references. Node emits the
* request's 'end' after the response's 'close' when body bytes arrived,
* and before it otherwise. */
static void scr_http_conn_end_after_response(ScrHttpConn *conn) {
ScrHttpReq *r = conn->req;
bool before_close = !r->body_seen;
conn->res_close_deferred = false;
r->received = true;
r->end_pending = true;
if (before_close) scr_http_req_queue_drain(r);
if (conn->res) scr_http_queue_res_close(conn->res);
if (!before_close) scr_http_req_queue_drain(r);
scr_http_queue_req_close(r);
conn->req = NULL;
scr_http_req_release(r);
if (conn->res) {
conn->res->conn = NULL;
scr_http_res_release(conn->res);
conn->res = NULL;
}
}
/* The response finished: with keep-alive the parser waits for the next
* request (pipelined bytes may already sit in the buffer — the read path
* keeps parsing); without it the socket half-closes, Node's teardown. */
* keeps parsing); without it the socket half-closes, Node's teardown. A
* request nothing consumed is dumped first. While its body is still
* arriving the exchange stays attached, and scr_http_conn_body_done ends
* the request when the body completes. */
static void scr_http_conn_response_finished(ScrHttpConn *conn, bool keep_alive) {
bool fire_end = conn->req != NULL && conn->state == SCR_HTTP_HEAD;
/* the handler answered before consuming the whole body: the request
* settles quietly (Node destroys unconsumed bodies on 'close') */
scr_http_conn_drop_request(conn, fire_end && false);
if (!keep_alive || conn->close_after) {
scr_net_sock_end(conn->sock);
ScrHttpReq *r = conn->req;
bool close = !keep_alive || conn->close_after;
if (r != NULL && !r->ended && r->gated && !r->flowing) scr_http_req_dump(r);
if (r != NULL && !r->ended && conn->state != SCR_HTTP_HEAD) {
/* The rest of the body may already sit in the buffer: inside the pump
* the response's 'close' waits until the pump stops, so a body that
* completes there orders its 'end' first. */
if (conn->pumping) conn->res_close_deferred = true;
else scr_http_queue_res_close(conn->res);
} else if (r != NULL && !r->ended) {
/* the body is complete (a request without one completes after the
* 'request' listeners return) */
scr_http_conn_end_after_response(conn);
} else {
scr_http_conn_drop_request(conn, false);
}
if (close) scr_net_sock_end(conn->sock);
}
/* One parsed header line (name lowercased into the req). */
@@ -2406,18 +2490,28 @@ static void scr_http_req_add_trailer(ScrHttpReq *r, const char *name, size_t nle
free(lower);
}
/* Malformed input: answer 400 and close. Node's lenient spots (bare LF
* line endings, obsolete folding) are NOT accepted; SEMANTICS.md states
* the bound. */
static void scr_http_conn_bad_request(ScrHttpConn *conn) {
static const char bad[] =
"HTTP/1.1 400 Bad Request\r\nConnection: close\r\nContent-Length: 0\r\n\r\n";
scr_net_sock_write_native(conn->sock, bad, sizeof bad - 1);
/* Parser errors answer Node's fixed responses and close: 400 for
* malformed input, 431 when a head or trailer block exceeds the field or
* byte bound. Node's lenient spots (bare LF line endings, obsolete
* folding) are NOT accepted; SEMANTICS.md states the bound. */
static void scr_http_conn_reject(ScrHttpConn *conn, const char *response, size_t len) {
scr_net_sock_write_native(conn->sock, response, len);
scr_net_sock_end(conn->sock);
conn->len = 0;
scr_http_conn_drop_request(conn, false);
}
static void scr_http_conn_bad_request(ScrHttpConn *conn) {
static const char bad[] = "HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n";
scr_http_conn_reject(conn, bad, sizeof bad - 1);
}
static void scr_http_conn_header_overflow(ScrHttpConn *conn) {
static const char overflow[] =
"HTTP/1.1 431 Request Header Fields Too Large\r\nConnection: close\r\n\r\n";
scr_http_conn_reject(conn, overflow, sizeof overflow - 1);
}
/* Node answers a missing HTTP/1.1 Host through ServerResponse rather than
* the parser-error path: the 400 is chunked and includes Date. */
static void scr_http_conn_missing_host(ScrHttpConn *conn) {
@@ -2436,9 +2530,10 @@ static void scr_http_conn_missing_host(ScrHttpConn *conn) {
* feed, and the h2 compat DATA-frame feed). */
static void scr_http_req_deliver(ScrHttpReq *r, const char *data, size_t n) {
if (!r || n == 0 || r->ended) return;
if (r->paused) {
/* req.pause(): the parser keeps consuming (memory is the buffer —
* a documented bound), delivery waits for resume()'s drain */
r->body_seen = true;
if (scr_http_req_holding(r)) {
/* paused: the parser keeps consuming (memory is the buffer — a
* documented bound), delivery waits for the drain */
if (r->pend_len + n > r->pend_cap) {
size_t cap = r->pend_cap ? r->pend_cap : 4096;
while (cap < r->pend_len + n) cap *= 2;
@@ -2493,7 +2588,13 @@ static void scr_http_conn_body_done(ScrHttpConn *conn) {
return;
}
ScrHttpReq *r = conn->req;
if (r && !r->ended && conn->res && conn->res->finished) {
/* the response finished while the body was arriving */
scr_http_conn_end_after_response(conn);
return;
}
if (r) {
r->received = true;
scr_http_req_retain(r);
scr_http_req_finish(r, true);
scr_http_req_release(r);
@@ -2506,8 +2607,9 @@ static void scr_http_conn_body_done(ScrHttpConn *conn) {
}
/* Parses one complete request HEAD sitting at buf[0..head_len). Returns
* false on malformed input (already answered). */
static bool scr_http_conn_parse_head(ScrHttpConn *conn, size_t head_len) {
* false on malformed input, with *overflow set when the head carries more
* fields than the parser accepts. */
static bool scr_http_conn_parse_head(ScrHttpConn *conn, size_t head_len, bool *overflow) {
const char *buf = conn->buf;
const char *line_end = memchr(buf, '\r', head_len);
if (!line_end || line_end + 1 >= buf + head_len || line_end[1] != '\n') return false;
@@ -2527,6 +2629,7 @@ static bool scr_http_conn_parse_head(ScrHttpConn *conn, size_t head_len) {
if (!req) scr_http_oom();
req->rc = 1;
req->status = -1; /* Node: statusCode is undefined on server requests */
req->gated = true;
req->join_dup = conn->srv != NULL && conn->srv->join_dup;
req->sock = scr_net_sock_retain(conn->sock);
#ifdef SCR_RC_AUDIT
@@ -2563,11 +2666,11 @@ static bool scr_http_conn_parse_head(ScrHttpConn *conn, size_t head_len) {
p = eol + 2;
}
if (!ok || req->nheaders > SCR_HTTP_MAX_HEADER_FIELDS) {
*overflow = ok;
scr_http_req_release(req);
return false;
}
conn->head_bytes = head_len;
conn->head_fields = req->nheaders;
conn->trailer_bytes = 0;
conn->trailer_fields = 0;
@@ -2763,14 +2866,14 @@ static void scr_http_conn_pump(ScrHttpConn *conn) {
if (!hit) {
if (conn->len > SCR_HTTP_MAX_HEADER_BYTES) {
if (conn->client_mode) scr_http_client_head_overflow(conn);
else scr_http_conn_bad_request(conn); /* header cap */
else scr_http_conn_header_overflow(conn);
}
return;
}
size_t head_len = (size_t)(hit - conn->buf) + 4;
if (head_len > SCR_HTTP_MAX_HEADER_BYTES) {
if (conn->client_mode) scr_http_client_head_overflow(conn);
else scr_http_conn_bad_request(conn);
else scr_http_conn_header_overflow(conn);
return;
}
if (conn->client_mode) {
@@ -2784,8 +2887,10 @@ static void scr_http_conn_pump(ScrHttpConn *conn) {
* response may still be streaming (keep-alive pipelining waits) */
if (conn->res != NULL && !conn->res->finished) return;
if (conn->res != NULL) scr_http_conn_drop_request(conn, false);
if (!scr_http_conn_parse_head(conn, head_len)) {
scr_http_conn_bad_request(conn);
bool overflow = false;
if (!scr_http_conn_parse_head(conn, head_len, &overflow)) {
if (overflow) scr_http_conn_header_overflow(conn);
else scr_http_conn_bad_request(conn);
return;
}
continue;
@@ -2878,7 +2983,7 @@ static void scr_http_conn_pump(ScrHttpConn *conn) {
if (conn->buf[0] == '\r' && conn->buf[1] == '\n') {
if (!scr_http_conn_trailer_fits(conn, 2, false)) {
if (conn->client_mode) scr_http_client_head_overflow(conn);
else scr_http_conn_bad_request(conn);
else scr_http_conn_header_overflow(conn);
return;
}
memmove(conn->buf, conn->buf + 2, conn->len - 2);
@@ -2897,7 +3002,7 @@ static void scr_http_conn_pump(ScrHttpConn *conn) {
if (!eol) {
if (!scr_http_conn_trailer_fits(conn, conn->len, false)) {
if (conn->client_mode) scr_http_client_head_overflow(conn);
else scr_http_conn_bad_request(conn);
else scr_http_conn_header_overflow(conn);
}
return;
}
@@ -2925,7 +3030,7 @@ static void scr_http_conn_pump(ScrHttpConn *conn) {
size_t consumed = (size_t)(eol - conn->buf) + 2;
if (!scr_http_conn_trailer_fits(conn, consumed, true)) {
if (conn->client_mode) scr_http_client_head_overflow(conn);
else scr_http_conn_bad_request(conn);
else scr_http_conn_header_overflow(conn);
return;
}
/* An early server response releases the request but deliberately leaves
@@ -2957,7 +3062,13 @@ static void scr_http_conn_data(void *ctx, const char *buf, size_t n) {
}
memcpy(conn->buf + conn->len, buf, n);
conn->len += n;
conn->pumping = true;
scr_http_conn_pump(conn);
conn->pumping = false;
if (conn->res_close_deferred) {
conn->res_close_deferred = false;
if (conn->res) scr_http_queue_res_close(conn->res);
}
}
static void scr_http_client_eof(ScrHttpConn *conn);
@@ -3200,7 +3311,7 @@ ScrStr *scr_http_req_http_version(ScrHttpReq *r) {
double scr_http_req_http_version_major(ScrHttpReq *r) { return r->http2 ? 2 : 1; }
double scr_http_req_http_version_minor(ScrHttpReq *r) { return r->http2 || r->http10 ? 0 : 1; }
bool scr_http_req_aborted_flag(ScrHttpReq *r) { return r->aborted; }
bool scr_http_req_complete(ScrHttpReq *r) { return r->ended && !r->aborted; }
bool scr_http_req_complete(ScrHttpReq *r) { return (r->ended || r->received) && !r->aborted; }
/* The stream died with the response side open: 'aborted' fires NOW (the
* teardown macrotask, Node's position), then the close rides the sweep. */
@@ -4051,7 +4162,6 @@ static bool scr_http_client_parse_head(ScrHttpConn *conn, size_t head_len) {
return false;
}
conn->head_bytes = head_len;
conn->head_fields = res->nheaders;
conn->trailer_bytes = 0;
conn->trailer_fields = 0;
@@ -0,0 +1,17 @@
// Small Buffers own exact-size ArrayBuffers here, while Node allocates them
// from a shared 64 KiB pool, so buf.buffer.byteLength and buf.byteOffset
// differ. Reading a Buffer's window of its ArrayBuffer through byteOffset
// and length gives the same bytes either way, and Buffer.alloc is never
// pooled.
const small = Buffer.from("hello");
const copy = small.buffer.slice(small.byteOffset, small.byteOffset + small.length);
console.log(copy.byteLength, new TextDecoder().decode(new Uint8Array(copy)));
const view = new Uint8Array(small.buffer, small.byteOffset, small.length);
console.log(Array.from(view).join(","));
const joined = Buffer.concat([Buffer.from("ab"), Buffer.from("cd")]);
const data = new DataView(joined.buffer, joined.byteOffset, joined.length);
console.log(data.getUint16(0), data.byteLength);
const unsafe = Buffer.allocUnsafe(4).fill(7);
console.log(Array.from(new Uint8Array(unsafe.buffer, unsafe.byteOffset, 4)).join(","));
const zeroed = Buffer.alloc(16, 1);
console.log(zeroed.buffer.byteLength, zeroed.byteOffset);
+12
View File
@@ -0,0 +1,12 @@
// Object.keys over util.types lists the object's own keys in Node's order;
// over the node:util/types module namespace it lists the export names
// sorted, with "default".
import * as util from "node:util";
import { types } from "node:util";
import * as typesNamespace from "node:util/types";
console.log(Object.keys(util.types));
const keys = Object.keys(types);
console.log(keys.length, keys[0], keys[keys.length - 1]);
console.log(Object.keys(typesNamespace).join(","));
console.log(keys.every((key) => key.startsWith("is")), keys.includes("isMap"));
@@ -0,0 +1,55 @@
// The HTTP server accepts up to 1000 header fields per block (Node's
// default with server.maxHeadersCount unset) and answers 431 past that,
// for the request head and for a chunked request's trailer block. A
// malformed request line answers 400. Both error responses carry only
// Connection: close.
import { createServer } from "node:http";
import { connect } from "node:net";
function fields(name: string, count: number): string {
let out = "";
for (let i = 0; i < count; i++) out += `${name}${i}: v\r\n`;
return out;
}
const server = createServer((req, res) => {
let size = 0;
req.on("data", (chunk: Buffer) => {
size += chunk.length;
});
req.on("end", () => {
res.end(`fields=${req.rawHeaders.length / 2} trailers=${req.rawTrailers.length / 2} body=${size}`);
});
});
function exchange(port: number, wire: string): Promise<string> {
return new Promise<string>((resolve) => {
let response = "";
const socket = connect({ port, host: "127.0.0.1" }, () => {
socket.write(wire);
});
socket.on("data", (chunk: Buffer) => {
response += chunk.toString("latin1");
});
socket.on("close", () => resolve(response));
});
}
function show(label: string, response: string): void {
const status = response.slice(0, response.indexOf("\r\n"));
if (status.includes(" 200 ")) console.log(label, status, response.slice(response.indexOf("\r\n\r\n") + 4));
else console.log(label, JSON.stringify(response));
}
server.listen(0, "127.0.0.1", async () => {
const { port } = server.address() as { port: number };
const head = "GET / HTTP/1.1\r\nHost: x\r\nConnection: close\r\n";
show("head 1000", await exchange(port, head + fields("x-h", 998) + "\r\n"));
show("head 1001", await exchange(port, head + fields("x-h", 999) + "\r\n"));
show("head 1500", await exchange(port, head + fields("x-h", 1498) + "\r\n"));
const post = "POST / HTTP/1.1\r\nHost: x\r\nConnection: close\r\nTransfer-Encoding: chunked\r\n";
show("trailers 1000", await exchange(port, post + fields("x-h", 500) + "\r\n3\r\nabc\r\n0\r\n" + fields("x-t", 1000) + "\r\n"));
show("trailers 1001", await exchange(port, post + "\r\n3\r\nabc\r\n0\r\n" + fields("x-t", 1001) + "\r\n"));
show("malformed", await exchange(port, "GARBAGE\r\n\r\n"));
server.close();
});
@@ -0,0 +1,58 @@
// A request the handler never reads still emits 'end': when the response
// finishes, the server dumps the unread body and the request ends, before
// the response's 'close' for an empty body and after it otherwise. A body
// that arrives with the head reaches 'data' listeners even when the
// handler responds first, and resume() alone also ends the request.
import { createServer } from "node:http";
import { connect } from "node:net";
const server = createServer((req, res) => {
const tag = `${req.method} ${req.url}`;
req.on("end", () => console.log("req end", tag, req.complete));
req.on("close", () => console.log("req close", tag));
res.on("finish", () => console.log("res finish", tag));
res.on("close", () => console.log("res close", tag));
if (req.url === "/data") {
req.on("data", (chunk: Buffer) => console.log("data", tag, chunk.toString()));
}
if (req.url === "/resume") req.resume();
if (req.url === "/late") {
setTimeout(() => res.end("late"), 20);
return;
}
res.end("ok");
});
const wires = [
"GET / HTTP/1.1\r\nHost: x\r\nConnection: close\r\n\r\n",
"POST /cl0 HTTP/1.1\r\nHost: x\r\nConnection: close\r\nContent-Length: 0\r\n\r\n",
"POST /chunk0 HTTP/1.1\r\nHost: x\r\nConnection: close\r\nTransfer-Encoding: chunked\r\n\r\n0\r\n\r\n",
"POST /chunk HTTP/1.1\r\nHost: x\r\nConnection: close\r\nTransfer-Encoding: chunked\r\n\r\n3\r\nabc\r\n0\r\n\r\n",
"POST /body HTTP/1.1\r\nHost: x\r\nConnection: close\r\nContent-Length: 3\r\n\r\nabc",
"POST /data HTTP/1.1\r\nHost: x\r\nConnection: close\r\nContent-Length: 3\r\n\r\nabc",
"POST /resume HTTP/1.1\r\nHost: x\r\nConnection: close\r\nContent-Length: 3\r\n\r\nabc",
"GET /resume HTTP/1.1\r\nHost: x\r\nConnection: close\r\n\r\n",
"POST /late HTTP/1.1\r\nHost: x\r\nConnection: close\r\nContent-Length: 3\r\n\r\nabc",
];
function exchange(port: number, wire: string): Promise<void> {
return new Promise<void>((resolve) => {
const socket = connect({ port, host: "127.0.0.1" }, () => {
socket.write(wire);
});
socket.on("data", () => {});
socket.on("close", () => resolve());
});
}
const pause = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms));
server.listen(0, "127.0.0.1", async () => {
const { port } = server.address() as { port: number };
for (const wire of wires) {
await exchange(port, wire);
await pause(20);
console.log("--");
}
server.close();
});
@@ -0,0 +1,57 @@
// Server requests start paused: body bytes wait for a consumer, so a 'data'
// listener attached after a timer still receives the whole body before
// 'end'. A request the handler never reads ends once the response
// finishes, on keep-alive connections too.
import { createServer, request } from "node:http";
const server = createServer((req, res) => {
const tag = `${req.method} ${req.url}`;
req.on("end", () => console.log("req end", tag));
req.on("close", () => console.log("req close", tag));
res.on("finish", () => console.log("res finish", tag));
res.on("close", () => console.log("res close", tag));
if (req.url === "/delayed") {
setTimeout(() => {
let body = "";
req.on("data", (chunk: Buffer) => {
body += chunk.toString();
});
req.on("end", () => res.end(`got ${body}`));
}, 30);
return;
}
res.end("ok");
});
const cases: [string, string, string][] = [
["GET", "/", ""],
["POST", "/", "body"],
["POST", "/delayed", "held body"],
["GET", "/keep", ""],
["POST", "/keep", "body"],
];
server.listen(0, "127.0.0.1", async () => {
const { port } = server.address() as { port: number };
for (const [method, path, body] of cases) {
await new Promise<void>((resolve) => {
const headers: Record<string, string> = path === "/keep" ? {} : { connection: "close" };
const req = request({ host: "127.0.0.1", port, method, path, headers: headers }, (res) => {
let text = "";
res.on("data", (chunk: Buffer) => {
text += chunk.toString();
});
res.on("end", () => {
console.log("client", method, path, res.statusCode, text);
resolve();
});
});
if (body) req.end(body);
else req.end();
});
await new Promise<void>((resolve) => setTimeout(resolve, 30));
console.log("--");
}
server.close();
server.closeAllConnections();
});
+4 -2
View File
@@ -154,13 +154,15 @@ describe(`server differential (${cases.length} programs${sanitize ? ", sanitized
120_000,
);
test("http-trailers rejects an aggregate trailer overflow", async () => {
test("http-trailers answers 431 for a trailer block over the field limit", async () => {
const entry = join(fixturesRoot, "cases/http-trailers/main.ts");
const driver = join(fixturesRoot, "cases/http-trailers/limit-driver.mjs");
const binary = await build(entry);
const nativeRes = await runLane(binary, [], driver);
expect(nativeRes.exitCode).toBe(0);
expect(nativeRes.driverStdout).toBe("limit HTTP/1.1 400 Bad Request\nlimit driver done\n");
expect(nativeRes.driverStdout).toBe(
"limit HTTP/1.1 431 Request Header Fields Too Large\nlimit driver done\n",
);
}, 120_000);
test("net-reuse-port emits the additive LLVM ABI", async () => {