Files
scriptc/packages/runtime/src/scr_http.c
T
Chris Tate a8ae330957 Release the in-progress client response when its trailers overflow
A chunked response whose trailers ran past the header limit failed the
request with "Parse Error: Header overflow" and marked the exchange done
while the parser still held the response. The socket-close cleanup then
skipped it, so the response, its socket and the parser kept one another
alive, and every such response leaked. The overflow path now settles the
response ('aborted' and 'close', as a premature close does) and drops the
parser's reference before the exchange completes.

Corpus 5652 repeats response trailer and head overflows and request
trailer overflows; under SCRIPTC_SAN=1 the RC audit fails it without this
change.
2026-10-15 18:35:02 -05:00

6368 lines
252 KiB
C

/* node:http — the SERVER and CLIENT slices, layered on scr_net.c's
* native hooks: a hand-written HTTP/1.1 parser (request OR status line,
* headers, Content-Length / chunked / read-to-EOF bodies, keep-alive)
* and Node-shaped serializers (the response's writeHead/setHeader/end
* with Content-Length or chunked framing, the Date header, keep-alive
* Connection headers; the client's request head with Node's exact
* header order and framing decisions).
*
* The CLIENT (scr_http_request — http.request/http.get): one dialed
* connection per request, NO agent pooling (the wire still carries
* Node's Connection: keep-alive; the socket closes when the response
* completes — SEMANTICS.md). The response is an ordinary ScrHttpReq
* (IncomingMessage) parsed in RESPONSE mode: status line instead of
* request line, and a body that may be EOF-delimited; HEAD/204/304
* responses have none. Error shapes are Node's: the net layer's
* 'connect ECONNREFUSED ip:port' via the socket's native error hook, a
* premature close before any response is 'socket hang up' on the
* request, and mid-body death is 'aborted' on the RESPONSE (the request
* just closes) — all pinned by the client differential fixtures.
* Deferred 'close' emits ride the emit QUEUE drained from scr_net.c's
* proto sweep, so req/res 'close' fire a pass after the work that
* flagged them, Node's later-than-the-handler ordering (client order:
* res 'end', req 'close', res 'close').
* NO external dependencies — the parser implements exactly what the
* differential fixtures and portless-shaped handlers exercise, and
* SEMANTICS.md states its bounds honestly.
*
* Object model: http.createServer returns an ORDINARY ScrNetServer (the
* frontend maps http.Server to the same netServer kind — listen/close/
* address()/error all reuse the net lowering) whose native-connection
* hook installs one ScrHttpConn parser per accepted socket. ScrHttpReq /
* ScrHttpRes are lean refcounted handles (the ScrChild story): the req
* holds the parsed method/url/headers and the body listener lists
* (dropped when the body completes or the connection dies); the res
* holds the socket (+1), the pending header list, and the framing state.
*
* Keep-alive: HTTP/1.1 requests keep the connection open (1.0 opts in
* with Connection: keep-alive) and the parser resets for the next
* request on the same socket, pipelining included; Connection: close —
* either side — ends the socket after the response finishes. An open
* connection idles for keepAliveTimeout + keepAliveTimeoutBuffer (6 s by
* default) after each response, then times out like Node's.
*
* Event dispatch rides scr_net.c wholesale: the request handler and the
* req 'data'/'end' listeners fire from the socket read path (macrotasks,
* snapshot semantics, once-before-run), and errors/teardown follow the
* socket's own story. */
#include "scr_runtime.h"
#include <ctype.h>
#include <math.h> /* INFINITY — the Agent's maxSockets default */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
/* 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 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 or SCR_HTTP_MAX_HEADER_SIZE, like Node. */
#define SCR_HTTP_MAX_HEADER_BYTES 65536u
#define SCR_HTTP_MAX_HEADER_FIELDS 1000u
/* Node's default maxHeaderSize (http.maxHeaderSize): the request target
* plus every header name and value, without separators or line endings,
* must stay below it. */
#define SCR_HTTP_MAX_HEADER_SIZE 16384u
static void scr_http_oom(void) {
fputs("scriptc: out of memory\n", stderr);
abort();
}
static bool scr_http_timeout_valid(double ms) {
if (isfinite(ms) && ms >= 0) return true;
char recv[48], msg[176];
scr_num_received(ms, recv);
int len = snprintf(msg, sizeof msg,
"The value of \"msecs\" is out of range. It must be a non-negative finite number. Received %s", recv);
scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)len, "ERR_OUT_OF_RANGE");
return false;
}
/* Node v24.22.0's STATUS_CODES table, shared by the module export and the
* default ServerResponse reason phrase. */
static const struct { int code; const char *reason; } scr_http_status_codes_table[] = {
{100, "Continue"}, {101, "Switching Protocols"}, {102, "Processing"},
{103, "Early Hints"}, {200, "OK"}, {201, "Created"}, {202, "Accepted"},
{203, "Non-Authoritative Information"}, {204, "No Content"},
{205, "Reset Content"}, {206, "Partial Content"}, {207, "Multi-Status"},
{208, "Already Reported"}, {226, "IM Used"}, {300, "Multiple Choices"},
{301, "Moved Permanently"}, {302, "Found"}, {303, "See Other"},
{304, "Not Modified"}, {305, "Use Proxy"}, {307, "Temporary Redirect"},
{308, "Permanent Redirect"}, {400, "Bad Request"}, {401, "Unauthorized"},
{402, "Payment Required"}, {403, "Forbidden"}, {404, "Not Found"},
{405, "Method Not Allowed"}, {406, "Not Acceptable"},
{407, "Proxy Authentication Required"}, {408, "Request Timeout"},
{409, "Conflict"}, {410, "Gone"}, {411, "Length Required"},
{412, "Precondition Failed"}, {413, "Payload Too Large"},
{414, "URI Too Long"}, {415, "Unsupported Media Type"},
{416, "Range Not Satisfiable"}, {417, "Expectation Failed"},
{418, "I'm a Teapot"}, {421, "Misdirected Request"},
{422, "Unprocessable Entity"}, {423, "Locked"},
{424, "Failed Dependency"}, {425, "Too Early"},
{426, "Upgrade Required"}, {428, "Precondition Required"},
{429, "Too Many Requests"}, {431, "Request Header Fields Too Large"},
{451, "Unavailable For Legal Reasons"}, {500, "Internal Server Error"},
{501, "Not Implemented"}, {502, "Bad Gateway"},
{503, "Service Unavailable"}, {504, "Gateway Timeout"},
{505, "HTTP Version Not Supported"}, {506, "Variant Also Negotiates"},
{507, "Insufficient Storage"}, {508, "Loop Detected"},
{509, "Bandwidth Limit Exceeded"}, {510, "Not Extended"},
{511, "Network Authentication Required"},
};
static const char *scr_http_reason(int code) {
for (size_t i = 0; i < sizeof scr_http_status_codes_table / sizeof scr_http_status_codes_table[0]; i++) {
if (scr_http_status_codes_table[i].code == code) return scr_http_status_codes_table[i].reason;
}
return "unknown";
}
static SCR_TL ScrDyn *scr_http_status_codes_value;
static void scr_http_status_codes_cleanup(void) {
scr_dyn_release(scr_http_status_codes_value);
scr_http_status_codes_value = NULL;
}
ScrDyn *scr_http_status_codes(void) {
if (!scr_http_status_codes_value) {
ScrDyn *codes = scr_dyn_new_obj();
for (size_t i = 0; i < sizeof scr_http_status_codes_table / sizeof scr_http_status_codes_table[0]; i++) {
const int code = scr_http_status_codes_table[i].code;
const char *reason = scr_http_status_codes_table[i].reason;
char key[4];
const int key_len = snprintf(key, sizeof key, "%d", code);
ScrStr *value = scr_str_new(reason, strlen(reason));
scr_dyn_obj_set(codes, key, (size_t)key_len, scr_dyn_new_str(value));
scr_str_release(value);
}
scr_http_status_codes_value = codes;
scr_atexit(scr_http_status_codes_cleanup);
}
return scr_dyn_retain(scr_http_status_codes_value);
}
static const char *const scr_http_methods_table[] = {
"ACL", "BIND", "CHECKOUT", "CONNECT", "COPY", "DELETE", "GET", "HEAD",
"LINK", "LOCK", "M-SEARCH", "MERGE", "MKACTIVITY", "MKCALENDAR",
"MKCOL", "MOVE", "NOTIFY", "OPTIONS", "PATCH", "POST", "PROPFIND",
"PROPPATCH", "PURGE", "PUT", "QUERY", "REBIND", "REPORT", "SEARCH",
"SOURCE", "SUBSCRIBE", "TRACE", "UNBIND", "UNLINK", "UNLOCK", "UNSUBSCRIBE",
};
static SCR_TL ScrArr *scr_http_methods_value;
static void scr_http_methods_cleanup(void) {
scr_arr_release(scr_http_methods_value);
scr_http_methods_value = NULL;
}
ScrArr *scr_http_methods(void) {
if (!scr_http_methods_value) {
const size_t n = sizeof scr_http_methods_table / sizeof scr_http_methods_table[0];
ScrArr *methods = scr_arr_new(SCR_ELEM_STR, n);
for (size_t i = 0; i < n; i++) {
const char *name = scr_http_methods_table[i];
scr_arr_push_ref(methods, scr_str_new(name, strlen(name)));
}
scr_http_methods_value = methods;
scr_atexit(scr_http_methods_cleanup);
}
return scr_arr_retain(scr_http_methods_value);
}
/* ── the h2 compat transport seam ────────────────────────────────────────
*
* Http2ServerRequest/Http2ServerResponse ARE these req/res handles: the
* http/1 surface is the API template, the h2 stream machinery is the
* transport. scr_http2.c (linked exactly when the program uses the real
* h2 surface — this unit can link WITHOUT it) installs a vtable for the
* response write paths (HEADERS/DATA frames instead of HTTP/1 bytes) and
* a request-registration hook so server.on("request", ...) on an
* h2-tagged server ctx routes to the h2 request list. The vtable's
* typedef lives in scr_runtime.h (both units name it). */
static SCR_TL const ScrHttpH2Ops *scr_http_h2_ops = NULL;
static SCR_TL void (*scr_http_h2_request_hook)(void *h2ctx, ScrClosure *cb /*moves*/,
void *fn, bool once) = NULL;
static SCR_TL void (*scr_http_h2_connect_hook)(void *h2ctx, ScrClosure *cb /*moves*/,
void *h1_fn, void *h2_fn, bool once) = NULL;
static SCR_TL void (*scr_http_h2_upgrade_hook)(void *h2ctx, ScrClosure *cb /*moves*/,
void *fn, bool once) = NULL;
void scr_http_set_h2_ops(const ScrHttpH2Ops *ops) { scr_http_h2_ops = ops; }
void scr_http_set_h2_request_hook(void (*hook)(void *h2ctx, ScrClosure *cb, void *fn, bool once)) {
scr_http_h2_request_hook = hook;
}
void scr_http_set_h2_connect_hook(void (*hook)(void *h2ctx, ScrClosure *cb,
void *h1_fn, void *h2_fn, bool once)) {
scr_http_h2_connect_hook = hook;
}
void scr_http_set_h2_upgrade_hook(void (*hook)(void *h2ctx, ScrClosure *cb, void *fn, bool once)) {
scr_http_h2_upgrade_hook = hook;
}
/* ── the request handle ──────────────────────────────────────────────── */
struct ScrHttpReq {
size_t rc;
int status; /* -1 on server requests (Node's undefined statusCode) */
ScrStr *method;
ScrStr *url;
ScrNetSocket *sock; /* +1 — req.socket, may be NULL defensively */
void *h2_stream; /* +1 through scr_http_h2_ops — the h2 compat request's
* stream (destroy() RSTs it, never the shared socket) */
ScrStr **hnames; /* lowercased */
ScrStr **hnames_raw; /* arrival case — rawHeaders */
ScrStr **hvalues;
size_t nheaders;
ScrStr **tnames; /* parsed chunk trailers: lowercased, separate from headers */
ScrStr **tnames_raw; /* wire case for rawTrailers */
ScrStr **tvalues;
size_t ntrailers;
ScrStr *status_msg; /* client responses' reason phrase; NULL on server requests */
ScrNetLs data_ls, end_ls, err_ls, close_ls, timeout_ls;
/* pipe destinations (req.pipe(...) — one of each kind, +1; released at
* finish, so a piped destination never outlives the body) */
ScrHttpRes *pipe_res;
struct ScrHttpClientReq *pipe_client;
ScrNetSocket *pipe_sock;
bool ended; /* body complete (or connection dead): data/end drop */
bool end_consumed; /* 'end' fired on a body read through data listeners or a pipe */
bool enc_utf8; /* setEncoding('utf8'): 'data' delivers strings */
bool http10; /* the parsed request/status line's version (httpVersion) */
bool http2; /* an h2 compat request (httpVersion "2.0") */
bool aborted; /* h2: the stream died with our writable side open */
bool close_queued;
bool close_emitted; /* settled: err/close listeners dropped */
bool join_dup; /* joinDuplicateHeaders: repeated names read joined ", " */
/* pause(): delivery holds — arrived body bytes buffer in pend (the
* parser keeps consuming; memory is the buffer, a documented bound)
* and 'end' defers (end_pending) until resume() drains through the
* emit queue, never the resuming stack. */
bool paused;
bool end_pending;
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 */
};
#ifdef SCR_RC_AUDIT
static SCR_TL long scr_http_live = 0;
long scr_http_live_count(void) { return scr_http_live; }
#endif
ScrHttpReq *scr_http_req_retain(ScrHttpReq *r) {
if (r->rc != SIZE_MAX) r->rc++;
return r;
}
/* req.setEncoding(enc) — IncomingMessage's readable setEncoding (server
* requests and client responses alike): utf8 flips 'data' delivery to
* strings (the chunk-encoding window); other REAL Node encodings throw
* the "not supported yet" Error; unknown names throw Node's
* ERR_UNKNOWN_ENCODING TypeError. */
void scr_http_req_set_encoding(ScrHttpReq *r, ScrStr *enc /*borrowed*/) {
if ((enc->len == 4 && memcmp(enc->data, "utf8", 4) == 0) ||
(enc->len == 5 && memcmp(enc->data, "utf-8", 5) == 0)) {
r->enc_utf8 = true;
return;
}
static const char *const known[] = { "ascii", "latin1", "binary", "base64",
"base64url", "hex", "ucs2", "ucs-2", "utf16le", "utf-16le", NULL };
for (size_t i = 0; known[i]; i++) {
if (enc->len == strlen(known[i]) && memcmp(enc->data, known[i], enc->len) == 0) {
char msg[128];
int n = snprintf(msg, sizeof msg, "setEncoding('%s') is not supported yet (only 'utf8' here)",
known[i]);
scr_throw_error_msg(SCR_ERR_ERROR, msg, (size_t)n);
return;
}
}
char msg[128];
int n = snprintf(msg, sizeof msg, "Unknown encoding: %.*s",
(int)(enc->len < 64 ? enc->len : 64), enc->data);
scr_throw_error_msg_code(SCR_ERR_TYPE, msg, (size_t)n, "ERR_UNKNOWN_ENCODING");
}
void scr_http_req_release(ScrHttpReq *r) {
if (!r || r->rc == SIZE_MAX) return;
if (--r->rc == 0) {
scr_str_release(r->method);
scr_str_release(r->url);
for (size_t i = 0; i < r->nheaders; i++) {
scr_str_release(r->hnames[i]);
scr_str_release(r->hnames_raw[i]);
scr_str_release(r->hvalues[i]);
}
free(r->hnames);
free(r->hnames_raw);
free(r->hvalues);
for (size_t i = 0; i < r->ntrailers; i++) {
scr_str_release(r->tnames[i]);
scr_str_release(r->tnames_raw[i]);
scr_str_release(r->tvalues[i]);
}
free(r->tnames);
free(r->tnames_raw);
free(r->tvalues);
scr_str_release(r->status_msg);
scr_net_ls_drop(&r->data_ls);
scr_net_ls_drop(&r->end_ls);
scr_net_ls_drop(&r->err_ls);
scr_net_ls_drop(&r->close_ls);
scr_net_ls_drop(&r->timeout_ls);
scr_net_ls_drop(&r->aborted_ls);
scr_http_res_release(r->pipe_res);
scr_http_client_release(r->pipe_client);
scr_net_sock_release(r->pipe_sock);
free(r->pend);
if (r->sock) scr_net_sock_release(r->sock);
if (r->h2_stream) scr_http_h2_ops->release(r->h2_stream);
#ifdef SCR_RC_AUDIT
scr_http_live--;
#endif
free(r);
}
}
void *scr_http_req_retain_v(void *p) { return scr_http_req_retain((ScrHttpReq *)p); }
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 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 (tolower((unsigned char)lower->data[j]) != tolower((unsigned char)name->data[j])) return false;
}
return true;
}
static bool scr_http_trailer_single_value(const ScrStr *name);
/* The value Node's IncomingMessage stores for names[index]'s field
* (_addHeaderLine): repeated fields join with ", " ("; " for cookie),
* except that the single-value fields (host, content-type, ...) keep the
* first unless joinDuplicateHeaders is set. set-cookie joins with ", " in
* this string form; its array form is scr_http_req_header_list. Always
* +1. */
static ScrStr *scr_http_field_value(ScrStr *const *names, ScrStr *const *values, size_t count,
size_t index, bool join_dup) {
const ScrStr *name = names[index];
if (!join_dup && scr_http_trailer_single_value(name)) return scr_str_retain(values[index]);
size_t total = 0, n = 0;
for (size_t i = 0; i < count; i++) {
if (!scr_http_name_eq(names[i], name)) continue;
total += values[i]->len;
n++;
}
if (n == 1) return scr_str_retain(values[index]);
bool cookie = name->len == 6 && memcmp(name->data, "cookie", 6) == 0;
const char *sep = cookie ? "; " : ", ";
char *buf = malloc(total + (n - 1) * 2);
if (!buf) scr_http_oom();
size_t off = 0;
for (size_t i = 0; i < count; i++) {
if (!scr_http_name_eq(names[i], name)) continue;
if (off) {
memcpy(buf + off, sep, 2);
off += 2;
}
memcpy(buf + off, values[i]->data, values[i]->len);
off += values[i]->len;
}
ScrStr *value = scr_str_new(buf, off);
free(buf);
return value;
}
/* The snapshot pairs behind `{ ...req.headers }` (the record-building
* helper's feed): [lowercased name, value, ...] in arrival order — the
* same keys the per-name reads answer: each name once, at its first
* position, with scr_http_field_value's value. Always a fresh +1 array. */
ScrArr *scr_http_req_header_pairs(ScrHttpReq *r) {
ScrArr *out = scr_arr_new(SCR_ELEM_STR, r->nheaders * 2);
for (size_t i = 0; i < r->nheaders; i++) {
bool seen = false;
for (size_t k = 0; k < i && !seen; k++) seen = scr_http_name_eq(r->hnames[k], r->hnames[i]);
if (seen) continue;
scr_arr_push_ref(out, scr_str_retain(r->hnames[i]));
scr_arr_push_ref(out, scr_http_field_value(r->hnames, r->hvalues, r->nheaders, i, r->join_dup));
}
return out;
}
/* Every header line as [lowercased name, value, ...] in arrival order,
* repeated names included: the fetch Response's Headers feed. Always a
* fresh +1 array. */
ScrArr *scr_http_req_header_lines(ScrHttpReq *r) {
ScrArr *out = scr_arr_new(SCR_ELEM_STR, r->nheaders * 2);
for (size_t i = 0; i < r->nheaders; i++) {
scr_arr_push_ref(out, scr_str_retain(r->hnames[i]));
scr_arr_push_ref(out, scr_str_retain(r->hvalues[i]));
}
return out;
}
/* req.rawHeaders: [name, value, name, value, ...] in arrival order, names
* in their ORIGINAL case (Node's shape). Always a fresh +1 array. */
ScrArr *scr_http_req_raw_headers(ScrHttpReq *r) {
ScrArr *out = scr_arr_new(SCR_ELEM_STR, r->nheaders * 2);
for (size_t i = 0; i < r->nheaders; i++) {
scr_arr_push_ref(out, scr_str_retain(r->hnames_raw[i]));
scr_arr_push_ref(out, scr_str_retain(r->hvalues[i]));
}
return out;
}
static ScrArr *scr_http_req_distinct_values(ScrStr *const *names, ScrStr *const *values,
size_t count, ScrStr *name) {
ScrArr *out = NULL;
for (size_t i = 0; i < count; i++) {
if (!scr_http_name_eq(names[i], name)) continue;
if (!out) out = scr_arr_new(SCR_ELEM_STR, count);
scr_arr_push_ref(out, scr_str_retain(values[i]));
}
return out;
}
ScrArr *scr_http_req_header_values(ScrHttpReq *r, ScrStr *name /*borrowed*/) {
return scr_http_req_distinct_values(r->hnames, r->hvalues, r->nheaders, name);
}
/* Every value of a header in arrival order, as Node stores set-cookie:
* a fresh +1 array, empty when the header is absent. */
ScrArr *scr_http_req_header_list(ScrHttpReq *r, ScrStr *name /*borrowed*/) {
ScrArr *out = scr_http_req_header_values(r, name);
return out ? out : scr_arr_new(SCR_ELEM_STR, 0);
}
ScrArr *scr_http_req_trailer_values(ScrHttpReq *r, ScrStr *name /*borrowed*/) {
return r->ended ? scr_http_req_distinct_values(r->tnames, r->tvalues, r->ntrailers, name) : NULL;
}
/* Trailers become visible only once the body reaches its natural end.
* Node keeps the first value of these single-value field names and joins
* other repeated fields (scr_http_field_value), in headers and trailers
* alike. */
static bool scr_http_trailer_single_value(const ScrStr *name) {
static const char *const names[] = { "age", "authorization", "content-length",
"content-type", "etag", "expires", "from", "host", "if-modified-since",
"if-unmodified-since", "last-modified", "location", "max-forwards",
"proxy-authorization", "referer", "retry-after", "server", "user-agent", NULL };
for (size_t i = 0; names[i]; i++) {
if (name->len == strlen(names[i]) && memcmp(name->data, names[i], name->len) == 0) return true;
}
return false;
}
static ScrStr *scr_http_req_trailer_at(ScrHttpReq *r, size_t index) {
return scr_http_field_value(r->tnames, r->tvalues, r->ntrailers, index, false);
}
ScrStr *scr_http_req_trailer(ScrHttpReq *r, ScrStr *name /*borrowed*/) {
if (!r->ended) return NULL;
for (size_t i = 0; i < r->ntrailers; i++) {
if (scr_http_name_eq(r->tnames[i], name)) return scr_http_req_trailer_at(r, i);
}
return NULL;
}
ScrArr *scr_http_req_trailer_pairs(ScrHttpReq *r) {
ScrArr *out = scr_arr_new(SCR_ELEM_STR, r->ended ? r->ntrailers * 2 : 0);
if (!r->ended) return out;
for (size_t i = 0; i < r->ntrailers; i++) {
bool seen = false;
for (size_t j = 0; j < i && !seen; j++) seen = scr_http_name_eq(r->tnames[j], r->tnames[i]);
if (seen) continue;
scr_arr_push_ref(out, scr_str_retain(r->tnames[i]));
scr_arr_push_ref(out, scr_http_req_trailer_at(r, i));
}
return out;
}
ScrArr *scr_http_req_raw_trailers(ScrHttpReq *r) {
ScrArr *out = scr_arr_new(SCR_ELEM_STR, r->ended ? r->ntrailers * 2 : 0);
if (!r->ended) return out;
for (size_t i = 0; i < r->ntrailers; i++) {
scr_arr_push_ref(out, scr_str_retain(r->tnames_raw[i]));
scr_arr_push_ref(out, scr_str_retain(r->tvalues[i]));
}
return out;
}
/* res.statusMessage: the reason phrase on client responses (+1, "" when
* the status line carried none); NULL on server requests — the
* compiler's undefined arm, the statusCode split. */
ScrStr *scr_http_req_status_message(ScrHttpReq *r) {
return r->status_msg ? scr_str_retain(r->status_msg) : NULL;
}
/* Header lookup by (case-insensitively matched) name: +1 value, or NULL —
* the compiler's undefined arm, exactly process.envGet's contract. */
ScrStr *scr_http_req_header(ScrHttpReq *r, ScrStr *name) {
for (size_t i = 0; i < r->nheaders; i++) {
if (scr_http_name_eq(r->hnames[i], name))
return scr_http_field_value(r->hnames, r->hvalues, r->nheaders, i, r->join_dup);
}
return NULL;
}
/* req.pipe(dest) — the proxy legs: an IncomingMessage body streams into
* a ServerResponse, a ClientRequest, or a raw socket (chunk-for-chunk, no
* 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);
return;
}
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
* the SOURCE socket turns raw chunks into response body writes (the
* response's own framing applies) and EOF into res.end(), pipe's
* default. The ctx owns the response (+1, released with the socket's
* native-reader teardown); errors/closes need no handling here — the
* caller registers its own socket listeners (the portless cleanup). */
typedef struct {
ScrHttpRes *res; /* +1 */
} ScrSockResPipe;
static void scr_http_sock_res_data(void *ctx, const char *buf, size_t n) {
ScrSockResPipe *p = (ScrSockResPipe *)ctx;
ScrBytes *chunk = scr_bytes_new(SCR_BYTES_U8, (double)n);
if (n > 0) memcpy(chunk->data, buf, n);
scr_http_res_write_bytes(p->res, chunk);
scr_bytes_release(chunk);
}
static void scr_http_sock_res_eof(void *ctx) {
ScrSockResPipe *p = (ScrSockResPipe *)ctx;
scr_http_res_end(p->res);
}
static void scr_http_sock_res_closed(void *ctx) { (void)ctx; }
static void scr_http_sock_res_free(void *ctx) {
ScrSockResPipe *p = (ScrSockResPipe *)ctx;
scr_http_res_release(p->res);
free(p);
}
void scr_http_sock_pipe_res(ScrNetSocket *src, ScrHttpRes *dst /*borrowed*/) {
ScrSockResPipe *p = calloc(1, sizeof *p);
if (!p) scr_http_oom();
p->res = scr_http_res_retain(dst);
scr_net_sock_set_native_reader(src, &scr_http_sock_res_data, &scr_http_sock_res_eof,
&scr_http_sock_res_closed, p, &scr_http_sock_res_free);
}
void scr_http_req_pipe_client(ScrHttpReq *r, ScrHttpClientReq *dst /*borrowed*/) {
if (r->ended) {
scr_http_client_end(dst);
return;
}
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*/) {
if (r->ended) {
scr_net_sock_end(dst);
return;
}
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) {
if (r->ended) {
scr_closure_release(cb);
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) {
if (r->ended) {
scr_closure_release(cb);
return;
}
scr_net_ls_add(&r->end_ls, cb, NULL, once);
}
double scr_http_req_status(ScrHttpReq *r) { return (double)r->status; }
ScrNetSocket *scr_http_req_socket(ScrHttpReq *r) {
/* +1; a NULL socket cannot reach the program (every req is built over
* a live connection), but stay defensive for the emitter's contract */
return r->sock ? scr_net_sock_retain(r->sock) : NULL;
}
/* the deferred-emit queue lives below (the proto sweep) — resume()'s
* drain rides it; the parser's deliver and the cork flush live below
* their callers too */
static void scr_http_emit_push(int kind, void *h /*moves +1*/);
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
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;
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;
* a completed body's 'end' waits too). */
void scr_http_req_pause(ScrHttpReq *r) {
if (!r->ended) r->paused = true;
}
/* req.setTimeout(ms[, cb]): Node registers the callback on the message,
* then arms the socket's idle timer. An incomplete server request can
* handle the timeout and keep its connection alive. */
void scr_http_req_set_timeout(ScrHttpReq *r, double ms, ScrClosure *cb /*moves, nullable*/) {
if (!scr_http_timeout_valid(ms)) {
if (cb) scr_closure_release(cb);
return;
}
if (r->ended || r->destroyed || r->close_emitted || !r->sock) {
/* the message is done — Node no-ops there (never arms, never fires) */
if (cb) scr_closure_release(cb);
return;
}
scr_net_sock_set_timeout(r->sock, ms);
if (cb) {
if (r->status < 0 && !r->h2_stream) scr_net_ls_add(&r->timeout_ls, cb, NULL, false);
else scr_net_sock_on_timeout(r->sock, cb, true);
}
}
void scr_http_req_set_timeout_plain(ScrHttpReq *r, double ms) {
scr_http_req_set_timeout(r, ms, NULL);
}
bool scr_http_req_destroyed_flag(ScrHttpReq *r) { return r->destroyed; }
bool scr_http_req_readable(ScrHttpReq *r) { return !r->ended && !r->destroyed; }
/* destroy(): tears the underlying connection down NOW; the teardown
* events flow through the socket's native hooks like a peer close. */
void scr_http_req_destroy(ScrHttpReq *r) {
r->destroyed = true;
if (r->h2_stream != NULL) {
scr_http_h2_ops->destroy(r->h2_stream);
return;
}
if (r->sock) scr_net_sock_destroy(r->sock);
}
void scr_http_req_on_error(ScrHttpReq *r, ScrClosure *cb /*moves*/, ScrChildErrFn fn, bool once) {
if (r->close_emitted) {
scr_closure_release(cb);
return;
}
scr_net_ls_add(&r->err_ls, cb, (void *)fn, once);
}
void scr_http_req_on_close(ScrHttpReq *r, ScrClosure *cb /*moves*/, bool once) {
if (r->close_emitted) {
scr_closure_release(cb);
return;
}
scr_net_ls_add(&r->close_ls, cb, NULL, once);
}
/* req.on('aborted') — the h2 compat event (an http/1 request registers
* too; the parser lane never fires it, matching its 'error'-only story). */
void scr_http_req_on_aborted(ScrHttpReq *r, ScrClosure *cb /*moves*/, bool once) {
if (r->close_emitted || r->aborted) {
scr_closure_release(cb);
return;
}
scr_net_ls_add(&r->aborted_ls, cb, NULL, once);
}
/* Body completion: 'end' fires, then both listener lists drop (the
* settle-releases-listeners story — a handler closure capturing its own
* req cannot cycle past the body). */
static void scr_http_req_finish(ScrHttpReq *r, bool fire) {
if (r->ended) return;
if (fire && scr_http_req_holding(r)) {
/* a held body holds 'end' too — the drain finishes the body */
r->end_pending = true;
return;
}
r->ended = true;
r->end_consumed = fire && (r->data_ls.n > 0 || r->pipe_res || r->pipe_client || r->pipe_sock);
if (fire) scr_net_fire0_this(&r->end_ls, r, SCR_DYNH_HTTP_REQ);
scr_net_ls_drop(&r->data_ls);
scr_net_ls_drop(&r->end_ls);
/* pipes: a NATURAL end ends the destination (Node's pipe end:true
* default); an aborted body just drops the edge — the destination's
* own error/close story is already running */
if (r->pipe_res) {
if (fire) scr_http_res_end(r->pipe_res);
scr_http_res_release(r->pipe_res);
r->pipe_res = NULL;
}
if (r->pipe_client) {
if (fire) scr_http_client_end(r->pipe_client);
scr_http_client_release(r->pipe_client);
r->pipe_client = NULL;
}
if (r->pipe_sock) {
if (fire) scr_net_sock_end(r->pipe_sock);
scr_net_sock_release(r->pipe_sock);
r->pipe_sock = NULL;
}
}
/* A pending outgoing trailer block belongs to exactly one message.
* Repeated addTrailers calls replace it, as Node's _trailer assignment does. */
typedef struct {
ScrStr **names, **values;
size_t len;
} ScrHttpTrailers;
static void scr_http_trailers_clear(ScrHttpTrailers *t) {
for (size_t i = 0; i < t->len; i++) {
scr_str_release(t->names[i]);
scr_str_release(t->values[i]);
}
free(t->names);
free(t->values);
t->names = t->values = NULL;
t->len = 0;
}
static bool scr_http_token_char(unsigned char ch) {
return (ch >= '0' && ch <= '9') || (ch >= 'A' && ch <= 'Z') ||
(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) {
scr_jb_write(&b, shown->data, shown->len);
} else {
scr_jb_puts(&b, "Header name");
}
scr_jb_puts(&b, " must be a valid HTTP token [\"");
scr_jb_write(&b, name->data, name->len);
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 \"");
scr_jb_write(&b, name->data, name->len);
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 [\"");
scr_jb_write(&b, name->data, name->len);
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);
if (n % 2 != 0) {
static const char msg[] = "The argument 'headers' is invalid.";
scr_throw_error_msg_code(SCR_ERR_TYPE, msg, sizeof msg - 1, "ERR_INVALID_ARG_VALUE");
return false;
}
t->names = calloc(n / 2, sizeof *t->names);
t->values = calloc(n / 2, sizeof *t->values);
if (n > 0 && (!t->names || !t->values)) scr_http_oom();
for (size_t i = 0; i < n; i += 2) {
ScrStr *name = (ScrStr *)scr_arr_get_ref(pairs, (double)i);
ScrStr *value = (ScrStr *)scr_arr_get_ref(pairs, (double)(i + 1));
bool valid = name->len > 0;
for (size_t j = 0; j < name->len; j++) valid &= scr_http_token_char((unsigned char)name->data[j]);
if (!valid) {
char msg[256];
int len = snprintf(msg, sizeof msg, "Trailer name must be a valid HTTP token [\"%.*s\"]",
(int)(name->len < 160 ? name->len : 160), name->data);
scr_throw_error_msg_code(SCR_ERR_TYPE, msg, (size_t)len, "ERR_INVALID_HTTP_TOKEN");
} else {
for (size_t j = 0; j < value->len; j++) {
unsigned char ch = (unsigned char)value->data[j];
if ((ch < 32 && ch != '\t') || ch == 127) { valid = false; break; }
}
if (!valid) {
char msg[256];
int len = snprintf(msg, sizeof msg, "Invalid character in trailer content [\"%.*s\"]",
(int)(name->len < 160 ? name->len : 160), name->data);
scr_throw_error_msg_code(SCR_ERR_TYPE, msg, (size_t)len, "ERR_INVALID_CHAR");
}
}
if (!valid) {
scr_str_release(name);
scr_str_release(value);
return false;
}
t->names[t->len] = name;
t->values[t->len] = value;
t->len++;
}
return true;
}
/* ── the response handle ─────────────────────────────────────────────── */
struct ScrHttpRes {
size_t rc;
ScrNetSocket *sock; /* +1; writes after the connection died are no-ops */
void *h2_stream; /* +1 through scr_http_h2_ops — set on h2 compat
* responses; every write path routes to frames */
struct ScrHttpConn *conn; /* backref, no ref (conn outlives via socket ctx) */
int status;
ScrStr *status_msg; /* res.statusMessage / writeHead's reason phrase;
* NULL = the status code's default phrase */
ScrStr **hnames; /* as-set (serialized verbatim, Node keeps the case) */
ScrStr **hvalues;
uint8_t *hkinds; /* SCR_HTTP_HV_*: how getHeader answers each line */
size_t nheaders, cap_headers;
ScrHttpTrailers trailers;
bool head_sent;
bool chunked;
bool finished;
bool no_date; /* res.sendDate = false: suppress the implicit Date header */
bool strict_content_length;
size_t strict_bytes_written; /* Node counts UTF-8 bytes, not JS characters */
bool keep_alive; /* the REQUEST's verdict; Connection: close overrides */
bool destroyed; /* res.destroy()/teardown — res.destroyed (true in 'close') */
bool socket_detached; /* finish/close removes the response's socket view */
/* cork()/uncork(): corked counts the nesting (res.writableCorked);
* writes while corked coalesce in cork_buf and flush as ONE write when
* the count reaches zero (or at end()) — Node's coalescing, and the
* h2 lane's single DATA frame. */
int corked;
char *cork_buf;
size_t cork_len, cork_cap;
/* A write while corked commits the head (headersSent, Node's implicit
* header) and holds its bytes here until the flush sends them first. */
char *held_head;
size_t held_head_len, held_head_cap;
/* res.req: the paired request (+1; req never points back — no cycle).
* A `res.req = null` write clears the READ (req_cleared) — Node's
* plain-property write. */
ScrHttpReq *req_ref;
bool no_body; /* HEAD request or a 1xx/204/304 status: Node's _hasBody false */
bool req_cleared;
ScrNetLs close_ls, timeout_ls;
ScrNetLs finish_ls; /* res.on('finish') — fires deferred once the body went out */
ScrNetLs endcb_ls; /* res.end(cb) — fires just before the 'finish' listeners */
ScrNetLs wcb_ls; /* res.write(chunk, cb) — fires from the queue */
bool writable_finished; /* set where Node emits 'finish' (writableFinished) */
bool finish_queued;
bool wcb_queued;
bool close_queued;
bool close_emitted;
};
ScrHttpRes *scr_http_res_retain(ScrHttpRes *r) {
if (r->rc != SIZE_MAX) r->rc++;
return r;
}
void scr_http_res_release(ScrHttpRes *r) {
if (!r || r->rc == SIZE_MAX) return;
if (--r->rc == 0) {
for (size_t i = 0; i < r->nheaders; i++) {
scr_str_release(r->hnames[i]);
scr_str_release(r->hvalues[i]);
}
free(r->hnames);
free(r->hvalues);
free(r->hkinds);
scr_http_trailers_clear(&r->trailers);
scr_str_release(r->status_msg);
scr_net_ls_drop(&r->close_ls);
scr_net_ls_drop(&r->timeout_ls);
scr_net_ls_drop(&r->finish_ls);
scr_net_ls_drop(&r->endcb_ls);
scr_net_ls_drop(&r->wcb_ls);
free(r->cork_buf);
free(r->held_head);
if (r->req_ref) scr_http_req_release(r->req_ref);
if (r->sock) scr_net_sock_release(r->sock);
if (r->h2_stream) scr_http_h2_ops->release(r->h2_stream);
#ifdef SCR_RC_AUDIT
scr_http_live--;
#endif
free(r);
}
}
void *scr_http_res_retain_v(void *p) { return scr_http_res_retain((ScrHttpRes *)p); }
void scr_http_res_release_v(void *p) { scr_http_res_release((ScrHttpRes *)p); }
bool scr_http_res_headers_sent(ScrHttpRes *r) { return r->head_sent; }
bool scr_http_res_writable_ended(ScrHttpRes *r) { return r->finished; }
bool scr_http_res_send_date(ScrHttpRes *r) { return !r->no_date; }
void scr_http_res_set_send_date(ScrHttpRes *r, bool value) { r->no_date = !value; }
bool scr_http_res_strict_content_length(ScrHttpRes *r) { return r->strict_content_length; }
void scr_http_res_set_strict_content_length(ScrHttpRes *r, bool value) { r->strict_content_length = value; }
ScrHttpReq *scr_http_res_request(ScrHttpRes *r) { return scr_http_req_retain(r->req_ref); }
ScrNetSocket *scr_http_res_socket(ScrHttpRes *r) {
return r->sock && !r->socket_detached ? scr_net_sock_retain(r->sock) : NULL;
}
/* HTTP/1 writableFinished turns true where 'finish' is emitted, not when
* end() returns; the h2 compat response keeps its stream's answer. */
bool scr_http_res_writable_finished(ScrHttpRes *r) {
return r->h2_stream != NULL ? r->finished : r->writable_finished;
}
static bool scr_http_res_has_header(ScrHttpRes *r, const char *name);
/* Node checks declared body length only while strictContentLength is true,
* with no transfer encoding and a body-bearing response. The stored header
* string has already passed setHeader's validation; strtod matches Node's
* numeric conversion for the ordinary decimal Content-Length values. */
static bool scr_http_res_expected_length(ScrHttpRes *r, double *expected) {
if (!r->strict_content_length || r->h2_stream || r->chunked ||
(r->status >= 100 && r->status < 200) || r->status == 204 || r->status == 304 ||
scr_http_res_has_header(r, "transfer-encoding")) return false;
if (r->req_ref && r->req_ref->method && r->req_ref->method->len == 4 &&
memcmp(r->req_ref->method->data, "HEAD", 4) == 0) return false;
for (size_t i = 0; i < r->nheaders; i++) {
if (r->hnames[i]->len != 14) continue;
bool match = true;
for (size_t j = 0; j < 14; j++) {
if (tolower((unsigned char)r->hnames[i]->data[j]) != "content-length"[j]) {
match = false;
break;
}
}
if (!match) continue;
*expected = strtod(r->hvalues[i]->data, NULL);
return true;
}
return false;
}
static void scr_http_res_length_mismatch(ScrHttpRes *r, double expected) {
char msg[256];
int n = snprintf(msg, sizeof msg,
"Response body's content-length of %zu byte(s) does not match the content-length of %.15g byte(s) set in header",
r->strict_bytes_written, expected);
scr_throw_error_msg_code(SCR_ERR_ERROR, msg, (size_t)n, "ERR_HTTP_CONTENT_LENGTH_MISMATCH");
}
/* res.statusCode: 200 until assigned (Node's fresh-response default);
* assignment after the head went out is inert (Node throws on the WRITE
* paths, not the property — the sent head simply no longer changes). */
double scr_http_res_status_get(ScrHttpRes *r) { return r->status > 0 ? (double)r->status : 200; }
void scr_http_res_status_set(ScrHttpRes *r, double status) {
if (r->head_sent) return;
r->status = (int)status;
}
/* An unset statusMessage is undefined until the head chooses the status
* code's default reason phrase. */
ScrStr *scr_http_res_status_msg_get(ScrHttpRes *r) {
if (r->status_msg) return scr_str_retain(r->status_msg);
if (!r->head_sent) return NULL;
const char *reason = scr_http_reason(r->status > 0 ? r->status : 200);
return scr_str_new(reason, strlen(reason));
}
void scr_http_res_status_msg_set(ScrHttpRes *r, ScrStr *msg /*borrowed*/) {
if (r->head_sent) return;
scr_str_release(r->status_msg);
r->status_msg = scr_str_retain(msg);
}
/* Outgoing header values keep the type they were set with, like Node's
* kOutHeaders entries: a string, a number (serialized as String(n)), or
* an array (one line per element). Each line records its kind; an array
* is its consecutive same-name LIST lines. */
enum { SCR_HTTP_HV_STR = 0, SCR_HTTP_HV_NUM = 1, SCR_HTTP_HV_LIST = 2 };
/* getHeader(name)'s first line, as a string (+1), or NULL. */
ScrStr *scr_http_res_get_header(ScrHttpRes *r, ScrStr *name) {
for (size_t i = 0; i < r->nheaders; i++) {
if (scr_http_name_eq(r->hnames[i], name)) return scr_str_retain(r->hvalues[i]);
}
return NULL;
}
/* The checked-dynamic value of one header line group: a number, a string,
* or an array of the group's strings (+1). */
static ScrDyn *scr_http_out_value(ScrStr *const *names, ScrStr *const *values, const uint8_t *kinds,
size_t count, size_t first) {
size_t n = 0;
for (size_t i = first; i < count; i++) n += scr_http_name_eq(names[i], names[first]);
uint8_t kind = kinds ? kinds[first] : SCR_HTTP_HV_STR;
if (n == 1 && kind == SCR_HTTP_HV_NUM) return scr_dyn_new_num(strtod(values[first]->data, NULL));
if (n == 1 && kind == SCR_HTTP_HV_STR) return scr_dyn_new_str(values[first]);
ScrDyn *list = scr_dyn_new_arr();
for (size_t i = first; i < count; i++) {
if (scr_http_name_eq(names[i], names[first])) scr_dyn_arr_push(list, scr_dyn_new_str(values[i]));
}
return list;
}
/* getHeader(name): the value as set (case-insensitive), +1; the
* undefined value when the header is absent. */
ScrDyn *scr_http_res_get_header_dyn(ScrHttpRes *r, ScrStr *name) {
for (size_t i = 0; i < r->nheaders; i++) {
if (scr_http_name_eq(r->hnames[i], name))
return scr_http_out_value(r->hnames, r->hvalues, r->hkinds, r->nheaders, i);
}
return scr_dyn_retain(scr_dyn_undefined());
}
/* 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;
}
/* getHeaders(): lowercased names to each group's value. `kinds` NULL
* reads every line as a string. */
static ScrDyn *scr_http_out_headers(ScrStr **names, ScrStr **values, const uint8_t *kinds,
size_t count) {
ScrDyn *out = scr_dyn_new_obj();
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;
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_http_out_value(names, values, kinds, count, 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->hkinds, 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);
return v != NULL;
}
/* Drops every case-insensitive match of `name`; answers the index the
* first one held (nheaders when there was none), where setHeader puts the
* replacement so the header keeps its place, like Node's key order. */
static size_t scr_http_res_drop_named(ScrHttpRes *r, const ScrStr *name) {
size_t w = 0, first = SIZE_MAX;
for (size_t i = 0; i < r->nheaders; i++) {
if (scr_http_name_eq(r->hnames[i], name)) {
if (first == SIZE_MAX) first = w;
scr_str_release(r->hnames[i]);
scr_str_release(r->hvalues[i]);
} else {
r->hnames[w] = r->hnames[i];
r->hvalues[w] = r->hvalues[i];
r->hkinds[w] = r->hkinds[i];
w++;
}
}
r->nheaders = w;
return first == SIZE_MAX ? w : first;
}
/* removeHeader(name): drops every case-insensitive match. */
void scr_http_res_remove_header(ScrHttpRes *r, ScrStr *name) {
if (r->head_sent) return; /* Node throws ERR_HTTP_HEADERS_SENT; this slice drops */
scr_http_res_drop_named(r, name);
}
static bool scr_http_res_has_header(ScrHttpRes *r, const char *name) {
size_t len = strlen(name);
for (size_t i = 0; i < r->nheaders; i++) {
if (r->hnames[i]->len == len) {
bool eq = true;
for (size_t j = 0; j < len && eq; j++) {
if (tolower((unsigned char)r->hnames[i]->data[j]) != tolower((unsigned char)name[j])) eq = false;
}
if (eq) return true;
}
}
return false;
}
/* Inserts one line at `pos` (name and value borrowed). */
static void scr_http_res_insert_header(ScrHttpRes *r, size_t pos, ScrStr *name, ScrStr *value,
uint8_t kind) {
if (r->nheaders == r->cap_headers) {
r->cap_headers = r->cap_headers ? r->cap_headers * 2 : 8;
r->hnames = realloc(r->hnames, r->cap_headers * sizeof *r->hnames);
r->hvalues = realloc(r->hvalues, r->cap_headers * sizeof *r->hvalues);
r->hkinds = realloc(r->hkinds, r->cap_headers * sizeof *r->hkinds);
if (!r->hnames || !r->hvalues || !r->hkinds) scr_http_oom();
}
memmove(r->hnames + pos + 1, r->hnames + pos, (r->nheaders - pos) * sizeof *r->hnames);
memmove(r->hvalues + pos + 1, r->hvalues + pos, (r->nheaders - pos) * sizeof *r->hvalues);
memmove(r->hkinds + pos + 1, r->hkinds + pos, (r->nheaders - pos) * sizeof *r->hkinds);
r->hnames[pos] = scr_str_retain(name);
r->hvalues[pos] = scr_str_retain(value);
r->hkinds[pos] = kind;
r->nheaders++;
}
/* Append one header line verbatim (the array-valued writeHead expansion:
* repeated names each get their own line). */
static void scr_http_res_append_header(ScrHttpRes *r, ScrStr *name /*borrowed*/,
ScrStr *value /*borrowed*/) {
if (r->head_sent) return;
scr_http_res_insert_header(r, r->nheaders, name, value, SCR_HTTP_HV_STR);
}
/* setHeader(name, string): replace-by-name (case-insensitive), Node's
* semantics. */
void scr_http_res_set_header(ScrHttpRes *r, ScrStr *name /*borrowed*/, ScrStr *value /*borrowed*/) {
if (r->head_sent) return; /* Node throws ERR_HTTP_HEADERS_SENT; slice drops (SEMANTICS) */
scr_http_res_insert_header(r, scr_http_res_drop_named(r, name), name, value, SCR_HTTP_HV_STR);
}
/* One element of an outgoing header value: a string or a number
* (String(n)); anything else throws Node's ERR_HTTP_INVALID_HEADER_VALUE.
* +1, or NULL with the exception pending. */
static ScrStr *scr_http_header_value_text(const ScrStr *name, const ScrDyn *v) {
if (v->kind == SCR_DYN_STR) return scr_str_retain(v->v.str);
if (v->kind == SCR_DYN_NUM) return scr_f64_to_scrstr(v->v.num);
ScrJsonBuf b;
scr_jb_init(&b);
scr_jb_puts(&b, "Invalid value \"");
scr_jb_puts(&b, v->kind == SCR_DYN_UNDEF ? "undefined" : "object");
scr_jb_puts(&b, "\" for header \"");
scr_jb_write(&b, name->data, name->len);
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 NULL;
}
/* Inserts `value` (a string, a number or an array of either) at `pos`;
* array elements become LIST lines. Answers the line count, or SIZE_MAX
* with the exception pending. */
static size_t scr_http_res_insert_value(ScrHttpRes *r, size_t pos, ScrStr *name, const ScrDyn *value,
bool as_list) {
if (value->kind == SCR_DYN_ARR) {
size_t n = value->v.arr.len;
for (size_t i = 0; i < n; i++) {
ScrStr *text = scr_http_header_value_text(name, value->v.arr.items[i]);
if (!text) return SIZE_MAX;
scr_http_res_insert_header(r, pos + i, name, text, SCR_HTTP_HV_LIST);
scr_str_release(text);
}
return n;
}
ScrStr *text = scr_http_header_value_text(name, value);
if (!text) return SIZE_MAX;
uint8_t kind = as_list ? SCR_HTTP_HV_LIST : value->kind == SCR_DYN_NUM ? SCR_HTTP_HV_NUM : SCR_HTTP_HV_STR;
scr_http_res_insert_header(r, pos, name, text, kind);
scr_str_release(text);
return 1;
}
/* A typed array arrives as a typed reference; header code reads plain
* dyn arrays. +1. */
static ScrDyn *scr_http_header_value_plain(ScrDyn *value) {
if (value->kind == SCR_DYN_TYPED_REF && scr_dyn_ref_is_array(value))
return scr_dyn_typed_ref_materialize(value);
return scr_dyn_retain(value);
}
/* setHeader(name, value) for a number, a string or an array value. */
void scr_http_res_set_header_dyn(ScrHttpRes *r, ScrStr *name /*borrowed*/, ScrDyn *raw /*borrowed*/) {
if (r->head_sent) return;
ScrDyn *value = scr_http_header_value_plain(raw);
if (scr_exc_pending()) {
scr_dyn_release(value);
return;
}
bool valid = true;
size_t n = value->kind == SCR_DYN_ARR ? value->v.arr.len : 1;
for (size_t i = 0; i < n && valid; i++) {
ScrStr *text = scr_http_header_value_text(name, value->kind == SCR_DYN_ARR ? value->v.arr.items[i] : value);
valid = text != NULL;
scr_str_release(text);
}
if (valid) scr_http_res_insert_value(r, scr_http_res_drop_named(r, name), name, value, false);
scr_dyn_release(value);
}
/* appendHeader(name, value): an absent header is set as given; an existing
* one becomes an array holding its values followed by the new ones. */
void scr_http_res_append_header_dyn(ScrHttpRes *r, ScrStr *name /*borrowed*/, ScrDyn *raw /*borrowed*/) {
if (r->head_sent) return;
ScrDyn *value = scr_http_header_value_plain(raw);
if (scr_exc_pending()) {
scr_dyn_release(value);
return;
}
bool valid = true;
size_t n = value->kind == SCR_DYN_ARR ? value->v.arr.len : 1;
for (size_t i = 0; i < n && valid; i++) {
ScrStr *text = scr_http_header_value_text(name, value->kind == SCR_DYN_ARR ? value->v.arr.items[i] : value);
valid = text != NULL;
scr_str_release(text);
}
if (!valid) {
scr_dyn_release(value);
return;
}
size_t last = SIZE_MAX;
for (size_t i = 0; i < r->nheaders; i++) {
if (!scr_http_name_eq(r->hnames[i], name)) continue;
r->hkinds[i] = SCR_HTTP_HV_LIST;
last = i;
}
if (last == SIZE_MAX) scr_http_res_insert_value(r, r->nheaders, name, value, false);
else scr_http_res_insert_value(r, last + 1, r->hnames[last], value, true);
scr_dyn_release(value);
}
/* Grow-and-append head builder. */
typedef struct {
char *data;
size_t len, cap;
} ScrHttpBuf;
static void scr_http_buf_append(ScrHttpBuf *b, const char *s, size_t n) {
if (b->len + n > b->cap) {
size_t cap = b->cap ? b->cap : 512;
while (cap < b->len + n) cap *= 2;
b->data = realloc(b->data, cap);
if (!b->data) scr_http_oom();
b->cap = cap;
}
memcpy(b->data + b->len, s, n);
b->len += n;
}
static void scr_http_buf_str(ScrHttpBuf *b, const char *s) { scr_http_buf_append(b, s, strlen(s)); }
/* Decimal digits of v (the status code, Content-Length) without printf. */
static void scr_http_buf_uint(ScrHttpBuf *b, unsigned long long v) {
char digits[24];
size_t at = sizeof digits;
do {
digits[--at] = (char)('0' + v % 10);
v /= 10;
} while (v != 0);
scr_http_buf_append(b, digits + at, sizeof digits - at);
}
/* ASCII case-insensitive name compare against a lowercase literal. */
static bool scr_http_name_is(const ScrStr *name, const char *lower) {
size_t len = strlen(lower);
if (name->len != len) return false;
for (size_t i = 0; i < len; i++) {
if (tolower((unsigned char)name->data[i]) != lower[i]) return false;
}
return true;
}
/* 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_buf_chunk_end(ScrHttpBuf *b, const ScrHttpTrailers *t) {
scr_http_buf_str(b, "0\r\n");
for (size_t i = 0; i < t->len; i++) {
scr_http_buf_append(b, t->names[i]->data, t->names[i]->len);
scr_http_buf_str(b, ": ");
scr_http_buf_append(b, t->values[i]->data, t->values[i]->len);
scr_http_buf_str(b, "\r\n");
}
scr_http_buf_str(b, "\r\n");
}
static void scr_http_send_chunk_end(ScrNetSocket *sock, const ScrHttpTrailers *t) {
ScrHttpBuf b = {NULL, 0, 0};
scr_http_buf_chunk_end(&b, t);
scr_net_sock_write_native(sock, b.data, b.len);
free(b.data);
}
/* The Date line has one-second resolution, so it is formatted once per
* second and reused (Node caches its utcDate string the same way); SCR_TL
* keeps thread-instanced library sessions on their own copy. */
static void scr_http_buf_date(ScrHttpBuf *b) {
static SCR_TL time_t cached_sec = (time_t)-1;
static SCR_TL char cached[64];
static SCR_TL size_t cached_len;
time_t now = time(NULL);
if (now != cached_sec) {
struct tm tm;
gmtime_r(&now, &tm);
static const char *days[] = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"};
static const char *months[] = {"Jan", "Feb", "Mar", "Apr", "May", "Jun",
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec"};
int n = snprintf(cached, sizeof cached, "Date: %s, %02d %s %04d %02d:%02d:%02d GMT\r\n",
days[tm.tm_wday], tm.tm_mday, months[tm.tm_mon], tm.tm_year + 1900,
tm.tm_hour, tm.tm_min, tm.tm_sec);
cached_len = n > 0 && (size_t)n < sizeof cached ? (size_t)n : 0;
cached_sec = cached_len > 0 ? now : (time_t)-1;
}
scr_http_buf_append(b, cached, cached_len);
}
static bool scr_http_header_has_token(const ScrStr *value, const char *token) {
size_t token_len = strlen(token);
for (size_t off = 0; off < value->len;) {
while (off < value->len &&
(value->data[off] == ',' || value->data[off] == ' ' || value->data[off] == '\t')) off++;
size_t end = off;
while (end < value->len && value->data[end] != ',' &&
value->data[end] != ' ' && value->data[end] != '\t') end++;
if (end - off == token_len) {
bool equal = true;
for (size_t i = 0; i < token_len && equal; i++) {
if (tolower((unsigned char)value->data[off + i]) != token[i]) equal = false;
}
if (equal) return true;
}
while (end < value->len && value->data[end] != ',') end++;
off = end;
}
return false;
}
/* Serializes and sends the head. `body_len` >= 0 fixes Content-Length
* (the end(data)-before-head path); -1 means chunked unless the user set
* Content-Length or Transfer-Encoding (Node's useChunkedEncodingByDefault
* for 1.1). Header order matches Node's storeHeader: user headers as set,
* then the injected Date / Connection (+Keep-Alive) / framing header. */
static bool scr_http_res_build_head(ScrHttpRes *r, long long body_len, ScrHttpBuf *out);
static void scr_http_res_send_head(ScrHttpRes *r, long long body_len) {
if (r->head_sent) return;
if (r->h2_stream != NULL) {
/* the h2 transport: a HEADERS frame — no HTTP/1 framing headers */
(void)body_len;
int status = r->status > 0 ? r->status : 200;
scr_http_h2_ops->respond(r->h2_stream, (double)status, r->hnames, r->hvalues,
r->nheaders, !r->no_date);
r->head_sent = true;
return;
}
ScrHttpBuf b = {NULL, 0, 0};
if (!scr_http_res_build_head(r, body_len, &b)) return;
if (r->sock) scr_net_sock_write_native(r->sock, b.data, b.len);
free(b.data);
}
/* The HTTP/1 head serialization behind scr_http_res_send_head: fills *out
* (caller frees) and commits head_sent/chunked, or throws
* ERR_HTTP_TRAILER_INVALID and returns false with nothing to free. Callers
* that already hold the body send head and body as one write. */
static bool scr_http_res_build_head(ScrHttpRes *r, long long body_len, ScrHttpBuf *out) {
int status = r->status > 0 ? r->status : 200;
ScrHttpBuf b = {NULL, 0, 0};
scr_http_buf_str(&b, "HTTP/1.1 ");
scr_http_buf_uint(&b, (unsigned long long)status);
scr_http_buf_str(&b, " ");
/* the assigned reason phrase wins (writeHead's statusMessage argument /
* the res.statusMessage assignment — "" serializes as an empty phrase,
* Node's "HTTP/1.1 200 " line) */
if (r->status_msg) scr_http_buf_append(&b, r->status_msg->data, r->status_msg->len);
else scr_http_buf_str(&b, scr_http_reason(status));
scr_http_buf_str(&b, "\r\n");
/* One pass serializes the user headers and notes the framing-relevant
* names (case-insensitive, as scr_http_res_has_header compares). */
bool user_chunked = false, have_date = false, have_connection = false;
bool have_len = false, declared_trailer = false;
for (size_t i = 0; i < r->nheaders; i++) {
const ScrStr *name = r->hnames[i];
scr_http_buf_append(&b, name->data, name->len);
scr_http_buf_str(&b, ": ");
scr_http_buf_append(&b, r->hvalues[i]->data, r->hvalues[i]->len);
scr_http_buf_str(&b, "\r\n");
switch (name->len) {
case 4: have_date = have_date || scr_http_name_is(name, "date"); break;
case 7: declared_trailer = declared_trailer || scr_http_name_is(name, "trailer"); break;
case 10: have_connection = have_connection || scr_http_name_is(name, "connection"); break;
case 14: have_len = have_len || scr_http_name_is(name, "content-length"); break;
case 17:
if (scr_http_name_is(name, "transfer-encoding")) {
have_len = true;
if (scr_http_header_has_token(r->hvalues[i], "chunked")) user_chunked = true;
}
break;
default: break;
}
}
r->chunked = user_chunked;
if (!r->no_date && !have_date) {
scr_http_buf_date(&b);
}
/* An HTTP/1.0 request gets no chunked default (Node's
* useChunkedEncodingByDefault): without a user length the body goes out
* unframed and the connection closes after it. */
/* Node's _hasBody: a HEAD request or a 1xx, 204 or 304 status sends no
* body and no framing header; write() and end() data are dropped. */
r->no_body = (status >= 100 && status < 200) || status == 204 || status == 304 ||
(r->req_ref != NULL && r->req_ref->method != NULL && r->req_ref->method->len == 4 &&
memcmp(r->req_ref->method->data, "HEAD", 4) == 0);
bool unframed = !have_len && (r->no_body || (r->req_ref != NULL && r->req_ref->http10));
if (unframed && !r->no_body) r->keep_alive = false;
bool user_close = false;
if (have_connection) {
/* the user's own Connection header decides (already serialized) */
user_close = true; /* trust their framing intent; keep-alive logic off */
} else if (r->keep_alive) {
/* Node's Keep-Alive hint: whole seconds of server.keepAliveTimeout,
* omitted when it is 0. */
scr_http_buf_str(&b, "Connection: keep-alive\r\n");
ScrNetServer *srv = r->sock != NULL ? scr_net_sock_server(r->sock) : NULL;
double ka = srv != NULL ? scr_net_server_keep_alive_timeout(srv) : 5000;
if (ka > 0) {
scr_http_buf_str(&b, "Keep-Alive: timeout=");
scr_http_buf_uint(&b, (unsigned long long)floor(ka / 1000));
scr_http_buf_str(&b, "\r\n");
}
} else {
scr_http_buf_str(&b, "Connection: close\r\n");
}
(void)user_close;
if (!have_len && !unframed) {
if (body_len >= 0 && !declared_trailer) {
scr_http_buf_str(&b, "Content-Length: ");
scr_http_buf_uint(&b, (unsigned long long)body_len);
scr_http_buf_str(&b, "\r\n");
} else {
scr_http_buf_str(&b, "Transfer-Encoding: chunked\r\n");
r->chunked = true;
}
}
if (declared_trailer && !r->chunked) {
static const char msg[] = "Trailers are invalid with this transfer encoding";
free(b.data);
scr_throw_error_msg_code(SCR_ERR_ERROR, msg, sizeof msg - 1, "ERR_HTTP_TRAILER_INVALID");
return false;
}
scr_http_buf_str(&b, "\r\n");
r->head_sent = true;
*out = b;
return true;
}
/* One socket write for [head][body in the response's framing][the final
* chunk + trailers when `last` on a chunked body] — Node likewise hands
* the implicit head and the first chunk (and end()'s terminator) to the
* socket as one write, so a small response is one segment instead of a
* head segment followed by a body segment. The bytes are unchanged. */
static void scr_http_res_emit(ScrHttpRes *r, const ScrHttpBuf *head, const char *data,
size_t len, bool last) {
if (!r->sock) return;
if (r->no_body) len = 0;
ScrNetSlice v[4];
size_t n = 0;
char size[32];
ScrHttpBuf tail = {NULL, 0, 0};
if (head != NULL && head->len > 0) v[n++] = (ScrNetSlice){head->data, head->len};
if (r->chunked) {
if (len > 0) {
int sn = snprintf(size, sizeof size, "%zx\r\n", len);
v[n++] = (ScrNetSlice){size, (size_t)sn};
v[n++] = (ScrNetSlice){data, len};
scr_http_buf_str(&tail, "\r\n");
}
if (last) scr_http_buf_chunk_end(&tail, &r->trailers);
if (tail.len > 0) v[n++] = (ScrNetSlice){tail.data, tail.len};
} else if (len > 0) {
v[n++] = (ScrNetSlice){data, len};
}
scr_net_sock_writev_native(r->sock, v, n);
free(tail.data);
}
/* Writes while corked coalesce here and flush as ONE write when the
* cork count reaches zero (or at end()) — Node's coalescing, and one
* DATA frame on the h2 lane. */
static void scr_http_cork_buffer(char **buf, size_t *held_len, size_t *held_cap,
const char *data, size_t len) {
if (len == 0) return;
if (*held_len + len > *held_cap) {
size_t cap = *held_cap ? *held_cap : 1024;
while (cap < *held_len + len) cap *= 2;
*buf = realloc(*buf, cap);
if (!*buf) scr_http_oom();
*held_cap = cap;
}
memcpy(*buf + *held_len, data, len);
*held_len += len;
}
static void scr_http_res_write_raw(ScrHttpRes *r, const char *data, size_t len) {
if (r->finished) return;
if (r->corked > 0) {
/* Node's write_() builds the implicit head even while corked */
if (!r->head_sent && r->h2_stream == NULL) {
ScrHttpBuf held = {NULL, 0, 0};
if (!scr_http_res_build_head(r, -1, &held)) return;
r->held_head = held.data;
r->held_head_len = held.len;
r->held_head_cap = held.cap;
}
if (scr_exc_pending()) return;
scr_http_cork_buffer(&r->cork_buf, &r->cork_len, &r->cork_cap, data, len);
return;
}
if (r->h2_stream != NULL) {
if (!r->head_sent) scr_http_res_send_head(r, -1);
if (scr_exc_pending()) return;
scr_http_h2_ops->write(r->h2_stream, data, len);
return;
}
/* streaming: the implicit head (chunked unless the user fixed the
* length) leaves with this first chunk */
ScrHttpBuf head = {r->held_head, r->held_head_len, r->held_head_cap};
r->held_head = NULL;
r->held_head_len = r->held_head_cap = 0;
if (!r->head_sent && !scr_http_res_build_head(r, -1, &head)) return;
if (!scr_exc_pending()) scr_http_res_emit(r, &head, data, len, false);
free(head.data);
}
/* Node 24.22's write_(): a write with the socket uncorked corks the
* message and uncorks it on the next tick, so the chunked writes of one
* tick go out as one chunk. */
static void scr_http_res_autocork_nt(ScrClosure *cb) {
ScrHttpRes *r = scr_box_get_ref(cb->caps[0]);
if (r->corked > 0) scr_http_res_uncork(r);
scr_http_res_release(r);
}
static void scr_http_res_autocork(ScrHttpRes *r) {
if (r->finished || r->h2_stream != NULL || r->sock == NULL || r->corked > 0) return;
r->corked++;
ScrClosure *cb = scr_closure_new((void *)&scr_http_res_autocork_nt, 1);
cb->caps[0] = scr_box_new_obj(&scr_http_res_retain_v, &scr_http_res_release_v, NULL);
scr_box_set_ref(cb->caps[0], scr_http_res_retain(r));
scr_next_tick(cb);
}
void scr_http_res_write_str(ScrHttpRes *r, ScrStr *data /*borrowed*/) {
double expected;
if (r->head_sent && scr_http_res_expected_length(r, &expected) &&
(double)(r->strict_bytes_written + data->len) > expected) {
r->strict_bytes_written += data->len;
scr_http_res_length_mismatch(r, expected);
r->strict_bytes_written -= data->len;
return;
}
if (r->strict_content_length) r->strict_bytes_written += data->len;
scr_http_res_autocork(r);
char *owned;
const char *bytes = scr_str_utf8(data, &owned);
scr_http_res_write_raw(r, bytes, data->len);
scr_str_utf8_end(owned);
}
void scr_http_res_write_bytes(ScrHttpRes *r, ScrBytes *data /*borrowed*/) {
SCR_BYTES_SNAPSHOT(data);
double expected;
if (r->head_sent && scr_http_res_expected_length(r, &expected) &&
(double)(r->strict_bytes_written + (data->len * scr_bytes_elem_size(data->elem))) > expected) {
r->strict_bytes_written += (data->len * scr_bytes_elem_size(data->elem));
scr_http_res_length_mismatch(r, expected);
r->strict_bytes_written -= (data->len * scr_bytes_elem_size(data->elem));
return;
}
if (r->strict_content_length) r->strict_bytes_written += (data->len * scr_bytes_elem_size(data->elem));
scr_http_res_autocork(r);
scr_http_res_write_raw(r, (const char *)data->data, (data->len * scr_bytes_elem_size(data->elem)));
}
/* res.flushHeaders(): the head goes out NOW (streaming framing — chunked
* unless the user fixed the length; the h2 lane's HEADERS frame). */
void scr_http_res_flush_headers(ScrHttpRes *r) {
if (!r->head_sent && !r->finished) scr_http_res_send_head(r, -1);
}
void scr_http_res_add_trailers(ScrHttpRes *r, ScrArr *pairs /*borrowed*/) {
if (r->finished) return;
scr_http_trailers_replace(&r->trailers, pairs);
}
/* cork()/uncork(): the count IS res.writableCorked; the last uncork
* flushes the coalesced bytes as one write. */
void scr_http_res_cork(ScrHttpRes *r) { r->corked++; }
void scr_http_res_uncork(ScrHttpRes *r) {
if (r->corked == 0) return;
if (--r->corked == 0) scr_http_res_cork_flush(r);
}
double scr_http_res_writable_corked(ScrHttpRes *r) { return (double)r->corked; }
bool scr_http_res_destroyed_flag(ScrHttpRes *r) { return r->destroyed || r->close_emitted; }
/* res.req — the paired request; a res.req = null write cleared it. */
void scr_http_res_set_req(ScrHttpRes *r, ScrHttpReq *req /*borrowed*/) {
if (r->req_ref) scr_http_req_release(r->req_ref);
r->req_ref = req ? scr_http_req_retain(req) : NULL;
}
/* res.setTimeout(ms[, cb]): a server response handles its own timeout
* event before the server decides whether to destroy the socket. */
void scr_http_res_set_timeout(ScrHttpRes *r, double ms, ScrClosure *cb /*moves, nullable*/) {
if (r->finished || r->close_emitted || !r->sock) {
if (cb) scr_closure_release(cb);
return;
}
scr_net_sock_set_timeout(r->sock, ms);
if (cb) {
if (!r->h2_stream) scr_net_ls_add(&r->timeout_ls, cb, NULL, false);
else scr_net_sock_on_timeout(r->sock, cb, true);
}
}
void scr_http_res_set_timeout_plain(ScrHttpRes *r, double ms) {
scr_http_res_set_timeout(r, ms, NULL);
}
static void scr_http_conn_response_finished(struct ScrHttpConn *conn, bool keep_alive);
static void scr_http_queue_res_finish(ScrHttpRes *res);
/* Flush corked bytes as one write (corked already zero, or forced by
* end()). */
static void scr_http_res_cork_flush(ScrHttpRes *r) {
if (r->cork_len == 0 && r->held_head_len == 0) return;
char *held = r->cork_buf;
size_t held_len = r->cork_len;
r->cork_buf = NULL;
r->cork_len = r->cork_cap = 0;
scr_http_res_write_raw(r, held, held_len);
free(held);
}
static void scr_http_res_end_raw(ScrHttpRes *r, const char *data, size_t len) {
if (r->finished) return;
double expected;
if (len > 0 && r->head_sent && scr_http_res_expected_length(r, &expected) &&
(double)(r->strict_bytes_written + len) != expected) {
r->strict_bytes_written += len;
scr_http_res_length_mismatch(r, expected);
r->strict_bytes_written -= len;
return;
}
if (r->strict_content_length) r->strict_bytes_written += len;
if (r->corked > 0 || r->cork_len > 0 || r->held_head_len > 0) {
/* end() flushes every cork level (Node) — the body streamed, so the
* framing below takes the already-committed streaming path. A corked
* chunked message buffers end()'s chunk too: one chunk goes out. */
if (r->corked > 0 && r->head_sent && r->chunked && r->h2_stream == NULL && len > 0) {
scr_http_cork_buffer(&r->cork_buf, &r->cork_len, &r->cork_cap, data, len);
data = "";
len = 0;
}
r->corked = 0;
scr_http_res_cork_flush(r);
if (scr_exc_pending()) return;
if (r->finished) return; /* a teardown inside the flush */
}
if (r->h2_stream != NULL) {
if (!r->head_sent) scr_http_res_send_head(r, (long long)len);
scr_http_h2_ops->end(r->h2_stream, data, len);
r->finished = true;
scr_http_queue_res_finish(r);
return;
}
if (!r->head_sent) {
/* whole body known NOW: Content-Length framing, Node's implicit head,
* head + body (+ the chunked terminator) as one write */
ScrHttpBuf head = {NULL, 0, 0};
if (!scr_http_res_build_head(r, (long long)len, &head)) return;
if (!scr_exc_pending()) scr_http_res_emit(r, &head, data, len, r->chunked);
free(head.data);
if (scr_exc_pending()) return;
} else {
scr_http_res_emit(r, NULL, data, len, r->chunked);
}
if (scr_http_res_expected_length(r, &expected) &&
(double)r->strict_bytes_written != expected) {
scr_http_res_length_mismatch(r, expected);
return;
}
r->finished = true;
scr_http_queue_res_finish(r);
if (r->conn) scr_http_conn_response_finished(r->conn, r->keep_alive);
}
void scr_http_res_end(ScrHttpRes *r) { scr_http_res_end_raw(r, "", 0); }
void scr_http_res_end_str(ScrHttpRes *r, ScrStr *data /*borrowed*/) {
char *owned;
const char *bytes = scr_str_utf8(data, &owned);
scr_http_res_end_raw(r, bytes, data->len);
scr_str_utf8_end(owned);
}
void scr_http_res_end_bytes(ScrHttpRes *r, ScrBytes *data /*borrowed*/) {
SCR_BYTES_SNAPSHOT(data);
scr_http_res_end_raw(r, (const char *)data->data, (data->len * scr_bytes_elem_size(data->elem)));
}
void scr_http_res_write_head(ScrHttpRes *r, double status) {
if (r->head_sent) return;
r->status = (int)status;
scr_http_res_send_head(r, -1); /* Node: an explicit writeHead means chunked unless CL was set */
}
/* writeHead(status, { ...literal headers... }): the packed form — names
* and values arrive as parallel string arrays (evaluation order was the
* literal's), each setHeader'd, then the head goes out. */
/* writeHead(status, headers) with a CHECKED-DYNAMIC headers value (an
* untyped JS helper's parameter — the suite's test(headers) idiom): OBJ
* entries setHeader in insertion order (string values verbatim, numbers
* via ToString, string arrays one line per element via the append path),
* then the head goes out. undefined/null headers are the plain head;
* other kinds throw the ERR_INVALID_ARG_TYPE-flavored fence. */
void scr_http_res_write_head_dyn(ScrHttpRes *r, double status, const ScrDyn *headers) {
if (r->head_sent) return;
if (headers->kind == SCR_DYN_UNDEF || headers->kind == SCR_DYN_NULL) {
scr_http_res_write_head(r, status);
return;
}
if (headers->kind != SCR_DYN_OBJ) {
scr_dyn_arg_type_fail("headers", "an instance of Object", headers);
return;
}
for (size_t i = 0; i < headers->v.obj.len; i++) {
const ScrDynEntry *e = &headers->v.obj.entries[i];
const ScrDyn *v = e->value;
ScrStr *name = scr_str_new(e->key, e->key_len);
if (v->kind == SCR_DYN_STR) {
scr_http_res_set_header(r, name, v->v.str);
} else if (v->kind == SCR_DYN_NUM) {
char num[32];
size_t n = scr_f64_to_str(v->v.num, num);
ScrStr *value = scr_str_new(num, n);
scr_http_res_set_header(r, name, value);
scr_str_release(value);
} else {
scr_str_release(name);
scr_dyn_arg_type_fail("value", "of type string or number", v);
return;
}
scr_str_release(name);
}
scr_http_res_write_head(r, status);
}
void scr_http_res_write_head_n(ScrHttpRes *r, double status, ScrArr *names, ScrArr *values) {
if (r->head_sent) return;
size_t n = (size_t)scr_arr_len(names);
for (size_t i = 0; i < n; i++) {
ScrStr *name = (ScrStr *)scr_arr_get_ref(names, (double)i);
ScrStr *value = (ScrStr *)scr_arr_get_ref(values, (double)i);
scr_http_res_set_header(r, name, value);
scr_str_release(name);
scr_str_release(value);
}
scr_http_res_write_head(r, status);
}
/* writeHead(status, record): the flat [k0, v0, k1, v1, ...] pairs the
* env.pairs helper produces — each entry setHeader'd, then the head goes
* out (undefined-valued entries were dropped by the helper, the Node-
* drops-undefined env rule; Node's writeHead would throw on them). */
void scr_http_res_write_head_pairs(ScrHttpRes *r, double status, ScrArr *pairs /*borrowed*/) {
if (r->head_sent) return;
size_t n = (size_t)scr_arr_len(pairs);
for (size_t i = 0; i + 1 < n; i += 2) {
ScrStr *name = (ScrStr *)scr_arr_get_ref(pairs, (double)i);
ScrStr *value = (ScrStr *)scr_arr_get_ref(pairs, (double)(i + 1));
/* A REPEATED name within this pairs array is an ARRAY-VALUED header
* (["a", "b"] expands to consecutive same-name pairs): the first
* occurrence replaces any earlier setHeader (Node's writeHead-wins
* merge), the repeats APPEND — Node writes one line per element. */
bool repeat = false;
for (size_t j = 0; j < i && !repeat; j += 2) {
ScrStr *prev = (ScrStr *)scr_arr_get_ref(pairs, (double)j);
if (prev->len == name->len) {
bool eq = true;
for (size_t k = 0; k < name->len && eq; k++) {
if (tolower((unsigned char)prev->data[k]) != tolower((unsigned char)name->data[k])) eq = false;
}
repeat = eq;
}
scr_str_release(prev);
}
if (repeat) scr_http_res_append_header(r, name, value);
else scr_http_res_set_header(r, name, value);
scr_str_release(name);
scr_str_release(value);
}
scr_http_res_write_head(r, status);
}
/* res.destroy(): tears the connection down NOW (mid-stream RST is the
* point — portless aborts a proxied response with it); 'close' follows
* through the teardown path. Node-matched: no 'error' from a destroy. */
void scr_http_res_destroy(ScrHttpRes *r) {
r->destroyed = true;
if (r->h2_stream != NULL) {
/* h2 compat: destroy the STREAM (RST), never the shared session */
scr_http_h2_ops->destroy(r->h2_stream);
return;
}
if (r->sock) scr_net_sock_destroy(r->sock);
}
void scr_http_res_on_close(ScrHttpRes *r, ScrClosure *cb /*moves*/, bool once) {
if (r->close_emitted) {
scr_closure_release(cb);
return;
}
scr_net_ls_add(&r->close_ls, cb, NULL, once);
}
/* ── the deferred-emit queue (scr_net.c's proto sweep drains it) ───────
*
* 'close' on requests/responses and the client's own 'close' fire a
* sweep pass AFTER the work that flagged them — Node's ordering (the
* handler's synchronous tail runs first; res 'close' precedes req
* 'close'; the client's res 'end' precedes req 'close' precedes res
* 'close'). RES_ABORTED is the mid-body premature-close 'error' on a
* client response ("aborted", Node's message) — queued between the
* client close and the response close, the oracle-pinned order. Each
* entry holds +1 on its handle; firing settles the handle (listener
* lists drop — the cycle story). */
enum {
SCR_HTTP_EMIT_RES_CLOSE = 1, /* ScrHttpRes */
SCR_HTTP_EMIT_REQ_CLOSE = 2, /* ScrHttpReq */
SCR_HTTP_EMIT_REQ_ABORTED = 3, /* ScrHttpReq: 'error' ("aborted") */
SCR_HTTP_EMIT_CLIENT_CLOSE = 4, /* ScrHttpClientReq */
SCR_HTTP_EMIT_RES_FINISH = 5, /* ScrHttpRes: the end(cb) callbacks */
SCR_HTTP_EMIT_REQ_DRAIN = SCR_HTTP_EMIT_REQ_DRAIN_K, /* ScrHttpReq: resume()'s drain */
SCR_HTTP_EMIT_RES_WCB = 7, /* ScrHttpRes: write(chunk, cb) callbacks */
SCR_HTTP_EMIT_CLIENT_SOCKET = 8, /* ScrHttpClientReq: assigned socket */
SCR_HTTP_EMIT_CLIENT_FINISH = 9, /* ScrHttpClientReq: outgoing body flushed */
SCR_HTTP_EMIT_CLIENT_ABORT = 10 /* ScrHttpClientReq: abort() notification */
};
typedef struct {
int kind;
void *h; /* +1 */
} ScrHttpEmit;
static SCR_TL ScrHttpEmit *scr_http_emits = NULL;
static SCR_TL size_t scr_http_emits_head = 0, scr_http_emits_len = 0, scr_http_emits_cap = 0;
static void scr_http_client_release_internal(struct ScrHttpClientReq *c);
static void scr_http_client_settle(struct ScrHttpClientReq *c);
static void scr_http_client_emit_socket(struct ScrHttpClientReq *c);
static void scr_http_client_emit_finish(struct ScrHttpClientReq *c);
static void scr_http_client_emit_abort(struct ScrHttpClientReq *c);
/* Exit-time flag (set by the atexit cleanup, which runs BEFORE the net
* unit's — atexit LIFO, http installs after net): once the emits queue
* has drained for the last time, teardown paths that would queue close
* emits (scr_net_cleanup_atexit freeing in-flight parsers) discard the
* payload instead — nothing will ever sweep again. */
static SCR_TL bool scr_http_exiting = false;
static void scr_http_emit_release(ScrHttpEmit *e);
static void scr_http_emit_push(int kind, void *h /*moves +1*/) {
if (scr_http_exiting) {
ScrHttpEmit e = { kind, h };
scr_http_emit_release(&e);
return;
}
if (scr_http_emits_len == scr_http_emits_cap) {
/* compact the consumed head first */
if (scr_http_emits_head > 0) {
memmove(scr_http_emits, scr_http_emits + scr_http_emits_head,
(scr_http_emits_len - scr_http_emits_head) * sizeof *scr_http_emits);
scr_http_emits_len -= scr_http_emits_head;
scr_http_emits_head = 0;
}
if (scr_http_emits_len == scr_http_emits_cap) {
scr_http_emits_cap = scr_http_emits_cap ? scr_http_emits_cap * 2 : 8;
scr_http_emits = realloc(scr_http_emits, scr_http_emits_cap * sizeof *scr_http_emits);
if (!scr_http_emits) scr_http_oom();
}
}
scr_http_emits[scr_http_emits_len].kind = kind;
scr_http_emits[scr_http_emits_len].h = h;
scr_http_emits_len++;
}
static void scr_http_emit_release(ScrHttpEmit *e) {
switch (e->kind) {
case SCR_HTTP_EMIT_RES_CLOSE:
case SCR_HTTP_EMIT_RES_WCB:
case SCR_HTTP_EMIT_RES_FINISH: scr_http_res_release((ScrHttpRes *)e->h); break;
case SCR_HTTP_EMIT_REQ_CLOSE:
case SCR_HTTP_EMIT_REQ_DRAIN:
case SCR_HTTP_EMIT_REQ_ABORTED: scr_http_req_release((ScrHttpReq *)e->h); break;
case SCR_HTTP_EMIT_CLIENT_CLOSE:
case SCR_HTTP_EMIT_CLIENT_SOCKET:
case SCR_HTTP_EMIT_CLIENT_FINISH:
case SCR_HTTP_EMIT_CLIENT_ABORT:
scr_http_client_release_internal((struct ScrHttpClientReq *)e->h);
break;
}
}
/* Enqueue helpers: idempotent per handle (the close_queued flags). */
static void scr_http_queue_res_close(ScrHttpRes *res) {
if (res->close_queued || res->close_emitted) return;
res->close_queued = true;
scr_http_emit_push(SCR_HTTP_EMIT_RES_CLOSE, scr_http_res_retain(res));
}
static void scr_http_queue_req_close(ScrHttpReq *req) {
if (req->close_queued || req->close_emitted) return;
req->close_queued = true;
scr_http_emit_push(SCR_HTTP_EMIT_REQ_CLOSE, scr_http_req_retain(req));
}
static void scr_http_queue_req_aborted(ScrHttpReq *req) {
scr_http_emit_push(SCR_HTTP_EMIT_REQ_ABORTED, scr_http_req_retain(req));
}
/* The end(cb) deferral: 'finish'-time callbacks fire one sweep after the
* body went out (the handler's synchronous tail runs first, Node's
* deferred emit — the res 'close' precedent). */
static void scr_http_queue_res_finish(ScrHttpRes *res) {
if (res->finish_queued) return;
res->finish_queued = true;
scr_http_emit_push(SCR_HTTP_EMIT_RES_FINISH, scr_http_res_retain(res));
}
/* res.on('finish', cb): fires deferred once the body went out, after the
* end(cb) callbacks and after the response detached its socket. */
void scr_http_res_on_finish(ScrHttpRes *r, ScrClosure *cb /*moves*/) {
scr_net_ls_add(&r->finish_ls, cb, NULL, true);
if (r->finished) scr_http_queue_res_finish(r);
}
/* res.end(cb)'s callback slot: registered just before the end call. The
* callbacks run where Node's OutgoingMessage flushes its end callbacks:
* after writableFinished turns true and before the 'finish' listeners,
* with the socket still attached. A callback registered after the
* response finished runs synchronously, where Node passes
* ERR_STREAM_ALREADY_FINISHED (this surface's callbacks take no
* arguments). The h2 compat response keeps the 'finish' listener order. */
void scr_http_res_on_end_callback(ScrHttpRes *r, ScrClosure *cb /*moves*/) {
if (r->h2_stream != NULL) {
scr_http_res_on_finish(r, cb);
return;
}
if (r->writable_finished) {
ScrNetLs now = {0};
scr_net_ls_add(&now, cb, NULL, true);
scr_net_fire0_this(&now, r, SCR_DYNH_HTTP_RES);
scr_net_ls_drop(&now);
return;
}
scr_net_ls_add(&r->endcb_ls, cb, NULL, true);
if (r->finished) scr_http_queue_res_finish(r);
}
/* res.write(chunk, cb): the callback fires from the queue (this surface
* flushes synchronously into the socket buffer — the deferral keeps the
* cb off the writing stack, Node's contract). */
void scr_http_res_on_write_flush(ScrHttpRes *r, ScrClosure *cb /*moves*/) {
scr_net_ls_add(&r->wcb_ls, cb, NULL, true);
if (!r->wcb_queued) {
r->wcb_queued = true;
scr_http_emit_push(SCR_HTTP_EMIT_RES_WCB, scr_http_res_retain(r));
}
}
static bool scr_http_proto_pending(void) { return scr_http_emits_head < scr_http_emits_len; }
static void scr_http_proto_sweep(void) {
while (scr_http_emits_head < scr_http_emits_len) {
ScrHttpEmit e = scr_http_emits[scr_http_emits_head++];
switch (e.kind) {
case SCR_HTTP_EMIT_RES_CLOSE: {
ScrHttpRes *res = (ScrHttpRes *)e.h;
if (!res->close_emitted) {
res->close_emitted = true;
res->destroyed = true; /* Node: destroyed reads true inside 'close' */
res->socket_detached = true;
scr_net_fire0_this(&res->close_ls, res, SCR_DYNH_HTTP_RES);
scr_net_ls_drop(&res->close_ls);
scr_net_ls_drop(&res->timeout_ls);
}
break;
}
case SCR_HTTP_EMIT_RES_WCB: {
ScrHttpRes *res = (ScrHttpRes *)e.h;
res->wcb_queued = false; /* later write(chunk, cb)s re-queue */
scr_net_fire0_this(&res->wcb_ls, res, SCR_DYNH_HTTP_RES);
break;
}
case SCR_HTTP_EMIT_REQ_CLOSE: {
ScrHttpReq *req = (ScrHttpReq *)e.h;
if (!req->close_emitted) {
req->close_emitted = true;
req->destroyed = true; /* Node: destroyed reads true inside 'close' */
scr_net_fire0_this(&req->close_ls, req, SCR_DYNH_HTTP_REQ);
scr_net_ls_drop(&req->err_ls);
scr_net_ls_drop(&req->close_ls);
scr_net_ls_drop(&req->timeout_ls);
scr_net_ls_drop(&req->aborted_ls);
}
break;
}
case SCR_HTTP_EMIT_REQ_DRAIN: {
/* resume() after pause(): the held bytes deliver as one chunk (the
* reassembled body is the contract), then a deferred 'end'. A
* re-pause mid-drain re-buffers — the swap keeps the walk sound. */
ScrHttpReq *req = (ScrHttpReq *)e.h;
req->drain_queued = false;
if (!req->paused) {
char *held = req->pend;
size_t held_len = req->pend_len;
req->pend = NULL;
req->pend_len = req->pend_cap = 0;
if (held_len > 0) scr_http_req_deliver(req, held, held_len);
free(held);
if (!scr_exc_pending() && !req->paused && req->end_pending) {
req->end_pending = false;
scr_http_req_finish(req, true);
}
}
break;
}
case SCR_HTTP_EMIT_REQ_ABORTED: {
ScrHttpReq *req = (ScrHttpReq *)e.h;
if (!req->close_emitted) {
ScrStr *msg = scr_str_new("aborted", 7);
scr_net_fire_err_this(&req->err_ls, msg, req, SCR_DYNH_HTTP_REQ);
scr_str_release(msg);
}
break;
}
case SCR_HTTP_EMIT_CLIENT_CLOSE:
scr_http_client_settle((struct ScrHttpClientReq *)e.h);
break;
case SCR_HTTP_EMIT_CLIENT_SOCKET:
scr_http_client_emit_socket((struct ScrHttpClientReq *)e.h);
break;
case SCR_HTTP_EMIT_CLIENT_FINISH:
scr_http_client_emit_finish((struct ScrHttpClientReq *)e.h);
break;
case SCR_HTTP_EMIT_CLIENT_ABORT:
scr_http_client_emit_abort((struct ScrHttpClientReq *)e.h);
break;
case SCR_HTTP_EMIT_RES_FINISH: {
ScrHttpRes *res = (ScrHttpRes *)e.h;
res->finish_queued = false; /* a later on('finish') on the finished res re-queues */
res->writable_finished = true;
ScrNetLs endcbs = res->endcb_ls;
res->endcb_ls = (ScrNetLs){0};
scr_net_fire0_this(&endcbs, res, SCR_DYNH_HTTP_RES);
scr_net_ls_drop(&endcbs);
res->socket_detached = true;
if (!scr_exc_pending()) scr_net_fire0_this(&res->finish_ls, res, SCR_DYNH_HTTP_RES);
break;
}
}
scr_http_emit_release(&e);
if (scr_exc_pending()) return;
}
scr_http_emits_head = scr_http_emits_len = 0;
}
static void scr_http_clients_cleanup(void);
static void scr_http_agents_cleanup(void);
static void scr_http_cleanup_atexit(void) {
scr_http_exiting = true;
while (scr_http_emits_head < scr_http_emits_len) {
ScrHttpEmit e = scr_http_emits[scr_http_emits_head++];
scr_http_emit_release(&e);
}
free(scr_http_emits);
scr_http_emits = NULL;
scr_http_emits_head = scr_http_emits_len = scr_http_emits_cap = 0;
scr_http_agents_cleanup(); /* break agent↔client edges first */
scr_http_clients_cleanup();
}
/* One-time wiring into the net sweep (both createServer and request call
* through here). */
static void scr_http_install(void) {
static SCR_TL bool installed = false;
if (installed) return;
installed = true;
scr_atexit(scr_http_cleanup_atexit);
scr_net_set_proto_sweep(&scr_http_proto_pending, &scr_http_proto_sweep);
}
/* ── the per-connection parser ───────────────────────────────────────── */
typedef enum {
SCR_HTTP_HEAD = 0,
SCR_HTTP_BODY_CL,
SCR_HTTP_CHUNK_SIZE,
SCR_HTTP_CHUNK_DATA,
SCR_HTTP_CHUNK_CRLF,
SCR_HTTP_CHUNK_TRAILER,
SCR_HTTP_BODY_EOF, /* RESPONSE mode: no framing header — body until EOF */
} ScrHttpParseState;
typedef struct ScrHttpConn {
ScrNetSocket *sock; /* BORROWED: the ctx lives and dies with the socket
* (a retained ref here would be a cycle the RC
* could never break) */
struct ScrHttpSrvCtx *srv; /* +1; NULL in client mode — the server ctx
* whose 'request' listeners fire per parsed
* request (EMIT-time resolution, Node's
* dispatch: a listener installed after the
* accept but before the request still runs) */
char *buf;
size_t len, cap;
ScrHttpParseState state;
size_t body_remaining;
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 keep_alive_timer; /* the socket's idle timer is the keep-alive timeout */
bool header_overflow; /* client mode: the response head exceeded a bound */
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
* runs below the client struct's definition) */
struct ScrHttpClientReq *client; /* +1 in client mode; released at close */
} ScrHttpConn;
/* Client-mode forward declarations (implementations live with the client
* below the server parser). */
static bool scr_http_client_parse_head(ScrHttpConn *conn, size_t head_len);
static void scr_http_client_head_overflow(ScrHttpConn *conn);
/* 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) {
if (conn->head_bytes > SCR_HTTP_MAX_HEADER_BYTES ||
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;
return !field || conn->trailer_fields < SCR_HTTP_MAX_HEADER_FIELDS;
}
/* The server-side ctx: the 'request' listener list, shared by every
* connection. REFCOUNTED: the server's native-conn chain holds one ref
* (released through scr_http_srv_ctx_free) and each live connection
* holds one — a socket can outlive its server handle. createServer's
* eager handler is just the first listener; server.on("request", ...)
* appends (scr_http_server_on_request), and requests fire the list
* snapshot-at-emit like every other event here. */
typedef struct ScrHttpSrvCtx {
int proto; /* SCR_NET_PROTO_HTTP1 — FIRST: the http_ctx alias slot is
* shared with scr_http2.c's ScrH2SrvCtx; each family checks
* the tag before treating the ctx as its own */
size_t rc;
ScrNetLs request_ls;
ScrNetLs upgrade_ls;
ScrNetLs connect_ls; /* HTTP CONNECT — the upgrade machinery's twin */
bool join_dup; /* createServer({ joinDuplicateHeaders: true }) */
bool require_host_header; /* HTTP/1.1 Host is required by default */
} ScrHttpSrvCtx;
static ScrHttpSrvCtx *scr_http_srv_ctx_retain(ScrHttpSrvCtx *ctx) {
ctx->rc++;
return ctx;
}
static void scr_http_srv_ctx_release(ScrHttpSrvCtx *ctx) {
if (--ctx->rc > 0) return;
scr_net_ls_drop(&ctx->request_ls);
scr_net_ls_drop(&ctx->upgrade_ls);
scr_net_ls_drop(&ctx->connect_ls);
free(ctx);
}
static void scr_http_srv_ctx_free(void *p) { scr_http_srv_ctx_release((ScrHttpSrvCtx *)p); }
/* The server-settle hook (scr_net_server_set_proto_settle): the ctx's own
* listener lists drop when the server settles — no request can fire past
* the settle (the fd is closed and every connection drained), and a
* handler closure capturing its own server through a dyn binding box
* would otherwise cycle through collector-invisible edges. */
static void scr_http_srv_ctx_settle(void *p) {
ScrHttpSrvCtx *ctx = (ScrHttpSrvCtx *)p;
scr_net_ls_drop(&ctx->request_ls);
scr_net_ls_drop(&ctx->upgrade_ls);
scr_net_ls_drop(&ctx->connect_ls);
}
/* Combined HTTP/2 allowHTTP1 servers retain the HTTP/1 parser ctx behind
* their h2-facing alias. These narrow hooks let scr_http2.c mirror request
* listeners and settle both protocol lists without exposing the ctx shape. */
void scr_http1_ctx_on_request(void *p, ScrClosure *cb /*moves*/, ScrHttpReqFn fn, bool once,
ScrNetOnce *shared_once /*moves, nullable*/) {
ScrHttpSrvCtx *ctx = (ScrHttpSrvCtx *)p;
if (ctx == NULL || ctx->proto != SCR_NET_PROTO_HTTP1) {
scr_closure_release(cb);
scr_net_once_release(shared_once);
return;
}
if (shared_once != NULL) {
scr_net_ls_add_shared_once(&ctx->request_ls, cb, (void *)fn, shared_once);
} else {
scr_net_ls_add(&ctx->request_ls, cb, (void *)fn, once);
}
}
void scr_http1_ctx_on_connect(void *p, ScrClosure *cb /*moves*/, ScrHttpUpgradeFn fn, bool once,
ScrNetOnce *shared_once /*moves, nullable*/) {
ScrHttpSrvCtx *ctx = (ScrHttpSrvCtx *)p;
if (ctx == NULL || ctx->proto != SCR_NET_PROTO_HTTP1) {
scr_closure_release(cb);
scr_net_once_release(shared_once);
return;
}
if (shared_once != NULL) {
scr_net_ls_add_shared_once(&ctx->connect_ls, cb, (void *)fn, shared_once);
} else {
scr_net_ls_add(&ctx->connect_ls, cb, (void *)fn, once);
}
}
void scr_http1_ctx_on_upgrade(void *p, ScrClosure *cb /*moves*/, ScrHttpUpgradeFn fn, bool once) {
ScrHttpSrvCtx *ctx = (ScrHttpSrvCtx *)p;
if (ctx == NULL || ctx->proto != SCR_NET_PROTO_HTTP1) {
scr_closure_release(cb);
return;
}
scr_net_ls_add(&ctx->upgrade_ls, cb, (void *)fn, once);
}
void scr_http1_ctx_settle(void *p) { scr_http_srv_ctx_settle(p); }
static void scr_http_conn_drop_request(ScrHttpConn *conn, bool fire_end) {
/* the deferred 'close' emits: a request whose body was read to 'end'
* was destroyed then and closes first; otherwise res before req (Node
* ends an unread request after the response finishes) */
if (conn->req && conn->req->end_consumed) scr_http_queue_req_close(conn->req);
if (conn->res) scr_http_queue_res_close(conn->res);
if (conn->req) scr_http_queue_req_close(conn->req);
if (conn->req) {
scr_http_req_finish(conn->req, fire_end);
scr_http_req_release(conn->req);
conn->req = NULL;
}
if (conn->res) {
conn->res->conn = NULL;
scr_http_res_release(conn->res);
conn->res = NULL;
}
}
/* 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. 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) {
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);
} else {
/* Node's resOnFinish: an open connection idles for keepAliveTimeout +
* keepAliveTimeoutBuffer, then the socket times out and is destroyed
* unless a 'timeout' listener handles it. */
ScrNetServer *srv = scr_net_sock_server(conn->sock);
double ms = srv != NULL ? scr_net_server_keep_alive_ms(srv) : 0;
if (ms > 0) {
scr_net_sock_set_timeout(conn->sock, ms);
conn->keep_alive_timer = true;
}
}
}
/* One parsed header line (name lowercased into the req). */
static void scr_http_req_add_header(ScrHttpReq *r, const char *name, size_t nlen,
const char *value, size_t vlen) {
size_t n = r->nheaders;
/* Geometric growth with the capacity implied by the count (4, 8, 16,
* ...): this is the arrays' only appender. */
if (n == 0 || (n >= 4 && (n & (n - 1)) == 0)) {
size_t cap = n == 0 ? 4 : n * 2;
r->hnames = realloc(r->hnames, cap * sizeof *r->hnames);
r->hnames_raw = realloc(r->hnames_raw, cap * sizeof *r->hnames_raw);
r->hvalues = realloc(r->hvalues, cap * sizeof *r->hvalues);
if (!r->hnames || !r->hnames_raw || !r->hvalues) scr_http_oom();
}
ScrStr *raw = scr_str_new(name, nlen);
size_t first_upper = 0;
while (first_upper < nlen && !(name[first_upper] >= 'A' && name[first_upper] <= 'Z')) first_upper++;
ScrStr *lower;
if (first_upper == nlen) {
lower = scr_str_retain(raw); /* already lowercase: one immutable string serves both */
} else {
char small[128];
char *buf = nlen <= sizeof small ? small : malloc(nlen);
if (!buf) scr_http_oom();
for (size_t i = 0; i < nlen; i++) buf[i] = (char)tolower((unsigned char)name[i]);
lower = scr_str_new(buf, nlen);
if (buf != small) free(buf);
}
r->hnames[n] = lower;
r->hnames_raw[n] = raw;
r->hvalues[n] = scr_str_new(value, vlen);
r->nheaders = n + 1;
}
static void scr_http_req_add_trailer(ScrHttpReq *r, const char *name, size_t nlen,
const char *value, size_t vlen) {
char *lower = malloc(nlen);
if (!lower && nlen > 0) scr_http_oom();
for (size_t i = 0; i < nlen; i++) lower[i] = (char)tolower((unsigned char)name[i]);
size_t n = r->ntrailers + 1;
r->tnames = realloc(r->tnames, n * sizeof *r->tnames);
r->tnames_raw = realloc(r->tnames_raw, n * sizeof *r->tnames_raw);
r->tvalues = realloc(r->tvalues, n * sizeof *r->tvalues);
if (!r->tnames || !r->tnames_raw || !r->tvalues) scr_http_oom();
r->tnames[r->ntrailers] = scr_str_new(lower, nlen);
r->tnames_raw[r->ntrailers] = scr_str_new(name, nlen);
r->tvalues[r->ntrailers] = scr_str_new(value, vlen);
r->ntrailers = n;
free(lower);
}
/* 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) {
ScrHttpBuf b = {NULL, 0, 0};
scr_http_buf_str(&b, "HTTP/1.1 400 Bad Request\r\nConnection: close\r\n");
scr_http_buf_date(&b);
scr_http_buf_str(&b, "Transfer-Encoding: chunked\r\n\r\n0\r\n\r\n");
scr_net_sock_write_native(conn->sock, b.data, b.len);
free(b.data);
scr_net_sock_end(conn->sock);
conn->len = 0;
scr_http_conn_drop_request(conn, false);
}
/* Delivers a body slice to a request's data listeners (the parser's
* 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;
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;
r->pend = realloc(r->pend, cap);
if (!r->pend) scr_http_oom();
r->pend_cap = cap;
}
memcpy(r->pend + r->pend_len, data, n);
r->pend_len += n;
return;
}
bool piped = r->pipe_res != NULL || r->pipe_client != NULL || r->pipe_sock != NULL;
if (r->data_ls.n == 0 && !piped) return;
ScrBytes *chunk = scr_bytes_new(SCR_BYTES_U8, (double)n);
memcpy(chunk->data, data, n);
if (r->data_ls.n > 0) {
ScrNetL *snap;
size_t nl = scr_net_ls_snapshot(&r->data_ls, &snap);
scr_dyn_this_push(r, SCR_DYNH_HTTP_REQ);
scr_dyn_chunk_enc(r->enc_utf8);
for (size_t i = 0; i < nl; i++) {
if (!scr_exc_pending()) ((ScrNetDataFn)snap[i].fn)(snap[i].cb, chunk);
scr_closure_release(snap[i].cb);
}
scr_dyn_chunk_enc(false);
scr_dyn_this_pop();
free(snap);
}
if (!scr_exc_pending()) {
if (r->pipe_res) scr_http_res_write_bytes(r->pipe_res, chunk);
if (r->pipe_client) scr_http_client_write_bytes(r->pipe_client, chunk);
if (r->pipe_sock) scr_net_sock_write_bytes(r->pipe_sock, chunk);
}
scr_bytes_release(chunk);
}
static void scr_http_conn_body_data(ScrHttpConn *conn, const char *data, size_t n) {
scr_http_req_deliver(conn->req, data, n);
}
static void scr_http_client_response_done(ScrHttpConn *conn);
/* Body complete: 'end' fires on the request; the response may already be
* finished (handler answered from the head alone) — then the connection
* resets here instead. Client mode: 'end' on the response, then the
* whole exchange tears down (one request per connection). */
static void scr_http_conn_body_done(ScrHttpConn *conn) {
conn->state = SCR_HTTP_HEAD;
conn->body_remaining = 0;
if (conn->client_mode) {
scr_http_client_response_done(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);
}
if (conn->res && conn->res->finished) {
bool keep = conn->res->keep_alive && !conn->close_after;
scr_http_conn_drop_request(conn, false);
if (!keep) scr_net_sock_end(conn->sock);
}
}
/* Node's continueExpression, /(?:^|\W)100-continue(?:$|\W)/i. */
static bool scr_http_expects_continue(const ScrStr *value) {
static const char token[] = "100-continue";
size_t n = sizeof token - 1;
for (size_t i = 0; i + n <= value->len; i++) {
bool before = i == 0 || !(isalnum((unsigned char)value->data[i - 1]) || value->data[i - 1] == '_');
size_t end = i + n;
bool after = end == value->len || !(isalnum((unsigned char)value->data[end]) || value->data[end] == '_');
bool match = before && after;
for (size_t k = 0; k < n && match; k++) match = tolower((unsigned char)value->data[i + k]) == token[k];
if (match) return true;
}
return false;
}
/* Parses one complete request HEAD sitting at buf[0..head_len). Returns
* 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;
/* METHOD SP request-target SP HTTP/1.x */
const char *sp1 = memchr(buf, ' ', (size_t)(line_end - buf));
if (!sp1) return false;
const char *sp2 = memchr(sp1 + 1, ' ', (size_t)(line_end - sp1 - 1));
if (!sp2) return false;
const char *ver = sp2 + 1;
size_t verlen = (size_t)(line_end - ver);
bool http10;
if (verlen == 8 && memcmp(ver, "HTTP/1.1", 8) == 0) http10 = false;
else if (verlen == 8 && memcmp(ver, "HTTP/1.0", 8) == 0) http10 = true;
else return false;
ScrHttpReq *req = calloc(1, sizeof *req);
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
scr_http_live++;
#endif
req->method = scr_str_new(buf, (size_t)(sp1 - buf));
req->url = scr_str_new(sp1 + 1, (size_t)(sp2 - sp1 - 1));
req->http10 = http10;
/* header lines until the blank line */
const char *p = line_end + 2;
const char *end = buf + head_len;
bool ok = true;
while (p < end) {
const char *eol = memchr(p, '\r', (size_t)(end - p));
if (!eol || eol + 1 >= end || eol[1] != '\n') {
ok = false;
break;
}
if (eol == p) {
p += 2; /* the blank line */
break;
}
const char *colon = memchr(p, ':', (size_t)(eol - p));
if (!colon || colon == p) {
ok = false;
break;
}
const char *v = colon + 1;
while (v < eol && (*v == ' ' || *v == '\t')) v++;
const char *ve = eol;
while (ve > v && (ve[-1] == ' ' || ve[-1] == '\t')) ve--;
scr_http_req_add_header(req, p, (size_t)(colon - p), v, (size_t)(ve - v));
p = eol + 2;
}
size_t header_size = req->url->len;
for (size_t i = 0; i < req->nheaders; i++) header_size += req->hnames[i]->len + req->hvalues[i]->len;
if (!ok || req->nheaders > SCR_HTTP_MAX_HEADER_FIELDS || header_size >= SCR_HTTP_MAX_HEADER_SIZE) {
*overflow = ok;
scr_http_req_release(req);
return false;
}
conn->head_bytes = head_len;
conn->trailer_bytes = 0;
conn->trailer_fields = 0;
if (!http10 && conn->srv->require_host_header) {
bool has_host = false;
for (size_t i = 0; i < req->nheaders; i++) {
const ScrStr *name = req->hnames[i];
if (name->len == 4 && memcmp(name->data, "host", 4) == 0) {
has_host = true;
break;
}
}
if (!has_host) {
scr_http_req_release(req);
scr_http_conn_missing_host(conn);
return true;
}
}
/* framing: chunked wins over Content-Length (RFC 9112) */
bool chunked = false;
long long content_length = 0;
bool req_keep_alive = !http10;
bool has_upgrade_hdr = false;
bool conn_upgrade_token = false;
for (size_t i = 0; i < req->nheaders; i++) {
const ScrStr *n = req->hnames[i];
const ScrStr *v = req->hvalues[i];
if (n->len == 17 && memcmp(n->data, "transfer-encoding", 17) == 0) {
if (v->len >= 7 && strstr(v->data, "chunked") != NULL) chunked = true;
} else if (n->len == 14 && memcmp(n->data, "content-length", 14) == 0) {
content_length = atoll(v->data);
if (content_length < 0) content_length = 0;
} else if (n->len == 7 && memcmp(n->data, "upgrade", 7) == 0) {
has_upgrade_hdr = true;
} else if (n->len == 10 && memcmp(n->data, "connection", 10) == 0) {
/* token scan, case-insensitive enough for close/keep-alive */
if (strcasestr(v->data, "upgrade") != NULL) conn_upgrade_token = true;
if (strcasestr(v->data, "close") != NULL) req_keep_alive = false;
else if (strcasestr(v->data, "keep-alive") != NULL) req_keep_alive = true;
}
}
/* A CONNECT request: 'connect' fires INSTEAD of 'request' — the
* upgrade machinery's exact shape ((req, socket, head); the parser
* steps aside; no listener destroys the socket, Node's default). The
* h2 compat server's HTTP/1.1 arm lands here too; its HTTP/2 arm is
* dispatched separately with the compatibility response handle. */
if (req->method->len == 7 && memcmp(req->method->data, "CONNECT", 7) == 0) {
memmove(conn->buf, conn->buf + head_len, conn->len - head_len);
conn->len -= head_len;
if (!scr_net_ls_has_live(&conn->srv->connect_ls)) {
scr_http_req_release(req);
conn->len = 0;
scr_net_sock_destroy(conn->sock);
return true;
}
ScrBytes *head = scr_bytes_new(SCR_BYTES_U8, (double)conn->len);
if (conn->len > 0) memcpy(head->data, conn->buf, conn->len);
conn->len = 0;
scr_net_sock_clear_native_reader(conn->sock);
ScrNetL *snap;
size_t n = scr_net_ls_snapshot(&conn->srv->connect_ls, &snap);
scr_dyn_this_push(scr_net_sock_server(conn->sock), SCR_DYNH_NET_SERVER);
for (size_t i = 0; i < n; i++) {
if (!scr_exc_pending()) {
((ScrHttpUpgradeFn)snap[i].fn)(snap[i].cb, scr_http_req_retain(req),
scr_net_sock_retain(conn->sock), scr_bytes_retain(head));
}
scr_closure_release(snap[i].cb);
}
scr_dyn_this_pop();
free(snap);
scr_bytes_release(head);
scr_http_req_release(req);
return true;
}
/* An Upgrade request (Connection: upgrade + an Upgrade header): the
* 'upgrade' listeners take the socket RAW — the parser steps aside
* (fn pointers clear; the conn ctx stays until the socket dies), bytes
* already read past the head travel as `head`, and no 'request' fires.
* With no listener Node destroys the socket; so does this slice. */
if (has_upgrade_hdr && conn_upgrade_token) {
memmove(conn->buf, conn->buf + head_len, conn->len - head_len);
conn->len -= head_len;
if (conn->srv->upgrade_ls.n == 0) {
scr_http_req_release(req);
conn->len = 0;
scr_net_sock_destroy(conn->sock);
return true;
}
ScrBytes *head = scr_bytes_new(SCR_BYTES_U8, (double)conn->len);
if (conn->len > 0) memcpy(head->data, conn->buf, conn->len);
conn->len = 0;
scr_net_sock_clear_native_reader(conn->sock);
ScrNetL *snap;
size_t n = scr_net_ls_snapshot(&conn->srv->upgrade_ls, &snap);
scr_dyn_this_push(scr_net_sock_server(conn->sock), SCR_DYNH_NET_SERVER);
for (size_t i = 0; i < n; i++) {
if (!scr_exc_pending()) {
((ScrHttpUpgradeFn)snap[i].fn)(snap[i].cb, scr_http_req_retain(req),
scr_net_sock_retain(conn->sock), scr_bytes_retain(head));
}
scr_closure_release(snap[i].cb);
}
scr_dyn_this_pop();
free(snap);
scr_bytes_release(head);
scr_http_req_release(req);
return true;
}
ScrHttpRes *res = calloc(1, sizeof *res);
if (!res) scr_http_oom();
res->rc = 1;
#ifdef SCR_RC_AUDIT
scr_http_live++;
#endif
res->sock = scr_net_sock_retain(conn->sock);
res->conn = conn;
res->keep_alive = req_keep_alive;
res->req_ref = scr_http_req_retain(req); /* res.req — req never points back */
conn->req = req; /* conn's +1 */
conn->res = res;
conn->close_after = !req_keep_alive;
if (chunked) {
conn->state = SCR_HTTP_CHUNK_SIZE;
} else if (content_length > 0) {
conn->state = SCR_HTTP_BODY_CL;
conn->body_remaining = (size_t)content_length;
} else {
conn->state = SCR_HTTP_HEAD; /* no body */
}
/* consume the head bytes */
memmove(conn->buf, conn->buf + head_len, conn->len - head_len);
conn->len -= head_len;
/* Expect on an HTTP/1.1 request, as Node handles it with no
* checkContinue or checkExpectation listener: 100-continue writes
* "100 Continue" before 'request'; any other expectation answers 417
* and emits no 'request'. */
const ScrStr *expect = NULL;
for (size_t i = 0; i < req->nheaders && !http10; i++) {
if (req->hnames[i]->len == 6 && memcmp(req->hnames[i]->data, "expect", 6) == 0) {
expect = req->hvalues[i];
break;
}
}
if (expect != NULL) {
if (scr_http_expects_continue(expect)) {
scr_http_res_write_continue(res);
} else {
scr_http_res_write_head(res, 417);
scr_http_res_end(res);
return true;
}
}
/* fire the 'request' listeners (macrotask, main stack) — snapshot at
* emit time, the scr_net_fire0 discipline; each listener gets its own
* req/res refs (the adapters release what their shape drops) */
{
ScrNetL *snap;
size_t n = scr_net_ls_snapshot(&conn->srv->request_ls, &snap);
scr_dyn_this_push(scr_net_sock_server(conn->sock), SCR_DYNH_NET_SERVER);
for (size_t i = 0; i < n; i++) {
if (!scr_exc_pending()) {
((ScrHttpReqFn)snap[i].fn)(snap[i].cb, scr_http_req_retain(req), scr_http_res_retain(res));
}
scr_closure_release(snap[i].cb);
}
scr_dyn_this_pop();
free(snap);
}
if (scr_exc_pending()) return true; /* the loop surfaces it */
if (!chunked && content_length == 0) {
/* headless body: 'end' delivers to the listeners the handler attached */
scr_http_conn_body_done(conn);
}
return true;
}
/* The parse pump: consume as much of conn->buf as the state machine can. */
static void scr_http_conn_pump(ScrHttpConn *conn) {
for (;;) {
if (scr_exc_pending()) return;
if (conn->state == SCR_HTTP_HEAD) {
if (conn->client_mode) {
if (conn->client == NULL || conn->client_resp_started) {
/* response already delivered (or torn down): stray bytes after
* the exchange are discarded — the socket is closing anyway */
conn->len = 0;
return;
}
} else if (conn->req != NULL && conn->res == NULL) {
return; /* torn down */
}
/* between requests, or awaiting a head */
if (conn->len == 0) return;
/* find CRLFCRLF */
char *hit = NULL;
if (conn->len >= 4) {
for (size_t i = 0; i + 3 < conn->len; i++) {
if (conn->buf[i] == '\r' && conn->buf[i + 1] == '\n' && conn->buf[i + 2] == '\r' &&
conn->buf[i + 3] == '\n') {
hit = conn->buf + i;
break;
}
}
}
if (!hit) {
if (conn->len > SCR_HTTP_MAX_HEADER_BYTES) {
conn->header_overflow = true;
if (conn->client_mode) scr_http_client_head_overflow(conn);
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) {
conn->header_overflow = true;
if (conn->client_mode) scr_http_client_head_overflow(conn);
else scr_http_conn_header_overflow(conn);
return;
}
if (conn->client_mode) {
if (!scr_http_client_parse_head(conn, head_len)) {
scr_http_client_head_overflow(conn); /* malformed: hang up */
}
if (scr_exc_pending()) return;
continue;
}
/* a previous exchange must be COMPLETE before the next parses: the
* 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);
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;
}
if (conn->state == SCR_HTTP_BODY_CL) {
if (conn->len == 0) return;
size_t take = conn->len < conn->body_remaining ? conn->len : conn->body_remaining;
scr_http_conn_body_data(conn, conn->buf, take);
memmove(conn->buf, conn->buf + take, conn->len - take);
conn->len -= take;
conn->body_remaining -= take;
if (conn->body_remaining == 0) scr_http_conn_body_done(conn);
else return;
continue;
}
if (conn->state == SCR_HTTP_CHUNK_SIZE) {
char *eol = NULL;
for (size_t i = 0; i + 1 < conn->len; i++) {
if (conn->buf[i] == '\r' && conn->buf[i + 1] == '\n') {
eol = conn->buf + i;
break;
}
}
if (!eol) return;
size_t size = 0;
bool any = false;
for (char *p = conn->buf; p < eol; p++) {
char c = *p;
if (c == ';') break; /* chunk extensions: ignored */
int digit;
if (c >= '0' && c <= '9') digit = c - '0';
else if (c >= 'a' && c <= 'f') digit = c - 'a' + 10;
else if (c >= 'A' && c <= 'F') digit = c - 'A' + 10;
else {
scr_http_conn_bad_request(conn);
return;
}
size = size * 16 + (size_t)digit;
any = true;
}
if (!any) {
scr_http_conn_bad_request(conn);
return;
}
size_t consumed = (size_t)(eol - conn->buf) + 2;
memmove(conn->buf, conn->buf + consumed, conn->len - consumed);
conn->len -= consumed;
if (size == 0) {
conn->state = SCR_HTTP_CHUNK_TRAILER;
} else {
conn->state = SCR_HTTP_CHUNK_DATA;
conn->body_remaining = size;
}
continue;
}
if (conn->state == SCR_HTTP_CHUNK_DATA) {
if (conn->len == 0) return;
size_t take = conn->len < conn->body_remaining ? conn->len : conn->body_remaining;
scr_http_conn_body_data(conn, conn->buf, take);
memmove(conn->buf, conn->buf + take, conn->len - take);
conn->len -= take;
conn->body_remaining -= take;
if (conn->body_remaining == 0) conn->state = SCR_HTTP_CHUNK_CRLF;
else return;
continue;
}
if (conn->state == SCR_HTTP_CHUNK_CRLF) {
if (conn->len < 2) return;
if (conn->buf[0] != '\r' || conn->buf[1] != '\n') {
scr_http_conn_bad_request(conn);
return;
}
memmove(conn->buf, conn->buf + 2, conn->len - 2);
conn->len -= 2;
conn->state = SCR_HTTP_CHUNK_SIZE;
continue;
}
if (conn->state == SCR_HTTP_BODY_EOF) {
/* RESPONSE mode, no framing header: every arrived byte is body;
* EOF completes it (the eof hook calls body_done). */
if (conn->len == 0) return;
scr_http_conn_body_data(conn, conn->buf, conn->len);
conn->len = 0;
return;
}
if (conn->state == SCR_HTTP_CHUNK_TRAILER) {
/* One complete field per pass; preserve wire case/order and keep
* trailers separate from the head until 'end' fires. */
if (conn->len < 2) return;
if (conn->buf[0] == '\r' && conn->buf[1] == '\n') {
if (!scr_http_conn_trailer_fits(conn, 2, false)) {
conn->header_overflow = true;
if (conn->client_mode) scr_http_client_head_overflow(conn);
else scr_http_conn_header_overflow(conn);
return;
}
memmove(conn->buf, conn->buf + 2, conn->len - 2);
conn->len -= 2;
conn->trailer_bytes += 2;
scr_http_conn_body_done(conn);
continue;
}
char *eol = NULL;
for (size_t i = 0; i + 1 < conn->len; i++) {
if (conn->buf[i] == '\r' && conn->buf[i + 1] == '\n') {
eol = conn->buf + i;
break;
}
}
if (!eol) {
if (!scr_http_conn_trailer_fits(conn, conn->len, false)) {
conn->header_overflow = true;
if (conn->client_mode) scr_http_client_head_overflow(conn);
else scr_http_conn_header_overflow(conn);
}
return;
}
char *colon = memchr(conn->buf, ':', (size_t)(eol - conn->buf));
bool valid = colon != NULL && colon != conn->buf;
for (char *p = conn->buf; valid && p < colon; p++) valid = scr_http_token_char((unsigned char)*p);
if (!valid) {
if (conn->client_mode) scr_http_client_head_overflow(conn);
else scr_http_conn_bad_request(conn);
return;
}
char *value = colon + 1;
while (value < eol && (*value == ' ' || *value == '\t')) value++;
char *end = eol;
while (end > value && (end[-1] == ' ' || end[-1] == '\t')) end--;
for (char *p = value; valid && p < end; p++) {
unsigned char ch = (unsigned char)*p;
valid = (ch >= 32 || ch == '\t') && ch != 127;
}
if (!valid) {
if (conn->client_mode) scr_http_client_head_overflow(conn);
else scr_http_conn_bad_request(conn);
return;
}
size_t consumed = (size_t)(eol - conn->buf) + 2;
if (!scr_http_conn_trailer_fits(conn, consumed, true)) {
conn->header_overflow = true;
if (conn->client_mode) scr_http_client_head_overflow(conn);
else scr_http_conn_header_overflow(conn);
return;
}
/* An early server response releases the request but deliberately leaves
* this parser in its chunk states so the wire remains validated. */
if (conn->req) {
scr_http_req_add_trailer(conn->req, conn->buf, (size_t)(colon - conn->buf),
value, (size_t)(end - value));
}
memmove(conn->buf, conn->buf + consumed, conn->len - consumed);
conn->len -= consumed;
conn->trailer_bytes += consumed;
conn->trailer_fields++;
continue;
}
return;
}
}
/* ── the native socket hooks ─────────────────────────────────────────── */
static void scr_http_conn_data(void *ctx, const char *buf, size_t n) {
ScrHttpConn *conn = (ScrHttpConn *)ctx;
if (conn->len + n > conn->cap) {
size_t cap = conn->cap ? conn->cap : 8192;
while (cap < conn->len + n) cap *= 2;
conn->buf = realloc(conn->buf, cap);
if (!conn->buf) scr_http_oom();
conn->cap = cap;
}
memcpy(conn->buf + conn->len, buf, n);
conn->len += n;
if (conn->keep_alive_timer) {
/* the next request restores server.timeout (Node's onParserExecute) */
conn->keep_alive_timer = false;
ScrNetServer *srv = scr_net_sock_server(conn->sock);
scr_net_sock_set_timeout(conn->sock, srv != NULL ? scr_net_server_idle_ms(srv) : 0);
}
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);
static void scr_http_client_closed(ScrHttpConn *conn);
static void scr_http_client_conn_free(ScrHttpConn *conn);
static void scr_http_conn_eof(void *ctx) {
ScrHttpConn *conn = (ScrHttpConn *)ctx;
if (conn->client_mode) {
scr_http_client_eof(conn);
return;
}
/* client FIN between requests: our half closes too (keep-alive over);
* mid-request it is an aborted request — everything settles quietly */
scr_http_conn_drop_request(conn, false);
conn->len = 0;
}
static void scr_http_conn_closed(void *ctx) {
ScrHttpConn *conn = (ScrHttpConn *)ctx;
if (conn->client_mode) {
scr_http_client_closed(conn);
return;
}
scr_http_conn_drop_request(conn, false);
}
/* Socket errors under a parser: the protocol layer owns the story. On
* the SERVER an abrupt client death (ECONNRESET mid-request) must never
* crash the process — Node's http server swallows these into request
* teardown ('error' listeners on the req get "aborted"); the CLIENT
* routes them to the request handle's 'error'. Always consumed: the
* underlying socket's own listeners never see a parser-owned error. */
static bool scr_http_client_sock_err(ScrHttpConn *conn, ScrStr *msg);
static bool scr_http_conn_err(void *ctx, ScrStr *msg) {
ScrHttpConn *conn = (ScrHttpConn *)ctx;
if (conn->client_mode) return scr_http_client_sock_err(conn, msg);
if (conn->req && conn->req->err_ls.n > 0) {
ScrStr *aborted = scr_str_new("aborted", 7);
scr_net_fire_err_this(&conn->req->err_ls, aborted, conn->req, SCR_DYNH_HTTP_REQ);
scr_str_release(aborted);
}
scr_http_conn_drop_request(conn, false);
return true;
}
static void scr_http_conn_free(void *ctx) {
ScrHttpConn *conn = (ScrHttpConn *)ctx;
if (conn->client_mode) {
scr_http_client_conn_free(conn);
return;
}
scr_http_conn_drop_request(conn, false);
if (conn->srv) scr_http_srv_ctx_release(conn->srv);
free(conn->buf);
free(conn);
}
static bool scr_http_conn_is_idle(void *ctx) {
ScrHttpConn *conn = (ScrHttpConn *)ctx;
return !conn->client_mode && conn->state == SCR_HTTP_HEAD &&
conn->req == NULL && conn->len == 0;
}
/* Node's server socketOnTimeout emits on an incomplete request, then on
* the active response. Listener presence, including a once listener
* removed by the emit, prevents the server's unhandled auto-destroy. */
static bool scr_http_conn_server_timeout(void *ctx) {
ScrHttpConn *conn = (ScrHttpConn *)ctx;
bool handled = false;
if (conn->client_mode) return false;
ScrHttpReq *req = conn->req;
if (req && !req->ended && !req->destroyed && req->timeout_ls.n > 0) {
handled = true;
scr_http_req_retain(req);
scr_net_fire0_this(&req->timeout_ls, req, SCR_DYNH_HTTP_REQ);
scr_http_req_release(req);
}
if (scr_exc_pending()) return handled;
ScrHttpRes *res = conn->res;
if (res && !res->close_emitted && res->timeout_ls.n > 0) {
handled = true;
scr_http_res_retain(res);
scr_net_fire0_this(&res->timeout_ls, res, SCR_DYNH_HTTP_RES);
scr_http_res_release(res);
}
return handled;
}
/* The server's native connection hook: one parser per accepted socket. */
static void scr_http_on_connection(void *ctx, ScrNetSocket *sock) {
ScrHttpSrvCtx *srv = (ScrHttpSrvCtx *)ctx;
ScrHttpConn *conn = calloc(1, sizeof *conn);
if (!conn) scr_http_oom();
conn->sock = sock; /* borrowed — see the struct comment */
conn->srv = scr_http_srv_ctx_retain(srv);
scr_net_sock_set_native_reader(sock, &scr_http_conn_data, &scr_http_conn_eof,
&scr_http_conn_closed, conn, &scr_http_conn_free);
scr_net_sock_set_native_idle_checker(sock, &scr_http_conn_is_idle);
scr_net_sock_set_native_events(sock, NULL, &scr_http_conn_err);
scr_net_sock_set_native_http_timeout(sock, &scr_http_conn_server_timeout);
}
/* The unguarded h2-only stream call (`req.stream.on(...)`): stream IS
* undefined on every connection this lowering accepts — and on every
* HTTP/1.1 connection of Node's own allowHTTP1 server — so calling
* through it is Node's member read on undefined: the exact catchable
* TypeError, member name interpolated. Borrows the name; never returns. */
void scr_http2_stream_undef_call(ScrStr *member) {
char msg[128];
int n = snprintf(msg, sizeof msg, "Cannot read properties of undefined (reading '%.*s')",
(int)member->len, member->data);
scr_throw_error_msg(SCR_ERR_TYPE, msg, (size_t)n);
}
/* http.createServer(handler): an ordinary net server whose connections
* feed the parser; listen/close/address/'error' all ride the net surface.
* A NULL handler (http2.createSecureServer's route) creates the server
* with an empty 'request' list — server.on("request", ...) fills it. */
ScrNetServer *scr_http_create_server(ScrClosure *handler /*moves, nullable*/, ScrHttpReqFn fn) {
scr_http_install();
ScrNetServer *s = scr_net_create_server(NULL, NULL);
scr_net_server_enable_http_timeout_surface(s);
ScrHttpSrvCtx *ctx = calloc(1, sizeof *ctx);
if (!ctx) scr_http_oom();
ctx->proto = SCR_NET_PROTO_HTTP1;
ctx->rc = 1;
ctx->require_host_header = true;
if (handler != NULL) scr_net_ls_add(&ctx->request_ls, handler, (void *)fn, false);
scr_net_server_set_native_conn(s, &scr_http_on_connection, ctx, &scr_http_srv_ctx_free);
scr_net_server_set_http_ctx(s, ctx);
scr_net_server_set_proto_settle(s, &scr_http_srv_ctx_settle);
return s;
}
/* createServer({ joinDuplicateHeaders: true, ... }): repeated request
* header names read back joined with ", " where Node's default (and this
* parser's) keeps the first occurrence. A no-op on parserless servers. */
void scr_http_server_join_duplicate_headers(ScrNetServer *s) {
ScrHttpSrvCtx *ctx = (ScrHttpSrvCtx *)scr_net_server_get_http_ctx(s);
if (ctx != NULL && ctx->proto == SCR_NET_PROTO_HTTP1) ctx->join_dup = true;
}
void scr_http_server_allow_missing_host_header(ScrNetServer *s) {
ScrHttpSrvCtx *ctx = (ScrHttpSrvCtx *)scr_net_server_get_http_ctx(s);
if (ctx != NULL && ctx->proto == SCR_NET_PROTO_HTTP1) ctx->require_host_header = false;
}
/* Late 'request' listener installs (server.on/once("request", ...)): the
* http2 allowHTTP1 server's handler route, and http.Server's 'request'
* event. On a server with no HTTP parser the closure releases
* unregistered — 'request' never fires there, like Node. */
void scr_http_server_on_request(ScrNetServer *s, ScrClosure *cb /*moves*/, ScrHttpReqFn fn,
bool once) {
ScrHttpSrvCtx *ctx = (ScrHttpSrvCtx *)scr_net_server_get_http_ctx(s);
if (ctx != NULL && ctx->proto != SCR_NET_PROTO_HTTP1) {
/* an h2 server's ctx: the compat seam — scr_http2.c's hook owns the
* registration (Http2ServerRequest/Response over h2 streams) */
if (scr_http_h2_request_hook != NULL && !scr_net_server_settled(s)) {
scr_http_h2_request_hook(ctx, cb, (void *)fn, once);
return;
}
ctx = NULL;
}
if (ctx == NULL || scr_net_server_settled(s)) {
/* no parser, or the server already settled ('request' can never fire
* again — an install would only re-arm the cycle the settle broke) */
scr_closure_release(cb);
return;
}
scr_net_ls_add(&ctx->request_ls, cb, (void *)fn, once);
}
/* ── the h2 compat pair (built by scr_http2.c per server stream) ───────
*
* The request materializes from the decoded header list (:method/:path
* feed the line; EVERY pair — pseudo-headers included — lands in
* req.headers, Node's compat shape); the response routes its write paths
* through the h2 ops vtable. Body DATA frames and END_STREAM feed the
* req exactly like the parser's body states; 'close' rides this unit's
* deferred-emit queue so ordering matches the http/1 story (res before
* req). */
ScrHttpReq *scr_http_h2_req_new(ScrNetSocket *sock /*borrowed, nullable*/,
void *stream /*borrowed, nullable*/) {
scr_http_install();
ScrHttpReq *req = calloc(1, sizeof *req);
if (!req) scr_http_oom();
req->rc = 1;
req->status = -1; /* server requests: statusCode is Node's undefined */
req->http2 = true;
req->sock = sock ? scr_net_sock_retain(sock) : NULL;
req->h2_stream = stream != NULL ? scr_http_h2_ops->retain(stream) : NULL;
req->method = scr_str_new("GET", 3); /* :method replaces it */
req->url = scr_str_new("/", 1); /* :path replaces it */
#ifdef SCR_RC_AUDIT
scr_http_live++;
#endif
return req;
}
void *scr_http_req_h2_stream(ScrHttpReq *r) {
return r->h2_stream != NULL ? scr_http_h2_ops->retain(r->h2_stream) : NULL;
}
void *scr_http_req_h2_stream_or_throw(ScrHttpReq *r, ScrStr *member /*borrowed*/) {
if (r->h2_stream != NULL) return scr_http_h2_ops->retain(r->h2_stream);
scr_http2_stream_undef_call(member);
return NULL;
}
void scr_http_h2_req_line(ScrHttpReq *r, ScrStr *method /*borrowed, nullable*/,
ScrStr *url /*borrowed, nullable*/) {
if (method != NULL) {
scr_str_release(r->method);
r->method = scr_str_retain(method);
}
if (url != NULL) {
scr_str_release(r->url);
r->url = scr_str_retain(url);
}
}
void scr_http_h2_req_header(ScrHttpReq *r, ScrStr *name /*borrowed*/, ScrStr *value /*borrowed*/) {
scr_http_req_add_header(r, name->data, name->len, value->data, value->len);
}
void scr_http_h2_req_data(ScrHttpReq *r, const char *data, size_t n) {
scr_http_req_deliver(r, data, n);
}
void scr_http_h2_req_end(ScrHttpReq *r) { scr_http_req_finish(r, true); }
/* Typed member reads shared by both parser lanes: the version triple and
* the compat pair's aborted/complete flags. */
ScrStr *scr_http_req_http_version(ScrHttpReq *r) {
return scr_str_new(r->http2 ? "2.0" : r->http10 ? "1.0" : "1.1", 3);
}
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->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. */
void scr_http_h2_req_aborted(ScrHttpReq *r) {
if (r->close_emitted || r->aborted) return;
r->aborted = true;
scr_net_fire0_this(&r->aborted_ls, r, SCR_DYNH_HTTP_REQ);
scr_net_ls_drop(&r->aborted_ls);
}
void scr_http_h2_req_close(ScrHttpReq *r) { scr_http_queue_req_close(r); }
ScrHttpRes *scr_http_h2_res_new(ScrNetSocket *sock /*borrowed, nullable*/,
void *stream /*borrowed*/) {
scr_http_install();
ScrHttpRes *res = calloc(1, sizeof *res);
if (!res) scr_http_oom();
res->rc = 1;
res->sock = sock ? scr_net_sock_retain(sock) : NULL;
res->h2_stream = scr_http_h2_ops->retain(stream);
res->keep_alive = true; /* inert on h2 — no Connection header exists */
#ifdef SCR_RC_AUDIT
scr_http_live++;
#endif
return res;
}
void scr_http_h2_res_close(ScrHttpRes *r) { scr_http_queue_res_close(r); }
bool scr_http_h2_res_finished(ScrHttpRes *r) { return r->finished; }
/* server.on("connect", (req, socket, head) => ...) — HTTP CONNECT, and
* server.on("upgrade", ...) below: the registration twins of on_request.
* On a server with no HTTP parser neither list ever fires — honest dead
* weight, Node's shape. */
void scr_http_server_on_connect(ScrNetServer *s, ScrClosure *cb /*moves*/,
ScrHttpUpgradeFn h1_fn, ScrHttpConnectH2Fn h2_fn,
bool once) {
ScrHttpSrvCtx *ctx = (ScrHttpSrvCtx *)scr_net_server_get_http_ctx(s);
if (ctx != NULL && ctx->proto != SCR_NET_PROTO_HTTP1) {
if (scr_http_h2_connect_hook != NULL && !scr_net_server_settled(s)) {
scr_http_h2_connect_hook(ctx, cb, (void *)h1_fn, (void *)h2_fn, once);
return;
}
ctx = NULL;
}
if (ctx == NULL) {
/* no HTTP parser on this server: the event can never fire (Node's
* net server never emits 'connect' either) — honest dead weight */
scr_closure_release(cb);
return;
}
scr_net_ls_add(&ctx->connect_ls, cb, (void *)h1_fn, once);
}
void scr_http_server_on_upgrade(ScrNetServer *s, ScrClosure *cb /*moves*/, ScrHttpUpgradeFn fn,
bool once) {
ScrHttpSrvCtx *ctx = (ScrHttpSrvCtx *)scr_net_server_get_http_ctx(s);
if (ctx != NULL && ctx->proto != SCR_NET_PROTO_HTTP1) {
if (scr_http_h2_upgrade_hook != NULL && !scr_net_server_settled(s)) {
scr_http_h2_upgrade_hook(ctx, cb, (void *)fn, once);
return;
}
ctx = NULL;
}
if (ctx == NULL) {
scr_closure_release(cb);
return;
}
scr_net_ls_add(&ctx->upgrade_ls, cb, (void *)fn, once);
}
/* The upgrade adapters: (req, socket, head), and every shorter prefix.
* All three arrive +1; unused ones release here. */
void scr_http_upgrade_thunk3(ScrClosure *cb, ScrHttpReq *req, ScrNetSocket *sock, ScrBytes *head) {
((void (*)(ScrClosure *, ScrHttpReq *, ScrNetSocket *, ScrBytes *))cb->fn)(cb, req, sock, head);
}
void scr_http_upgrade_thunk2(ScrClosure *cb, ScrHttpReq *req, ScrNetSocket *sock, ScrBytes *head) {
scr_bytes_release(head);
((void (*)(ScrClosure *, ScrHttpReq *, ScrNetSocket *))cb->fn)(cb, req, sock);
}
void scr_http_upgrade_thunk1(ScrClosure *cb, ScrHttpReq *req, ScrNetSocket *sock, ScrBytes *head) {
scr_bytes_release(head);
scr_net_sock_release(sock);
((void (*)(ScrClosure *, ScrHttpReq *))cb->fn)(cb, req);
}
void scr_http_upgrade_thunk0(ScrClosure *cb, ScrHttpReq *req, ScrNetSocket *sock, ScrBytes *head) {
scr_bytes_release(head);
scr_net_sock_release(sock);
scr_http_req_release(req);
((void (*)(ScrClosure *))cb->fn)(cb);
}
/* The handler adapters: (req, res), (req), and (). Both handles arrive
* +1; unused ones release here. */
void scr_http_handler_thunk2(ScrClosure *cb, ScrHttpReq *req, ScrHttpRes *res) {
((void (*)(ScrClosure *, ScrHttpReq *, ScrHttpRes *))cb->fn)(cb, req, res);
}
void scr_http_handler_thunk1(ScrClosure *cb, ScrHttpReq *req, ScrHttpRes *res) {
scr_http_res_release(res);
((void (*)(ScrClosure *, ScrHttpReq *))cb->fn)(cb, req);
}
void scr_http_handler_thunk0(ScrClosure *cb, ScrHttpReq *req, ScrHttpRes *res) {
scr_http_req_release(req);
scr_http_res_release(res);
((void (*)(ScrClosure *))cb->fn)(cb);
}
/* ── the CLIENT (http.request / http.get) ────────────────────────────────
*
* One handle per request, one dialed connection per handle (no agent
* pooling — SEMANTICS.md; the wire is Node's regardless, Connection:
* keep-alive included, and the socket closes when the response
* completes). Ownership: the handle holds the socket (+1) and, once
* parsed, the response req (+1); the socket's parser ctx holds the
* handle (+1) until the connection dies — the registry-shaped cycle
* breaks at teardown exactly like the server's conn→req edge. A live
* client is registered (+1, atexit-cleaned) so abandoned handles behave
* like abandoned sockets. */
struct ScrHttpClientReq {
size_t rc;
ScrNetSocket *sock; /* +1 */
ScrHttpConn *conn; /* borrowed backref (the socket's ctx); NULL after free */
ScrStr *host, *path, *method;
int port;
int default_port; /* the Host header omits it (80 http, 443 https) */
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;
bool chunked; /* streaming framing committed */
int corked;
char *cork_buf;
size_t cork_len, cork_cap;
/* the head a corked write committed, sent first at the flush */
char *held_head;
size_t held_head_len, held_head_cap;
bool hold_head;
bool ended;
bool destroyed;
bool aborted;
bool abort_emitted;
bool sock_ready;
bool socket_emitted;
bool finish_queued;
bool writable_finished;
bool response_started; /* head parsed, res exists */
bool response_done;
bool had_error;
bool close_queued;
bool close_emitted; /* settled: listeners dropped, off the registry */
ScrHttpReq *res; /* +1 once the head parses */
ScrNetLs resp_ls, err_ls, timeout_ls, close_ls, upgrade_ls, socket_ls, finish_ls, abort_ls;
/* the owning Agent (+1; the agent's entry holds this client +1 too —
* the cycle breaks at settle, or at the atexit agent sweep) */
struct ScrHttpAgent *agent;
bool in_registry;
struct ScrHttpClientReq *next;
};
static SCR_TL ScrHttpClientReq *scr_http_clients = NULL; /* registry: +1 each */
ScrHttpClientReq *scr_http_client_retain(ScrHttpClientReq *c) {
if (c->rc != SIZE_MAX) c->rc++;
return c;
}
void scr_http_client_release(ScrHttpClientReq *c) {
if (!c || c->rc == SIZE_MAX) return;
if (--c->rc == 0) {
scr_net_ls_drop(&c->resp_ls);
scr_net_ls_drop(&c->err_ls);
scr_net_ls_drop(&c->timeout_ls);
scr_net_ls_drop(&c->close_ls);
scr_net_ls_drop(&c->upgrade_ls);
scr_net_ls_drop(&c->socket_ls);
scr_net_ls_drop(&c->finish_ls);
scr_net_ls_drop(&c->abort_ls);
scr_str_release(c->host);
scr_str_release(c->path);
scr_str_release(c->method);
for (size_t i = 0; i < c->nheaders; i++) {
scr_str_release(c->hnames[i]);
scr_str_release(c->hvalues[i]);
}
free(c->hnames);
free(c->hvalues);
scr_http_trailers_clear(&c->trailers);
free(c->cork_buf);
free(c->held_head);
if (c->res) scr_http_req_release(c->res);
if (c->sock) scr_net_sock_release(c->sock);
#ifdef SCR_RC_AUDIT
scr_http_live--;
#endif
free(c);
}
}
static void scr_http_client_release_internal(struct ScrHttpClientReq *c) {
scr_http_client_release(c);
}
void *scr_http_client_retain_v(void *p) { return scr_http_client_retain((ScrHttpClientReq *)p); }
void scr_http_client_release_v(void *p) { scr_http_client_release((ScrHttpClientReq *)p); }
static void scr_http_client_register(ScrHttpClientReq *c) {
if (c->in_registry) return;
c->in_registry = true;
c->next = NULL;
ScrHttpClientReq **link = &scr_http_clients;
while (*link) link = &(*link)->next;
*link = scr_http_client_retain(c);
}
static void scr_http_client_unregister(ScrHttpClientReq *c) {
if (!c->in_registry) return;
ScrHttpClientReq **link = &scr_http_clients;
while (*link && *link != c) link = &(*link)->next;
if (*link) {
*link = c->next;
c->next = NULL;
c->in_registry = false;
scr_http_client_release(c);
}
}
/* Agent bookkeeping (implementation below, with the Agent unit): the
* settling client leaves its agent's lists and frees a slot. */
static void scr_http_agent_client_done(struct ScrHttpAgent *ag, struct ScrHttpClientReq *c);
static struct ScrHttpAgent *scr_http_agent_release_p(struct ScrHttpAgent *ag);
static void scr_http_client_agent_detach(struct ScrHttpClientReq *c);
/* The queue's CLIENT_CLOSE emit: 'close' fires, the handle settles
* (listeners drop — the cycle story) and leaves the registry. */
static void scr_http_client_settle(struct ScrHttpClientReq *c) {
if (c->close_emitted) return;
c->close_emitted = true;
c->destroyed = true;
scr_http_client_agent_detach(c); /* usually already detached at socket close */
scr_net_fire0(&c->close_ls);
scr_net_ls_drop(&c->resp_ls);
scr_net_ls_drop(&c->err_ls);
scr_net_ls_drop(&c->timeout_ls);
scr_net_ls_drop(&c->close_ls);
scr_net_ls_drop(&c->socket_ls);
scr_net_ls_drop(&c->finish_ls);
scr_http_client_unregister(c);
}
static void scr_http_client_queue_close(ScrHttpClientReq *c) {
if (c->close_queued || c->close_emitted) return;
c->close_queued = true;
scr_http_emit_push(SCR_HTTP_EMIT_CLIENT_CLOSE, scr_http_client_retain(c));
}
static void scr_http_client_queue_finish(ScrHttpClientReq *c) {
if (!c->ended || !c->sock_ready || c->destroyed || c->close_emitted || c->finish_queued || c->writable_finished) return;
c->finish_queued = true;
scr_http_emit_push(SCR_HTTP_EMIT_CLIENT_FINISH, scr_http_client_retain(c));
}
static void scr_http_client_emit_socket(ScrHttpClientReq *c) {
if (c->socket_emitted) return;
if (c->destroyed) {
scr_net_ls_drop(&c->socket_ls);
return;
}
c->socket_emitted = true;
ScrNetL *snap;
size_t count = scr_net_ls_snapshot(&c->socket_ls, &snap);
scr_dyn_this_push(c, SCR_DYNH_HTTP_CLIENT);
for (size_t i = 0; i < count; i++) {
if (!scr_exc_pending()) ((ScrNetConnFn)snap[i].fn)(snap[i].cb, scr_net_sock_retain(c->sock));
scr_closure_release(snap[i].cb);
}
scr_dyn_this_pop();
free(snap);
scr_net_ls_drop(&c->socket_ls);
}
static void scr_http_client_emit_finish(ScrHttpClientReq *c) {
c->finish_queued = false;
if (c->destroyed || c->close_emitted || !c->ended || !c->sock_ready || c->writable_finished) return;
c->writable_finished = true;
scr_net_fire0_this(&c->finish_ls, c, SCR_DYNH_HTTP_CLIENT);
scr_net_ls_drop(&c->finish_ls);
}
static void scr_http_client_emit_abort(ScrHttpClientReq *c) {
c->abort_emitted = true;
scr_net_fire0_this(&c->abort_ls, c, SCR_DYNH_HTTP_CLIENT);
scr_net_ls_drop(&c->abort_ls);
}
/* 'error' on the request handle: fires NOW (the callers sit in the net
* sweep / dispatch, Node's own emit spots); no listener exits 1, the
* unhandled-'error' story. The close follows through the queue. */
static void scr_http_client_error(ScrHttpClientReq *c, ScrStr *msg /*borrowed*/) {
if (c->close_emitted) return;
c->had_error = true;
scr_net_fire_err(&c->err_ls, msg);
}
/* ── the wire: Node's exact request head ─────────────────────────────── */
static bool scr_http_client_has_header(ScrHttpClientReq *c, const char *name) {
size_t len = strlen(name);
for (size_t i = 0; i < c->nheaders; i++) {
if (c->hnames[i]->len == len) {
bool eq = true;
for (size_t j = 0; j < len && eq; j++) {
if (tolower((unsigned char)c->hnames[i]->data[j]) != tolower((unsigned char)name[j])) eq = false;
}
if (eq) return true;
}
}
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, NULL, 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; }
bool scr_http_client_writable_finished(ScrHttpClientReq *c) { return c->writable_finished; }
ScrNetSocket *scr_http_client_socket(ScrHttpClientReq *c) { return scr_net_sock_retain(c->sock); }
bool scr_http_client_reused_socket(ScrHttpClientReq *c) { (void)c; return false; }
void scr_http_client_set_nodelay(ScrHttpClientReq *c, bool enable) {
if (!c->sock) return;
scr_net_sock_release(scr_net_sock_set_nodelay(c->sock, enable));
}
void scr_http_client_set_socket_keepalive(ScrHttpClientReq *c, bool enable, double delay_ms) {
if (!c->sock) return;
scr_net_sock_set_keepalive(c->sock, enable, delay_ms);
}
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");
}
struct ScrHttpAgent;
static bool scr_http_agent_keeps_alive(const struct ScrHttpAgent *a);
/* 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
* framing. Node's order: user headers verbatim, then Host (unless user-
* set), Connection: keep-alive, and the framing header. */
static void scr_http_client_send_head(ScrHttpClientReq *c, long long body_len) {
if (c->head_sent) return;
ScrHttpBuf b = {NULL, 0, 0};
scr_http_buf_append(&b, c->method->data, c->method->len);
scr_http_buf_str(&b, " ");
scr_http_buf_append(&b, c->path->data, c->path->len);
scr_http_buf_str(&b, " HTTP/1.1\r\n");
for (size_t i = 0; i < c->nheaders; i++) {
scr_http_buf_append(&b, c->hnames[i]->data, c->hnames[i]->len);
scr_http_buf_str(&b, ": ");
scr_http_buf_append(&b, c->hvalues[i]->data, c->hvalues[i]->len);
scr_http_buf_str(&b, "\r\n");
bool transfer = c->hnames[i]->len == 17;
for (size_t j = 0; j < 17 && transfer; j++) {
if (tolower((unsigned char)c->hnames[i]->data[j]) != "transfer-encoding"[j]) transfer = false;
}
if (transfer && scr_http_header_has_token(c->hvalues[i], "chunked")) c->chunked = true;
}
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;
scr_http_buf_str(&b, "Host: ");
if (v6) scr_http_buf_str(&b, "[");
scr_http_buf_append(&b, c->host->data, c->host->len);
if (v6) scr_http_buf_str(&b, "]");
if (c->port != c->default_port) {
char pbuf[16];
snprintf(pbuf, sizeof pbuf, ":%d", c->port);
scr_http_buf_str(&b, pbuf);
}
scr_http_buf_str(&b, "\r\n");
}
if (!c->removed_connection && !scr_http_client_has_header(c, "connection")) {
/* Node asks the server to close only for an agent without keepAlive
* and without a finite maxSockets (scr_http_agent_keeps_alive). */
if (c->agent != NULL && !scr_http_agent_keeps_alive(c->agent)) scr_http_buf_str(&b, "Connection: close\r\n");
else scr_http_buf_str(&b, "Connection: keep-alive\r\n");
}
if (!c->user_cl) {
if (body_len < 0 || scr_http_client_has_header(c, "trailer")) {
scr_http_buf_str(&b, "Transfer-Encoding: chunked\r\n");
c->chunked = true;
} else if (body_len > 0) {
char cl[48];
snprintf(cl, sizeof cl, "Content-Length: %lld\r\n", body_len);
scr_http_buf_str(&b, cl);
} else {
/* empty body: POST/PUT/PATCH send Content-Length: 0, everything
* else sends no framing header at all — Node's method split */
const ScrStr *m = c->method;
bool bodied = (m->len == 4 && memcmp(m->data, "POST", 4) == 0) ||
(m->len == 3 && memcmp(m->data, "PUT", 3) == 0) ||
(m->len == 5 && memcmp(m->data, "PATCH", 5) == 0);
if (bodied) scr_http_buf_str(&b, "Content-Length: 0\r\n");
}
}
if (scr_http_client_has_header(c, "trailer") && !c->chunked) {
static const char msg[] = "Trailers are invalid with this transfer encoding";
free(b.data);
scr_throw_error_msg_code(SCR_ERR_ERROR, msg, sizeof msg - 1, "ERR_HTTP_TRAILER_INVALID");
return;
}
scr_http_buf_str(&b, "\r\n");
c->head_sent = true;
if (c->hold_head) {
c->held_head = b.data;
c->held_head_len = b.len;
c->held_head_cap = b.cap;
return;
}
if (c->sock) scr_net_sock_write_native(c->sock, b.data, b.len);
free(b.data);
}
static void scr_http_client_send_held_head(ScrHttpClientReq *c) {
if (c->held_head == NULL) return;
char *head = c->held_head;
size_t len = c->held_head_len;
c->held_head = NULL;
c->held_head_len = c->held_head_cap = 0;
if (c->sock) scr_net_sock_write_native(c->sock, head, len);
free(head);
}
static void scr_http_client_write_raw(ScrHttpClientReq *c, const char *data, size_t len) {
if (c->ended || c->destroyed || c->close_emitted) return; /* write-after-end drops (divergence 48's stance) */
if (c->corked > 0) {
/* Node's write_() builds the implicit head even while corked */
if (!c->head_sent) {
c->hold_head = true;
scr_http_client_send_head(c, -1);
c->hold_head = false;
if (scr_exc_pending()) return;
}
scr_http_cork_buffer(&c->cork_buf, &c->cork_len, &c->cork_cap, data, len);
return;
}
if (!c->head_sent) scr_http_client_send_head(c, -1); /* streaming: chunked */
if (scr_exc_pending()) return;
scr_http_client_send_held_head(c);
if (!c->sock || len == 0) return;
if (c->chunked) {
char size[32];
snprintf(size, sizeof size, "%zx\r\n", len);
scr_net_sock_write_native(c->sock, size, strlen(size));
scr_net_sock_write_native(c->sock, data, len);
scr_net_sock_write_native(c->sock, "\r\n", 2);
} else {
scr_net_sock_write_native(c->sock, data, len);
}
}
/* Node 24.22's write_(): once the request has its socket, a write with the
* socket uncorked corks the message and uncorks it on the next tick, so the
* chunked writes of one tick go out as one chunk. */
static void scr_http_client_autocork_nt(ScrClosure *cb) {
ScrHttpClientReq *c = scr_box_get_ref(cb->caps[0]);
if (c->corked > 0) scr_http_client_uncork(c);
scr_http_client_release(c);
}
static void scr_http_client_autocork(ScrHttpClientReq *c) {
if (c->ended || c->destroyed || c->close_emitted || !c->socket_emitted || c->sock == NULL ||
c->corked > 0)
return;
c->corked++;
ScrClosure *cb = scr_closure_new((void *)&scr_http_client_autocork_nt, 1);
cb->caps[0] = scr_box_new_obj(&scr_http_client_retain_v, &scr_http_client_release_v, NULL);
scr_box_set_ref(cb->caps[0], scr_http_client_retain(c));
scr_next_tick(cb);
}
void scr_http_client_write_str(ScrHttpClientReq *c, ScrStr *data /*borrowed*/) {
scr_http_client_autocork(c);
char *owned;
const char *bytes = scr_str_utf8(data, &owned);
scr_http_client_write_raw(c, bytes, data->len);
scr_str_utf8_end(owned);
}
void scr_http_client_write_bytes(ScrHttpClientReq *c, ScrBytes *data /*borrowed*/) {
SCR_BYTES_SNAPSHOT(data);
scr_http_client_autocork(c);
scr_http_client_write_raw(c, (const char *)data->data, (data->len * scr_bytes_elem_size(data->elem)));
}
static void scr_http_client_cork_flush(ScrHttpClientReq *c) {
if (c->cork_len == 0 && c->held_head == NULL) return;
char *held = c->cork_buf;
size_t held_len = c->cork_len;
c->cork_buf = NULL;
c->cork_len = c->cork_cap = 0;
scr_http_client_write_raw(c, held, held_len);
free(held);
}
void scr_http_client_cork(ScrHttpClientReq *c) { c->corked++; }
void scr_http_client_uncork(ScrHttpClientReq *c) {
if (c->corked == 0) return;
if (--c->corked == 0) scr_http_client_cork_flush(c);
}
double scr_http_client_writable_corked(ScrHttpClientReq *c) { return (double)c->corked; }
static void scr_http_client_end_raw(ScrHttpClientReq *c, const char *data, size_t len) {
if (c->ended || c->destroyed || c->close_emitted) return;
if (c->corked > 0 || c->cork_len > 0 || c->held_head != NULL) {
/* A corked chunked request buffers end()'s chunk too: one chunk. */
if (c->corked > 0 && c->head_sent && c->chunked && len > 0) {
scr_http_cork_buffer(&c->cork_buf, &c->cork_len, &c->cork_cap, data, len);
data = "";
len = 0;
}
c->corked = 0;
scr_http_client_cork_flush(c);
if (scr_exc_pending()) return;
if (c->destroyed || c->close_emitted) return;
}
if (!c->head_sent) {
/* whole body known NOW: Content-Length framing (or none — the head
* serializer's empty-body method split) */
scr_http_client_send_head(c, (long long)len);
if (scr_exc_pending()) return;
if (c->chunked) {
scr_http_client_write_raw(c, data, len);
if (c->sock) scr_http_send_chunk_end(c->sock, &c->trailers);
} else if (c->sock && len > 0) {
scr_net_sock_write_native(c->sock, data, len);
}
} else {
scr_http_client_write_raw(c, data, len);
if (c->chunked && c->sock) scr_http_send_chunk_end(c->sock, &c->trailers);
}
c->ended = true;
scr_http_client_queue_finish(c);
}
void scr_http_client_end(ScrHttpClientReq *c) { scr_http_client_end_raw(c, "", 0); }
void scr_http_client_end_str(ScrHttpClientReq *c, ScrStr *data /*borrowed*/) {
char *owned;
const char *bytes = scr_str_utf8(data, &owned);
scr_http_client_end_raw(c, bytes, data->len);
scr_str_utf8_end(owned);
}
void scr_http_client_end_bytes(ScrHttpClientReq *c, ScrBytes *data /*borrowed*/) {
SCR_BYTES_SNAPSHOT(data);
scr_http_client_end_raw(c, (const char *)data->data, (data->len * scr_bytes_elem_size(data->elem)));
}
void scr_http_client_flush_headers(ScrHttpClientReq *c) {
if (!c->head_sent && !c->ended && !c->destroyed) scr_http_client_send_head(c, -1);
}
void scr_http_client_add_trailers(ScrHttpClientReq *c, ScrArr *pairs /*borrowed*/) {
if (c->ended || c->destroyed) return;
scr_http_trailers_replace(&c->trailers, pairs);
}
void scr_http_client_set_timeout(ScrHttpClientReq *c, double ms) {
if (!scr_http_timeout_valid(ms)) return;
if (c->sock) scr_net_sock_set_timeout(c->sock, ms);
}
void scr_http_client_set_timeout_cb(ScrHttpClientReq *c, double ms, ScrClosure *cb /*moves*/) {
scr_http_client_set_timeout(c, ms);
if (scr_exc_pending()) {
scr_closure_release(cb);
return;
}
scr_http_client_on_timeout(c, cb, false);
}
/* destroy(): tears the connection down NOW. A destroy after the response
* completed is a quiet no-op past the socket close (Node: just 'close');
* before it, the teardown surfaces as 'socket hang up' — both flow
* through the closed hook below. */
void scr_http_client_destroy(ScrHttpClientReq *c) {
if (c->destroyed) return;
c->destroyed = true;
if (c->sock) scr_net_sock_destroy(c->sock);
}
void scr_http_client_abort(ScrHttpClientReq *c) {
if (c->aborted) return;
c->aborted = true;
bool early_or_complete = !c->socket_emitted || c->response_done;
/* A request whose socket was already handed to JS emits abort before
* teardown; an early abort reports close first. The queued emits keep
* both paths off the caller's stack. */
if (!early_or_complete)
scr_http_emit_push(SCR_HTTP_EMIT_CLIENT_ABORT, scr_http_client_retain(c));
scr_http_client_destroy(c);
if (early_or_complete) {
scr_http_client_queue_close(c);
scr_http_emit_push(SCR_HTTP_EMIT_CLIENT_ABORT, scr_http_client_retain(c));
}
}
bool scr_http_client_aborted(ScrHttpClientReq *c) { return c->aborted; }
bool scr_http_client_destroyed(ScrHttpClientReq *c) {
return c->destroyed || c->close_emitted;
}
void scr_http_client_on_response(ScrHttpClientReq *c, ScrClosure *cb /*moves*/, ScrHttpRespFn fn, bool once) {
if (c->close_emitted || c->response_started) {
/* the event is past (or the handle settled): never fires */
scr_closure_release(cb);
return;
}
scr_net_ls_add(&c->resp_ls, cb, (void *)fn, once);
}
void scr_http_client_on_socket(ScrHttpClientReq *c, ScrClosure *cb /*moves*/, ScrNetConnFn fn, bool once) {
if (c->socket_emitted || c->close_emitted) {
scr_closure_release(cb);
return;
}
scr_net_ls_add(&c->socket_ls, cb, (void *)fn, once);
}
void scr_http_client_on_finish(ScrHttpClientReq *c, ScrClosure *cb /*moves*/, bool once) {
if (c->writable_finished || c->close_emitted) {
scr_closure_release(cb);
return;
}
scr_net_ls_add(&c->finish_ls, cb, NULL, once);
}
void scr_http_client_on_abort(ScrHttpClientReq *c, ScrClosure *cb /*moves*/, bool once) {
if (c->abort_emitted) {
scr_closure_release(cb);
return;
}
scr_net_ls_add(&c->abort_ls, cb, NULL, once);
}
void scr_http_client_on_error(ScrHttpClientReq *c, ScrClosure *cb /*moves*/, ScrChildErrFn fn, bool once) {
if (c->close_emitted) {
scr_closure_release(cb);
return;
}
scr_net_ls_add(&c->err_ls, cb, (void *)fn, once);
}
void scr_http_client_on_upgrade(ScrHttpClientReq *c, ScrClosure *cb /*moves*/, ScrHttpUpgradeFn fn, bool once) {
if (c->close_emitted || c->response_started) {
/* the exchange already resolved (a 101 fired or a response landed) */
scr_closure_release(cb);
return;
}
scr_net_ls_add(&c->upgrade_ls, cb, (void *)fn, once);
}
void scr_http_client_on_timeout(ScrHttpClientReq *c, ScrClosure *cb /*moves*/, bool once) {
if (c->close_emitted) {
scr_closure_release(cb);
return;
}
scr_net_ls_add(&c->timeout_ls, cb, NULL, once);
}
void scr_http_client_on_close(ScrHttpClientReq *c, ScrClosure *cb /*moves*/, bool once) {
if (c->close_emitted) {
scr_closure_release(cb);
return;
}
scr_net_ls_add(&c->close_ls, cb, NULL, once);
}
/* ── response parsing (the conn's client mode) ───────────────────────── */
/* Parses one complete response HEAD sitting at buf[0..head_len):
* "HTTP/1.x SP status SP reason CRLF" then headers. Returns false on
* malformed input (the caller hangs up). */
static bool scr_http_client_parse_head(ScrHttpConn *conn, size_t head_len) {
ScrHttpClientReq *c = conn->client;
if (!c) return false;
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;
const char *sp1 = memchr(buf, ' ', (size_t)(line_end - buf));
if (!sp1) return false;
size_t verlen = (size_t)(sp1 - buf);
if (!(verlen == 8 && (memcmp(buf, "HTTP/1.1", 8) == 0 || memcmp(buf, "HTTP/1.0", 8) == 0))) {
return false;
}
const char *st = sp1 + 1;
int status = 0;
int digits = 0;
while (st < line_end && *st >= '0' && *st <= '9' && digits < 3) {
status = status * 10 + (*st - '0');
st++;
digits++;
}
if (digits != 3) return false;
ScrHttpReq *res = calloc(1, sizeof *res);
if (!res) scr_http_oom();
res->rc = 1;
res->status = status;
res->sock = scr_net_sock_retain(conn->sock);
#ifdef SCR_RC_AUDIT
scr_http_live++;
#endif
res->method = scr_str_new("", 0);
res->url = scr_str_new("", 0); /* Node: res.url is "" on client responses */
res->http10 = memcmp(buf, "HTTP/1.0", 8) == 0;
{
/* the reason phrase — res.statusMessage ("" when absent, Node) */
const char *reason = st;
if (reason < line_end && *reason == ' ') reason++;
res->status_msg = scr_str_new(reason, (size_t)(line_end - reason));
}
/* header lines until the blank line */
const char *p = line_end + 2;
const char *end = buf + head_len;
bool ok = true;
while (p < end) {
const char *eol = memchr(p, '\r', (size_t)(end - p));
if (!eol || eol + 1 >= end || eol[1] != '\n') {
ok = false;
break;
}
if (eol == p) break; /* the blank line */
const char *colon = memchr(p, ':', (size_t)(eol - p));
if (!colon || colon == p) {
ok = false;
break;
}
const char *v = colon + 1;
while (v < eol && (*v == ' ' || *v == '\t')) v++;
const char *ve = eol;
while (ve > v && (ve[-1] == ' ' || ve[-1] == '\t')) ve--;
scr_http_req_add_header(res, p, (size_t)(colon - p), v, (size_t)(ve - v));
p = eol + 2;
}
size_t header_size = res->status_msg->len;
for (size_t i = 0; i < res->nheaders; i++) header_size += res->hnames[i]->len + res->hvalues[i]->len;
if (!ok || res->nheaders > SCR_HTTP_MAX_HEADER_FIELDS || header_size >= SCR_HTTP_MAX_HEADER_SIZE) {
conn->header_overflow = ok;
scr_http_req_release(res);
return false;
}
conn->head_bytes = head_len;
conn->trailer_bytes = 0;
conn->trailer_fields = 0;
/*
* Informational responses do not settle the request. Node emits an
* `information` event for these and keeps parsing until the final
* response; this runtime has no information listener surface yet, so
* consume the head and continue. 101 is the upgrade case below.
*/
if (status >= 100 && status < 200 && status != 101) {
memmove(conn->buf, conn->buf + head_len, conn->len - head_len);
conn->len -= head_len;
scr_http_req_release(res);
return true;
}
/* framing: HEAD requests and 204/304 responses have NO body; chunked
* wins over Content-Length; neither means EOF-delimited */
bool chunked = false;
long long content_length = -1;
for (size_t i = 0; i < res->nheaders; i++) {
const ScrStr *n = res->hnames[i];
const ScrStr *v = res->hvalues[i];
if (n->len == 17 && memcmp(n->data, "transfer-encoding", 17) == 0) {
if (strstr(v->data, "chunked") != NULL) chunked = true;
} else if (n->len == 14 && memcmp(n->data, "content-length", 14) == 0) {
content_length = atoll(v->data);
if (content_length < 0) content_length = 0;
}
}
bool head_req = c->method->len == 4 && memcmp(c->method->data, "HEAD", 4) == 0;
bool no_body = head_req || status == 204 || status == 304;
/* 101 Switching Protocols: 'upgrade' fires INSTEAD of 'response' with
* (res, socket, head) — the parser steps aside and the raw socket is
* the listener's (the server-side handover, mirrored). With no
* listener Node destroys the connection; so does this slice. */
if (status == 101) {
conn->client_resp_started = true;
c->response_started = true;
c->response_done = true; /* the exchange settled: no timeout/hang-up paths */
c->res = scr_http_req_retain(res);
memmove(conn->buf, conn->buf + head_len, conn->len - head_len);
conn->len -= head_len;
if (c->upgrade_ls.n == 0) {
scr_http_req_release(res);
conn->len = 0;
scr_net_sock_destroy(conn->sock);
return true;
}
ScrBytes *head = scr_bytes_new(SCR_BYTES_U8, (double)conn->len);
if (conn->len > 0) memcpy(head->data, conn->buf, conn->len);
conn->len = 0;
scr_net_sock_clear_native_reader(conn->sock);
ScrNetL *snap;
size_t n = scr_net_ls_snapshot(&c->upgrade_ls, &snap);
for (size_t i = 0; i < n; i++) {
if (!scr_exc_pending()) {
((ScrHttpUpgradeFn)snap[i].fn)(snap[i].cb, scr_http_req_retain(res),
scr_net_sock_retain(conn->sock), scr_bytes_retain(head));
}
scr_closure_release(snap[i].cb);
}
free(snap);
scr_bytes_release(head);
scr_http_req_release(res);
/* The REQUEST handle's exchange is over: break the ctx→client edge
* (the parser's native hooks are cleared, so the socket's 'closed'
* would never break it — the cycle story) and settle 'close' through
* the queue, Node's req 'close' after an upgrade. The handed-over
* SOCKET lives on through its own refs. */
conn->client = NULL;
scr_http_client_queue_close(c);
scr_http_client_release(c);
return true;
}
conn->req = res; /* conn's +1 */
conn->client_resp_started = true;
c->response_started = true;
c->res = scr_http_req_retain(res);
/* consume the head bytes */
memmove(conn->buf, conn->buf + head_len, conn->len - head_len);
conn->len -= head_len;
if (no_body) {
conn->state = SCR_HTTP_HEAD;
} else if (chunked) {
conn->state = SCR_HTTP_CHUNK_SIZE;
} else if (content_length > 0) {
conn->state = SCR_HTTP_BODY_CL;
conn->body_remaining = (size_t)content_length;
} else if (content_length == 0) {
conn->state = SCR_HTTP_HEAD;
} else {
conn->state = SCR_HTTP_BODY_EOF;
}
/* 'response' fires NOW (macrotask, main stack — Node emits it from the
* socket's data path too) */
ScrNetL *snap;
size_t n = scr_net_ls_snapshot(&c->resp_ls, &snap);
for (size_t i = 0; i < n; i++) {
if (!scr_exc_pending()) ((ScrHttpRespFn)snap[i].fn)(snap[i].cb, scr_http_req_retain(res));
scr_closure_release(snap[i].cb);
}
free(snap);
if (scr_exc_pending()) return true;
if (conn->state == SCR_HTTP_HEAD) {
/* bodiless response: 'end' + teardown immediately */
scr_http_conn_body_done(conn);
}
return true;
}
/* The response completed: 'end' on the response, then the exchange tears
* down — the socket closes (no pooling) and the deferred closes queue in
* Node's order (req 'close' before res 'close'). */
static void scr_http_client_response_done(ScrHttpConn *conn) {
ScrHttpClientReq *c = conn->client;
ScrHttpReq *res = conn->req;
if (!c || c->response_done) return;
c->response_done = true;
if (res) {
scr_http_req_retain(res);
scr_http_req_finish(res, true); /* fires 'end' */
}
/* Node's order: the REQUEST's 'close' precedes the response's */
scr_http_client_queue_close(c);
if (res) {
scr_http_queue_req_close(res);
scr_http_req_release(res);
/* drop the conn's ref NOW: the response outlives the exchange only
* through user refs / c->res — a retained conn->req would cycle
* (res→sock→ctx→res) past every settle */
conn->req = NULL;
scr_http_req_release(res);
}
if (c->sock) scr_net_sock_destroy(c->sock); /* quiet close, no pooling */
}
/* Premature teardown classification, shared by eof/closed/error:
* before any response head → 'socket hang up' on the REQUEST; mid-body →
* 'aborted' on the RESPONSE (the request just closes) — both orders
* oracle-pinned. */
/* An incomplete client response that will never complete: 'aborted' and
* 'close' queue on it, its body never ends, and the conn drops its
* reference so the res→sock→ctx→res cycle breaks. */
static void scr_http_client_settle_response(ScrHttpConn *conn) {
ScrHttpReq *res = conn->req;
if (res == NULL) return;
scr_http_h2_req_aborted(res);
if (!res->destroyed) scr_http_queue_req_aborted(res);
scr_http_queue_req_close(res);
scr_http_req_finish(res, false); /* body never completes */
conn->req = NULL;
scr_http_req_release(res);
}
static void scr_http_client_premature(ScrHttpConn *conn) {
ScrHttpClientReq *c = conn->client;
if (!c || c->response_done || c->close_emitted) return;
c->response_done = true; /* the exchange is over, one way or another */
if (!c->response_started) {
if (!c->had_error && (!c->aborted || c->socket_emitted)) {
ScrStr *msg = scr_str_new("socket hang up", 14);
scr_http_client_error(c, msg);
scr_str_release(msg);
if (scr_exc_pending()) return;
}
scr_http_client_queue_close(c);
} else {
/* An incomplete client response fires 'aborted' before 'close'. A
* caller-initiated response.destroy() does not emit an 'error'; a
* premature peer close still does. */
scr_http_client_queue_close(c);
scr_http_client_settle_response(conn);
}
}
static void scr_http_client_eof(ScrHttpConn *conn) {
ScrHttpClientReq *c = conn->client;
if (!c) return;
if (conn->state == SCR_HTTP_BODY_EOF && c->response_started && !c->response_done) {
/* EOF-delimited body: this IS completion */
scr_http_conn_body_done(conn);
return;
}
scr_http_client_premature(conn);
}
/* Detach a client from its agent (the socket died, or the handle
* settled): the entry leaves the lists BEFORE the socket's own 'close'
* listeners run — Node's agent removes its socket first too, so
* agent.sockets no longer names it inside a user 'close' handler. */
static void scr_http_client_agent_detach(ScrHttpClientReq *c) {
if (!c->agent) return;
struct ScrHttpAgent *ag = c->agent;
c->agent = NULL;
scr_http_agent_client_done(ag, c); /* frees the slot; may dial the next */
scr_http_agent_release_p(ag);
}
static void scr_http_client_closed(ScrHttpConn *conn) {
ScrHttpClientReq *c = conn->client;
if (!c) return;
scr_http_client_agent_detach(c);
scr_http_client_premature(conn);
/* the connection is gone: break the ctx→client edge (the client stays
* alive through user refs / the queue until its 'close' settles) */
conn->client = NULL;
scr_http_client_release(c);
}
static bool scr_http_client_sock_err(ScrHttpConn *conn, ScrStr *msg) {
ScrHttpClientReq *c = conn->client;
if (!c) return true;
if (c->response_done || c->close_emitted || (c->aborted && !c->socket_emitted)) return true; /* teardown noise */
if (!c->response_started) {
/* connect/read failure before any response: the socket's message IS
* Node's ('connect ECONNREFUSED ip:port') — fire it on the request */
c->response_done = true;
scr_http_client_error(c, msg);
if (!scr_exc_pending()) scr_http_client_queue_close(c);
} else {
scr_http_client_premature(conn);
}
return true;
}
/* Header-cap overflow / malformed response head: Node surfaces a parse
* error on the request; this slice hangs up with the same teardown shape
* ('socket hang up' when nothing parsed yet). */
static void scr_http_client_head_overflow(ScrHttpConn *conn) {
conn->len = 0;
ScrHttpClientReq *c = conn->client;
if (conn->header_overflow && c != NULL && !c->response_done && !c->close_emitted) {
/* Node's parser error for a head over maxHeaderSize or the field
* bound: code HPE_HEADER_OVERFLOW on the request. */
c->response_done = true;
ScrStr *msg = scr_str_new("Parse Error: Header overflow", 28);
scr_http_client_error(c, msg);
scr_str_release(msg);
/* a trailer overflow ends a response already in progress: settle it
* now, since the exchange no longer reaches the premature-close path */
scr_http_client_settle_response(conn);
if (scr_exc_pending()) return;
scr_http_client_queue_close(c);
} else {
scr_http_client_premature(conn);
}
if (conn->client && conn->client->sock) scr_net_sock_destroy(conn->client->sock);
}
/* The client socket's idle timeout: 'timeout' on the request handle
* (never destroys anything — Node; the caller destroys). */
static void scr_http_client_sock_timeout(void *ctx) {
ScrHttpConn *conn = (ScrHttpConn *)ctx;
ScrHttpClientReq *c = conn->client;
if (!c || c->response_done || c->close_emitted) return;
scr_net_fire0(&c->timeout_ls);
}
static void scr_http_client_sock_established(void *ctx) {
ScrHttpConn *conn = (ScrHttpConn *)ctx;
ScrHttpClientReq *c = conn->client;
if (!c) return;
c->sock_ready = true;
scr_http_client_queue_finish(c);
}
static void scr_http_client_conn_free(ScrHttpConn *conn) {
if (conn->req) {
scr_http_req_finish(conn->req, false);
scr_http_req_release(conn->req);
conn->req = NULL;
}
if (conn->client) {
ScrHttpClientReq *c = conn->client;
conn->client = NULL;
scr_http_client_release(c);
}
free(conn->buf);
free(conn);
}
/* ── http.request / http.get ─────────────────────────────────────────── */
static ScrHttpClientReq *scr_http_request_impl(ScrStr *host /*borrowed*/, double port,
ScrStr *path /*borrowed*/, ScrStr *method /*borrowed*/,
double timeout_ms, ScrArr *header_pairs /*borrowed*/,
bool auto_end, ScrClosure *cb /*moves, nullable*/,
ScrHttpRespFn fn, int default_port,
void (*wrap)(ScrNetSocket *, void *), void *wrap_ctx,
ScrNetSocket *presock /*moves, nullable*/) {
/* Node's ClientRequest constructor validates the method token
* SYNCHRONOUSLY (RFC 9110 tchar, at least one): the catchable
* ERR_INVALID_HTTP_TOKEN TypeError with Node's exact message, the raw
* method text inside the quotes. Moved-in arguments release — the
* request never exists. */
bool method_ok = method->len > 0;
for (size_t i = 0; i < method->len && method_ok; i++) {
unsigned char ch = (unsigned char)method->data[i];
method_ok = (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') ||
(ch >= '0' && ch <= '9') ||
(ch != 0 && strchr("!#$%&'*+-.^_`|~", ch) != NULL);
}
if (!method_ok) {
if (cb) scr_closure_release(cb);
if (presock) scr_net_sock_release(presock);
ScrJsonBuf b;
scr_jb_init(&b);
scr_jb_puts(&b, "Method must be a valid HTTP token [\"");
scr_jb_write(&b, method->data, method->len);
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);
return NULL;
}
scr_http_install();
ScrHttpClientReq *c = calloc(1, sizeof *c);
if (!c) scr_http_oom();
c->rc = 1;
#ifdef SCR_RC_AUDIT
scr_http_live++;
#endif
c->host = scr_str_retain(host);
c->path = scr_str_retain(path);
c->method = scr_str_retain(method);
c->port = (int)port;
c->default_port = default_port;
size_t npairs = (size_t)scr_arr_len(header_pairs);
if (npairs >= 2) {
c->hnames = malloc((npairs / 2) * sizeof *c->hnames);
c->hvalues = malloc((npairs / 2) * sizeof *c->hvalues);
if (!c->hnames || !c->hvalues) scr_http_oom();
for (size_t i = 0; i + 1 < npairs; i += 2) {
c->hnames[c->nheaders] = (ScrStr *)scr_arr_get_ref(header_pairs, (double)i);
c->hvalues[c->nheaders] = (ScrStr *)scr_arr_get_ref(header_pairs, (double)(i + 1));
c->nheaders++;
}
}
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);
if (presock != NULL) {
c->sock = presock;
} else {
ScrStr *dial = scr_net_blocking_lookup(host);
c->sock = scr_net_connect(port, dial, NULL);
scr_str_release(dial);
}
ScrHttpConn *conn = calloc(1, sizeof *conn);
if (!conn) scr_http_oom();
conn->sock = c->sock; /* borrowed */
conn->client_mode = true;
conn->client = scr_http_client_retain(c);
c->conn = conn;
scr_net_sock_set_native_reader(c->sock, &scr_http_conn_data, &scr_http_conn_eof,
&scr_http_conn_closed, conn, &scr_http_conn_free);
scr_net_sock_set_native_events(c->sock, &scr_http_client_sock_timeout, &scr_http_conn_err);
/* the transport (scr_tls.c's https) wraps the dialed socket before any
* bytes go out — everything below buffers until its handshake ends */
if (wrap) wrap(c->sock, wrap_ctx);
scr_net_sock_set_native_established(c->sock, &scr_http_client_sock_established);
c->sock_ready = scr_net_sock_established(c->sock);
if (timeout_ms > 0) scr_net_sock_set_timeout(c->sock, timeout_ms);
scr_http_client_register(c);
scr_http_emit_push(SCR_HTTP_EMIT_CLIENT_SOCKET, scr_http_client_retain(c));
if (auto_end) scr_http_client_end(c);
return c;
}
ScrHttpClientReq *scr_http_request_ex(ScrStr *host /*borrowed*/, double port,
ScrStr *path /*borrowed*/, ScrStr *method /*borrowed*/,
double timeout_ms, ScrArr *header_pairs /*borrowed*/,
bool auto_end, ScrClosure *cb /*moves, nullable*/,
ScrHttpRespFn fn, int default_port,
void (*wrap)(ScrNetSocket *, void *), void *wrap_ctx) {
return scr_http_request_impl(host, port, path, method, timeout_ms, header_pairs, auto_end,
cb, fn, default_port, wrap, wrap_ctx, NULL);
}
/* http.request({ createConnection, ... }): the caller's DIALER supplies
* the socket — conn_cb runs ONCE, synchronously (Node invokes it from
* the request constructor's onSocket path), and its socket carries the
* exchange exactly like a dialed one (head bytes buffer through connect
* and flush on establishment). The Host header defaults to "localhost"
* — the proxy shape sets headers.host itself, which wins verbatim. */
ScrHttpClientReq *scr_http_request_conn(ScrClosure *conn_cb /*moves*/, ScrStr *path /*borrowed*/,
ScrStr *method /*borrowed*/, double timeout_ms,
ScrArr *header_pairs /*borrowed*/, bool auto_end,
ScrClosure *cb /*moves, nullable*/, ScrHttpRespFn fn) {
ScrNetSocket *sock = ((ScrNetSocket *(*)(ScrClosure *))conn_cb->fn)(conn_cb); /* +1 result */
scr_closure_release(conn_cb);
if (scr_exc_pending() || sock == NULL) {
/* the dialer threw: NULL result, dummy-released past the emitted
* pending check (the libCall is in the may-throw seed set) */
if (cb != NULL) scr_closure_release(cb);
scr_net_sock_release(sock);
return NULL;
}
ScrStr *host = scr_str_new("localhost", 9);
ScrHttpClientReq *c = scr_http_request_impl(host, 80, path, method, timeout_ms, header_pairs,
auto_end, cb, fn, 80, NULL, NULL, sock);
scr_str_release(host);
return c;
}
ScrHttpClientReq *scr_http_request(ScrStr *host /*borrowed*/, double port,
ScrStr *path /*borrowed*/, ScrStr *method /*borrowed*/,
double timeout_ms, ScrArr *header_pairs /*borrowed*/,
bool auto_end, ScrClosure *cb /*moves, nullable*/,
ScrHttpRespFn fn) {
return scr_http_request_ex(host, port, path, method, timeout_ms, header_pairs, auto_end, cb,
fn, 80, NULL, NULL);
}
/* ── the http Agent ──────────────────────────────────────────────────────
*
* Options, getName, destroy, and REAL maxSockets accounting over the
* one-dial-per-request connection model: an over-limit request's socket
* defers its dial (scr_net_connect_deferred) and queues; a dying
* active connection frees the slot and starts the next dial. What the
* runtime cannot express stays a NAMED fence: keep-alive socket POOLING
* (keepAlive: true) fences at construction — agent.freeSockets is
* always empty; the runtime does not emulate a pool.
*
* Ownership: the agent registers (+1, atexit-swept); entries hold their
* client +1 and the client holds the agent +1 — the cycle breaks when
* the client's socket dies (scr_http_client_agent_detach) or at the
* atexit sweep. */
typedef struct ScrHttpAgentEnt {
ScrStr *name; /* getName's shape: "host:port:" */
ScrHttpClientReq *client; /* +1 while listed */
bool queued; /* waiting for a slot: the dial is deferred */
struct ScrHttpAgentEnt *next;
} ScrHttpAgentEnt;
typedef struct ScrHttpAgent {
size_t rc;
bool secure; /* https.Agent: protocol/defaultPort answers */
bool keep_alive; /* always false here (true fences at construction) */
double ka_msecs;
double max_sockets; /* INFINITY = Node's default */
double max_free;
double timeout_ms; /* < 0 = unset */
double default_port; /* settable (agent.defaultPort = p) */
bool destroyed;
ScrHttpAgentEnt *ents; /* append order — Node's FIFO queue */
bool in_registry;
struct ScrHttpAgent *next;
} ScrHttpAgent;
static SCR_TL ScrHttpAgent *scr_http_agents = NULL; /* registry: +1 each, atexit-swept */
/* Node's ClientRequest: an agent without keepAlive and without a finite
* maxSockets asks the server to close; any other agent keeps alive. */
static bool scr_http_agent_keeps_alive(const ScrHttpAgent *a) {
return a->keep_alive || isfinite(a->max_sockets);
}
static ScrHttpAgent *scr_http_agent_retain(ScrHttpAgent *a) {
a->rc++;
return a;
}
static void scr_http_agent_release(ScrHttpAgent *a) {
if (!a || --a->rc > 0) return;
ScrHttpAgentEnt *e = a->ents;
while (e) {
ScrHttpAgentEnt *next = e->next;
scr_str_release(e->name);
scr_http_client_release(e->client);
free(e);
e = next;
}
free(a);
}
static struct ScrHttpAgent *scr_http_agent_release_p(struct ScrHttpAgent *ag) {
scr_http_agent_release(ag);
return NULL;
}
static void *scr_http_agent_retain_v(void *p) { return scr_http_agent_retain((ScrHttpAgent *)p); }
static void scr_http_agent_release_v(void *p) { scr_http_agent_release((ScrHttpAgent *)p); }
/* Actives (dialed, not settled) under a name. */
static size_t scr_http_agent_active(const ScrHttpAgent *a, const ScrStr *name) {
size_t n = 0;
for (const ScrHttpAgentEnt *e = a->ents; e; e = e->next) {
if (!e->queued && e->name->len == name->len &&
memcmp(e->name->data, name->data, name->len) == 0) n++;
}
return n;
}
/* The settling/dying client leaves the lists; a freed slot starts the
* first queued dial for the same name (Node's FIFO). */
static void scr_http_agent_client_done(struct ScrHttpAgent *ag, struct ScrHttpClientReq *c) {
ScrHttpAgentEnt **link = &ag->ents;
ScrStr *name = NULL;
while (*link && (*link)->client != c) link = &(*link)->next;
if (*link) {
ScrHttpAgentEnt *e = *link;
*link = e->next;
name = e->name; /* ownership moves out for the pump below */
scr_http_client_release(e->client);
free(e);
}
if (name != NULL && !ag->destroyed) {
for (ScrHttpAgentEnt *e = ag->ents; e; e = e->next) {
if (e->queued && e->name->len == name->len &&
memcmp(e->name->data, name->data, name->len) == 0) {
if (scr_http_agent_active(ag, name) < ag->max_sockets) {
e->queued = false;
if (e->client->sock) scr_net_sock_dial_start(e->client->sock);
}
break;
}
}
}
scr_str_release(name);
}
/* getName's string builder — Node's exact shape:
* host:port:localAddress[:family][:socketPath]. */
static ScrStr *scr_http_agent_name(const char *host, size_t host_len, const char *port,
size_t port_len, const char *laddr, size_t laddr_len,
int family, const char *spath, size_t spath_len) {
ScrJsonBuf b;
scr_jb_init(&b);
if (host_len == 0) { scr_jb_puts(&b, "localhost"); }
else for (size_t i = 0; i < host_len; i++) scr_jb_putc(&b, host[i]);
scr_jb_putc(&b, ':');
for (size_t i = 0; i < port_len; i++) scr_jb_putc(&b, port[i]);
scr_jb_putc(&b, ':');
for (size_t i = 0; i < laddr_len; i++) scr_jb_putc(&b, laddr[i]);
if (family == 4 || family == 6) {
scr_jb_putc(&b, ':');
scr_jb_putc(&b, family == 4 ? '4' : '6');
}
if (spath_len > 0) {
scr_jb_putc(&b, ':');
for (size_t i = 0; i < spath_len; i++) scr_jb_putc(&b, spath[i]);
}
return scr_jb_finish(&b);
}
ScrDyn *scr_http_agent_new(bool secure, bool keep_alive, double ka_msecs,
double max_sockets, double max_free, double timeout_ms,
double port /* < 0 = unset */) {
if (keep_alive) {
static const char msg[] =
"an http Agent with keepAlive: true (socket pooling and reuse — compiled clients "
"dial one connection per request and close it with the response) is not supported "
"yet — construct the Agent without keepAlive, or drop the agent option";
scr_throw_error_msg(SCR_ERR_ERROR, msg, sizeof msg - 1);
return NULL;
}
scr_http_install();
ScrHttpAgent *a = calloc(1, sizeof *a);
if (!a) scr_http_oom();
a->rc = 1;
a->secure = secure;
a->keep_alive = false;
a->ka_msecs = ka_msecs >= 0 ? ka_msecs : 1000;
a->max_sockets = max_sockets >= 0 ? max_sockets : (double)INFINITY;
a->max_free = max_free >= 0 ? max_free : 256;
a->timeout_ms = timeout_ms;
/* an agent-options `port` merges under portless request options (Node
* merges agent options into each connection) — the settable
* defaultPort carries it */
a->default_port = port >= 0 ? port : secure ? 443 : 80;
/* registry (+1): the atexit sweep breaks agent↔client edges a program
* legitimately leaves in flight */
a->in_registry = true;
a->next = scr_http_agents;
scr_http_agents = scr_http_agent_retain(a);
ScrDyn *d = scr_dyn_new_handle(a, SCR_DYNH_HTTP_AGENT);
scr_http_agent_release(a); /* the handle's retain holds it */
return d;
}
/* agent.destroy(): tears down every listed connection (actives destroy
* their sockets, queued dials never start) — Node destroys in-use
* sockets too; there is no free pool here. */
static void scr_http_agent_destroy(ScrHttpAgent *a) {
a->destroyed = true;
/* destroying sockets detaches entries re-entrantly — walk a snapshot */
for (;;) {
ScrHttpClientReq *victim = NULL;
for (ScrHttpAgentEnt *e = a->ents; e; e = e->next) {
if (e->client->sock && !e->client->close_emitted) {
victim = e->client;
break;
}
}
if (!victim) break;
scr_http_client_agent_detach(victim); /* leaves the list first */
scr_net_sock_destroy(victim->sock);
}
}
ScrHttpClientReq *scr_http_request_agent_ex(ScrStr *host /*borrowed*/, double port,
ScrStr *path /*borrowed*/, ScrStr *method /*borrowed*/,
double timeout_ms, ScrArr *header_pairs /*borrowed*/,
bool auto_end, const ScrDyn *agent /*borrowed*/,
ScrClosure *cb /*moves, nullable*/,
ScrHttpRespFn fn, int default_port,
void (*wrap)(ScrNetSocket *, void *), void *wrap_ctx) {
/* the null/undefined arms: the default path (port < 0 = no port option) */
if (agent == NULL || agent->kind == SCR_DYN_UNDEF || agent->kind == SCR_DYN_NULL) {
double p = port >= 0 ? port : (double)default_port;
return scr_http_request_impl(host, p, path, method, timeout_ms, header_pairs, auto_end,
cb, fn, default_port, wrap, wrap_ctx, NULL);
}
/* agent: false — Node's one-shot Agent: exactly this client's model,
* plus its Connection: close request header (unless the caller set one) */
if (agent->kind == SCR_DYN_BOOL && !agent->v.b) {
double p = port >= 0 ? port : (double)default_port;
bool has_conn = false;
size_t npairs = (size_t)scr_arr_len(header_pairs);
for (size_t i = 0; i + 1 < npairs && !has_conn; i += 2) {
ScrStr *n = (ScrStr *)scr_arr_get_ref(header_pairs, (double)i);
if (n->len == 10) {
bool eq = true;
for (size_t j = 0; j < 10 && eq; j++) {
if (tolower((unsigned char)n->data[j]) != "connection"[j]) eq = false;
}
has_conn = eq;
}
scr_str_release(n);
}
ScrArr *pairs = header_pairs;
ScrArr *copy = NULL;
if (!has_conn) {
copy = scr_arr_new(SCR_ELEM_STR, npairs + 2);
for (size_t i = 0; i < npairs; i++) scr_arr_push_ref(copy, scr_arr_get_ref(header_pairs, (double)i));
scr_arr_push_ref(copy, scr_str_new("Connection", 10));
scr_arr_push_ref(copy, scr_str_new("close", 5));
pairs = copy;
}
ScrHttpClientReq *c = scr_http_request_impl(host, p, path, method, timeout_ms, pairs,
auto_end, cb, fn, default_port, wrap, wrap_ctx, NULL);
if (copy) scr_arr_release(copy);
return c;
}
if (agent->kind != SCR_DYN_HANDLE || agent->v.handle.tag != SCR_DYNH_HTTP_AGENT) {
if (cb) scr_closure_release(cb);
scr_dyn_arg_type_fail("options.agent", "an instance of http.Agent, false, null, or undefined", agent);
return NULL;
}
ScrHttpAgent *ag = (ScrHttpAgent *)agent->v.handle.ptr;
double p = port >= 0 ? port
: ag->default_port > 0 ? ag->default_port
: (double)default_port;
char portbuf[16];
int portn = snprintf(portbuf, sizeof portbuf, "%d", (int)p);
ScrStr *name = scr_http_agent_name(host->data, host->len, portbuf, (size_t)portn,
NULL, 0, 0, NULL, 0);
bool queue = scr_http_agent_active(ag, name) >= ag->max_sockets;
ScrNetSocket *presock = queue ? scr_net_connect_deferred(p, host) : NULL;
ScrHttpClientReq *c = scr_http_request_impl(host, p, path, method, timeout_ms, header_pairs,
auto_end, cb, fn, default_port, wrap, wrap_ctx,
presock);
if (c == NULL) { /* the method-token throw: nothing registered */
scr_str_release(name);
return NULL;
}
if (ag->timeout_ms >= 0 && timeout_ms <= 0) scr_net_sock_set_timeout(c->sock, ag->timeout_ms);
c->agent = scr_http_agent_retain(ag);
ScrHttpAgentEnt *e = calloc(1, sizeof *e);
if (!e) scr_http_oom();
e->name = name; /* ownership moves */
e->client = scr_http_client_retain(c);
e->queued = queue;
ScrHttpAgentEnt **link = &ag->ents;
while (*link) link = &(*link)->next;
*link = e;
return c;
}
ScrHttpClientReq *scr_http_request_agent(ScrStr *host /*borrowed*/, double port,
ScrStr *path /*borrowed*/, ScrStr *method /*borrowed*/,
double timeout_ms, ScrArr *header_pairs /*borrowed*/,
bool auto_end, const ScrDyn *agent /*borrowed*/,
ScrClosure *cb /*moves, nullable*/, ScrHttpRespFn fn) {
return scr_http_request_agent_ex(host, port, path, method, timeout_ms, header_pairs, auto_end,
agent, cb, fn, 80, NULL, NULL);
}
/* The atexit sweep: break agent↔client edges a program leaves in flight
* (queued dials that never ran) so the RC audit sees a clean heap. */
static void scr_http_agents_cleanup(void) {
while (scr_http_agents) {
ScrHttpAgent *a = scr_http_agents;
scr_http_agents = a->next;
a->in_registry = false;
ScrHttpAgentEnt *e = a->ents;
a->ents = NULL;
while (e) {
ScrHttpAgentEnt *next = e->next;
if (e->client->agent) {
scr_http_agent_release(e->client->agent);
e->client->agent = NULL;
}
scr_str_release(e->name);
scr_http_client_release(e->client);
free(e);
e = next;
}
scr_http_agent_release(a);
}
}
/* Exit-time registry cleanup (the net-unit precedent): clients a program
* legitimately leaves in flight at exit release their listeners and
* registry references so the RC audit sees a clean heap. */
static void scr_http_clients_cleanup(void) {
while (scr_http_clients) {
ScrHttpClientReq *c = scr_http_clients;
scr_net_ls_drop(&c->resp_ls);
scr_net_ls_drop(&c->err_ls);
scr_net_ls_drop(&c->timeout_ls);
scr_net_ls_drop(&c->close_ls);
scr_net_ls_drop(&c->upgrade_ls);
scr_http_client_unregister(c);
}
}
/* The response-callback adapters: res arrives +1; the zero-param shape
* releases it. */
void scr_http_resp_thunk0(ScrClosure *cb, ScrHttpReq *res) {
scr_http_req_release(res);
((void (*)(ScrClosure *))cb->fn)(cb);
}
void scr_http_resp_thunk_res(ScrClosure *cb, ScrHttpReq *res) {
((void (*)(ScrClosure *, ScrHttpReq *))cb->fn)(cb, res);
}
/* ── checked-dynamic handle dispatch (SCR_DYN_HANDLE ops) ──────────────
*
* The receiver surface behind `server.on('request', mustCall((req, res)
* => ...))`: the mustCall wrapper makes the listener dyn, req/res box
* into the checked-dynamic tree as HANDLE values, and member uses inside the listener
* body dispatch HERE — onto the very entry points the static lowerings
* use, with per-argument gates shaped like Node's (the libCall signature
* table in validate.ts is the modeled surface; this dispatcher mirrors
* it name for name).
*
* Honesty ladder (the scr_dyn_invoke stance):
* - modeled member: the static entry point's exact semantics;
* - a member the class HAS but this table does not model: a
* "not supported yet" Error instead of a wrong result;
* - anything else: Node's "<spelling> is not a function" for calls,
* the undefined singleton for reads (the checked-dynamic tree's own-property answer;
* SEMANTICS.md documents the remainder).
*
* Ownership: invoke/get/set receive BORROWED handles/args and answer
* owned (+1) results; listener registrations retain the dyn callback
* into a runtime-built adapter closure (scr_dyn_listener_closure*). */
static void scr_http_dynh_unsupported(const char *cls, const char *member, const char *what) {
char msg[160];
int n = snprintf(msg, sizeof msg, "'%s.prototype.%s' on a dynamic value is not supported yet%s%s",
cls, member, what ? " — " : "", what ? what : "");
scr_throw_error_msg(SCR_ERR_ERROR, msg, (size_t)n);
}
static void scr_http_dynh_not_fn(const char *what) {
char msg[160];
int n = snprintf(msg, sizeof msg, "%s is not a function", what);
scr_throw_error_msg(SCR_ERR_TYPE, msg, (size_t)n);
}
static void scr_http_dynh_event_unsupported(const char *cls, const ScrDyn *name) {
ScrJsonBuf b;
scr_jb_init(&b);
scr_jb_puts(&b, "listening for '");
if (name->kind == SCR_DYN_STR) {
scr_jb_write(&b, name->v.str->data, name->v.str->len);
}
scr_jb_puts(&b, "' on a dynamic ");
scr_jb_puts(&b, cls);
scr_jb_puts(&b, " is not supported yet");
scr_throw_error(SCR_ERR_ERROR, scr_jb_finish(&b));
}
static bool scr_http_dynh_name_is(const ScrDyn *name, const char *lit) {
size_t n = strlen(lit);
return name->kind == SCR_DYN_STR && name->v.str->len == n &&
memcmp(name->v.str->data, lit, n) == 0;
}
static bool scr_http_dynh_in(const char *m, const char *const *names) {
for (size_t i = 0; names[i]; i++) {
if (strcmp(m, names[i]) == 0) return true;
}
return false;
}
/* The registration-family gate shared by every handle class: on/once/
* addListener (prepend* is unmodeled — these per-event lists know no
* front). Answers the once flag; leaves reg=false for other names. */
static bool scr_http_dynh_reg(const char *method, bool *once) {
if (strcmp(method, "on") == 0 || strcmp(method, "addListener") == 0) {
*once = false;
return true;
}
if (strcmp(method, "once") == 0) {
*once = true;
return true;
}
return false;
}
/* String/Buffer chunk gate (res.end/res.write/socket.write): answers
* which flavor, throws Node's ERR_INVALID_ARG_TYPE otherwise. */
static bool scr_http_dynh_chunk_ok(const ScrDyn *chunk) {
if (chunk->kind == SCR_DYN_STR || chunk->kind == SCR_DYN_BYTES) return true;
scr_dyn_arg_type_fail("chunk", "of type string or an instance of Buffer or Uint8Array", chunk);
return false;
}
/* An optional trailing encoding argument on write/end: utf8 spellings
* pass through (string chunks already carry utf8 bytes); every other
* real Node encoding decodes the STRING chunk through Buffer.from's
* decoder into *out (+1; buffer chunks ignore the encoding, Node);
* unknown names throw Node's ERR_UNKNOWN_ENCODING. False = exception
* pending. */
static bool scr_http_dynh_encode(const ScrDyn *chunk, const ScrDyn *enc, ScrBytes **out) {
*out = NULL;
if (enc->kind != SCR_DYN_STR || !scr_bytes_is_encoding(enc->v.str)) {
ScrJsonBuf b;
scr_jb_init(&b);
scr_jb_puts(&b, "Unknown encoding: ");
if (enc->kind == SCR_DYN_STR) {
for (size_t i = 0; i < enc->v.str->len && i < 64; i++) scr_jb_putc(&b, enc->v.str->data[i]);
}
ScrStr *msg = scr_jb_finish(&b);
scr_throw_error_msg_code(SCR_ERR_TYPE, msg->data, msg->len, "ERR_UNKNOWN_ENCODING");
scr_str_release(msg);
return false;
}
if (chunk->kind != SCR_DYN_STR) return true; /* buffers carry their bytes */
/* lowercase + alias normalization down to scr_bytes_from_str's arms */
char low[10];
size_t n = enc->v.str->len < 9 ? enc->v.str->len : 9;
for (size_t i = 0; i < n; i++) {
char c = enc->v.str->data[i];
low[i] = c >= 'A' && c <= 'Z' ? (char)(c + 32) : c;
}
low[n] = 0;
const char *canon = low;
if (strcmp(low, "utf8") == 0 || strcmp(low, "utf-8") == 0) return true; /* passthrough */
if (strcmp(low, "binary") == 0) canon = "latin1";
else if (strcmp(low, "ucs2") == 0 || strcmp(low, "ucs-2") == 0 || strcmp(low, "utf-16le") == 0) canon = "utf16le";
ScrStr *cs = scr_str_new(canon, strlen(canon));
*out = scr_bytes_from_str(chunk->v.str, cs);
scr_str_release(cs);
return true;
}
/* ── IncomingMessage (SCR_DYNH_HTTP_REQ) ─────────────────────────────── */
static ScrDyn *scr_http_dynh_req_invoke(void *h, ScrDyn *self, const char *method,
ScrDyn *const *args, size_t argc, const char *what) {
ScrHttpReq *r = (ScrHttpReq *)h;
bool once = false;
if (scr_http_dynh_reg(method, &once)) {
const ScrDyn *name = argc > 0 ? args[0] : scr_dyn_undefined();
const ScrDyn *cb = argc > 1 ? args[1] : scr_dyn_undefined();
scr_dyn_check_listener(cb, "listener");
if (scr_exc_pending()) return NULL;
if (scr_http_dynh_name_is(name, "data")) {
scr_http_req_on_data(r, scr_dyn_listener_closure_data(cb), (ScrNetDataFn)&scr_dyn_listener_fire_data, once);
} else if (scr_http_dynh_name_is(name, "end")) {
scr_http_req_on_end(r, scr_dyn_listener_closure0(cb), once);
} else if (scr_http_dynh_name_is(name, "error")) {
scr_http_req_on_error(r, scr_dyn_listener_closure_err(cb), (ScrChildErrFn)&scr_dyn_listener_fire_err, once);
} else if (scr_http_dynh_name_is(name, "close")) {
scr_http_req_on_close(r, scr_dyn_listener_closure0(cb), once);
} else if (scr_http_dynh_name_is(name, "aborted")) {
scr_http_req_on_aborted(r, scr_dyn_listener_closure0(cb), once);
} else if (scr_http_dynh_name_is(name, "timeout") && r->sock != NULL) {
/* the socket's idle timer — req.setTimeout's event, Node's delegation */
scr_net_sock_on_timeout(r->sock, scr_dyn_listener_closure0(cb), once);
} else {
scr_http_dynh_event_unsupported("IncomingMessage", name);
return NULL;
}
return scr_dyn_retain(self); /* chaining, like Node */
}
if (strcmp(method, "resume") == 0) {
scr_http_req_resume(r);
return scr_dyn_retain(self);
}
if (strcmp(method, "pause") == 0) {
scr_http_req_pause(r);
return scr_dyn_retain(self);
}
if (strcmp(method, "setTimeout") == 0) {
const ScrDyn *ms = argc > 0 ? args[0] : scr_dyn_undefined();
if (ms->kind != SCR_DYN_NUM) {
scr_dyn_arg_type_fail("msecs", "of type number", ms);
return NULL;
}
ScrClosure *cb = NULL;
if (argc > 1 && args[1]->kind == SCR_DYN_FUNC) cb = scr_dyn_listener_closure0(args[1]);
else if (argc > 1 && args[1]->kind != SCR_DYN_UNDEF) {
scr_dyn_check_listener(args[1], "callback");
return NULL;
}
scr_http_req_set_timeout(r, ms->v.num, cb);
return scr_dyn_retain(self);
}
if (strcmp(method, "setEncoding") == 0) {
const ScrDyn *enc = argc > 0 ? args[0] : scr_dyn_undefined();
if (enc->kind != SCR_DYN_STR) {
scr_dyn_arg_type_fail("encoding", "of type string", enc);
return NULL;
}
scr_http_req_set_encoding(r, enc->v.str);
if (scr_exc_pending()) return NULL;
return scr_dyn_retain(self);
}
if (strcmp(method, "destroy") == 0) {
if (argc > 0 && args[0]->kind != SCR_DYN_UNDEF) {
scr_http_dynh_unsupported("IncomingMessage", "destroy", "destroy(error) carries a payload this surface does not model");
return NULL;
}
scr_http_req_destroy(r);
return scr_dyn_retain(self);
}
if (strcmp(method, "pipe") == 0) {
const ScrDyn *dst = argc > 0 ? args[0] : scr_dyn_undefined();
if (dst->kind == SCR_DYN_HANDLE && dst->v.handle.tag == SCR_DYNH_HTTP_RES) {
scr_http_req_pipe_res(r, (ScrHttpRes *)dst->v.handle.ptr);
return scr_dyn_retain((ScrDyn *)dst); /* pipe answers the destination */
}
if (dst->kind == SCR_DYN_HANDLE && dst->v.handle.tag == SCR_DYNH_NET_SOCKET) {
scr_http_req_pipe_sock(r, (ScrNetSocket *)dst->v.handle.ptr);
return scr_dyn_retain((ScrDyn *)dst);
}
scr_http_dynh_unsupported("IncomingMessage", "pipe", "only ServerResponse and Socket destinations are modeled");
return NULL;
}
if (strcmp(method, "toString") == 0) {
ScrStr *s = scr_str_new("[object Object]", 15);
ScrDyn *d = scr_dyn_new_str(s);
scr_str_release(s);
return d;
}
{
/* Real prototype members without a modeled dispatch: "not supported yet". */
static const char *const known[] = { "unpipe",
"read", "push", "off", "removeListener", "removeAllListeners", "emit",
"prependListener", "prependOnceListener", "listenerCount", "listeners",
"isPaused", "wrap", "cork", "uncork", NULL };
if (scr_http_dynh_in(method, known)) {
scr_http_dynh_unsupported("IncomingMessage", method, NULL);
return NULL;
}
}
scr_http_dynh_not_fn(what);
return NULL;
}
static ScrDyn *scr_http_dynh_req_get(void *h, const char *key, size_t key_len) {
ScrHttpReq *r = (ScrHttpReq *)h;
(void)key_len;
if (strcmp(key, "url") == 0) {
ScrStr *s = scr_http_req_url(r);
ScrDyn *d = scr_dyn_new_str(s);
scr_str_release(s);
return d;
}
if (strcmp(key, "httpVersion") == 0) {
/* The parsed request/status line's version — the 1.0 tests read it;
* the h2 compat request answers Node's "2.0". */
ScrStr *v = scr_str_new(r->http2 ? "2.0" : r->http10 ? "1.0" : "1.1", 3);
ScrDyn *d = scr_dyn_new_str(v);
scr_str_release(v);
return d;
}
if (strcmp(key, "httpVersionMajor") == 0) return scr_dyn_new_num(r->http2 ? 2 : 1);
if (strcmp(key, "httpVersionMinor") == 0) return scr_dyn_new_num(r->http2 ? 0 : r->http10 ? 0 : 1);
if (strcmp(key, "method") == 0) {
ScrStr *s = scr_http_req_method(r);
ScrDyn *d = scr_dyn_new_str(s);
scr_str_release(s);
return d;
}
if (strcmp(key, "headers") == 0) {
/* The snapshot object `{ ...req.headers }` builds — lowercased
* names, arrival order, keep-first duplicates (joined under the
* joinDuplicateHeaders option), the static lane's exact feed. */
ScrArr *pairs = scr_http_req_header_pairs(r);
ScrDyn *obj = scr_dyn_new_obj();
size_t n = (size_t)scr_arr_len(pairs);
for (size_t i = 0; i + 1 < n; i += 2) {
ScrStr *name = (ScrStr *)scr_arr_get_ref(pairs, (double)i);
ScrStr *value = (ScrStr *)scr_arr_get_ref(pairs, (double)(i + 1));
/* Keep-FIRST on repeated names (scr_dyn_obj_set is later-wins) —
* the per-name read's rule (scr_http_req_header), so h.headers.x
* and h.headers['x'] agree; joinDuplicateHeaders pairs arrive
* pre-joined and unique. */
if (!scr_dyn_obj_get(obj, name->data, name->len)) {
scr_dyn_obj_set(obj, name->data, name->len, scr_dyn_new_str(value));
}
scr_str_release(name);
scr_str_release(value);
}
scr_arr_release(pairs);
return obj;
}
if (strcmp(key, "rawHeaders") == 0) {
ScrArr *raw = scr_http_req_raw_headers(r);
ScrDyn *arr = scr_dyn_new_arr();
size_t n = (size_t)scr_arr_len(raw);
for (size_t i = 0; i < n; i++) {
ScrStr *s = (ScrStr *)scr_arr_get_ref(raw, (double)i);
scr_dyn_arr_push(arr, scr_dyn_new_str(s));
scr_str_release(s);
}
scr_arr_release(raw);
return arr;
}
if (strcmp(key, "statusCode") == 0) {
double st = scr_http_req_status(r);
/* Node's IncomingMessage constructor sets statusCode = null; only a
* client response assigns a number (the static lane's typed surface
* spells this number|undefined — the checked-dynamic tree can afford Node's null). */
return st < 0 ? scr_dyn_new_null() : scr_dyn_new_num(st);
}
if (strcmp(key, "statusMessage") == 0) {
ScrStr *s = scr_http_req_status_message(r);
if (!s) return scr_dyn_new_null(); /* null on server requests, like statusCode */
ScrDyn *d = scr_dyn_new_str(s);
scr_str_release(s);
return d;
}
if (strcmp(key, "socket") == 0 || strcmp(key, "connection") == 0) {
ScrNetSocket *s = scr_http_req_socket(r);
if (!s) return NULL;
ScrDyn *d = scr_dyn_new_handle(s, SCR_DYNH_NET_SOCKET);
scr_net_sock_release(s);
return d;
}
if (strcmp(key, "aborted") == 0) return scr_dyn_new_bool(r->aborted);
if (strcmp(key, "complete") == 0) return scr_dyn_new_bool(r->ended);
if (strcmp(key, "destroyed") == 0) return scr_dyn_new_bool(scr_http_req_destroyed_flag(r));
if (strcmp(key, "readable") == 0) return scr_dyn_new_bool(scr_http_req_readable(r));
if (strcmp(key, "readableEnded") == 0) return scr_dyn_new_bool(r->ended);
if (strcmp(key, "closed") == 0) return scr_dyn_new_bool(r->close_emitted);
{
/* Real instance properties without a modeled read throw "not supported
* yet" instead of answering undefined where Node has a value. */
static const char *const known[] = {
"trailers", "rawTrailers", NULL };
if (scr_http_dynh_in(key, known)) {
scr_http_dynh_unsupported("IncomingMessage", key, NULL);
return NULL;
}
}
return NULL; /* unknown key: the undefined singleton (SEMANTICS.md) */
}
static bool scr_http_dynh_req_set(void *h, const char *key, size_t key_len, const ScrDyn *value) {
(void)h; (void)key; (void)key_len; (void)value;
return false; /* no modeled writable properties — the caller throws "not supported yet" */
}
/* ── ServerResponse (SCR_DYNH_HTTP_RES) ──────────────────────────────── */
/* writeHead's header-object form over a dyn OBJ: flatten into the
* [k0, v0, k1, v1, ...] pairs the static lowering feeds
* scr_http_res_write_head_pairs (repeats append — array values expand to
* consecutive same-name pairs). Numbers format through String(n). NULL =
* a value this surface cannot honestly carry (exception pending). */
static ScrArr *scr_http_dynh_pairs(const ScrDyn *headers) {
ScrArr *pairs = scr_arr_new(SCR_ELEM_STR, headers->v.obj.len * 2);
for (size_t i = 0; i < headers->v.obj.len; i++) {
const ScrDynEntry *e = &headers->v.obj.entries[i];
const ScrDyn *v = e->value;
size_t reps = v->kind == SCR_DYN_ARR ? v->v.arr.len : 1;
for (size_t k = 0; k < reps; k++) {
const ScrDyn *one = v->kind == SCR_DYN_ARR ? v->v.arr.items[k] : v;
ScrStr *vs;
if (one->kind == SCR_DYN_STR) {
vs = scr_str_retain(one->v.str);
} else if (one->kind == SCR_DYN_NUM) {
vs = scr_f64_to_scrstr(one->v.num);
} else {
ScrJsonBuf b;
scr_jb_init(&b);
scr_jb_puts(&b, "Invalid value \"");
scr_jb_puts(&b, one->kind == SCR_DYN_UNDEF ? "undefined" : "object");
scr_jb_puts(&b, "\" for header \"");
for (size_t c = 0; c < e->key_len; c++) scr_jb_putc(&b, e->key[c]);
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);
scr_arr_release(pairs);
return NULL;
}
scr_arr_push_ref(pairs, scr_str_new(e->key, e->key_len));
scr_arr_push_ref(pairs, vs);
}
}
return pairs;
}
/* writeHead's RAW-array form over a dyn ARR: [k0, v0, k1, v1, ...]
* flattens into the same pairs feed; an odd length throws Node's
* ERR_INVALID_ARG_VALUE; value slots may be arrays (consecutive
* same-name lines) or numbers (String(n)). NULL = exception pending. */
static ScrArr *scr_http_dynh_flat_pairs(const ScrDyn *list) {
size_t n = list->v.arr.len;
if (n % 2 != 0) {
static const char msg[] = "The argument 'headers' is invalid.";
scr_throw_error_msg_code(SCR_ERR_TYPE, msg, sizeof msg - 1, "ERR_INVALID_ARG_VALUE");
return NULL;
}
ScrArr *pairs = scr_arr_new(SCR_ELEM_STR, n);
for (size_t i = 0; i + 1 < n; i += 2) {
const ScrDyn *k = list->v.arr.items[i];
const ScrDyn *v = list->v.arr.items[i + 1];
if (k->kind != SCR_DYN_STR) {
scr_arr_release(pairs);
scr_dyn_arg_type_fail("name", "of type string", k);
return NULL;
}
size_t reps = v->kind == SCR_DYN_ARR ? v->v.arr.len : 1;
for (size_t rep = 0; rep < reps; rep++) {
const ScrDyn *one = v->kind == SCR_DYN_ARR ? v->v.arr.items[rep] : v;
ScrStr *vs;
if (one->kind == SCR_DYN_STR) {
vs = scr_str_retain(one->v.str);
} else if (one->kind == SCR_DYN_NUM) {
vs = scr_f64_to_scrstr(one->v.num);
} else {
ScrJsonBuf b;
scr_jb_init(&b);
scr_jb_puts(&b, "Invalid value \"");
scr_jb_puts(&b, one->kind == SCR_DYN_UNDEF ? "undefined" : "object");
scr_jb_puts(&b, "\" for header \"");
scr_jb_write(&b, k->v.str->data, k->v.str->len);
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);
scr_arr_release(pairs);
return NULL;
}
scr_arr_push_ref(pairs, scr_str_new(k->v.str->data, k->v.str->len));
scr_arr_push_ref(pairs, vs);
}
}
return pairs;
}
static ScrDyn *scr_http_dynh_res_invoke(void *h, ScrDyn *self, const char *method,
ScrDyn *const *args, size_t argc, const char *what) {
ScrHttpRes *r = (ScrHttpRes *)h;
bool once = false;
if (scr_http_dynh_reg(method, &once)) {
const ScrDyn *name = argc > 0 ? args[0] : scr_dyn_undefined();
const ScrDyn *cb = argc > 1 ? args[1] : scr_dyn_undefined();
scr_dyn_check_listener(cb, "listener");
if (scr_exc_pending()) return NULL;
if (scr_http_dynh_name_is(name, "close")) {
scr_http_res_on_close(r, scr_dyn_listener_closure0(cb), once);
} else if (scr_http_dynh_name_is(name, "finish")) {
scr_http_res_on_finish(r, scr_dyn_listener_closure0(cb));
} else if (scr_http_dynh_name_is(name, "drain")) {
/* This surface never backpressures (write answers true), so Node's
* contract says 'drain' never fires — an accepted, never-fired
* registration is the consistent answer (SEMANTICS.md). */
} else if (scr_http_dynh_name_is(name, "timeout") && r->sock != NULL) {
/* the socket's idle timer — res.setTimeout's event, Node's delegation */
scr_net_sock_on_timeout(r->sock, scr_dyn_listener_closure0(cb), once);
} else {
scr_http_dynh_event_unsupported("ServerResponse", name);
return NULL;
}
return scr_dyn_retain(self);
}
if (strcmp(method, "end") == 0) {
/* end() / end(chunk) / end(chunk, enc) / end(cb) / end(chunk, cb) /
* end(chunk, enc, cb) — Node's whole shape family. */
size_t i = 0;
const ScrDyn *chunk = NULL;
const ScrDyn *cb = NULL;
ScrBytes *decoded = NULL;
if (i < argc && args[i]->kind == SCR_DYN_FUNC) {
cb = args[i++];
} else if (i < argc && args[i]->kind != SCR_DYN_UNDEF && args[i]->kind != SCR_DYN_NULL) {
if (!scr_http_dynh_chunk_ok(args[i])) return NULL;
chunk = args[i++];
if (i < argc && args[i]->kind == SCR_DYN_STR) { /* encoding */
if (!scr_http_dynh_encode(chunk, args[i], &decoded)) return NULL;
i++;
}
if (i < argc && args[i]->kind == SCR_DYN_FUNC) cb = args[i++];
}
if (i < argc && args[i]->kind != SCR_DYN_UNDEF) {
scr_bytes_release(decoded);
scr_http_dynh_unsupported("ServerResponse", "end", "this argument shape is not modeled");
return NULL;
}
if (cb) scr_http_res_on_end_callback(r, scr_dyn_listener_closure0(cb));
if (decoded != NULL) {
scr_http_res_end_bytes(r, decoded);
scr_bytes_release(decoded);
} else if (chunk == NULL) {
scr_http_res_end(r);
} else if (chunk->kind == SCR_DYN_STR) {
scr_http_res_end_str(r, chunk->v.str);
} else {
scr_http_res_end_bytes(r, chunk->v.bytes);
}
return scr_dyn_retain(self);
}
if (strcmp(method, "write") == 0) {
const ScrDyn *chunk = argc > 0 ? args[0] : scr_dyn_undefined();
if (!scr_http_dynh_chunk_ok(chunk)) return NULL;
ScrBytes *decoded = NULL;
size_t i = 1;
if (i < argc && args[i]->kind == SCR_DYN_STR) { /* encoding */
if (!scr_http_dynh_encode(chunk, args[i], &decoded)) return NULL;
i++;
}
if (i < argc && args[i]->kind == SCR_DYN_FUNC) {
/* write(chunk[, enc], cb): fires from the queue once the chunk
* entered the socket buffer — this surface's flush moment */
scr_http_res_on_write_flush(r, scr_dyn_listener_closure0(args[i]));
i++;
}
if (decoded != NULL) {
scr_http_res_write_bytes(r, decoded);
scr_bytes_release(decoded);
} else if (chunk->kind == SCR_DYN_STR) {
scr_http_res_write_str(r, chunk->v.str);
} else {
scr_http_res_write_bytes(r, chunk->v.bytes);
}
/* Always-true: this surface buffers without a highWaterMark verdict
* (SEMANTICS.md — backpressure is not modeled). */
return scr_dyn_new_bool(true);
}
if (strcmp(method, "flushHeaders") == 0) {
scr_http_res_flush_headers(r);
return scr_dyn_retain(scr_dyn_undefined());
}
if (strcmp(method, "cork") == 0) {
scr_http_res_cork(r);
return scr_dyn_retain(scr_dyn_undefined());
}
if (strcmp(method, "uncork") == 0) {
scr_http_res_uncork(r);
return scr_dyn_retain(scr_dyn_undefined());
}
if (strcmp(method, "setTimeout") == 0) {
const ScrDyn *ms = argc > 0 ? args[0] : scr_dyn_undefined();
if (ms->kind != SCR_DYN_NUM) {
scr_dyn_arg_type_fail("msecs", "of type number", ms);
return NULL;
}
ScrClosure *tcb = NULL;
if (argc > 1 && args[1]->kind == SCR_DYN_FUNC) tcb = scr_dyn_listener_closure0(args[1]);
else if (argc > 1 && args[1]->kind != SCR_DYN_UNDEF) {
scr_dyn_check_listener(args[1], "callback");
return NULL;
}
scr_http_res_set_timeout(r, ms->v.num, tcb);
return scr_dyn_retain(self);
}
if (strcmp(method, "getHeaders") == 0) return scr_http_res_get_headers(r);
if (strcmp(method, "getHeaderNames") == 0) {
ScrArr *names = scr_http_res_get_header_names(r);
ScrDyn *out = scr_dyn_new_arr();
for (size_t k = 0; k < names->len; k++) {
ScrStr *n = (ScrStr *)scr_arr_get_ref(names, (double)k);
scr_dyn_arr_push(out, scr_dyn_new_str(n));
scr_str_release(n);
}
scr_arr_release(names);
return out;
}
if (strcmp(method, "writeHead") == 0) {
const ScrDyn *st = argc > 0 ? args[0] : scr_dyn_undefined();
if (st->kind != SCR_DYN_NUM) {
scr_dyn_arg_type_fail("statusCode", "of type number", st);
return NULL;
}
size_t i = 1;
if (i < argc && args[i]->kind == SCR_DYN_STR) {
scr_http_res_status_msg_set(r, args[i]->v.str);
i++;
}
if (i < argc && args[i]->kind == SCR_DYN_OBJ) {
ScrArr *pairs = scr_http_dynh_pairs(args[i]);
if (!pairs) return NULL;
scr_http_res_write_head_pairs(r, st->v.num, pairs);
scr_arr_release(pairs);
i++;
} else if (i < argc && args[i]->kind == SCR_DYN_ARR) {
/* the RAW-array form: [k0, v0, k1, v1, ...] — an even length is
* Node's contract (ERR_INVALID_ARG_VALUE otherwise), values may be
* arrays (consecutive same-name lines, the setHeader expansion),
* and per NAME the list overrides what setHeader() stored while
* other names survive — the object form's writeHead-wins merge
* (oracle-pinned: a res with setHeader('a') plus writeHead(200,
* ['test', ...]) sends both). */
ScrArr *pairs = scr_http_dynh_flat_pairs(args[i]);
if (!pairs) return NULL;
scr_http_res_write_head_pairs(r, st->v.num, pairs);
scr_arr_release(pairs);
i++;
} else if (i < argc && args[i]->kind != SCR_DYN_UNDEF) {
scr_http_dynh_unsupported("ServerResponse", "writeHead", "only the header-object and raw-array forms are modeled");
return NULL;
} else {
scr_http_res_write_head(r, st->v.num);
}
return scr_dyn_retain(self); /* chaining: writeHead(...).end(...) */
}
if (strcmp(method, "setHeader") == 0) {
const ScrDyn *name = argc > 0 ? args[0] : scr_dyn_undefined();
const ScrDyn *value = argc > 1 ? args[1] : scr_dyn_undefined();
if (name->kind != SCR_DYN_STR) {
scr_dyn_arg_type_fail("name", "of type string", name);
return NULL;
}
scr_http_res_set_header_dyn(r, name->v.str, (ScrDyn *)value);
if (scr_exc_pending()) return NULL;
return scr_dyn_retain(self);
}
if (strcmp(method, "appendHeader") == 0) {
const ScrDyn *name = argc > 0 ? args[0] : scr_dyn_undefined();
const ScrDyn *value = argc > 1 ? args[1] : scr_dyn_undefined();
if (name->kind != SCR_DYN_STR) {
scr_dyn_arg_type_fail("name", "of type string", name);
return NULL;
}
scr_http_res_append_header_dyn(r, name->v.str, (ScrDyn *)value);
if (scr_exc_pending()) return NULL;
return scr_dyn_retain(self);
}
if (strcmp(method, "getHeader") == 0) {
const ScrDyn *name = argc > 0 ? args[0] : scr_dyn_undefined();
if (name->kind != SCR_DYN_STR) {
scr_dyn_arg_type_fail("name", "of type string", name);
return NULL;
}
return scr_http_res_get_header_dyn(r, name->v.str);
}
if (strcmp(method, "hasHeader") == 0) {
const ScrDyn *name = argc > 0 ? args[0] : scr_dyn_undefined();
if (name->kind != SCR_DYN_STR) {
scr_dyn_arg_type_fail("name", "of type string", name);
return NULL;
}
return scr_dyn_new_bool(scr_http_res_has_header_named(r, name->v.str));
}
if (strcmp(method, "removeHeader") == 0) {
const ScrDyn *name = argc > 0 ? args[0] : scr_dyn_undefined();
if (name->kind != SCR_DYN_STR) {
scr_dyn_arg_type_fail("name", "of type string", name);
return NULL;
}
scr_http_res_remove_header(r, name->v.str);
return scr_dyn_retain(scr_dyn_undefined());
}
if (strcmp(method, "destroy") == 0) {
scr_http_res_destroy(r);
return scr_dyn_retain(self);
}
if (strcmp(method, "toString") == 0) {
ScrStr *s = scr_str_new("[object Object]", 15);
ScrDyn *d = scr_dyn_new_str(s);
scr_str_release(s);
return d;
}
{
static const char *const known[] = { "writeContinue", "writeEarlyHints",
"addTrailers", "off", "removeListener", "removeAllListeners", "emit",
"prependListener", "prependOnceListener", "listenerCount", "listeners", "setDefaultEncoding", NULL };
if (scr_http_dynh_in(method, known)) {
scr_http_dynh_unsupported("ServerResponse", method, NULL);
return NULL;
}
}
scr_http_dynh_not_fn(what);
return NULL;
}
static ScrDyn *scr_http_dynh_res_get(void *h, const char *key, size_t key_len) {
ScrHttpRes *r = (ScrHttpRes *)h;
(void)key_len;
if (strcmp(key, "statusCode") == 0) return scr_dyn_new_num(scr_http_res_status_get(r));
if (strcmp(key, "statusMessage") == 0) {
ScrStr *s = scr_http_res_status_msg_get(r);
if (!s) return scr_dyn_retain(scr_dyn_undefined());
ScrDyn *d = scr_dyn_new_str(s);
scr_str_release(s);
return d;
}
if (strcmp(key, "headersSent") == 0) return scr_dyn_new_bool(scr_http_res_headers_sent(r));
if (strcmp(key, "finished") == 0 || strcmp(key, "writableEnded") == 0) {
/* One flag serves both (Node splits ended-vs-flushed; this surface
* flushes synchronously into the socket buffer — SEMANTICS.md). */
return scr_dyn_new_bool(r->finished);
}
if (strcmp(key, "socket") == 0 || strcmp(key, "connection") == 0) {
ScrNetSocket *sock = scr_http_res_socket(r);
if (!sock) return scr_dyn_new_null();
ScrDyn *view = scr_dyn_new_handle(sock, SCR_DYNH_NET_SOCKET);
scr_net_sock_release(sock);
return view;
}
if (strcmp(key, "req") == 0) {
if (r->req_cleared || r->req_ref == NULL) return scr_dyn_new_null();
return scr_dyn_new_handle(r->req_ref, SCR_DYNH_HTTP_REQ);
}
if (strcmp(key, "writableCorked") == 0) return scr_dyn_new_num(scr_http_res_writable_corked(r));
if (strcmp(key, "writableFinished") == 0) return scr_dyn_new_bool(scr_http_res_writable_finished(r));
if (strcmp(key, "writableHighWaterMark") == 0) {
/* Node's default socket highWaterMark — a constant here (backpressure
* is not modeled; SEMANTICS.md) */
return scr_dyn_new_num(16384);
}
if (strcmp(key, "destroyed") == 0) return scr_dyn_new_bool(scr_http_res_destroyed_flag(r));
if (strcmp(key, "closed") == 0) return scr_dyn_new_bool(r->close_emitted);
{
static const char *const known[] = { "chunkedEncoding", "sendDate",
"strictContentLength", "writableLength",
"writableObjectMode", "errored", NULL };
if (scr_http_dynh_in(key, known)) {
scr_http_dynh_unsupported("ServerResponse", key, NULL);
return NULL;
}
}
return NULL;
}
static bool scr_http_dynh_res_set(void *h, const char *key, size_t key_len, const ScrDyn *value) {
ScrHttpRes *r = (ScrHttpRes *)h;
(void)key_len;
if (strcmp(key, "statusCode") == 0) {
if (value->kind != SCR_DYN_NUM) {
scr_dyn_arg_type_fail("statusCode", "of type number", value);
return true; /* handled: the exception is pending */
}
scr_http_res_status_set(r, value->v.num);
return true;
}
if (strcmp(key, "statusMessage") == 0) {
if (value->kind != SCR_DYN_STR) {
scr_dyn_arg_type_fail("statusMessage", "of type string", value);
return true;
}
scr_http_res_status_msg_set(r, value->v.str);
return true;
}
if (strcmp(key, "sendDate") == 0) {
/* Node's boolean flag over the implicit Date header (ToBoolean, like
* the property's own coercion). */
r->no_date = !scr_dyn_truthy(value);
return true;
}
if (strcmp(key, "req") == 0 &&
(value->kind == SCR_DYN_NULL || value->kind == SCR_DYN_UNDEF)) {
/* res.req is a plain property in Node — a null/undefined write
* clears the read; other writes raise the named fence (the handle
* cannot carry arbitrary values). */
r->req_cleared = true;
return true;
}
return false;
}
/* Pipe DESTINATION hook: `req.pipe(res)` dispatches inside this unit,
* but `socket.pipe(res)` arrives from scr_net.c, which cannot name this
* unit's entry points — the destination accepts the source here. */
static bool scr_http_dynh_res_pipe_from(void *dst, const ScrDyn *src) {
ScrHttpRes *r = (ScrHttpRes *)dst;
if (src->kind == SCR_DYN_HANDLE && src->v.handle.tag == SCR_DYNH_NET_SOCKET) {
scr_http_sock_pipe_res((ScrNetSocket *)src->v.handle.ptr, r);
return true;
}
if (src->kind == SCR_DYN_HANDLE && src->v.handle.tag == SCR_DYNH_HTTP_REQ) {
scr_http_req_pipe_res((ScrHttpReq *)src->v.handle.ptr, r);
return true;
}
return false;
}
static const ScrDynHandleOps scr_http_dynh_req_ops = {
"IncomingMessage",
&scr_http_req_retain_v,
&scr_http_req_release_v,
&scr_http_dynh_req_invoke,
&scr_http_dynh_req_get,
&scr_http_dynh_req_set,
NULL,
};
static const ScrDynHandleOps scr_http_dynh_res_ops = {
"ServerResponse",
&scr_http_res_retain_v,
&scr_http_res_release_v,
&scr_http_dynh_res_invoke,
&scr_http_dynh_res_get,
&scr_http_dynh_res_set,
&scr_http_dynh_res_pipe_from,
};
/* The 'request' fire for a DYN listener registered through the netServer
* handle dispatch (`server.on('request', wrapper)` where `server` lives
* in a dyn binding): box req/res by reference and call through the
* checked-dynamic machinery. The ScrHttpReqFn ABI — req/res arrive +1
* and the adapter owns them, like the compiler-emitted adapters. */
static void scr_http_dynh_fire_reqres(ScrClosure *cb, ScrHttpReq *req /* +1 */,
ScrHttpRes *res /* +1 */) {
ScrDyn *fn = scr_dyn_listener_fn(cb);
ScrDyn *dreq = scr_dyn_new_handle(req, SCR_DYNH_HTTP_REQ);
ScrDyn *dres = scr_dyn_new_handle(res, SCR_DYNH_HTTP_RES);
ScrDyn *args[2] = { dreq, dres };
ScrDyn *r = scr_dyn_call(fn, args, 2, "listener");
scr_dyn_release(r);
scr_dyn_release(dres);
scr_dyn_release(dreq);
scr_dyn_release(fn);
scr_http_req_release(req);
scr_http_res_release(res);
}
/* The netServer dyn dispatch's http-event hook (scr_net_set_dynh_http_on):
* 'request' registers; anything else answers false and the net side
* throws the "not supported yet" Error. */
static bool scr_http_dynh_server_on(ScrNetServer *s, const char *event, const ScrDyn *cb,
bool once) {
if (strcmp(event, "request") == 0) {
scr_http_server_on_request(s, scr_dyn_listener_closure_fn(cb, (void *)&scr_http_dynh_fire_reqres),
(ScrHttpReqFn)&scr_http_dynh_fire_reqres, once);
return true;
}
return false;
}
/* ── ClientRequest (SCR_DYNH_HTTP_CLIENT) ────────────────────────────── */
/* The 'response' fire for a DYN listener (the reqres adapter's shape):
* res arrives +1, boxes, and the checked-dynamic call runs. */
static void scr_http_dynh_fire_resp(ScrClosure *cb, ScrHttpReq *res /* +1 */) {
ScrDyn *fn = scr_dyn_listener_fn(cb);
ScrDyn *dres = scr_dyn_new_handle(res, SCR_DYNH_HTTP_REQ);
ScrDyn *args1[1] = { dres };
ScrDyn *r = scr_dyn_call(fn, args1, 1, "listener");
scr_dyn_release(r);
scr_dyn_release(dres);
scr_dyn_release(fn);
scr_http_req_release(res);
}
static ScrDyn *scr_http_dynh_client_invoke(void *h, ScrDyn *self, const char *method,
ScrDyn *const *args, size_t argc, const char *what) {
ScrHttpClientReq *c = (ScrHttpClientReq *)h;
bool once = false;
if (scr_http_dynh_reg(method, &once)) {
const ScrDyn *name = argc > 0 ? args[0] : scr_dyn_undefined();
const ScrDyn *cb = argc > 1 ? args[1] : scr_dyn_undefined();
scr_dyn_check_listener(cb, "listener");
if (scr_exc_pending()) return NULL;
if (scr_http_dynh_name_is(name, "response")) {
scr_http_client_on_response(c, scr_dyn_listener_closure_fn(cb, (void *)&scr_http_dynh_fire_resp),
(ScrHttpRespFn)&scr_http_dynh_fire_resp, once);
} else if (scr_http_dynh_name_is(name, "error")) {
scr_http_client_on_error(c, scr_dyn_listener_closure_err(cb), (ScrChildErrFn)&scr_dyn_listener_fire_err, once);
} else if (scr_http_dynh_name_is(name, "close")) {
scr_http_client_on_close(c, scr_dyn_listener_closure0(cb), once);
} else if (scr_http_dynh_name_is(name, "timeout")) {
scr_http_client_on_timeout(c, scr_dyn_listener_closure0(cb), once);
} else {
scr_http_dynh_event_unsupported("ClientRequest", name);
return NULL;
}
return scr_dyn_retain(self);
}
if (strcmp(method, "end") == 0) {
const ScrDyn *chunk = argc > 0 ? args[0] : scr_dyn_undefined();
if (chunk->kind == SCR_DYN_STR) scr_http_client_end_str(c, chunk->v.str);
else if (chunk->kind == SCR_DYN_BYTES) scr_http_client_end_bytes(c, chunk->v.bytes);
else if (chunk->kind == SCR_DYN_UNDEF || chunk->kind == SCR_DYN_NULL) scr_http_client_end(c);
else {
scr_dyn_arg_type_fail("chunk", "of type string or an instance of Buffer or Uint8Array", chunk);
return NULL;
}
return scr_dyn_retain(self);
}
if (strcmp(method, "write") == 0) {
const ScrDyn *chunk = argc > 0 ? args[0] : scr_dyn_undefined();
if (!scr_http_dynh_chunk_ok(chunk)) return NULL;
if (chunk->kind == SCR_DYN_STR) scr_http_client_write_str(c, chunk->v.str);
else scr_http_client_write_bytes(c, chunk->v.bytes);
return scr_dyn_new_bool(true);
}
if (strcmp(method, "destroy") == 0) {
scr_http_client_destroy(c);
return scr_dyn_retain(self);
}
if (strcmp(method, "setTimeout") == 0) {
const ScrDyn *ms = argc > 0 ? args[0] : scr_dyn_undefined();
if (ms->kind != SCR_DYN_NUM) {
scr_dyn_arg_type_fail("msecs", "of type number", ms);
return NULL;
}
scr_http_client_set_timeout(c, ms->v.num);
if (argc > 1 && args[1]->kind == SCR_DYN_FUNC) {
scr_http_client_on_timeout(c, scr_dyn_listener_closure0(args[1]), true);
}
return scr_dyn_retain(self);
}
if (strcmp(method, "flushHeaders") == 0) {
if (!c->head_sent && !c->destroyed) scr_http_client_send_head(c, -1);
return scr_dyn_retain(scr_dyn_undefined());
}
if (strcmp(method, "toString") == 0) {
ScrStr *s = scr_str_new("[object Object]", 15);
ScrDyn *d = scr_dyn_new_str(s);
scr_str_release(s);
return d;
}
{
static const char *const known[] = { "abort", "setNoDelay", "setSocketKeepAlive",
"getHeader", "setHeader", "removeHeader", "getHeaders", "getHeaderNames", "hasHeader",
"cork", "uncork", "pipe", "off", "removeListener", "removeAllListeners", "emit",
"prependListener", "prependOnceListener", "listenerCount", "listeners", NULL };
if (scr_http_dynh_in(method, known)) {
scr_http_dynh_unsupported("ClientRequest", method, NULL);
return NULL;
}
}
scr_http_dynh_not_fn(what);
return NULL;
}
static ScrDyn *scr_http_dynh_client_get(void *h, const char *key, size_t key_len) {
ScrHttpClientReq *c = (ScrHttpClientReq *)h;
(void)key_len;
if (strcmp(key, "socket") == 0 || strcmp(key, "connection") == 0) {
if (!c->sock) return scr_dyn_new_null();
return scr_dyn_new_handle(c->sock, SCR_DYNH_NET_SOCKET);
}
if (strcmp(key, "destroyed") == 0) return scr_dyn_new_bool(scr_http_client_destroyed(c));
if (strcmp(key, "writableEnded") == 0) return scr_dyn_new_bool(c->ended);
if (strcmp(key, "path") == 0) {
ScrDyn *d = scr_dyn_new_str(c->path);
return d;
}
if (strcmp(key, "method") == 0) return scr_dyn_new_str(c->method);
{
static const char *const known[] = { "aborted", "host", "protocol", "res",
"reusedSocket", "maxHeadersCount", "headersSent", "writableFinished", NULL };
if (scr_http_dynh_in(key, known)) {
scr_http_dynh_unsupported("ClientRequest", key, NULL);
return NULL;
}
}
return NULL;
}
static bool scr_http_dynh_client_set(void *h, const char *key, size_t key_len, const ScrDyn *value) {
(void)h; (void)key; (void)key_len; (void)value;
return false;
}
static const ScrDynHandleOps scr_http_dynh_client_ops = {
"ClientRequest",
&scr_http_client_retain_v,
&scr_http_client_release_v,
&scr_http_dynh_client_invoke,
&scr_http_dynh_client_get,
&scr_http_dynh_client_set,
NULL,
};
/* ── Agent (SCR_DYNH_HTTP_AGENT) ─────────────────────────────────────── */
/* A string-ish option field out of a dyn OBJ: STR answers its bytes,
* NUM formats (the port), everything else reads as absent. */
static ScrStr *scr_http_dynh_opt_str(const ScrDyn *opts, const char *key) {
if (opts == NULL || opts->kind != SCR_DYN_OBJ) return NULL;
const ScrDyn *v = scr_dyn_obj_get(opts, key, strlen(key));
if (v == NULL) return NULL;
if (v->kind == SCR_DYN_STR) return scr_str_retain(v->v.str);
if (v->kind == SCR_DYN_NUM) return scr_f64_to_scrstr(v->v.num);
return NULL;
}
static ScrDyn *scr_http_dynh_agent_invoke(void *h, ScrDyn *self, const char *method,
ScrDyn *const *args, size_t argc, const char *what) {
ScrHttpAgent *a = (ScrHttpAgent *)h;
bool once = false;
if (scr_http_dynh_reg(method, &once)) {
/* Agent events ('free'/'timeout') ride the pooling the runtime does
* not have — a named error, never an inert listener. */
const ScrDyn *name = argc > 0 ? args[0] : scr_dyn_undefined();
scr_http_dynh_event_unsupported("Agent", name);
return NULL;
}
if (strcmp(method, "getName") == 0) {
const ScrDyn *opts = argc > 0 ? args[0] : scr_dyn_undefined();
ScrDyn *view = opts->kind == SCR_DYN_TYPED_REF ? scr_dyn_typed_ref_materialize(opts) : NULL;
if (view != NULL) opts = view;
ScrStr *host = scr_http_dynh_opt_str(opts, "host");
ScrStr *port = scr_http_dynh_opt_str(opts, "port");
ScrStr *laddr = scr_http_dynh_opt_str(opts, "localAddress");
ScrStr *spath = scr_http_dynh_opt_str(opts, "socketPath");
int family = 0;
if (opts != NULL && opts->kind == SCR_DYN_OBJ) {
const ScrDyn *f = scr_dyn_obj_get(opts, "family", 6);
if (f != NULL && f->kind == SCR_DYN_NUM && (f->v.num == 4 || f->v.num == 6)) {
family = (int)f->v.num;
}
}
ScrStr *name = scr_http_agent_name(host ? host->data : NULL, host ? host->len : 0,
port ? port->data : NULL, port ? port->len : 0,
laddr ? laddr->data : NULL, laddr ? laddr->len : 0,
family, spath ? spath->data : NULL,
spath ? spath->len : 0);
scr_str_release(host);
scr_str_release(port);
scr_str_release(laddr);
scr_str_release(spath);
scr_dyn_release(view);
ScrDyn *d = scr_dyn_new_str(name);
scr_str_release(name);
return d;
}
if (strcmp(method, "destroy") == 0) {
scr_http_agent_destroy(a);
return scr_dyn_retain(scr_dyn_undefined());
}
if (strcmp(method, "toString") == 0) {
ScrStr *s = scr_str_new("[object Object]", 15);
ScrDyn *d = scr_dyn_new_str(s);
scr_str_release(s);
return d;
}
{
static const char *const known[] = { "createConnection", "createSocket", "keepSocketAlive",
"reuseSocket", "addRequest", "removeSocket", "off", "removeListener",
"removeAllListeners", "emit", "prependListener", "prependOnceListener",
"listenerCount", "listeners", NULL };
if (scr_http_dynh_in(method, known)) {
scr_http_dynh_unsupported("Agent", method, NULL);
return NULL;
}
}
scr_http_dynh_not_fn(what);
(void)self;
return NULL;
}
/* The sockets/requests snapshots: name → array (fresh objects per read;
* the tests read lengths, membership, and key presence — empty buckets
* are OMITTED, Node deletes drained keys). */
static ScrDyn *scr_http_dynh_agent_table(ScrHttpAgent *a, bool queued) {
ScrDyn *obj = scr_dyn_new_obj();
for (ScrHttpAgentEnt *e = a->ents; e; e = e->next) {
if (e->queued != queued) continue;
ScrDyn *item = queued
? scr_dyn_new_handle(e->client, SCR_DYNH_HTTP_CLIENT)
: e->client->sock ? scr_dyn_new_handle(e->client->sock, SCR_DYNH_NET_SOCKET) : NULL;
if (item == NULL) continue;
ScrDyn *arr = scr_dyn_obj_get(obj, e->name->data, e->name->len); /* borrowed */
if (arr == NULL || arr->kind != SCR_DYN_ARR) {
ScrDyn *fresh = scr_dyn_new_arr();
scr_dyn_arr_push(fresh, item); /* moves */
scr_dyn_obj_set(obj, e->name->data, e->name->len, fresh); /* moves */
} else {
scr_dyn_arr_push(arr, item); /* moves */
}
}
return obj;
}
static ScrDyn *scr_http_dynh_agent_get(void *h, const char *key, size_t key_len) {
ScrHttpAgent *a = (ScrHttpAgent *)h;
(void)key_len;
if (strcmp(key, "maxSockets") == 0) return scr_dyn_new_num(a->max_sockets);
if (strcmp(key, "maxFreeSockets") == 0) return scr_dyn_new_num(a->max_free);
if (strcmp(key, "keepAlive") == 0) return scr_dyn_new_bool(a->keep_alive);
if (strcmp(key, "keepAliveMsecs") == 0) return scr_dyn_new_num(a->ka_msecs);
if (strcmp(key, "defaultPort") == 0) return scr_dyn_new_num(a->default_port);
if (strcmp(key, "protocol") == 0) {
ScrStr *s = scr_str_new(a->secure ? "https:" : "http:", a->secure ? 6 : 5);
ScrDyn *d = scr_dyn_new_str(s);
scr_str_release(s);
return d;
}
if (strcmp(key, "sockets") == 0) return scr_http_dynh_agent_table(a, false);
if (strcmp(key, "requests") == 0) return scr_http_dynh_agent_table(a, true);
if (strcmp(key, "freeSockets") == 0) {
/* Always empty: the runtime pools nothing (keepAlive fences). */
return scr_dyn_new_obj();
}
if (strcmp(key, "totalSocketCount") == 0) {
size_t n = 0;
for (ScrHttpAgentEnt *e = a->ents; e; e = e->next) {
if (!e->queued) n++;
}
return scr_dyn_new_num((double)n);
}
{
static const char *const known[] = { "options", "maxTotalSockets", "scheduling", NULL };
if (scr_http_dynh_in(key, known)) {
scr_http_dynh_unsupported("Agent", key, NULL);
return NULL;
}
}
return NULL;
}
static bool scr_http_dynh_agent_set(void *h, const char *key, size_t key_len, const ScrDyn *value) {
ScrHttpAgent *a = (ScrHttpAgent *)h;
(void)key_len;
if (strcmp(key, "defaultPort") == 0 || strcmp(key, "maxSockets") == 0 ||
strcmp(key, "maxFreeSockets") == 0 || strcmp(key, "keepAliveMsecs") == 0) {
if (value->kind != SCR_DYN_NUM) {
scr_dyn_arg_type_fail(key, "of type number", value);
return true; /* handled: the exception is pending */
}
if (strcmp(key, "defaultPort") == 0) a->default_port = value->v.num;
else if (strcmp(key, "maxSockets") == 0) a->max_sockets = value->v.num;
else if (strcmp(key, "maxFreeSockets") == 0) a->max_free = value->v.num;
else a->ka_msecs = value->v.num;
return true;
}
return false; /* createSocket/createConnection installs: the named fence */
}
static const ScrDynHandleOps scr_http_dynh_agent_ops = {
"Agent",
&scr_http_agent_retain_v,
&scr_http_agent_release_v,
&scr_http_dynh_agent_invoke,
&scr_http_dynh_agent_get,
&scr_http_dynh_agent_set,
NULL,
};
void scr_http_dyn_install(void) {
scr_dyn_handle_install(SCR_DYNH_HTTP_REQ, &scr_http_dynh_req_ops);
scr_dyn_handle_install(SCR_DYNH_HTTP_RES, &scr_http_dynh_res_ops);
scr_dyn_handle_install(SCR_DYNH_HTTP_CLIENT, &scr_http_dynh_client_ops);
scr_dyn_handle_install(SCR_DYNH_HTTP_AGENT, &scr_http_dynh_agent_ops);
scr_net_set_dynh_http_on(&scr_http_dynh_server_on);
}
/* Checked-dynamic chunks into TYPED res/client handles — the
* scr_net_sock_write_dynv story (the static lowering's dyn-data arm). */
void scr_http_res_write_dynv(ScrHttpRes *r, const ScrDyn *d) {
if (d->kind == SCR_DYN_STR) scr_http_res_write_str(r, d->v.str);
else if (d->kind == SCR_DYN_BYTES) scr_http_res_write_bytes(r, d->v.bytes);
else scr_dyn_arg_type_fail("chunk", "of type string or an instance of Buffer or Uint8Array", d);
}
void scr_http_res_end_dynv(ScrHttpRes *r, const ScrDyn *d) {
if (d->kind == SCR_DYN_STR) scr_http_res_end_str(r, d->v.str);
else if (d->kind == SCR_DYN_BYTES) scr_http_res_end_bytes(r, d->v.bytes);
else if (d->kind == SCR_DYN_UNDEF || d->kind == SCR_DYN_NULL) scr_http_res_end(r);
else scr_dyn_arg_type_fail("chunk", "of type string or an instance of Buffer or Uint8Array", d);
}
void scr_http_client_write_dynv(ScrHttpClientReq *c, const ScrDyn *d) {
if (d->kind == SCR_DYN_STR) scr_http_client_write_str(c, d->v.str);
else if (d->kind == SCR_DYN_BYTES) scr_http_client_write_bytes(c, d->v.bytes);
else scr_dyn_arg_type_fail("chunk", "of type string or an instance of Buffer or Uint8Array", d);
}
void scr_http_client_end_dynv(ScrHttpClientReq *c, const ScrDyn *d) {
if (d->kind == SCR_DYN_STR) scr_http_client_end_str(c, d->v.str);
else if (d->kind == SCR_DYN_BYTES) scr_http_client_end_bytes(c, d->v.bytes);
else if (d->kind == SCR_DYN_UNDEF || d->kind == SCR_DYN_NULL) scr_http_client_end(c);
else scr_dyn_arg_type_fail("chunk", "of type string or an instance of Buffer or Uint8Array", d);
}
/* http.request/get with a URL-STRING first argument — the suite's
* `http.get(`http://127.0.0.1:${port}/x`)` spelling. Parses through the
* WHATWG unit (scr_url.c, always linked): an unparsable input throws
* scr_url_new's catchable "Invalid URL" TypeError; a non-http scheme
* throws Node's ERR_INVALID_PROTOCOL shape. Path is pathname + search
* (fragment dropped, Node's client behavior). cb MOVES (nullable);
* result +1 or NULL with the exception pending. */
#include "scr_url_internal.h"
bool scr_http_url_parts(ScrStr *url /*borrowed*/, bool secure, ScrStr **host_out /*+1*/,
double *port_out, ScrStr **path_out /*+1*/) {
ScrUrl *u = scr_url_new(url);
if (!u) return false; /* Invalid URL pending */
/* The scheme is checked against the MODULE the call came from, not read
* off the URL: http.get('https://…') is Node's ERR_INVALID_PROTOCOL, not
* an implicit upgrade to https. Hence `secure` as a parameter. */
const char *want = secure ? "https" : "http";
const size_t wantlen = secure ? 5 : 4;
if (!(u->scheme->len == wantlen && memcmp(u->scheme->data, want, wantlen) == 0)) {
char msg[128];
int n = snprintf(msg, sizeof msg, "Protocol \"%.*s:\" not supported. Expected \"%s:\"",
(int)(u->scheme->len < 32 ? u->scheme->len : 32), u->scheme->data, want);
scr_throw_error_msg_code(SCR_ERR_TYPE, msg, (size_t)n, "ERR_INVALID_PROTOCOL");
scr_url_release(u);
return false;
}
double port = secure ? 443 : 80;
if (u->port->len > 0) {
port = 0;
for (size_t i = 0; i < u->port->len; i++) port = port * 10 + (u->port->data[i] - '0');
}
/* IPv6 authority arrives bracketed ("[::1]") — the dial wants the bare
* address, like Node's client strips them. */
if (u->host->len >= 2 && u->host->data[0] == '[' && u->host->data[u->host->len - 1] == ']') {
*host_out = scr_str_new(u->host->data + 1, u->host->len - 2);
} else {
*host_out = scr_str_retain(u->host);
}
if (u->query->len > 0) {
*path_out = scr_str_concat(u->path, u->query);
} else {
*path_out = u->path->len > 0 ? scr_str_retain(u->path) : scr_str_new("/", 1);
}
*port_out = port;
scr_url_release(u);
return true;
}
ScrHttpClientReq *scr_http_request_url(ScrStr *url /*borrowed*/, ScrStr *method /*borrowed*/,
bool auto_end, ScrClosure *cb /*moves, nullable*/,
ScrHttpRespFn fn) {
ScrStr *host;
ScrStr *path;
double port;
if (!scr_http_url_parts(url, false, &host, &port, &path)) {
if (cb) scr_closure_release(cb);
return NULL; /* Invalid URL / ERR_INVALID_PROTOCOL pending */
}
ScrArr *pairs = scr_arr_new(SCR_ELEM_STR, 0); /* request_impl reads the pairs array */
ScrHttpClientReq *c = scr_http_request_ex(host, port, path, method, 0, pairs, auto_end,
cb, fn, 80, NULL, NULL);
scr_arr_release(pairs);
scr_str_release(host);
scr_str_release(path);
return c;
}