Files
scriptc/packages/runtime/src/scr_http.c
T

7572 lines
303 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;
/* The AsyncLocalStorage context of its creation (+1, NULL outside a
* store): the deferred emits run in it (scr_http_proto_sweep). A server
* request is created in its connection's dispatch, which runs in the
* server's listen() context; a client response in its request's. */
ScrAlsCtx *als;
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 and
* 'end' defers (end_pending) until resume() drains through the emit
* queue, never the resuming stack. A held HTTP/1 server request stops
* its socket's reads once pend reaches SCR_HTTP_REQ_HOLD_BYTES
* (sock_held), as Node's readStop does. */
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 */
bool sock_held; /* this request stopped its socket's reads */
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_als_ctx_release(r->als);
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) {
if (r->sock_held) scr_net_sock_hold(r->sock, false);
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);
}
/* IncomingMessage's readableHighWaterMark (its socket's, 64 KiB): a held
* request buffers this many body bytes before its socket stops reading. */
#define SCR_HTTP_REQ_HOLD_BYTES 65536u
/* Stops the socket's reads while an HTTP/1 server request holds a full
* buffer, and restarts them once it flows, ends or is dumped, as Node's
* readStop when push() returns false. Client responses and h2 compat
* requests keep reading. */
static void scr_http_req_update_hold(ScrHttpReq *r) {
bool hold = r->sock != NULL && r->status == -1 && !r->http2 && !r->ended &&
scr_http_req_holding(r) && r->pend_len >= SCR_HTTP_REQ_HOLD_BYTES;
if (hold == r->sock_held) return;
r->sock_held = hold;
scr_net_sock_hold(r->sock, hold);
}
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);
scr_http_req_update_hold(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. */
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;
scr_http_req_update_hold(r);
}
/* 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;
scr_http_req_update_hold(r);
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);
}
static void scr_http_throw_invalid_header_name(ScrStr *name /*borrowed*/, ScrStr *label /*borrowed*/);
/* 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;
scr_http_throw_invalid_header_name(name, label);
}
/* Node's ERR_INVALID_HTTP_TOKEN for a header name; `name` is the name's
* string form. */
static void scr_http_throw_invalid_header_name(ScrStr *name /*borrowed*/, ScrStr *label /*borrowed*/) {
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;
ScrAlsCtx *als; /* +1: as ScrHttpReq's */
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 */
/* The AsyncLocalStorage context of the end() call (+1, NULL outside a
* store) once `end_ctx_set`: 'finish', the end() callbacks and 'close'
* after an end run in it, as Node's do. An http2 compatibility response
* never sets it: its events follow the server stream, which has no
* store. */
ScrAlsCtx *end_als;
bool end_ctx_set;
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) {
scr_als_ctx_release(r->als);
scr_als_ctx_release(r->end_als);
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;
if (!r->end_ctx_set && r->h2_stream == NULL) {
r->end_ctx_set = true;
r->end_als = scr_als_capture();
}
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 */
SCR_HTTP_EMIT_CLIENT_DESTROY_ERROR = 11 /* ScrHttpClientReq: destroy(err) before a dial */
};
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_destroy_error(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:
case SCR_HTTP_EMIT_CLIENT_DESTROY_ERROR:
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 ScrAlsCtx *scr_http_client_ctx(struct ScrHttpClientReq *c);
/* The context an emit's handle was created in (borrowed). */
static ScrAlsCtx *scr_http_emit_ctx(const ScrHttpEmit *e) {
switch (e->kind) {
case SCR_HTTP_EMIT_RES_CLOSE:
case SCR_HTTP_EMIT_RES_FINISH: {
const ScrHttpRes *res = (const ScrHttpRes *)e->h;
return res->end_ctx_set ? res->end_als : res->als;
}
case SCR_HTTP_EMIT_RES_WCB: return ((ScrHttpRes *)e->h)->als;
case SCR_HTTP_EMIT_REQ_CLOSE:
case SCR_HTTP_EMIT_REQ_DRAIN:
case SCR_HTTP_EMIT_REQ_ABORTED: return ((ScrHttpReq *)e->h)->als;
default: return scr_http_client_ctx((struct ScrHttpClientReq *)e->h);
}
}
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++];
ScrAlsCtx *prev = scr_als_dispatch_enter(scr_http_emit_ctx(&e));
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);
scr_http_req_update_hold(req);
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_CLIENT_DESTROY_ERROR:
scr_http_client_emit_destroy_error((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_als_dispatch_leave(prev);
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;
/* agent.totalSocketCount: the sockets an agent dialed that have not
* closed yet, idle pooled ones included. The agent and each counted
* connection hold the cell (+1), so a socket that closes after the agent
* is gone still lowers a live count. */
typedef struct ScrHttpSockCount {
size_t rc;
size_t n;
} ScrHttpSockCount;
static void scr_http_sock_count_release(ScrHttpSockCount *k) {
if (k != NULL && --k->rc == 0) free(k);
}
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 */
size_t max_header_size; /* maxHeaderSize option; 0 = SCR_HTTP_MAX_HEADER_SIZE */
struct ScrHttpSockCount *sock_count; /* client mode: the agent count that includes
* this socket (+1), or NULL */
struct ScrHttpAgent *pool_agent; /* client mode: the agent whose free list holds
* this idle socket (borrowed), or NULL */
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;
/* The connection's socket joins an agent's totalSocketCount when it starts
* to dial, and leaves it when it closes. */
static void scr_http_conn_count(ScrHttpConn *conn, ScrHttpSockCount *k) {
if (conn == NULL || conn->sock_count != NULL) return;
k->rc++;
k->n++;
conn->sock_count = k;
}
static void scr_http_conn_uncount(ScrHttpConn *conn) {
ScrHttpSockCount *k = conn != NULL ? conn->sock_count : NULL;
if (k == NULL) return;
conn->sock_count = NULL;
k->n--;
scr_http_sock_count_release(k);
}
/* 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 maxHeaderSize bound (target or status message plus header names and
* values), and the raw-byte bound that backs it while a head is still
* arriving: large enough for any head under the size bound. */
static size_t scr_http_conn_header_limit(const ScrHttpConn *conn) {
return conn->max_header_size ? conn->max_header_size : SCR_HTTP_MAX_HEADER_SIZE;
}
static size_t scr_http_conn_raw_limit(const ScrHttpConn *conn) {
size_t limit = 2 * scr_http_conn_header_limit(conn) + 8192;
return limit > SCR_HTTP_MAX_HEADER_BYTES ? limit : SCR_HTTP_MAX_HEADER_BYTES;
}
/* 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) {
size_t raw = scr_http_conn_raw_limit(conn);
if (conn->head_bytes > raw || conn->trailer_bytes > raw - conn->head_bytes) return false;
size_t remaining = raw - 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 check_continue_ls; /* 'checkContinue' (req, res) */
ScrNetLs check_expectation_ls; /* 'checkExpectation' (req, res) */
ScrNetLs upgrade_ls;
ScrNetLs connect_ls; /* HTTP CONNECT — the upgrade machinery's twin */
bool join_dup; /* createServer({ joinDuplicateHeaders: true }) */
size_t max_header_size; /* createServer({ maxHeaderSize }); 0 = the default */
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->check_continue_ls);
scr_net_ls_drop(&ctx->check_expectation_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->check_continue_ls);
scr_net_ls_drop(&ctx->check_expectation_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)) {
/* held: delivery waits for the drain; a full buffer stops the
* socket's reads */
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;
scr_http_req_update_hold(r);
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);
chunk->is_buffer = true; /* Node delivers a Buffer */
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->als = scr_als_capture();
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_conn_header_limit(conn)) {
*overflow = ok;
scr_http_req_release(req);
return false;
}
conn->head_bytes = head_len;
conn->trailer_bytes = 0;
conn->trailer_fields = 0;
/* 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;
}
}
/* HTTP/1.1 requires Host (requireHostHeader). CONNECT requests, and
* upgrade requests an 'upgrade' listener takes, leave the parser before
* Node's check. */
bool is_connect = req->method->len == 7 && memcmp(req->method->data, "CONNECT", 7) == 0;
bool takes_upgrade = has_upgrade_hdr && conn_upgrade_token && conn->srv->upgrade_ls.n > 0;
if (!http10 && conn->srv->require_host_header && !is_connect && !takes_upgrade) {
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;
}
}
/* 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 (is_connect) {
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);
head->is_buffer = true; /* the upgrade head is a Buffer */
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);
head->is_buffer = true; /* the upgrade head is a Buffer */
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;
res->als = scr_als_capture();
#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's server handles it: with a
* 'checkContinue' listener, 100-continue goes to it instead of
* 'request' (the handler calls writeContinue()); without one the server
* writes "100 Continue" and emits 'request'. Any other expectation goes
* to 'checkExpectation', or answers 417 without a 'request'. */
ScrNetLs *handlers = &conn->srv->request_ls;
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)) {
if (scr_net_ls_has_live(&conn->srv->check_continue_ls)) handlers = &conn->srv->check_continue_ls;
else scr_http_res_write_continue(res);
} else if (scr_net_ls_has_live(&conn->srv->check_expectation_ls)) {
handlers = &conn->srv->check_expectation_ls;
} 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(handlers, &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_conn_raw_limit(conn)) {
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_conn_raw_limit(conn)) {
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->pool_agent != NULL) {
/* Data on an idle pooled socket: the agent destroys the socket, as
* Node's free-socket read guard does, so the bytes are never read as
* the next request's response. */
scr_net_sock_destroy(conn->sock);
return;
}
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);
conn->max_header_size = srv->max_header_size;
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;
}
/* Node's validateInteger(value, 'maxHeaderSize', 0). False with the
* exception pending. */
static bool scr_http_max_header_size_valid(double value) {
bool integer = isfinite(value) && value == floor(value);
if (integer && value >= 0 && value <= 9007199254740991.0) return true;
char shown[48];
scr_num_received(value, shown);
char msg[192];
int n = snprintf(msg, sizeof msg,
integer ? "The value of \"maxHeaderSize\" is out of range. It must be >= 0 && <= 9007199254740991. Received %s"
: "The value of \"maxHeaderSize\" is out of range. It must be an integer. Received %s",
shown);
scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)n, "ERR_OUT_OF_RANGE");
return false;
}
/* createServer({ maxHeaderSize }): the server's header size bound. */
void scr_http_server_max_header_size(ScrNetServer *s, double value) {
if (!scr_http_max_header_size_valid(value)) return;
ScrHttpSrvCtx *ctx = (ScrHttpSrvCtx *)scr_net_server_get_http_ctx(s);
if (ctx != NULL && ctx->proto == SCR_NET_PROTO_HTTP1) ctx->max_header_size = (size_t)value;
}
/* 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);
}
/* server.on('checkContinue' | 'checkExpectation', (req, res) => ...). */
void scr_http_server_on_check(ScrNetServer *s, ScrClosure *cb /*moves*/, ScrHttpReqFn fn, bool once,
bool expectation) {
ScrHttpSrvCtx *ctx = (ScrHttpSrvCtx *)scr_net_server_get_http_ctx(s);
if (ctx == NULL || ctx->proto != SCR_NET_PROTO_HTTP1 || scr_net_server_settled(s)) {
scr_closure_release(cb);
return;
}
scr_net_ls_add(expectation ? &ctx->check_expectation_ls : &ctx->check_continue_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->als = scr_als_capture();
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->als = scr_als_capture();
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;
/* The http.request() call's AsyncLocalStorage context (+1): the
* request's deferred emits ('socket', 'finish', 'close') run in it. */
ScrAlsCtx *als;
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;
ScrNetLs continue_ls; /* 'continue': a 100 Continue response arrived */
/* 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;
ScrError *destroy_error; /* destroy(err) or the signal's AbortError (+1):
* fired in place of the teardown's own error */
ScrDyn *abort_dispose; /* options.signal: removes the abort listener (+1) */
double timeout_ms; /* the timeout option (0 = none), reapplied on a pooled socket */
size_t max_header_size; /* the maxHeaderSize option (0 = the default), applied to
* whichever parser serves the request */
bool reused; /* req.reusedSocket: the socket came from an agent's free list */
bool sock_pooled; /* the socket went back to the agent after the response */
ScrStr *pool_key; /* https: TLS options a pooled socket must match (+1), or NULL */
ScrClosure *abort_listener; /* its closure (borrowed); caps[0] points back here */
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_als_ctx_release(c->als);
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_net_ls_drop(&c->continue_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);
if (c->destroy_error) scr_error_release(c->destroy_error);
scr_str_release(c->pool_key);
if (c->abort_listener) scr_box_set_ref(c->abort_listener->caps[0], NULL);
scr_dyn_release(c->abort_dispose);
#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);
}
static ScrAlsCtx *scr_http_client_ctx(struct ScrHttpClientReq *c) { return c->als; }
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 void scr_http_agent_free_remove(struct ScrHttpAgent *a, struct ScrHttpConn *conn);
static bool scr_http_client_pool(struct ScrHttpConn *conn, struct ScrHttpClientReq *c, double hint);
static double scr_http_response_keep_alive(ScrHttpReq *res, const 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. */
/* Detaches the options.signal listener: the closure stops reaching this
* request and the signal drops the listener. */
static void scr_http_client_signal_detach(ScrHttpClientReq *c) {
if (c->abort_listener) {
scr_box_set_ref(c->abort_listener->caps[0], NULL);
c->abort_listener = NULL;
}
if (c->abort_dispose) {
ScrDyn *dispose = c->abort_dispose;
c->abort_dispose = NULL;
ScrDyn *r = scr_dyn_call(dispose, NULL, 0, "dispose");
if (r) scr_dyn_release(r);
scr_dyn_release(dispose);
}
}
static void scr_http_client_settle(struct ScrHttpClientReq *c) {
if (c->close_emitted) return;
c->close_emitted = true;
c->destroyed = true;
scr_http_client_signal_detach(c);
scr_http_client_agent_detach(c); /* usually already detached at socket close */
/* a socket that does not go back to the pool leaves totalSocketCount
* before the request's 'close', as Node's agent removes it at the
* socket's 'close' (c->sock keeps c->conn alive) */
if (c->sock != NULL && !c->sock_pooled) scr_http_conn_uncount(c->conn);
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_net_ls_drop(&c->continue_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;
if (c->destroy_error != NULL) {
/* Node emits the error destroy() was given instead of the teardown's */
ScrError *saved = scr_event_error_payload;
scr_event_error_payload = c->destroy_error;
scr_net_fire_err(&c->err_ls, c->destroy_error->message);
scr_event_error_payload = saved;
return;
}
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) { return c->reused; }
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). Head and body go out as one
* write, as Node sends them: on a connected (reused) socket two writes
* would be two segments, and the server could answer before the body
* arrives. */
c->hold_head = true;
scr_http_client_send_head(c, (long long)len);
c->hold_head = false;
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->held_head != NULL) {
char *head = c->held_head;
size_t head_len = c->held_head_len;
c->held_head = NULL;
c->held_head_len = c->held_head_cap = 0;
if (c->sock) {
if (len > 0) {
char *joined = realloc(head, head_len + len);
if (!joined) scr_http_oom();
head = joined;
memcpy(head + head_len, data, len);
}
scr_net_sock_write_native(c->sock, head, head_len + len);
}
free(head);
}
} 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 && !c->sock_pooled) 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;
/* a socket already back in the agent's pool belongs to the agent */
if (c->sock && !c->sock_pooled) scr_net_sock_destroy(c->sock);
}
static void scr_http_client_emit_destroy_error(ScrHttpClientReq *c) {
if (c->close_emitted) return;
scr_http_client_error(c, c->destroy_error->message);
if (!scr_exc_pending()) scr_http_client_queue_close(c);
}
/* http.request({ maxHeaderSize }): the response head's size bound. */
void scr_http_client_max_header_size(ScrHttpClientReq *c, double value) {
if (!scr_http_max_header_size_valid(value)) {
/* Node throws before dialing: the request goes away without events */
c->had_error = true;
scr_http_client_destroy(c);
return;
}
c->max_header_size = (size_t)value;
if (c->conn != NULL) c->conn->max_header_size = c->max_header_size;
}
/* req.destroy(err): tears the request down like destroy() and reports
* `err` (borrowed) as its 'error'. */
void scr_http_client_destroy_error(ScrHttpClientReq *c, ScrError *err) {
if (c->destroyed || c->close_emitted) return;
if (c->destroy_error == NULL) c->destroy_error = scr_error_retain(err);
if (c->sock == NULL) {
/* not dialed yet: nothing will tear down, so report it from the queue */
c->destroyed = true;
c->response_done = true;
scr_http_emit_push(SCR_HTTP_EMIT_CLIENT_DESTROY_ERROR, scr_http_client_retain(c));
return;
}
scr_http_client_destroy(c);
}
static void *scr_http_ptr_keep_v(void *p) { return p; }
static void scr_http_ptr_drop_v(void *p) { (void)p; }
/* signal.reason (+1, NULL when absent). */
static ScrDyn *scr_http_signal_reason(ScrDyn *signal) {
if (signal->kind == SCR_DYN_OBJ) {
ScrDyn *v = scr_dyn_obj_get(signal, "reason", 6);
return v ? scr_dyn_retain(v) : NULL;
}
ScrStr *key = scr_str_new("reason", 6);
ScrDyn *reason = scr_dyn_handle_key_get(signal, key);
scr_str_release(key);
return reason;
}
/* The options.signal listener: caps[0] the request (a plain pointer the
* request clears when it settles), caps[1] the signal. */
static ScrDyn *scr_http_client_abort_thunk(ScrClosure *clo, ScrDyn *const *args, size_t argc) {
(void)args;
(void)argc;
ScrHttpClientReq *c = scr_box_get_ref(clo->caps[0]);
if (c != NULL) {
ScrDyn *signal = scr_box_get_ref(clo->caps[1]); /* +1 */
ScrDyn *reason = scr_http_signal_reason(signal);
ScrError *e = scr_error_abort(reason);
if (reason) scr_dyn_release(reason);
scr_dyn_release(signal);
scr_http_client_retain(c);
scr_http_client_destroy_error(c, e);
scr_http_client_release(c);
scr_error_release(e);
}
return scr_dyn_retain(scr_dyn_undefined());
}
static void *scr_http_dyn_retain_v(void *p) { return p ? scr_dyn_retain(p) : NULL; }
static void scr_http_dyn_release_v(void *p) { scr_dyn_release(p); }
/* The signal's `aborted` value (+1), or NULL with Node's
* ERR_INVALID_ARG_TYPE pending when the value is not an AbortSignal
* (validateAbortSignal: an object with an `aborted` property). */
static ScrDyn *scr_http_signal_aborted(const ScrDyn *signal) {
ScrDyn *aborted_value = NULL;
if (signal->kind == SCR_DYN_OBJ) {
ScrDyn *v = scr_dyn_obj_get(signal, "aborted", 7);
if (v) aborted_value = scr_dyn_retain(v);
} else if (signal->kind == SCR_DYN_HANDLE) {
ScrStr *key = scr_str_new("aborted", 7);
aborted_value = scr_dyn_handle_key_get(signal, key);
scr_str_release(key);
if (aborted_value && aborted_value->kind == SCR_DYN_UNDEF) {
scr_dyn_release(aborted_value);
aborted_value = NULL;
}
}
if (aborted_value == NULL) scr_dyn_arg_type_fail("signal", "an instance of AbortSignal", signal);
return aborted_value;
}
/* http.request's `signal` option before the request is built: Node's
* ClientRequest constructor validates a truthy signal before it dials, so
* a value that is not an AbortSignal throws ERR_INVALID_ARG_TYPE and no
* request exists. Borrows the signal. */
void scr_http_client_signal_check(const ScrDyn *signal) {
if (signal == NULL || !scr_dyn_truthy(signal)) return;
ScrDyn *aborted_value = scr_http_signal_aborted(signal);
if (aborted_value != NULL) scr_dyn_release(aborted_value);
}
/* http.request's `signal` option: an aborted signal destroys the request
* with Node's AbortError now; otherwise the abort event does, until the
* request closes. A falsy signal is ignored; anything else that is not an
* AbortSignal throws ERR_INVALID_ARG_TYPE. Borrows the signal. */
void scr_http_client_add_abort_signal(ScrHttpClientReq *c, ScrDyn *signal,
ScrDyn *(*add)(ScrDyn *signal, ScrDyn *listener)) {
if (signal == NULL || !scr_dyn_truthy(signal)) return;
ScrDyn *aborted_value = scr_http_signal_aborted(signal);
if (aborted_value == NULL) return;
bool aborted = scr_dyn_truthy(aborted_value);
scr_dyn_release(aborted_value);
if (aborted) {
ScrDyn *reason = scr_http_signal_reason(signal);
ScrError *e = scr_error_abort(reason);
if (reason) scr_dyn_release(reason);
scr_http_client_destroy_error(c, e);
scr_error_release(e);
return;
}
ScrClosure *clo = scr_closure_new((void *)&scr_http_client_abort_thunk, 2);
clo->caps[0] = scr_box_new_obj(&scr_http_ptr_keep_v, &scr_http_ptr_drop_v, NULL);
scr_box_set_ref(clo->caps[0], c);
clo->caps[1] = scr_box_new_obj(&scr_http_dyn_retain_v, &scr_http_dyn_release_v, NULL);
scr_box_set_ref(clo->caps[1], scr_dyn_retain(signal));
ScrDyn *listener = scr_dyn_new_func(clo, &scr_http_client_abort_thunk, 0, "()", "onAbort");
ScrDyn *dispose = add(signal, listener);
scr_dyn_release(listener);
if (dispose == NULL) return; /* exception pending */
c->abort_listener = clo;
c->abort_dispose = dispose;
}
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_continue(ScrHttpClientReq *c, ScrClosure *cb /*moves*/, bool once) {
if (c->close_emitted || c->response_started) {
scr_closure_release(cb);
return;
}
scr_net_ls_add(&c->continue_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->als = scr_als_capture();
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_conn_header_limit(conn)) {
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
* 'continue' for a 100 and keeps parsing until the final response. The
* 'information' event has no listener surface here yet. 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);
if (status == 100) scr_net_fire0(&c->continue_ls);
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);
head->is_buffer = true; /* the upgrade head is a Buffer */
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;
/* the response's reuse verdict, read before the response detaches:
* -2 = the connection closes, -1 = reusable with no Keep-Alive timeout
* hint, otherwise the hint in milliseconds */
double keep_alive_hint = res != NULL ? scr_http_response_keep_alive(res, c) : -2;
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);
}
/* a keepAlive agent takes the socket back; otherwise it closes quietly */
if (c->sock && !scr_http_client_pool(conn, c, keep_alive_hint)) scr_net_sock_destroy(c->sock);
}
/* 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 (conn->pool_agent != NULL) {
/* the server closed an idle pooled connection */
scr_net_sock_destroy(conn->sock);
return;
}
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) {
scr_http_conn_uncount(conn);
if (conn->pool_agent != NULL) scr_http_agent_free_remove(conn->pool_agent, 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;
if (conn->pool_agent != NULL) {
/* an idle pooled socket timed out: the agent destroys it */
scr_net_sock_destroy(conn->sock);
return;
}
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) {
scr_http_conn_uncount(conn);
if (conn->pool_agent != NULL) scr_http_agent_free_remove(conn->pool_agent, 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;
c->als = scr_als_capture();
#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;
scr_net_sock_set_context(c->sock, c->als);
} 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);
c->timeout_ms = timeout_ms > 0 ? timeout_ms : 0;
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;
}
/* http.request without an agent option: the request goes through
* http.globalAgent, as in Node. */
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_agent_ex(host, port, path, method, timeout_ms, header_pairs, auto_end,
NULL, cb, fn, 80, NULL, 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; /* keepAlive: finished connections return to free_socks */
bool fifo; /* scheduling: 'fifo' takes the oldest free socket, 'lifo' the newest */
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 */
struct ScrHttpAgentFree *free_socks; /* keepAlive: idle sockets, most recent first */
ScrHttpSockCount *sock_count; /* totalSocketCount (+1) */
bool in_registry;
struct ScrHttpAgent *next;
} ScrHttpAgent;
/* One idle socket in a keepAlive agent's free list, with its parser. */
typedef struct ScrHttpAgentFree {
ScrStr *name; /* getName's key */
ScrStr *pool_key; /* https: the TLS options the socket was verified with (+1), or NULL */
ScrNetSocket *sock; /* +1 */
struct ScrHttpConn *conn; /* the socket's parser (owned by the socket) */
struct ScrHttpAgentFree *next;
} ScrHttpAgentFree;
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;
while (a->free_socks) {
ScrHttpAgentFree *f = a->free_socks;
a->free_socks = f->next;
f->conn->pool_agent = NULL;
scr_str_release(f->name);
scr_str_release(f->pool_key);
scr_net_sock_release(f->sock);
free(f);
}
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;
}
scr_http_sock_count_release(a->sock_count);
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;
}
static bool scr_http_name_same(const ScrStr *a, const ScrStr *b) {
return a->len == b->len && memcmp(a->data, b->data, a->len) == 0;
}
static size_t scr_http_agent_free_count(const ScrHttpAgent *a, const ScrStr *name) {
size_t n = 0;
for (const ScrHttpAgentFree *f = a->free_socks; f; f = f->next) n += scr_http_name_same(f->name, name);
return n;
}
static bool scr_http_key_same(const ScrStr *a, const ScrStr *b) {
return a == NULL ? b == NULL : b != NULL && scr_http_name_same(a, b);
}
/* Takes a free socket for `name` off the list, or NULL: the most recently
* freed one under 'lifo' scheduling, the oldest under 'fifo'. */
static ScrHttpAgentFree *scr_http_agent_free_take(ScrHttpAgent *a, const ScrStr *name,
const ScrStr *pool_key) {
ScrHttpAgentFree **found = NULL;
for (ScrHttpAgentFree **link = &a->free_socks; *link; link = &(*link)->next) {
if (!scr_http_name_same((*link)->name, name) || !scr_http_key_same((*link)->pool_key, pool_key))
continue;
found = link;
if (!a->fifo) break;
}
if (found == NULL) return NULL;
ScrHttpAgentFree *f = *found;
*found = f->next;
return f;
}
/* Puts a socket taken by scr_http_agent_free_take back where it was
* relative to the other sockets for its name. */
static void scr_http_agent_free_untake(ScrHttpAgent *a, ScrHttpAgentFree *f) {
ScrHttpAgentFree **link = &a->free_socks;
if (a->fifo) {
while (*link) link = &(*link)->next;
}
f->next = *link;
*link = f;
}
/* The idle socket closed (or its parser is going away): it leaves the
* free list. */
static void scr_http_agent_free_remove(struct ScrHttpAgent *a, struct ScrHttpConn *conn) {
for (ScrHttpAgentFree **link = &a->free_socks; *link; link = &(*link)->next) {
if ((*link)->conn != conn) continue;
ScrHttpAgentFree *f = *link;
*link = f->next;
conn->pool_agent = NULL;
scr_str_release(f->name);
scr_str_release(f->pool_key);
scr_net_sock_release(f->sock);
free(f);
return;
}
conn->pool_agent = NULL;
}
/* Binds request `c` to a pooled socket in place of its deferred,
* never-dialed one: the deferred socket's parser is dropped, its buffered
* bytes move to the pooled socket, and the pooled parser serves `c`.
* `reused` is req.reusedSocket: Node sets it when a new request takes a
* free socket, not when a queued request receives a freed one. */
static void scr_http_client_rebind(ScrHttpClientReq *c, ScrHttpAgentFree *f, bool reused) {
ScrNetSocket *old = c->sock;
ScrHttpConn *oldconn = c->conn;
scr_net_sock_set_native_reader(old, NULL, NULL, NULL, NULL, NULL);
scr_net_sock_set_native_events(old, NULL, NULL);
scr_net_sock_set_native_established(old, NULL);
scr_net_sock_move_writes(old, f->sock);
if (oldconn != NULL) {
scr_http_conn_uncount(oldconn);
if (oldconn->client) scr_http_client_release(oldconn->client);
free(oldconn->buf);
free(oldconn);
}
scr_net_sock_destroy(old);
scr_net_sock_release(old);
ScrHttpConn *conn = f->conn;
c->sock = f->sock; /* the free entry's +1 moves */
/* Node runs a pooled socket's events for this request in its context. */
scr_net_sock_set_context(c->sock, c->als);
c->conn = conn;
conn->client = scr_http_client_retain(c);
conn->pool_agent = NULL;
/* the pooled parser still holds the previous request's limit */
conn->max_header_size = c->max_header_size;
conn->state = SCR_HTTP_HEAD;
conn->len = 0;
conn->body_remaining = 0;
conn->client_resp_started = false;
conn->header_overflow = false;
conn->head_bytes = conn->trailer_bytes = conn->trailer_fields = 0;
scr_str_release(f->name);
scr_str_release(f->pool_key);
free(f);
c->reused = reused;
c->sock_ready = true;
scr_net_sock_set_ref(c->sock, true);
scr_net_sock_set_timeout(c->sock, c->timeout_ms);
scr_http_client_queue_finish(c);
}
/* Whether the request's own header `name` carries `token`. */
static bool scr_http_client_has_header_value(const ScrHttpClientReq *c, const char *name,
const char *token) {
for (size_t i = 0; i < c->nheaders; i++) {
if (scr_http_header_name_is(c->hnames[i], name) && scr_http_header_has_token(c->hvalues[i], token))
return true;
}
return false;
}
/* A client response's keep-alive verdict: -2 when the connection closes
* after it (Connection: close, HTTP/1.0 without keep-alive, or a body
* delimited by the connection closing), otherwise the Keep-Alive header's
* /^timeout=(\d+)/ hint in milliseconds, or -1 without one. */
static double scr_http_response_keep_alive(ScrHttpReq *res, const struct ScrHttpClientReq *c) {
bool keep = !res->http10, framed = false;
double hint = -1;
for (size_t i = 0; i < res->nheaders; i++) {
const ScrStr *n = res->hnames[i];
const ScrStr *v = res->hvalues[i];
if (scr_http_header_name_is((ScrStr *)n, "connection")) {
if (scr_http_header_has_token(v, "close")) keep = false;
else if (scr_http_header_has_token(v, "keep-alive")) keep = true;
} else if (scr_http_header_name_is((ScrStr *)n, "content-length") ||
scr_http_header_name_is((ScrStr *)n, "transfer-encoding")) {
framed = true;
} else if (scr_http_header_name_is((ScrStr *)n, "keep-alive") && v->len > 8 &&
memcmp(v->data, "timeout=", 8) == 0 && isdigit((unsigned char)v->data[8])) {
double seconds = 0;
for (size_t k = 8; k < v->len && isdigit((unsigned char)v->data[k]); k++) seconds = seconds * 10 + (v->data[k] - '0');
hint = seconds * 1000;
}
}
bool head = c->method->len == 4 && memcmp(c->method->data, "HEAD", 4) == 0;
bool bodiless = head || res->status == 204 || res->status == 304 || (res->status >= 100 && res->status < 200);
if (!keep || (!framed && !bodiless)) return -2;
return hint;
}
/* Node's agent.keepSocketAlive after a response: the socket returns to the
* agent's free list (unref'd, with the agent's timeout lowered to the
* server's Keep-Alive hint less one second) unless the response or request
* asked to close, the request did not finish, or the free list for the
* name is full. Answers whether the socket was kept. */
static bool scr_http_client_pool(struct ScrHttpConn *conn, struct ScrHttpClientReq *c, double hint) {
ScrHttpAgent *ag = c->agent;
if (ag == NULL || !ag->keep_alive || ag->destroyed || c->destroyed || c->aborted) return false;
if (hint == -2 || !c->ended || conn->len != 0 || scr_http_client_has_header_value(c, "connection", "close"))
return false;
ScrStr *name = NULL;
for (ScrHttpAgentEnt *e = ag->ents; e && name == NULL; e = e->next)
if (e->client == c) name = scr_str_retain(e->name);
if (name == NULL) return false;
if ((double)scr_http_agent_free_count(ag, name) >= ag->max_free) {
scr_str_release(name);
return false;
}
double timeout = ag->timeout_ms > 0 ? ag->timeout_ms : 0;
if (hint >= 0) {
double server = hint - 1000;
if (server < 0) {
scr_str_release(name);
return false;
}
if (server < timeout) timeout = server;
}
ScrHttpAgentFree *f = calloc(1, sizeof *f);
if (!f) scr_http_oom();
f->name = name;
f->pool_key = c->pool_key ? scr_str_retain(c->pool_key) : NULL;
f->sock = scr_net_sock_retain(c->sock);
f->conn = conn;
/* the parser no longer serves this request */
conn->client = NULL;
conn->pool_agent = ag;
f->next = ag->free_socks;
ag->free_socks = f;
c->sock_pooled = true;
scr_net_sock_set_ref(f->sock, false);
scr_net_sock_set_timeout(f->sock, timeout);
scr_http_client_agent_detach(c); /* frees the slot: a queued request takes the socket */
scr_http_client_release(c); /* the parser's reference */
return true;
}
/* 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) {
ScrHttpAgentFree *f = scr_http_agent_free_take(ag, name, e->client->pool_key);
if (f != NULL) {
e->queued = false;
scr_http_client_rebind(e->client, f, false);
} else if (scr_http_agent_active(ag, name) < ag->max_sockets) {
e->queued = false;
if (e->client->sock) {
scr_http_conn_count(e->client->conn, ag->sock_count);
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 */, const ScrDyn *scheduling) {
scr_http_install();
/* Node: options.scheduling || 'lifo', then validateOneOf */
bool fifo = false;
if (scheduling != NULL && scr_dyn_truthy(scheduling)) {
const ScrStr *s = scheduling->kind == SCR_DYN_STR ? scheduling->v.str : NULL;
if (s != NULL && s->len == 4 && memcmp(s->data, "fifo", 4) == 0) fifo = true;
else if (s == NULL || s->len != 4 || memcmp(s->data, "lifo", 4) != 0) {
scr_dyn_arg_value_fail("scheduling", "must be one of: 'fifo', 'lifo'", scheduling);
return NULL;
}
}
ScrHttpAgent *a = calloc(1, sizeof *a);
if (!a) scr_http_oom();
a->rc = 1;
a->sock_count = calloc(1, sizeof *a->sock_count);
if (!a->sock_count) scr_http_oom();
a->sock_count->rc = 1;
a->secure = secure;
a->keep_alive = keep_alive;
a->fifo = fifo;
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;
}
/* http.globalAgent / https.globalAgent: Node's
* new Agent({ keepAlive: true, scheduling: 'lifo', timeout: 5000 }),
* created on first use. Borrowed (the thread keeps it until exit). */
static SCR_TL ScrDyn *scr_http_global_agents[2];
const ScrDyn *scr_http_global_agent(bool secure) {
ScrDyn **slot = &scr_http_global_agents[secure ? 1 : 0];
if (*slot == NULL) *slot = scr_http_agent_new(secure, true, -1, -1, -1, 5000, -1, NULL);
return *slot;
}
/* http.globalAgent as a value (+1). */
ScrDyn *scr_http_global_agent_value(bool secure) {
return scr_dyn_retain((ScrDyn *)scr_http_global_agent(secure));
}
/* 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;
/* idle pooled sockets close; their 'close' removes each from the list */
while (a->free_socks) {
ScrNetSocket *sock = scr_net_sock_retain(a->free_socks->sock);
struct ScrHttpConn *conn = a->free_socks->conn;
scr_http_agent_free_remove(a, conn);
scr_net_sock_destroy(sock);
scr_net_sock_release(sock);
}
/* 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);
}
}
/* The socketPath option of the next request built through
* scr_http_request_agent_ex (borrowed; the caller keeps it alive across
* the call), like https's servername: that request dials the Unix socket
* instead of host:port, and the agent keys its sockets by the path, as
* Node's getName does. */
static SCR_TL ScrStr *scr_http_next_socket_path = NULL;
/* The Unix-socket dialer, set by the runtime options path that sets the
* path; a pointer (not static) so ordinary requests do not link the
* Unix-socket half of the net unit. */
extern SCR_TL ScrNetSocket *(*scr_http_socket_dialer)(ScrStr *path, bool deferred);
SCR_TL ScrNetSocket *(*scr_http_socket_dialer)(ScrStr *path, bool deferred) = NULL;
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,
ScrStr *pool_key /*borrowed, nullable*/) {
ScrStr *socket_path = scr_http_next_socket_path;
scr_http_next_socket_path = NULL;
/* the null/undefined arms: the scheme's global agent, like Node */
if (agent == NULL || agent->kind == SCR_DYN_UNDEF || agent->kind == SCR_DYN_NULL)
agent = scr_http_global_agent(default_port == 443);
/* 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;
}
ScrNetSocket *presock = socket_path != NULL ? scr_http_socket_dialer(socket_path, false) : NULL;
ScrHttpClientReq *c = scr_http_request_impl(host, p, path, method, timeout_ms, pairs,
auto_end, cb, fn, default_port, wrap, wrap_ctx,
presock);
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, socket_path ? socket_path->data : NULL,
socket_path ? socket_path->len : 0);
/* a free pooled socket for the name serves the request right away */
ScrHttpAgentFree *reuse = ag->keep_alive ? scr_http_agent_free_take(ag, name, pool_key) : NULL;
bool queue = reuse == NULL && scr_http_agent_active(ag, name) >= ag->max_sockets;
ScrNetSocket *presock = socket_path != NULL ? scr_http_socket_dialer(socket_path, queue || reuse)
: queue || reuse ? 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 */
if (reuse != NULL) scr_http_agent_free_untake(ag, reuse); /* back to the list it came from */
scr_str_release(name);
return NULL;
}
c->pool_key = pool_key ? scr_str_retain(pool_key) : NULL;
if (ag->timeout_ms >= 0 && timeout_ms <= 0) {
c->timeout_ms = ag->timeout_ms;
scr_net_sock_set_timeout(c->sock, ag->timeout_ms);
}
if (reuse != NULL) scr_http_client_rebind(c, reuse, true);
else if (!queue) scr_http_conn_count(c->conn, ag->sock_count); /* dialed now */
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, 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) {
for (size_t i = 0; i < 2; i++) {
scr_dyn_release(scr_http_global_agents[i]);
scr_http_global_agents[i] = NULL;
}
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, "scheduling") == 0) {
ScrStr *s = scr_str_new(a->fifo ? "fifo" : "lifo", 4);
ScrDyn *d = scr_dyn_new_str(s);
scr_str_release(s);
return d;
}
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) {
/* { name: [socket, ...] }, oldest first like Node's arrays */
ScrDyn *obj = scr_dyn_new_obj();
for (ScrHttpAgentFree *f = a->free_socks; f; f = f->next) {
ScrDyn *arr = scr_dyn_obj_get(obj, f->name->data, f->name->len);
if (arr == NULL) {
arr = scr_dyn_new_arr();
scr_dyn_obj_set(obj, f->name->data, f->name->len, arr);
}
ScrDyn *item = scr_dyn_new_handle(f->sock, SCR_DYNH_NET_SOCKET);
scr_dyn_arr_push(arr, item);
}
/* the list is most recent first: reverse each array into Node's order */
for (size_t i = 0; i < obj->v.obj.len; i++) {
ScrDyn *arr = obj->v.obj.entries[i].value;
for (size_t l = 0, r = arr->v.arr.len ? arr->v.arr.len - 1 : 0; l < r; l++, r--) {
ScrDyn *t = arr->v.arr.items[l];
arr->v.arr.items[l] = arr->v.arr.items[r];
arr->v.arr.items[r] = t;
}
}
return obj;
}
if (strcmp(key, "totalSocketCount") == 0) return scr_dyn_new_num((double)a->sock_count->n);
{
static const char *const known[] = { "options", "maxTotalSockets", 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;
}
/* ── http.request(options) with a runtime options record ─────────────────
*
* The options value of http.request/get when the compiler cannot see an
* object literal: a RequestOptions variable, or a literal that spreads one
* or sets socketPath. Node's ClientRequest constructor reads it at run
* time; so does this walk, in Node's validation order and with its error
* shapes. Documented options outside the implemented set throw the
* runtime fence when they carry a value, rather than being ignored;
* undocumented keys are ignored, as Node ignores them. */
/* The documented options this walk does not implement. A truthy value
* fences; setHost and setDefaultHeaders fence only when false. */
static const char *const SCR_HTTP_REQ_FENCED_OPTIONS[] = {
"auth", "createConnection", "family", "hints", "httpValidation", "insecureHTTPParser",
"joinDuplicateHeaders", "localAddress", "localPort", "lookup", "uniqueHeaders", NULL,
};
static const char SCR_HTTP_REQ_SUPPORTED[] =
"hostname/host, port, path, method, headers, timeout, agent, signal, socketPath, "
"defaultPort, protocol, and maxHeaderSize are the supported options";
/* validateHeaderName and validateHeaderValue over one pair of the flat
* headers array: a name that is not a string throws
* ERR_INVALID_HTTP_TOKEN with its string form, and an array value
* validates each element. */
static void scr_http_req_validate_header_pair(const ScrDyn *name, const ScrDyn *value) {
ScrStr *label = scr_str_new("", 0);
if (name->kind == SCR_DYN_STR) {
scr_http_validate_header_name(name->v.str, label);
} else {
ScrStr *text = scr_dyn_string_coerce_js(name);
if (text != NULL) {
scr_http_throw_invalid_header_name(text, label);
scr_str_release(text);
}
}
scr_str_release(label);
if (scr_exc_pending()) return;
if (value->kind == SCR_DYN_ARR) {
for (size_t k = 0; k < value->v.arr.len && !scr_exc_pending(); k++)
scr_http_validate_header_value(name->v.str, value->v.arr.items[k]);
} else {
scr_http_validate_header_value(name->v.str, value);
}
}
/* util.inspect's one-line rendering of a short array ("[ 'x-a', 5 ]"): up
* to six primitive entries whose rendering stays within inspect's
* 80-column break length (a start of n + 11, plus the separators and each
* entry). NULL when inspect would group or break the entries, or an entry
* is not a primitive or renders outside ASCII (inspect counts UTF-16
* units). */
static ScrStr *scr_http_inspect_short_array(const ScrDyn *list) {
size_t n = list->v.arr.len;
if (n == 0 || n > 6) return NULL;
ScrJsonBuf b;
scr_jb_init(&b);
scr_jb_puts(&b, "[ ");
size_t total = 2 * n + 11;
bool fits = true;
for (size_t i = 0; i < n && fits; i++) {
const ScrDyn *v = list->v.arr.items[i];
ScrStr *part = NULL;
switch (v->kind) {
case SCR_DYN_STR: part = scr_str_inspect_quoted(v->v.str, false); break;
case SCR_DYN_NUM:
part = v->v.num == 0 && signbit(v->v.num) ? scr_str_new("-0", 2) : scr_f64_to_scrstr(v->v.num);
break;
case SCR_DYN_BOOL: part = v->v.b ? scr_str_new("true", 4) : scr_str_new("false", 5); break;
case SCR_DYN_NULL: part = scr_str_new("null", 4); break;
case SCR_DYN_UNDEF: part = scr_str_new("undefined", 9); break;
default: break;
}
if (part == NULL) {
fits = false;
break;
}
for (size_t k = 0; k < part->len && fits; k++) fits = (unsigned char)part->data[k] < 0x80;
total += part->len;
if (total > 80) fits = false;
if (i > 0) scr_jb_puts(&b, ", ");
scr_jb_write(&b, part->data, part->len);
scr_str_release(part);
}
scr_jb_puts(&b, " ]");
ScrStr *out = scr_jb_finish(&b);
if (fits) return out;
scr_str_release(out);
return NULL;
}
/* Node's ERR_INVALID_ARG_VALUE for a flat headers array of odd length.
* Arrays whose inspected form scr_http_inspect_short_array cannot
* reproduce fence instead. */
static void scr_http_req_odd_headers_fail(const char *api, const ScrDyn *list) {
ScrStr *shown = scr_http_inspect_short_array(list);
if (shown == NULL) {
scr_http_req_opts_fence(api, "headers",
"the odd-length headers array error prints up to six short primitive entries here");
return;
}
ScrJsonBuf b;
scr_jb_init(&b);
scr_jb_puts(&b, "The argument 'headers' is invalid. Received ");
scr_jb_write(&b, shown->data, shown->len);
scr_str_release(shown);
ScrStr *msg = scr_jb_finish(&b);
scr_throw_error_msg_code(SCR_ERR_TYPE, msg->data, msg->len, "ERR_INVALID_ARG_VALUE");
scr_str_release(msg);
}
void scr_http_req_opts_fence(const char *api, const char *key, const char *hint) {
ScrJsonBuf b;
scr_jb_init(&b);
scr_jb_puts(&b, api);
scr_jb_puts(&b, " option '");
scr_jb_puts(&b, key);
scr_jb_puts(&b, "' has no static lowering — ");
scr_jb_puts(&b, hint);
scr_throw_error(SCR_ERR_ERROR, scr_jb_finish(&b));
}
void scr_http_req_opts_free(ScrHttpReqOpts *o) {
scr_str_release(o->host);
scr_str_release(o->path);
scr_str_release(o->method);
scr_str_release(o->socket_path);
if (o->headers) scr_arr_release(o->headers);
scr_dyn_release(o->agent);
scr_dyn_release(o->signal);
scr_dyn_release(o->view);
memset(o, 0, sizeof *o);
}
/* A member of the options record, NULL when absent or undefined. */
static const ScrDyn *scr_http_req_opt(const ScrDyn *opts, const char *key) {
const ScrDyn *v = scr_dyn_obj_get(opts, key, strlen(key));
return v == NULL || v->kind == SCR_DYN_UNDEF ? NULL : v;
}
static bool scr_http_req_truthy(const ScrDyn *v) { return v != NULL && scr_dyn_truthy(v); }
/* validatePort's accepted values: an integer in [0, 65535] or a string
* holding one. NaN when the value is not a valid port. */
static double scr_http_req_port_value(const ScrDyn *v) {
if (v->kind == SCR_DYN_NUM) {
double n = v->v.num;
return n == n && trunc(n) == n && n >= 0 && n <= 65535 ? n : NAN;
}
if (v->kind == SCR_DYN_STR) {
bool blank = true;
for (size_t i = 0; i < v->v.str->len && blank; i++)
blank = isspace((unsigned char)v->v.str->data[i]) != 0;
if (blank) return NAN;
ScrStr *s = scr_str_retain(v->v.str);
double n = scr_string_to_number(s);
scr_str_release(s);
return n == n && trunc(n) == n && n >= 0 && n <= 65535 ? n : NAN;
}
return NAN;
}
/* Reads `opts` (borrowed) into `o` in the ClientRequest constructor's
* order. False with the exception pending (`o` holds nothing). */
bool scr_http_req_opts_read(const ScrDyn *opts, bool secure, const char *api, ScrHttpReqOpts *o) {
memset(o, 0, sizeof *o);
o->port = -1;
o->default_port = secure ? 443 : 80;
if (opts != NULL && opts->kind == SCR_DYN_TYPED_REF) {
o->view = scr_dyn_typed_ref_materialize(opts);
opts = o->view;
}
if (opts == NULL || opts->kind == SCR_DYN_UNDEF || opts->kind == SCR_DYN_NULL) {
opts = NULL; /* Node spreads it: every option takes its default */
} else if (opts->kind != SCR_DYN_OBJ) {
scr_http_req_opts_fence(api, "options", "pass an options object or a URL string");
scr_http_req_opts_free(o);
return false;
}
#define OPT(key) (opts != NULL ? scr_http_req_opt(opts, key) : NULL)
/* agent: false, null/undefined (the global agent), or an Agent */
const ScrDyn *agent = OPT("agent");
if (agent != NULL && agent->kind != SCR_DYN_NULL &&
!(agent->kind == SCR_DYN_BOOL && !agent->v.b) &&
!(agent->kind == SCR_DYN_HANDLE && agent->v.handle.tag == SCR_DYNH_HTTP_AGENT)) {
scr_dyn_prop_type_fail("options.agent", "one of Agent-like Object, undefined, or false", agent);
scr_http_req_opts_free(o);
return false;
}
if (agent != NULL) o->agent = scr_dyn_retain((ScrDyn *)agent);
bool agent_secure = secure;
if (agent != NULL && agent->kind == SCR_DYN_HANDLE)
agent_secure = ((ScrHttpAgent *)agent->v.handle.ptr)->secure;
/* path: String(options.path) when truthy, without unescaped characters */
const ScrDyn *path = OPT("path");
if (scr_http_req_truthy(path)) {
o->path = scr_dyn_string_coerce_js(path);
if (o->path == NULL) {
scr_http_req_opts_free(o);
return false;
}
bool bad = false;
for (size_t i = 0; i < o->path->len && !bad; i++) {
unsigned char ch = (unsigned char)o->path->data[i];
if (ch < 0x21) bad = true;
else if (ch >= 0x80) {
/* U+0080..U+00FF (lead byte C2/C3) pass; anything above is unescaped */
if (ch != 0xc2 && ch != 0xc3) bad = true;
else i++;
}
}
if (bad) {
static const char msg[] = "Request path contains unescaped characters";
scr_throw_error_msg_code(SCR_ERR_TYPE, msg, sizeof msg - 1, "ERR_UNESCAPED_CHARACTERS");
scr_http_req_opts_free(o);
return false;
}
} else {
o->path = scr_str_new("/", 1);
}
/* protocol: options.protocol || the agent's, which must match */
const ScrDyn *protocol = OPT("protocol");
const char *expected = agent_secure ? "https:" : "http:";
if (scr_http_req_truthy(protocol)) {
ScrStr *p = scr_dyn_string_coerce_js(protocol);
if (p == NULL) {
scr_http_req_opts_free(o);
return false;
}
if (p->len != strlen(expected) || memcmp(p->data, expected, p->len) != 0) {
char msg[192];
int n = snprintf(msg, sizeof msg, "Protocol \"%.*s\" not supported. Expected \"%s\"",
(int)(p->len < 64 ? p->len : 64), p->data, expected);
scr_str_release(p);
scr_throw_error_msg_code(SCR_ERR_TYPE, msg, (size_t)n, "ERR_INVALID_PROTOCOL");
scr_http_req_opts_free(o);
return false;
}
scr_str_release(p);
}
/* defaultPort, then port (validated when the dial is TCP) */
const ScrDyn *default_port = OPT("defaultPort");
if (scr_http_req_truthy(default_port) && default_port->kind == SCR_DYN_NUM)
o->default_port = (int)default_port->v.num;
else if (agent != NULL && agent->kind == SCR_DYN_HANDLE)
o->default_port = (int)((ScrHttpAgent *)agent->v.handle.ptr)->default_port;
const ScrDyn *port = OPT("port");
/* host: validateHost(hostname) || validateHost(host) || 'localhost' */
static const char *const host_keys[] = { "hostname", "host" };
for (size_t i = 0; i < 2; i++) {
const ScrDyn *h = OPT(host_keys[i]);
if (h == NULL || h->kind == SCR_DYN_NULL) continue;
if (h->kind != SCR_DYN_STR) {
char name[24];
snprintf(name, sizeof name, "options.%s", host_keys[i]);
scr_dyn_prop_type_fail(name, "of type string or one of undefined or null", h);
scr_http_req_opts_free(o);
return false;
}
if (o->host == NULL && h->v.str->len > 0) o->host = scr_str_retain(h->v.str);
}
if (o->host == NULL) o->host = scr_str_new("localhost", 9);
const ScrDyn *set_host = OPT("setHost");
if (set_host != NULL && set_host->kind == SCR_DYN_BOOL && !set_host->v.b) {
scr_http_req_opts_fence(api, "setHost", "the Host header is always sent — leave setHost at its true default");
scr_http_req_opts_free(o);
return false;
}
const ScrDyn *set_default = OPT("setDefaultHeaders");
if (set_default != NULL && set_default->kind == SCR_DYN_BOOL && !set_default->v.b) {
scr_http_req_opts_fence(api, "setDefaultHeaders", "the default headers are always sent — leave setDefaultHeaders at its true default");
scr_http_req_opts_free(o);
return false;
}
const ScrDyn *socket_path = OPT("socketPath");
/* timeout: getTimerDuration(options.timeout, 'timeout') */
const ScrDyn *timeout = OPT("timeout");
if (timeout != NULL) {
if (timeout->kind != SCR_DYN_NUM) {
scr_dyn_arg_type_fail("timeout", "of type number", timeout);
scr_http_req_opts_free(o);
return false;
}
double t = timeout->v.num;
if (!(t >= 0 && isfinite(t))) {
char recv[48], msg[192];
scr_num_received(t, recv);
int n = snprintf(msg, sizeof msg,
"The value of \"timeout\" 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)n, "ERR_OUT_OF_RANGE");
scr_http_req_opts_free(o);
return false;
}
o->timeout = t;
}
/* method: a string token, upper-cased; GET when absent or empty */
const ScrDyn *method = OPT("method");
if (method != NULL && method->kind != SCR_DYN_NULL && method->kind != SCR_DYN_STR) {
scr_dyn_prop_type_fail("options.method", "of type string", method);
scr_http_req_opts_free(o);
return false;
}
if (method != NULL && method->kind == SCR_DYN_STR && method->v.str->len > 0) {
o->method = scr_str_new(method->v.str->data, method->v.str->len);
for (size_t i = 0; i < o->method->len; i++) {
if (!scr_http_token_char((unsigned char)o->method->data[i])) {
ScrJsonBuf b;
scr_jb_init(&b);
scr_jb_puts(&b, "Method must be a valid HTTP token [\"");
scr_jb_write(&b, o->method->data, o->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);
scr_http_req_opts_free(o);
return false;
}
}
for (size_t i = 0; i < o->method->len; i++) {
unsigned char ch = (unsigned char)o->method->data[i];
if (ch >= 'a' && ch <= 'z') o->method->data[i] = (char)(ch - 'a' + 'A');
}
} else {
o->method = scr_str_new("GET", 3);
}
const ScrDyn *max_header = OPT("maxHeaderSize");
if (max_header != NULL) {
if (max_header->kind != SCR_DYN_NUM) {
scr_dyn_arg_type_fail("maxHeaderSize", "of type number", max_header);
scr_http_req_opts_free(o);
return false;
}
o->max_header_size = max_header->v.num;
o->has_max_header_size = true;
}
for (size_t i = 0; SCR_HTTP_REQ_FENCED_OPTIONS[i] != NULL; i++) {
if (scr_http_req_truthy(OPT(SCR_HTTP_REQ_FENCED_OPTIONS[i]))) {
scr_http_req_opts_fence(api, SCR_HTTP_REQ_FENCED_OPTIONS[i], SCR_HTTP_REQ_SUPPORTED);
scr_http_req_opts_free(o);
return false;
}
}
/* headers: an object (String(number) values, arrays as repeated lines)
* or the flat [name, value, ...] array */
const ScrDyn *headers = OPT("headers");
ScrDyn *headers_view = NULL;
if (headers != NULL && headers->kind == SCR_DYN_TYPED_REF) {
headers_view = scr_dyn_typed_ref_materialize(headers);
headers = headers_view;
}
if (headers != NULL && headers->kind == SCR_DYN_OBJ) {
for (size_t i = 0; i < headers->v.obj.len; i++) {
const ScrDynEntry *e = &headers->v.obj.entries[i];
ScrStr *name = scr_str_new(e->key, e->key_len);
ScrStr *label = scr_str_new("", 0);
scr_http_validate_header_name(name, label);
scr_str_release(label);
const ScrDyn *v = e->value;
if (!scr_exc_pending()) {
if (v->kind == SCR_DYN_ARR) {
for (size_t k = 0; k < v->v.arr.len && !scr_exc_pending(); k++)
scr_http_validate_header_value(name, v->v.arr.items[k]);
} else {
scr_http_validate_header_value(name, v);
}
}
scr_str_release(name);
if (scr_exc_pending()) break;
}
if (!scr_exc_pending()) o->headers = scr_http_dynh_pairs(headers);
} else if (headers != NULL && headers->kind == SCR_DYN_ARR) {
/* Node's _storeHeader checks that the flat [name, value, ...] form has
* an even length, then validates each name and value. The
* [[name, value], ...] entry form is not modeled. */
size_t n = headers->v.arr.len;
if (n > 0 && headers->v.arr.items[0]->kind == SCR_DYN_ARR) {
scr_http_req_opts_fence(
api, "headers",
"headers is an object of string or number values or a flat [name, value, ...] array here");
} else if (n % 2 != 0) {
scr_http_req_odd_headers_fail(api, headers);
} else {
for (size_t i = 0; i < n && !scr_exc_pending(); i += 2)
scr_http_req_validate_header_pair(headers->v.arr.items[i], headers->v.arr.items[i + 1]);
}
if (!scr_exc_pending()) o->headers = scr_http_dynh_flat_pairs(headers);
} else if (headers != NULL && headers->kind != SCR_DYN_NULL) {
scr_http_req_opts_fence(api, "headers", "headers is an object of string or number values here");
}
scr_dyn_release(headers_view);
if (scr_exc_pending()) {
scr_http_req_opts_free(o);
return false;
}
if (o->headers == NULL) o->headers = scr_arr_new(SCR_ELEM_STR, 0);
const ScrDyn *signal = OPT("signal");
if (signal != NULL) o->signal = scr_dyn_retain((ScrDyn *)signal);
/* the connection: a Unix socket (net validates the path) or host:port */
if (scr_http_req_truthy(socket_path)) {
if (socket_path->kind != SCR_DYN_STR) {
scr_dyn_prop_type_fail("options.path", "of type string", socket_path);
scr_http_req_opts_free(o);
return false;
}
o->socket_path = scr_str_retain(socket_path->v.str);
if (scr_http_req_truthy(port)) {
double n = scr_http_req_port_value(port);
if (n == n) o->port = n;
}
} else if (scr_http_req_truthy(port)) {
double n = scr_http_req_port_value(port);
if (n != n) {
char detail[96], msg[192];
const char *d = scr_dyn_specific_type(port, detail, sizeof detail);
int len = snprintf(msg, sizeof msg, "Port should be >= 0 and < 65536. Received %s.", d);
scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)len, "ERR_SOCKET_BAD_PORT");
scr_http_req_opts_free(o);
return false;
}
o->port = n;
}
#undef OPT
return true;
}
/* Builds the request `o` describes (its references stay with `o`). The
* signal is checked before construction and attached after it, like the
* literal lowering's sequence. +1, or NULL with the exception pending. */
ScrHttpClientReq *scr_http_req_opts_send(ScrHttpReqOpts *o, bool auto_end,
ScrClosure *cb /*moves, nullable*/, ScrHttpRespFn fn,
ScrDyn *(*add_abort)(ScrDyn *signal, ScrDyn *listener),
void (*wrap)(ScrNetSocket *, void *), void *wrap_ctx,
ScrStr *pool_key /*borrowed, nullable*/) {
if (o->signal != NULL) {
scr_http_client_signal_check(o->signal);
if (scr_exc_pending()) {
if (cb) scr_closure_release(cb);
return NULL;
}
}
/* without a port option the agent's default port applies (< 0), and
* without an agent the scheme's */
double port = o->port >= 0 ? o->port : o->agent != NULL && o->agent->kind == SCR_DYN_HANDLE ? -1
: o->default_port;
scr_http_next_socket_path = o->socket_path;
scr_http_socket_dialer = &scr_net_connect_path;
ScrHttpClientReq *c = scr_http_request_agent_ex(
o->host, port, o->path, o->method, o->timeout, o->headers, auto_end, o->agent, cb, fn,
o->default_port, wrap, wrap_ctx, pool_key);
if (c == NULL) return NULL;
if (o->has_max_header_size) {
scr_http_client_max_header_size(c, o->max_header_size);
if (scr_exc_pending()) return c;
}
if (o->signal != NULL) scr_http_client_add_abort_signal(c, o->signal, add_abort);
return c;
}
/* http.request/http.get(options[, cb]) with a runtime options record. */
ScrHttpClientReq *scr_http_request_dyn(const ScrDyn *opts /*borrowed*/, bool auto_end,
ScrClosure *cb /*moves, nullable*/, ScrHttpRespFn fn,
ScrDyn *(*add_abort)(ScrDyn *signal, ScrDyn *listener)) {
ScrHttpReqOpts o;
if (!scr_http_req_opts_read(opts, false, "http.request", &o)) {
if (cb) scr_closure_release(cb);
return NULL;
}
ScrHttpClientReq *c = scr_http_req_opts_send(&o, auto_end, cb, fn, add_abort, NULL, NULL, NULL);
scr_http_req_opts_free(&o);
return c;
}