mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-24 06:14:01 +08:00
Fix HTTP server timeout property lowering
This commit is contained in:
+3
-1
@@ -2138,11 +2138,13 @@ declare module "http" {
|
||||
};
|
||||
/* The options-record stance (the RequestOptions precedent): every
|
||||
* documented key typechecks; the lowering's option walk decides per
|
||||
* key — requireHostHeader: false and joinDuplicateHeaders lower,
|
||||
* key — requireHostHeader: false, joinDuplicateHeaders, and
|
||||
* keepAliveTimeoutBuffer lower,
|
||||
* documented-but-unlowered keys fence by name, unknown keys drop. */
|
||||
export interface ServerOptions {
|
||||
requireHostHeader?: boolean;
|
||||
joinDuplicateHeaders?: boolean;
|
||||
keepAliveTimeoutBuffer?: number;
|
||||
[option: string]: unknown;
|
||||
}
|
||||
/* The outgoing-header shape, @types/node's matrix: numbers format via
|
||||
|
||||
@@ -4469,6 +4469,8 @@ export function emitExpr(E: CEmitter, e: IrExpr): Temp {
|
||||
case "http.serverTimeoutSet":
|
||||
E.line(`scr_net_server_timeout_set(${arg(0)}, ${arg(1)}, ${arg(2)});${E.srcComment(e.loc)}`);
|
||||
return { name: "", type: e.type };
|
||||
case "http.serverTimeoutOptionSet":
|
||||
return finish(`scr_net_server_timeout_option_set(${arg(0)}, ${arg(1)}, ${arg(2)})`);
|
||||
case "net.serverOnListening": {
|
||||
const cb = args[1]!;
|
||||
E.moveTemp(cb);
|
||||
|
||||
@@ -688,6 +688,7 @@ const LIB_FN_SYMS: Record<string, string> = {
|
||||
"http.serverJoinDupHeaders": "scr_http_server_join_duplicate_headers",
|
||||
"http.serverTimeoutGet": "scr_net_server_timeout_get",
|
||||
"http.serverTimeoutSet": "scr_net_server_timeout_set",
|
||||
"http.serverTimeoutOptionSet": "scr_net_server_timeout_option_set",
|
||||
"http.clientWrite": "scr_http_client_write_str",
|
||||
"http.clientWriteBytes": "scr_http_client_write_bytes",
|
||||
"http.clientEnd": "scr_http_client_end",
|
||||
|
||||
@@ -2001,7 +2001,7 @@ function lowerH2StreamMethodCall(L: Lowerer, call: ts.CallExpression,
|
||||
* the two reads never materializes; the runtime answers the bound port
|
||||
* directly. Null for every other property shape. */
|
||||
export function lowerServerProperty(L: Lowerer, expr: ts.PropertyAccessExpression): IrExpr | null {
|
||||
if (expr.questionDotToken) return null;
|
||||
if (expr.questionDotToken && !L.chainHandled.has(expr)) return null;
|
||||
const loc = locOf(expr);
|
||||
// req.url / req.method — always-present strings on server requests;
|
||||
// req.headers.NAME — the `string | undefined` union (envGet's type).
|
||||
@@ -2277,13 +2277,15 @@ function lowerRequestHandlerArg(L: Lowerer, node: ts.Expression): IrExpr {
|
||||
}
|
||||
|
||||
/** The http.createServer / http.Server options object — { requireHostHeader?,
|
||||
* joinDuplicateHeaders?, ... }: the two lowered keys are exactly the
|
||||
* parser behaviors this runtime has (requireHostHeader: false IS the
|
||||
* parser's stance — it never answers 400 for a missing Host;
|
||||
* joinDuplicateHeaders: true joins repeated request-header reads with
|
||||
* ", "). Other documented keys fence by name; unknown keys drop like
|
||||
* Node drops them. Answers whether joinDuplicateHeaders was requested. */
|
||||
function lowerHttpServerOptions(L: Lowerer, node: ts.Expression, what: string): { joinDup: boolean } {
|
||||
* joinDuplicateHeaders?, keepAliveTimeoutBuffer?, ... }. The first two
|
||||
* select parser behavior; keepAliveTimeoutBuffer initializes the matching
|
||||
* writable server field after Node's non-negative-safe-integer validation.
|
||||
* Other documented keys fence by name; unknown keys drop like Node drops
|
||||
* them. */
|
||||
function lowerHttpServerOptions(L: Lowerer, node: ts.Expression, what: string): {
|
||||
joinDup: boolean;
|
||||
keepAliveTimeoutBuffer: IrExpr | null;
|
||||
} {
|
||||
if (!ts.isObjectLiteralExpression(node)) {
|
||||
L.noLowering(
|
||||
`${what} with a non-literal options argument`,
|
||||
@@ -2292,6 +2294,7 @@ function lowerHttpServerOptions(L: Lowerer, node: ts.Expression, what: string):
|
||||
);
|
||||
}
|
||||
let joinDup = false;
|
||||
let keepAliveTimeoutBuffer: IrExpr | null = null;
|
||||
for (const prop of node.properties) {
|
||||
let initializer: ts.Expression | null;
|
||||
if (ts.isPropertyAssignment(prop) &&
|
||||
@@ -2337,20 +2340,34 @@ function lowerHttpServerOptions(L: Lowerer, node: ts.Expression, what: string):
|
||||
"the lowered forms are the literals joinDuplicateHeaders: true (repeats join \", \") and false (the keep-first default)",
|
||||
);
|
||||
}
|
||||
if (key === "keepAliveTimeoutBuffer") {
|
||||
if (keepAliveTimeoutBuffer !== null) {
|
||||
L.noLowering(
|
||||
`${what} with duplicate keepAliveTimeoutBuffer options`,
|
||||
prop,
|
||||
"write keepAliveTimeoutBuffer once in the options literal",
|
||||
);
|
||||
}
|
||||
const value = initializer !== null
|
||||
? L.lowerExpr(initializer)
|
||||
: L.lowerShorthandValue(prop as ts.ShorthandPropertyAssignment);
|
||||
keepAliveTimeoutBuffer = L.coerceToExpected(value, F64);
|
||||
continue;
|
||||
}
|
||||
fenceOrDropOptionKey(
|
||||
L, prop, key, what, HTTP_SERVER_DOCUMENTED_OPTIONS,
|
||||
"requireHostHeader: false and joinDuplicateHeaders are the supported options",
|
||||
"requireHostHeader: false, joinDuplicateHeaders, and keepAliveTimeoutBuffer are the supported options",
|
||||
);
|
||||
// An undocumented key, dropped like Node drops it.
|
||||
}
|
||||
return { joinDup };
|
||||
return { joinDup, keepAliveTimeoutBuffer };
|
||||
}
|
||||
|
||||
/** The shared createServer([options][, handler]) shapes behind
|
||||
* http.createServer, http.Server, and `new http.Server`: no arguments
|
||||
* (the on("request") route), a lone handler, a lone options object, and
|
||||
* (options, handler). joinDuplicateHeaders wraps the construction in the
|
||||
* flag-setting helper. */
|
||||
* (options, handler). Modeled option values wrap the construction in an
|
||||
* interned option-setting helper. */
|
||||
function lowerHttpCreateServerForms(L: Lowerer, expr: ts.CallExpression | ts.NewExpression,
|
||||
what: string, loc: SrcLoc,): IrExpr {
|
||||
const args = expr.arguments ?? ([] as unknown as ts.NodeArray<ts.Expression>);
|
||||
@@ -2370,36 +2387,62 @@ function lowerHttpCreateServerForms(L: Lowerer, expr: ts.CallExpression | ts.New
|
||||
if (ts.isObjectLiteralExpression(args[0]!)) optsNode = args[0]!;
|
||||
else handlerNode = args[0]!;
|
||||
}
|
||||
const joinDup = optsNode !== null && lowerHttpServerOptions(L, optsNode, what).joinDup;
|
||||
const opts = optsNode !== null
|
||||
? lowerHttpServerOptions(L, optsNode, what)
|
||||
: { joinDup: false, keepAliveTimeoutBuffer: null };
|
||||
const server: IrExpr = handlerNode !== null
|
||||
? { kind: "libCall", fn: "http.createServer", args: [lowerRequestHandlerArg(L, handlerNode)], type: NETSERVER_T, loc }
|
||||
: { kind: "libCall", fn: "http.createServerEmpty", args: [], type: NETSERVER_T, loc };
|
||||
if (!joinDup) return server;
|
||||
// The flag-setting composition: an interned helper takes the fresh
|
||||
// server, sets the parser flag, and answers it — one expression.
|
||||
const key = `server.joindup`;
|
||||
if (!opts.joinDup && opts.keepAliveTimeoutBuffer === null) return server;
|
||||
// The option-setting composition: an interned helper takes the option
|
||||
// values first (preserving Node's options-before-handler evaluation
|
||||
// order), then the fresh server, applies the modeled fields, and answers
|
||||
// the server as the constructor/factory result.
|
||||
const hasBuffer = opts.keepAliveTimeoutBuffer !== null;
|
||||
const key = `server.opts:${opts.joinDup ? 1 : 0}:${hasBuffer ? 1 : 0}`;
|
||||
const existing = L.arrHofHelpers.get(key);
|
||||
const name = existing ?? `%server.joindup.${L.arrHofHelpers.size}`;
|
||||
const name = existing ?? `%server.opts.${L.arrHofHelpers.size}`;
|
||||
if (!existing) {
|
||||
L.arrHofHelpers.set(key, name);
|
||||
const params = [
|
||||
...(hasBuffer ? [{ localId: "b.0", name: "buffer", type: F64 }] : []),
|
||||
{ localId: "s.0", name: "s", type: NETSERVER_T },
|
||||
];
|
||||
const ref: IrExpr = { kind: "varRef", localId: "s.0", type: NETSERVER_T, loc };
|
||||
const body: IrStmt[] = [];
|
||||
if (hasBuffer) {
|
||||
const selector: IrExpr = { kind: "numLit", value: 4, type: F64, loc };
|
||||
const value: IrExpr = { kind: "varRef", localId: "b.0", type: F64, loc };
|
||||
body.push({
|
||||
kind: "exprStmt",
|
||||
expr: { kind: "libCall", fn: "http.serverTimeoutOptionSet", args: [ref, selector, value], type: VOID, loc },
|
||||
loc,
|
||||
});
|
||||
}
|
||||
if (opts.joinDup) {
|
||||
body.push({
|
||||
kind: "exprStmt",
|
||||
expr: { kind: "libCall", fn: "http.serverJoinDupHeaders", args: [ref], type: VOID, loc },
|
||||
loc,
|
||||
});
|
||||
}
|
||||
body.push({ kind: "return", value: ref, loc });
|
||||
L.liftedFns.push({
|
||||
name,
|
||||
params: [{ localId: "s.0", name: "s", type: NETSERVER_T }],
|
||||
params,
|
||||
returnType: NETSERVER_T,
|
||||
locals: [{ id: "s.0", name: "s", type: NETSERVER_T, mutable: false }],
|
||||
body: [
|
||||
{
|
||||
kind: "exprStmt",
|
||||
expr: { kind: "libCall", fn: "http.serverJoinDupHeaders", args: [ref], type: VOID, loc },
|
||||
loc,
|
||||
},
|
||||
{ kind: "return", value: ref, loc },
|
||||
],
|
||||
locals: params.map((p) => ({ id: p.localId, name: p.name, type: p.type, mutable: false })),
|
||||
body,
|
||||
loc,
|
||||
});
|
||||
}
|
||||
return { kind: "call", callee: name, args: [server], type: NETSERVER_T, loc };
|
||||
return {
|
||||
kind: "call",
|
||||
callee: name,
|
||||
args: [...(opts.keepAliveTimeoutBuffer !== null ? [opts.keepAliveTimeoutBuffer] : []), server],
|
||||
type: NETSERVER_T,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** `new http.Server([options][, handler])` — the constructor spelling of
|
||||
|
||||
@@ -195,12 +195,13 @@ export const HTTP_CLIENT_DOCUMENTED_OPTIONS: ReadonlySet<string> = new Set([
|
||||
|
||||
/** http.createServer / http.Server's documented option keys (Node v24 —
|
||||
* http.createServer options + the net.Server knobs it forwards). The
|
||||
* lowered pair is requireHostHeader/joinDuplicateHeaders; the rest fence
|
||||
* by name here, and unknown keys drop like Node drops them. */
|
||||
* lowered set is requireHostHeader/joinDuplicateHeaders plus
|
||||
* keepAliveTimeoutBuffer storage; the rest fence by name here, and
|
||||
* unknown keys drop like Node drops them. */
|
||||
export const HTTP_SERVER_DOCUMENTED_OPTIONS: ReadonlySet<string> = new Set([
|
||||
"IncomingMessage", "ServerResponse", "allowHalfOpen", "connectionsCheckingInterval",
|
||||
"headersTimeout", "highWaterMark", "insecureHTTPParser", "joinDuplicateHeaders",
|
||||
"keepAlive", "keepAliveInitialDelay", "keepAliveTimeout", "maxHeaderSize",
|
||||
"keepAlive", "keepAliveInitialDelay", "keepAliveTimeout", "keepAliveTimeoutBuffer", "maxHeaderSize",
|
||||
"noDelay", "pauseOnConnect", "rejectNonStandardBodyWrites", "requestTimeout",
|
||||
"requireHostHeader", "uniqueHeaders",
|
||||
]);
|
||||
|
||||
@@ -2341,6 +2341,7 @@ export type IrLibFn =
|
||||
* the property values; timer enforcement remains outside this surface. */
|
||||
| "http.serverTimeoutGet"
|
||||
| "http.serverTimeoutSet"
|
||||
| "http.serverTimeoutOptionSet"
|
||||
/** server.on("listening", cb) — the deferred listen-callback list. */
|
||||
| "net.serverOnListening"
|
||||
/** The ServerResponse member surface: statusCode/statusMessage reads
|
||||
@@ -6758,6 +6759,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
|
||||
"dc.chanBindStore",
|
||||
"dc.chanRunStores",
|
||||
"http.resWriteHeadDyn",
|
||||
"http.serverTimeoutOptionSet",
|
||||
"net.sockSetEncoding",
|
||||
"http.reqSetEncoding",
|
||||
"fs.readFileSyncBuf",
|
||||
|
||||
@@ -427,6 +427,7 @@ export const LIB_FN_SIGS: Record<IrLibFn, { argTypes: (IrType | null)[]; result:
|
||||
"http.serverJoinDupHeaders": { argTypes: [NETSERVER_T], result: VOID },
|
||||
"http.serverTimeoutGet": { argTypes: [NETSERVER_T, F64], result: F64 },
|
||||
"http.serverTimeoutSet": { argTypes: [NETSERVER_T, F64, F64], result: VOID },
|
||||
"http.serverTimeoutOptionSet": { argTypes: [NETSERVER_T, F64, F64], result: VOID },
|
||||
"net.serverOnListening": { argTypes: [NETSERVER_T, { kind: "func", params: [], ret: VOID }, BOOL], result: VOID },
|
||||
"http.resStatusGet": { argTypes: [HTTPRES_T], result: F64 },
|
||||
"http.resStatusSet": { argTypes: [HTTPRES_T, F64], result: VOID },
|
||||
|
||||
@@ -1400,6 +1400,37 @@ void scr_net_server_timeout_set(ScrNetServer *s, double field, double value) {
|
||||
s->http_timeouts[i] = value;
|
||||
}
|
||||
|
||||
/* Constructor-option validation is intentionally separate from the plain
|
||||
* writable-property setter above. Node validates these option values as
|
||||
* non-negative safe integers, while later property assignments store the
|
||||
* number directly. */
|
||||
void scr_net_server_timeout_option_set(ScrNetServer *s, double field, double value) {
|
||||
static const char *const names[] = {
|
||||
"timeout", "keepAliveTimeout", "headersTimeout", "requestTimeout",
|
||||
"keepAliveTimeoutBuffer",
|
||||
};
|
||||
int i = (int)field;
|
||||
if (i < 0 || i >= 5) abort(); /* compiler/runtime ABI violation */
|
||||
char recv[48], msg[224];
|
||||
if (!(isfinite(value) && trunc(value) == value)) {
|
||||
scr_num_received(value, recv);
|
||||
int len = snprintf(msg, sizeof msg,
|
||||
"The value of \"%s\" is out of range. It must be an integer. Received %s",
|
||||
names[i], recv);
|
||||
scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)len, "ERR_OUT_OF_RANGE");
|
||||
return;
|
||||
}
|
||||
if (value < 0 || value > 9007199254740991.0) {
|
||||
scr_num_received(value, recv);
|
||||
int len = snprintf(msg, sizeof msg,
|
||||
"The value of \"%s\" is out of range. It must be >= 0 && <= 9007199254740991. Received %s",
|
||||
names[i], recv);
|
||||
scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)len, "ERR_OUT_OF_RANGE");
|
||||
return;
|
||||
}
|
||||
s->http_timeouts[i] = value;
|
||||
}
|
||||
|
||||
/* address()'s other two fields — the bound host ('::'/'0.0.0.0' for the
|
||||
* host-less any, the normalized explicit host otherwise) and the family
|
||||
* string. Answer the any-form defaults before listen (Node answers null
|
||||
@@ -3321,13 +3352,25 @@ static ScrDyn *scr_net_dynh_srv_invoke(void *h, ScrDyn *self, const char *method
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static int scr_net_dynh_srv_timeout_field(const char *key) {
|
||||
if (strcmp(key, "timeout") == 0) return 0;
|
||||
if (strcmp(key, "keepAliveTimeout") == 0) return 1;
|
||||
if (strcmp(key, "headersTimeout") == 0) return 2;
|
||||
if (strcmp(key, "requestTimeout") == 0) return 3;
|
||||
if (strcmp(key, "keepAliveTimeoutBuffer") == 0) return 4;
|
||||
return -1;
|
||||
}
|
||||
|
||||
static ScrDyn *scr_net_dynh_srv_get(void *h, const char *key, size_t key_len) {
|
||||
ScrNetServer *s = (ScrNetServer *)h;
|
||||
(void)key_len;
|
||||
if (strcmp(key, "listening") == 0) return scr_dyn_new_bool(s->listening);
|
||||
int timeout_field = scr_net_dynh_srv_timeout_field(key);
|
||||
if (timeout_field >= 0) {
|
||||
return scr_dyn_new_num(scr_net_server_timeout_get(s, (double)timeout_field));
|
||||
}
|
||||
{
|
||||
static const char *const known[] = { "maxConnections", "connections", "maxHeadersCount",
|
||||
"timeout", "keepAliveTimeout", "headersTimeout", "requestTimeout", NULL };
|
||||
static const char *const known[] = { "maxConnections", "connections", "maxHeadersCount", NULL };
|
||||
for (size_t i = 0; known[i]; i++) {
|
||||
if (strcmp(key, known[i]) == 0) {
|
||||
scr_net_dynh_srv_unsupported(key, NULL);
|
||||
@@ -3339,7 +3382,17 @@ static ScrDyn *scr_net_dynh_srv_get(void *h, const char *key, size_t key_len) {
|
||||
}
|
||||
|
||||
static bool scr_net_dynh_srv_set(void *h, const char *key, size_t key_len, const ScrDyn *value) {
|
||||
(void)h; (void)key; (void)key_len; (void)value;
|
||||
ScrNetServer *s = (ScrNetServer *)h;
|
||||
(void)key_len;
|
||||
int timeout_field = scr_net_dynh_srv_timeout_field(key);
|
||||
if (timeout_field >= 0) {
|
||||
if (value->kind != SCR_DYN_NUM) {
|
||||
scr_dyn_arg_type_fail(key, "of type number", value);
|
||||
return true; /* handled: the exception is pending */
|
||||
}
|
||||
scr_net_server_timeout_set(s, (double)timeout_field, value->v.num);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -5118,6 +5118,9 @@ double scr_net_server_port(ScrNetServer *s); /* address().port */
|
||||
* keepAliveTimeoutBuffer. */
|
||||
double scr_net_server_timeout_get(ScrNetServer *s, double field);
|
||||
void scr_net_server_timeout_set(ScrNetServer *s, double field, double value);
|
||||
/* Constructor-option twin: validates a non-negative safe integer before
|
||||
* storing, matching Node's option ladder (plain property writes do not). */
|
||||
void scr_net_server_timeout_option_set(ScrNetServer *s, double field, double value);
|
||||
ScrStr *scr_net_server_addr_ip(ScrNetServer *s); /* +1 — address().address */
|
||||
ScrStr *scr_net_server_addr_family(ScrNetServer *s); /* +1 — address().family */
|
||||
void scr_net_server_close(ScrNetServer *s, ScrClosure *cb /*moves, nullable*/);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// The five writable numeric http.Server timeout fields: Node 24 defaults,
|
||||
// independent per-server storage, and static reads/writes through a typed
|
||||
// helper. Timer enforcement is a separate server behavior surface.
|
||||
// independent per-server storage, constructor initialization, optional and
|
||||
// dynamic access, and static reads/writes through a typed helper. Timer
|
||||
// enforcement is a separate server behavior surface.
|
||||
import { createServer, Server } from "node:http";
|
||||
|
||||
function configure(server: Server): void {
|
||||
@@ -13,6 +14,7 @@ function configure(server: Server): void {
|
||||
|
||||
const configured = createServer();
|
||||
const untouched = new Server();
|
||||
const fromOption = createServer({ keepAliveTimeoutBuffer: 4321 });
|
||||
|
||||
console.log(
|
||||
configured.timeout,
|
||||
@@ -37,3 +39,25 @@ console.log(
|
||||
untouched.headersTimeout,
|
||||
untouched.requestTimeout,
|
||||
);
|
||||
|
||||
console.log("option", fromOption.keepAliveTimeoutBuffer);
|
||||
|
||||
function logOptional(server: Server | undefined): void {
|
||||
console.log("optional", server?.keepAliveTimeoutBuffer);
|
||||
}
|
||||
logOptional(fromOption);
|
||||
logOptional(undefined);
|
||||
|
||||
const dynamic: any = fromOption;
|
||||
console.log("dynamic", dynamic.keepAliveTimeoutBuffer);
|
||||
dynamic.keepAliveTimeoutBuffer = 8765;
|
||||
console.log("dynamic-set", dynamic.keepAliveTimeoutBuffer, fromOption.keepAliveTimeoutBuffer);
|
||||
|
||||
try {
|
||||
createServer({ keepAliveTimeoutBuffer: -1 });
|
||||
} catch (err) {
|
||||
if (err instanceof Error) {
|
||||
const code = (err as NodeJS.ErrnoException).code;
|
||||
console.log(err.name, code, err.message);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user