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

7127 lines
242 KiB
C

/* Native Fetch, AbortSignal, Headers, and ReadableStream implementations.
* Requests use the native socket, TLS, and event-loop runtime. */
/* Fetch's bad-port table applies to the initial request and each redirect. */
#define SCR_FETCH_BAD_PORTS \
1, 7, 9, 11, 13, 15, 17, 19, 20, 21, 22, 23, 25, 37, 42, 43, 53, 69, \
77, 79, 87, 95, 101, 102, 103, 104, 109, 110, 111, 113, 115, 117, 119, \
123, 135, 137, 139, 143, 161, 179, 389, 427, 465, 512, 513, 514, 515, \
526, 530, 531, 532, 540, 548, 554, 556, 563, 587, 601, 636, 989, 990, \
993, 995, 1719, 1720, 1723, 2049, 3659, 4045, 5060, 5061, 6000, 6566, \
6665, 6666, 6667, 6668, 6669, 6679, 6697, 10080
#include "scr_runtime.h"
#include "scr_system_errors.h"
#include "scr_url_internal.h"
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#ifndef SCR_FETCH_SIGNAL_ONLY
#include <zlib.h>
#else
static void sf_signal_install(void);
#endif
typedef struct SfTransfer SfTransfer;
typedef struct SfSignal SfSignal;
typedef struct SfSignalWatch SfSignalWatch;
typedef struct SfStream SfStream;
typedef struct SfReader SfReader;
typedef struct SfResponse SfResponse;
typedef struct SfHeaders SfHeaders;
typedef struct SfCollector SfCollector;
typedef struct SfReadRequest SfReadRequest;
typedef struct SfPullWait SfPullWait;
typedef struct SfStartWait SfStartWait;
typedef struct SfCancelWait SfCancelWait;
typedef struct SfTypedRef SfTypedRef;
typedef struct SfChunk {
ScrDyn *value;
struct SfChunk *next;
} SfChunk;
struct SfReadRequest {
ScrPromise *promise; /* or NULL for a sink request */
const ScrFetchReadSink *sink; /* readableStreamDefaultReaderRead's read request */
void *sink_ctx;
SfReadRequest *next;
};
struct SfTypedRef {
ScrDyn *value;
SfTypedRef *next;
};
struct SfSignalWatch {
SfSignal *source; /* weak; NULL after the source fires */
void *owner; /* owner keeps source alive */
void (*fire)(SfSignalWatch *, SfSignal *);
SfSignalWatch *next;
};
struct SfSignal {
size_t rc;
bool aborted;
ScrDyn *reason;
ScrError *error_reason;
bool reason_self;
ScrDynHandleTag reason_self_tag;
bool reason_tracked;
SfSignal *reason_next;
ScrEventTargetCore core; /* listeners and the onabort attribute */
SfSignalWatch *watchers;
SfSignal **sources;
SfSignalWatch **source_watches;
size_t source_count;
double timer_id;
};
struct SfReader {
size_t rc;
size_t handle_refs;
SfStream *stream; /* weak; handle refs retain it while the lock is live */
SfReadRequest *pending_head;
SfReadRequest *pending_tail;
ScrPromise *closed;
};
enum {
SF_COLLECT_JSON,
SF_COLLECT_TEXT,
SF_COLLECT_BYTES,
SF_COLLECT_ARRAY_BUFFER,
};
enum {
SF_REDIRECT_FOLLOW,
SF_REDIRECT_ERROR,
SF_REDIRECT_MANUAL,
};
struct SfCollector {
ScrPromise *promise;
SfStream *held_stream;
char *data;
size_t len;
size_t cap;
int mode;
};
struct SfPullWait {
SfStream *stream;
ScrPromise *promise;
};
struct SfStartWait {
SfStream *stream;
ScrPromise *promise;
};
struct SfCancelWait {
ScrPromise *source;
ScrPromise *result;
};
typedef struct SfThenState {
size_t rc;
bool called;
ScrPromise *promise;
} SfThenState;
struct SfStream {
size_t rc;
SfChunk *head;
SfChunk *tail;
size_t queued;
bool started;
bool close_requested;
bool closed;
bool disturbed;
bool internal_lock;
bool initial_pull_pending;
bool enqueue_draining;
bool pulling;
bool pull_again;
bool discarded;
bool request_discarding;
/* A zero high-water mark (a TransformStream's readable side): pull only
* while a read is pending. */
bool hwm_zero;
ScrDyn *error;
SfReader *reader; /* owned while locked */
SfCollector *collector; /* owned while a body reader is active */
ScrDyn *pull_cb;
ScrDyn *cancel_cb;
ScrDyn *source_this;
ScrDyn *from_dyn;
ScrArr *from_array;
ScrBytes *from_bytes;
ScrStr *from_string;
ScrDyn *(*from_array_item)(ScrArr *, double);
double from_index;
/* Canonical transit capsules for repeated references in a typed source
* array. Keeping one capsule per source identity gives both typed
* structural conversions and unknown/any reads one stable JS object. */
SfTypedRef *typed_refs;
SfTransfer *request_owner; /* weak; transfer owns the stream */
SfTransfer *response_owner; /* weak; transfer owns the stream */
bool callbacks_tracked;
SfStream *callbacks_next;
/* Told when the stream errors (a tee watching its source's closed
* promise); ctx is borrowed and the watcher clears it before it goes. */
void (*error_watch)(void *ctx, ScrDyn *reason);
void *error_watch_ctx;
};
struct SfHeaders {
size_t rc;
ScrArr *pairs;
/* Fetch's "sort and combine" list of `pairs` as flat name/value pairs,
* built on the first iteration step and dropped by every mutation. */
ScrArr *sorted;
bool immutable;
/* `pairs` is in the form sf_add_converted_headers builds: lowercase valid
* names, trimmed valid UTF-8 values, one combined pair per name except
* set-cookie. Headers built from wire lines are not (duplicates stay). */
bool canonical;
};
struct SfResponse {
size_t rc;
SfStream *body;
SfHeaders *headers;
ScrStr *url;
ScrStr *status_text;
int status;
bool redirected;
bool null_body;
/* @hono/node-server's lightweight-response state (scr_fetch_response_light):
* whether a constructed Response is still one the package would write
* from its constructor arguments, and in which header form. 0: not (it
* was materialized by a body/metadata read, or never qualified); 1: no
* init headers; 2: a Headers (or pair list) form; 3: a plain record,
* whose raw entries light_record keeps in order. */
int8_t light;
/* Form 2 reached from form 1 by a headers read: the package's Headers
* start with its default text content type when the body is not null. */
bool light_default_ct;
/* The constructor added the text content type (a string body without
* one), which the package's lightweight headers never hold. */
bool auto_ct;
/* The body's byte length (string or Uint8Array), -1 for a null body,
* -2 for a stream. */
double light_length;
ScrArr *light_record;
};
#ifndef SCR_FETCH_SIGNAL_ONLY
struct SfTransfer {
size_t rc; /* live registry + one per listener closure */
ScrPromise *promise; /* owned */
ScrUrl *url; /* current hop, owned */
ScrStr *method; /* current method, owned */
ScrArr *headers; /* user header pairs, owned */
ScrDyn *body; /* replayable body or source stream, owned */
int redirect_mode;
int hops;
bool redirected;
ScrHttpClientReq *client; /* owned */
ScrNetSocket *response_socket; /* owned while the response is live */
bool response_paused;
bool response_null_body;
SfSignal *signal; /* owned */
SfSignalWatch *signal_watch;
SfStream *request_stream; /* owned */
SfStream *response_stream; /* owned */
bool request_has_content_length;
size_t request_content_length;
size_t request_body_sent;
z_stream zs;
ScrBytes *inflate_pending;
bool inflating;
bool inflate_member_end;
bool inflate_needs_drain;
bool inflate_draining;
bool response_ended;
bool response_sent;
bool done;
struct SfTransfer *next;
};
static SfTransfer *sf_live;
#endif
/* Weak registries: owners unlink themselves on destruction. They let the
* fetch teardown sever opaque native→dyn edges before final cycle collection,
* including otherwise-untraceable callback/reason→handle backedges. */
static SfSignal *sf_reason_signals;
#ifndef SCR_FETCH_SIGNAL_ONLY
static SfStream *sf_callback_streams;
#endif
static void sf_oom(void) {
fputs("scriptc: out of memory\n", stderr);
abort();
}
static bool sf_name(const char *key, size_t len, const char *want) {
return strlen(want) == len && memcmp(key, want, len) == 0;
}
static void sf_type_error(const char *message) {
scr_throw_error_msg(SCR_ERR_TYPE, message, strlen(message));
}
static void sf_not_function(const char *what) {
size_t n = strlen(what);
char *message = malloc(n + 19);
if (!message) sf_oom();
memcpy(message, what, n);
memcpy(message + n, " is not a function", 19);
scr_throw_error_msg(SCR_ERR_TYPE, message, n + 18);
free(message);
}
static bool sf_is_error_dyn(const ScrDyn *d) {
if (!d || d->kind != SCR_DYN_OBJ) return false;
const ScrDyn *mark = scr_dyn_obj_get(d, "%error", 6);
return mark && mark->kind == SCR_DYN_BOOL && mark->v.b;
}
static void sf_throw_dyn_reason(ScrDyn *reason) {
if (sf_is_error_dyn(reason)) {
ScrError *e = scr_error_from_dyn(reason);
scr_throw_obj(e, &scr_error_retain_v, &scr_error_release_v,
scr_error_trace_arg());
return;
}
switch (reason ? reason->kind : SCR_DYN_UNDEF) {
case SCR_DYN_NUM:
scr_throw_f64(reason->v.num);
return;
case SCR_DYN_BOOL:
scr_throw_bool(reason->v.b);
return;
case SCR_DYN_STR:
scr_throw_str(scr_str_retain(reason->v.str));
return;
default:
scr_throw_ref_classified(scr_dyn_retain(reason ? reason : scr_dyn_undefined()),
&scr_dyn_retain_v, &scr_dyn_release_v, NULL,
scr_dyn_is_object(reason ? reason : scr_dyn_undefined()));
return;
}
}
static void sf_reject_promise_reason(ScrPromise *p, ScrDyn *reason) {
sf_throw_dyn_reason(reason);
scr_promise_reject_pending(p);
}
static ScrDyn *sf_dom_reason(const char *name, const char *message,
ScrError **error_out) {
ScrStr *message_str = scr_str_new(message, strlen(message));
ScrStr *name_str = scr_str_new(name, strlen(name));
ScrDyn *m = scr_dyn_new_str(message_str);
ScrDyn *n = scr_dyn_new_str(name_str);
scr_str_release(message_str);
scr_str_release(name_str);
ScrError *e = scr_domex_new(m, n);
scr_dyn_release(m);
scr_dyn_release(n);
ScrDyn *d = scr_dyn_from_error(e);
*error_out = e; /* constructor +1 moves to the signal */
return d;
}
static ScrDyn *sf_type_reason(const char *message, ScrError **error_out) {
ScrStr *message_str = scr_str_new(message, strlen(message));
ScrError *e = scr_error_new(SCR_ERR_TYPE, message_str);
scr_str_release(message_str);
ScrDyn *d = scr_dyn_from_error(e);
*error_out = e;
return d;
}
/* Request's URL parse failure: with the URL parser's TypeError pending,
* replace it by undici's TypeError "Failed to parse URL from <input>"
* whose (non-enumerable) cause is the parser error. */
static void sf_throw_url_parse_failure(const ScrStr *input) {
ScrCaught *caught = scr_exc_take();
ScrDyn *cause = scr_caught_to_dyn(caught);
scr_caught_release(caught);
/* Sized without the NUL so it stays out of the unit's shared mergeable
* string section (which every program linking this unit carries). */
static const char prefix[25] = "Failed to parse URL from ";
size_t message_len = sizeof prefix + input->len;
char *message = malloc(message_len);
if (!message) sf_oom();
memcpy(message, prefix, sizeof prefix);
memcpy(message + sizeof prefix, input->data, input->len);
ScrStr *text = scr_str_new(message, message_len);
free(message);
ScrError *e = scr_error_new(SCR_ERR_TYPE, text);
scr_str_release(text);
scr_error_set_cause(e, cause);
e->cause_enumerable = false;
scr_dyn_release(cause);
scr_throw_obj(e, &scr_error_retain_v, &scr_error_release_v, scr_error_trace_arg());
}
/* ── AbortSignal ─────────────────────────────────────────────────── */
static void sf_signal_track_reason(SfSignal *s) {
if (s->reason_tracked) return;
s->reason_tracked = true;
s->reason_next = sf_reason_signals;
sf_reason_signals = s;
}
static void sf_signal_untrack_reason(SfSignal *s) {
if (!s->reason_tracked) return;
for (SfSignal **at = &sf_reason_signals; *at;
at = &(*at)->reason_next) {
if (*at == s) {
*at = s->reason_next;
break;
}
}
s->reason_tracked = false;
s->reason_next = NULL;
}
/* A controller may abort with itself or its own signal as the reason.
* Those handles point back to this SfSignal, so retaining their ScrDyn box
* would create an opaque RC cycle. Remember the identity as tag + pointer
* instead and mint an equivalent handle whenever the reason is observed;
* checked-dynamic strict equality uses that same pair. Other reasons stay
* strongly owned and join the teardown registry so larger reason/handle
* cycles can be severed once no Web object remains observable. */
static ScrDyn *sf_signal_reason_ref(SfSignal *s) {
if (s->reason_self) {
return scr_dyn_new_handle(s, s->reason_self_tag);
}
return scr_dyn_retain(s->reason);
}
static void sf_signal_drop_reason(SfSignal *s) {
ScrDyn *reason = s->reason;
ScrError *error_reason = s->error_reason;
sf_signal_untrack_reason(s);
s->reason = NULL;
s->error_reason = NULL;
s->reason_self = false;
scr_error_release(error_reason);
/* May release a handle that owns s and destroy it reentrantly. All s
* fields used by this cleanup are therefore cleared before this call. */
scr_dyn_release(reason);
}
static SfSignal *sf_signal_new(void) {
#ifdef SCR_FETCH_SIGNAL_ONLY
sf_signal_install();
#endif
SfSignal *s = calloc(1, sizeof *s);
if (!s) sf_oom();
s->rc = 1;
/* AbortSignal sets kMaxEventTargetListeners to 0: no leak warning. */
scr_et_core_init(&s->core, 0, "AbortSignal");
return s;
}
static SfSignal *sf_signal_retain(SfSignal *s) {
s->rc++;
return s;
}
static void sf_signal_release(SfSignal *s);
static void sf_watch_free(SfSignalWatch *w) {
if (!w) return;
if (w->source) {
for (SfSignalWatch **at = &w->source->watchers; *at; at = &(*at)->next) {
if (*at == w) {
*at = w->next;
break;
}
}
}
free(w);
}
static void sf_signal_release(SfSignal *s) {
if (!s || --s->rc > 0) return;
for (size_t i = 0; i < s->source_count; i++) {
sf_watch_free(s->source_watches[i]);
sf_signal_release(s->sources[i]);
}
free(s->source_watches);
free(s->sources);
scr_et_core_drop(&s->core);
sf_signal_drop_reason(s);
free(s);
}
static void *sf_signal_retain_v(void *p) {
return sf_signal_retain((SfSignal *)p);
}
static void sf_signal_release_v(void *p) {
sf_signal_release((SfSignal *)p);
}
static SfSignalWatch *sf_signal_watch(SfSignal *source, void *owner,
void (*fire)(SfSignalWatch *,
SfSignal *)) {
SfSignalWatch *w = calloc(1, sizeof *w);
if (!w) sf_oom();
w->source = source;
w->owner = owner;
w->fire = fire;
w->next = source->watchers;
source->watchers = w;
return w;
}
/* runAbort: a trusted 'abort' Event through the signal's EventTarget. */
static void sf_signal_dispatch_abort(SfSignal *s) {
ScrDyn *event = scr_event_new_trusted("abort");
ScrDyn *self = scr_dyn_new_handle(s, SCR_DYNH_ABORT_SIGNAL);
scr_et_dispatch(&s->core, self, event);
scr_dyn_release(self);
scr_dyn_release(event);
}
static void sf_signal_abort_full(SfSignal *s, ScrDyn *reason,
ScrError *error_reason) {
if (s->aborted) return;
s->aborted = true;
if (reason && reason->kind == SCR_DYN_HANDLE &&
reason->v.handle.ptr == s &&
(reason->v.handle.tag == SCR_DYNH_ABORT_SIGNAL ||
reason->v.handle.tag == SCR_DYNH_ABORT_CONTROLLER)) {
s->reason_self = true;
s->reason_self_tag = reason->v.handle.tag;
} else {
s->reason = scr_dyn_retain(reason);
sf_signal_track_reason(s);
}
s->error_reason = error_reason ? scr_error_retain(error_reason) : NULL;
while (s->watchers) {
SfSignalWatch *w = s->watchers;
s->watchers = w->next;
w->source = NULL;
w->fire(w, s); /* the owner may free w */
}
sf_signal_dispatch_abort(s);
}
static void sf_signal_abort_default(SfSignal *s, bool timeout) {
ScrError *e = NULL;
ScrDyn *reason = sf_dom_reason(
timeout ? "TimeoutError" : "AbortError",
timeout ? "The operation was aborted due to timeout"
: "This operation was aborted",
&e);
sf_signal_abort_full(s, reason, e);
scr_dyn_release(reason);
scr_error_release(e);
}
static SfSignal *sf_signal_of(const ScrDyn *d, const char *where) {
if (!d || d->kind != SCR_DYN_HANDLE ||
d->v.handle.tag != SCR_DYNH_ABORT_SIGNAL) {
sf_type_error(where);
return NULL;
}
return (SfSignal *)d->v.handle.ptr;
}
/* options.signal for the EventTarget engine. */
static ScrEventTargetCore *sf_signal_core_of(const ScrDyn *d, bool *aborted) {
if (!d || d->kind != SCR_DYN_HANDLE || d->v.handle.tag != SCR_DYNH_ABORT_SIGNAL) return NULL;
SfSignal *s = (SfSignal *)d->v.handle.ptr;
*aborted = s->aborted;
return &s->core;
}
static void sf_signal_timer_fire(ScrClosure *cb) {
SfSignal *s = (SfSignal *)scr_box_get_ref(cb->caps[0]);
if (!s) return;
s->timer_id = 0;
sf_signal_abort_default(s, true);
sf_signal_release(s);
}
/* The mocked AbortSignal.timeout's timer: Node's mock calls abort() on
* the signal's controller, so the reason is the AbortError. */
static void sf_signal_mock_timer_fire(ScrClosure *cb) {
SfSignal *s = (SfSignal *)scr_box_get_ref(cb->caps[0]);
if (!s) return;
if (!s->aborted) sf_signal_abort_default(s, false);
sf_signal_release(s);
}
ScrDyn *scr_fetch_abort_timeout(ScrDyn *delay) {
if (!delay || delay->kind != SCR_DYN_NUM) {
if (delay) {
scr_dyn_arg_type_fail("delay", "of type number", delay);
} else {
static const char message[] =
"The \"delay\" argument must be of type number. Received undefined";
scr_throw_error_msg_code(SCR_ERR_TYPE, message, sizeof message - 1,
"ERR_INVALID_ARG_TYPE");
}
return NULL;
}
double ms = delay->v.num;
if (!isfinite(ms) || trunc(ms) != ms || ms < 0 || ms > 4294967295.0) {
char received[48];
scr_num_received(ms, received);
const char *range =
!isfinite(ms) || trunc(ms) != ms
? "an integer"
: ">= 0 && <= 4294967295";
char message[224];
int len = snprintf(
message, sizeof message,
"The value of \"delay\" is out of range. It must be %s. Received %s",
range, received);
if (len < 0 || (size_t)len >= sizeof message) sf_oom();
scr_throw_error_msg_code(SCR_ERR_RANGE, message, (size_t)len,
"ERR_OUT_OF_RANGE");
return NULL;
}
SfSignal *s = sf_signal_new();
bool mocked = scr_mt_abort_timeout_arm != NULL;
ScrClosure *cb = scr_closure_new(
mocked ? (void *)&sf_signal_mock_timer_fire : (void *)&sf_signal_timer_fire, 1);
ScrBox *box =
scr_box_new_obj(&sf_signal_retain_v, &sf_signal_release_v, NULL);
scr_box_set_ref(box, sf_signal_retain(s));
cb->caps[0] = box;
if (mocked) {
scr_mt_abort_timeout_arm(cb, ms);
} else {
s->timer_id = scr_set_timeout_handle(cb, ms);
scr_timer_unref(s->timer_id);
}
ScrDyn *out = scr_dyn_new_handle(s, SCR_DYNH_ABORT_SIGNAL);
sf_signal_release(s);
return out;
}
ScrDyn *scr_fetch_abort_now(ScrDyn *reason) {
SfSignal *s = sf_signal_new();
if (!reason || reason->kind == SCR_DYN_UNDEF) {
sf_signal_abort_default(s, false);
} else {
ScrError *e = sf_is_error_dyn(reason) ? scr_error_from_dyn(reason) : NULL;
sf_signal_abort_full(s, reason, e);
scr_error_release(e);
}
ScrDyn *out = scr_dyn_new_handle(s, SCR_DYNH_ABORT_SIGNAL);
sf_signal_release(s);
return out;
}
static void sf_any_source_abort(SfSignalWatch *w, SfSignal *source) {
SfSignal *out = (SfSignal *)w->owner;
ScrDyn *reason = sf_signal_reason_ref(source);
sf_signal_abort_full(out, reason, source->error_reason);
scr_dyn_release(reason);
}
ScrDyn *scr_fetch_abort_any(ScrDyn *signals) {
if (!signals || signals->kind != SCR_DYN_ARR) {
static const char message[] =
"signals cannot be converted to sequence.";
scr_throw_error_msg_code(SCR_ERR_TYPE, message, sizeof message - 1,
"ERR_INVALID_ARG_TYPE");
return NULL;
}
SfSignal *out = sf_signal_new();
out->source_count = signals->v.arr.len;
if (out->source_count) {
out->sources = calloc(out->source_count, sizeof *out->sources);
out->source_watches =
calloc(out->source_count, sizeof *out->source_watches);
if (!out->sources || !out->source_watches) sf_oom();
}
for (size_t i = 0; i < out->source_count; i++) {
SfSignal *source =
sf_signal_of(signals->v.arr.items[i],
"AbortSignal.any requires an array of AbortSignals");
if (!source) {
sf_signal_release(out);
return NULL;
}
out->sources[i] = sf_signal_retain(source);
if (source->aborted) {
ScrDyn *reason = sf_signal_reason_ref(source);
sf_signal_abort_full(out, reason, source->error_reason);
scr_dyn_release(reason);
} else {
out->source_watches[i] =
sf_signal_watch(source, out, &sf_any_source_abort);
}
}
ScrDyn *boxed = scr_dyn_new_handle(out, SCR_DYNH_ABORT_SIGNAL);
sf_signal_release(out);
return boxed;
}
/* AbortController is a distinct public handle over the same native signal
* state fetch and AbortSignal.any observe. Reading `.signal` boxes that
* state with the AbortSignal tag, so repeated reads preserve JS identity
* through the checked-dynamic handle equality rule (tag + pointer). */
ScrDyn *scr_fetch_abort_controller_new(void) {
SfSignal *s = sf_signal_new();
ScrDyn *out = scr_dyn_new_handle(s, SCR_DYNH_ABORT_CONTROLLER);
sf_signal_release(s);
return out;
}
static ScrDyn *sf_abort_controller_invoke(
void *ptr, ScrDyn *self, const char *method, ScrDyn *const *args,
size_t argc, const char *what) {
SfSignal *s = ptr;
if (strcmp(method, "abort") == 0) {
if (!s->aborted) {
ScrDyn *reason = argc > 0 ? args[0] : NULL;
if (!reason || reason->kind == SCR_DYN_UNDEF) {
sf_signal_abort_default(s, false);
} else {
ScrError *e = sf_is_error_dyn(reason) ? scr_error_from_dyn(reason) : NULL;
sf_signal_abort_full(s, reason, e);
scr_error_release(e);
}
}
return scr_dyn_retain(scr_dyn_undefined());
}
(void)self;
sf_not_function(what);
return NULL;
}
static ScrDyn *sf_abort_controller_get(
void *ptr, const char *key, size_t len) {
if (sf_name(key, len, "signal")) {
return scr_dyn_new_handle(ptr, SCR_DYNH_ABORT_SIGNAL);
}
return NULL;
}
static ScrDyn *sf_signal_invoke(void *ptr, ScrDyn *self, const char *method,
ScrDyn *const *args, size_t argc,
const char *what) {
SfSignal *s = ptr;
if (strcmp(method, "throwIfAborted") == 0) {
if (s->aborted) {
ScrDyn *reason = sf_signal_reason_ref(s);
sf_throw_dyn_reason(reason);
scr_dyn_release(reason);
return NULL;
}
return scr_dyn_retain(scr_dyn_undefined());
}
if (strcmp(method, "addEventListener") == 0) return scr_et_add_listener(&s->core, args, argc);
if (strcmp(method, "removeEventListener") == 0)
return scr_et_remove_listener(&s->core, args, argc);
if (strcmp(method, "dispatchEvent") == 0)
return scr_et_dispatch_event(&s->core, self, args, argc);
(void)self;
sf_not_function(what);
return NULL;
}
static ScrDyn *sf_signal_get(void *ptr, const char *key, size_t len) {
SfSignal *s = ptr;
if (sf_name(key, len, "aborted")) return scr_dyn_new_bool(s->aborted);
if (sf_name(key, len, "reason")) {
return s->aborted ? sf_signal_reason_ref(s)
: scr_dyn_retain(scr_dyn_undefined());
}
if (sf_name(key, len, "onabort")) return scr_et_handler_get(&s->core, "abort");
/* Inherited from EventTarget: the function every target shares. */
return scr_event_target_method_value(key, len);
}
static bool sf_signal_set(void *ptr, const char *key, size_t len,
const ScrDyn *value) {
SfSignal *s = ptr;
if (!sf_name(key, len, "onabort")) return false;
scr_et_handler_set(&s->core, "abort", value);
return true;
}
/* ── ReadableStream ──────────────────────────────────────────────── */
#ifndef SCR_FETCH_SIGNAL_ONLY
static void sf_stream_track_callbacks(SfStream *s) {
if (s->callbacks_tracked || (!s->pull_cb && !s->cancel_cb)) return;
s->callbacks_tracked = true;
s->callbacks_next = sf_callback_streams;
sf_callback_streams = s;
}
static void sf_stream_untrack_callbacks(SfStream *s) {
if (!s->callbacks_tracked) return;
for (SfStream **at = &sf_callback_streams; *at;
at = &(*at)->callbacks_next) {
if (*at == s) {
*at = s->callbacks_next;
break;
}
}
s->callbacks_tracked = false;
s->callbacks_next = NULL;
}
static SfStream *sf_stream_new_native(void) {
SfStream *s = calloc(1, sizeof *s);
if (!s) sf_oom();
s->rc = 1;
s->started = true;
return s;
}
static SfStream *sf_stream_retain(SfStream *s) {
s->rc++;
return s;
}
static SfReader *sf_reader_retain(SfReader *r) {
r->rc++;
return r;
}
static SfReadRequest *sf_reader_take_request(SfReader *r) {
SfReadRequest *request = r->pending_head;
if (!request) return NULL;
r->pending_head = request->next;
if (!r->pending_head) r->pending_tail = NULL;
request->next = NULL;
return request;
}
static void sf_reader_reject_message_all(SfReader *r, const char *message) {
SfReadRequest *request;
while ((request = sf_reader_take_request(r)) != NULL) {
if (request->sink) {
request->sink->error(request->sink_ctx, NULL);
free(request);
continue;
}
sf_type_error(message);
scr_promise_reject_pending(request->promise);
scr_promise_release(request->promise);
free(request);
}
}
static void sf_reader_release(SfReader *r) {
if (!r || --r->rc > 0) return;
sf_reader_reject_message_all(r, "ReadableStream was released");
scr_promise_release(r->closed);
free(r);
}
static void sf_chunk_release(SfChunk *c) {
if (!c) return;
scr_dyn_release(c->value);
free(c);
}
static void sf_stream_drop_chunks(SfStream *s) {
while (s->head) {
SfChunk *c = s->head;
s->head = c->next;
sf_chunk_release(c);
}
s->tail = NULL;
s->queued = 0;
}
static void sf_inflate_finish_if_ended(SfTransfer *t);
/*
* The live transfer owns one stream reference. When every user-visible
* Response/body/reader/collector reference is gone, only that transfer
* reference remains. Stop queueing at that point and drain the wire so a
* paused unread response cannot keep the process alive forever.
*
* A pending read remains a consumer even if its reader handle was dropped:
* fulfill that request first, then reconsider from sf_stream_drain().
*/
static void sf_response_discard_if_unobserved(SfStream *s) {
if (s->rc != 1 || s->discarded || !s->response_owner ||
s->collector || (s->reader && s->reader->pending_head)) {
return;
}
s->discarded = true;
sf_stream_drop_chunks(s);
SfTransfer *t = s->response_owner;
scr_bytes_release(t->inflate_pending);
t->inflate_pending = NULL;
t->inflate_needs_drain = false;
if (!t->inflate_draining) {
t->zs.next_in = Z_NULL;
t->zs.avail_in = 0;
}
/*
* EOF may already have arrived while the decoded high-water-mark chunk
* kept sf_inflate_finish_if_ended() from settling. Dropping that last
* chunk removes the only remaining blocker. Finish before attempting to
* resume an already-ended socket; settlement may release both t and s.
*/
if (t->response_ended) {
sf_inflate_finish_if_ended(t);
return;
}
if (!t->done && t->response_paused && t->response_socket) {
t->response_paused = false;
scr_net_sock_release(scr_net_sock_resume(t->response_socket));
}
}
static void sf_stream_drop_source_callbacks(
SfStream *s, bool include_cancel) {
ScrDyn *pull = s->pull_cb;
ScrDyn *cancel = include_cancel ? s->cancel_cb : NULL;
ScrDyn *source_this = NULL;
s->pull_cb = NULL;
if (include_cancel) s->cancel_cb = NULL;
if (!s->pull_cb && !s->cancel_cb) {
source_this = s->source_this;
s->source_this = NULL;
sf_stream_untrack_callbacks(s);
}
/* Unlink both edges before either closure release can re-enter through a
* captured stream handle. */
scr_dyn_release(pull);
scr_dyn_release(cancel);
scr_dyn_release(source_this);
}
static void sf_stream_release(SfStream *s) {
if (!s) return;
if (--s->rc > 0) {
sf_response_discard_if_unobserved(s);
return;
}
scr_weak_dispose_inline(s);
sf_stream_drop_chunks(s);
if (s->reader) {
s->reader->stream = NULL;
sf_reader_reject_message_all(s->reader, "ReadableStream was released");
sf_reader_release(s->reader);
s->reader = NULL;
}
scr_dyn_release(s->error);
sf_stream_drop_source_callbacks(s, true);
scr_dyn_release(s->from_dyn);
scr_arr_release(s->from_array);
scr_bytes_release(s->from_bytes);
scr_str_release(s->from_string);
while (s->typed_refs) {
SfTypedRef *ref = s->typed_refs;
s->typed_refs = ref->next;
scr_dyn_release(ref->value);
free(ref);
}
free(s);
}
static void *sf_stream_retain_v(void *p) {
return sf_stream_retain((SfStream *)p);
}
static void sf_stream_release_v(void *p) {
sf_stream_release((SfStream *)p);
}
/*
* The stream owns its active reader so the lock survives loss of the
* user-visible reader box. Conversely, every reader HANDLE retains the
* stream while the lock is live. Keeping those two ownership classes
* distinct avoids a permanent stream↔reader cycle while making chained
* expressions such as `new ReadableStream(...).getReader()` safe.
*/
static void *sf_reader_handle_retain_v(void *p) {
SfReader *r = p;
sf_reader_retain(r);
r->handle_refs++;
if (r->stream) sf_stream_retain(r->stream);
return r;
}
static void sf_reader_handle_release_v(void *p) {
SfReader *r = p;
SfStream *stream = r->stream;
if (r->handle_refs > 0) r->handle_refs--;
sf_reader_release(r);
sf_stream_release(stream);
}
static ScrBytes *sf_chunk_bytes(const ScrDyn *chunk) {
if (chunk && chunk->kind == SCR_DYN_TYPED_REF) {
ScrDyn *materialized = scr_dyn_typed_ref_materialize(chunk);
ScrBytes *bytes = sf_chunk_bytes(materialized);
scr_dyn_release(materialized);
return bytes;
}
if (chunk && scr_dyn_bytes_is(chunk, SCR_BYTES_U8)) {
return scr_bytes_copy(chunk->v.bytes);
}
if (chunk && chunk->kind == SCR_DYN_STR) {
ScrBytes *b = scr_bytes_new(SCR_BYTES_U8, (double)chunk->v.str->len);
memcpy(b->data, chunk->v.str->data, chunk->v.str->len);
if (!chunk->v.str->ascii) scr_wtf8_to_utf8((char *)b->data, chunk->v.str->len);
return b;
}
sf_type_error("ReadableStream chunks must be Uint8Array or string values");
return NULL;
}
static ScrDyn *sf_read_result(bool done, ScrDyn *value) {
ScrDyn *result = scr_dyn_new_obj();
scr_dyn_obj_set(result, "value", 5,
value ? scr_dyn_retain(value)
: scr_dyn_retain(scr_dyn_undefined()));
scr_dyn_obj_set(result, "done", 4, scr_dyn_new_bool(done));
return result;
}
static void sf_reader_fulfill_one(SfReader *r, bool done, ScrDyn *value) {
if (!r) return;
SfReadRequest *request = sf_reader_take_request(r);
if (!request) return;
if (request->sink) {
if (done) request->sink->close(request->sink_ctx);
else request->sink->chunk(request->sink_ctx, value);
free(request);
return;
}
scr_promise_fulfill_ref(request->promise, sf_read_result(done, value),
&scr_dyn_retain_v, &scr_dyn_release_v, NULL);
scr_promise_release(request->promise);
free(request);
}
static void sf_reader_reject_all(SfReader *r, ScrDyn *reason) {
if (!r) return;
SfReadRequest *request;
while ((request = sf_reader_take_request(r)) != NULL) {
if (request->sink) {
request->sink->error(request->sink_ctx, reason);
free(request);
continue;
}
sf_reject_promise_reason(request->promise, reason);
scr_promise_release(request->promise);
free(request);
}
}
static void sf_collector_append(SfCollector *c, const ScrBytes *bytes) {
if (bytes->len == 0) return;
if (c->len > SIZE_MAX - bytes->len) sf_oom();
size_t need = c->len + bytes->len;
if (need > c->cap) {
size_t cap = c->cap ? c->cap : 4096;
while (cap < need) {
if (cap > SIZE_MAX / 2) {
cap = need;
break;
}
cap *= 2;
}
char *next = realloc(c->data, cap);
if (!next) sf_oom();
c->data = next;
c->cap = cap;
}
scr_bytes_read(bytes, 0, c->data + c->len, bytes->len);
c->len = need;
}
static void sf_collector_drop(SfStream *s) {
SfCollector *c = s->collector;
if (!c) return;
s->collector = NULL;
/*
* Body-mixin consumption acquires a reader that is never exposed or
* released. Keep that lock after text()/json()/bytes() settles: Node
* reports response.body.locked === true and rejects a later getReader(),
* on both fulfillment and rejection.
*/
scr_promise_release(c->promise);
free(c->data);
SfStream *held = c->held_stream;
free(c);
sf_stream_release(held);
}
static void sf_collector_finish(SfStream *s) {
SfCollector *c = s->collector;
if (!c) return;
ScrDyn *value = NULL;
ScrBytes *bytes = scr_bytes_new(SCR_BYTES_U8, (double)c->len);
if (c->len) memcpy(bytes->data, c->data, c->len);
if (c->mode == SF_COLLECT_JSON) {
ScrStr *text = scr_text_decode(bytes);
value = scr_json_parse(text);
scr_str_release(text);
} else if (c->mode == SF_COLLECT_TEXT) {
ScrStr *text = scr_text_decode(bytes);
value = scr_dyn_new_str(text);
scr_str_release(text);
} else if (c->mode == SF_COLLECT_ARRAY_BUFFER) {
value = scr_array_buffer_from_bytes(bytes);
} else {
value = scr_dyn_new_bytes(bytes);
}
scr_bytes_release(bytes);
if (scr_exc_pending()) {
scr_dyn_release(value);
scr_promise_reject_pending(c->promise);
} else {
scr_promise_fulfill_ref(c->promise, value, &scr_dyn_retain_v,
&scr_dyn_release_v, NULL);
}
sf_collector_drop(s);
}
static void sf_collector_reject(SfStream *s, ScrDyn *reason) {
if (!s->collector) return;
sf_reject_promise_reason(s->collector->promise, reason);
sf_collector_drop(s);
}
static void sf_stream_request_flush(SfStream *s);
static void sf_transfer_stream_error(SfTransfer *t, ScrDyn *reason);
static void sf_settle(SfTransfer *t);
static SfTransfer *sf_retain(SfTransfer *t);
static void sf_release(SfTransfer *t);
static void sf_inflate_resume(SfTransfer *t);
static void sf_stream_pull(SfStream *s);
static void sf_stream_schedule_initial_pull(SfStream *s);
/*
* A default ReadableStream has a one-chunk high-water mark. Apply that
* bound to the transport itself: pausing the IncomingMessage only moves
* bytes into its parser-side buffer, while pausing the socket leaves them
* in the kernel/TCP window. resume() merely re-arms the poller, so calling
* it while satisfying a read never re-enters this stack.
*/
static void sf_response_pause_if_full(SfStream *s) {
SfTransfer *t = s->response_owner;
if (s->discarded || !t || t->done || t->response_paused ||
!t->response_socket ||
s->queued < 1) {
return;
}
t->response_paused = true;
scr_net_sock_release(scr_net_sock_pause(t->response_socket));
}
static void sf_response_resume_if_ready(SfStream *s) {
SfTransfer *t = s->response_owner;
if (!t || t->done || !t->response_paused || !t->response_socket ||
s->queued >= 1 || s->close_requested || s->closed || s->error) {
return;
}
if (t->inflate_draining) return;
if (t->inflating && (t->inflate_pending || t->inflate_needs_drain)) {
sf_inflate_resume(t);
return;
}
t->response_paused = false;
scr_net_sock_release(scr_net_sock_resume(t->response_socket));
}
static void sf_stream_finish_close(SfStream *s) {
if (!s->close_requested || s->head || s->closed || s->error) return;
s->closed = true;
sf_stream_drop_source_callbacks(s, true);
if (s->reader) scr_promise_fulfill_void(s->reader->closed);
}
static void sf_stream_drain(SfStream *s) {
if (s->request_owner) {
sf_stream_request_flush(s);
return;
}
if (s->request_discarding) {
/* Fetch keeps a reader attached to an upload body after aborting the
* transfer. Its chunks are no longer written, but demand continues
* until the source closes/errors; notably, source.cancel is not run. */
sf_stream_drop_chunks(s);
sf_stream_finish_close(s);
return;
}
if (s->collector) {
while (s->head) {
SfChunk *c = s->head;
s->head = c->next;
if (!s->head) s->tail = NULL;
s->queued--;
ScrBytes *bytes = sf_chunk_bytes(c->value);
if (!bytes) {
sf_chunk_release(c);
scr_promise_reject_pending(s->collector->promise);
sf_collector_drop(s);
sf_stream_drop_chunks(s);
return;
}
sf_collector_append(s->collector, bytes);
scr_bytes_release(bytes);
sf_chunk_release(c);
}
sf_stream_finish_close(s);
sf_response_resume_if_ready(s);
if (s->error) sf_collector_reject(s, s->error);
else if (s->closed) sf_collector_finish(s);
return;
}
if (s->reader) {
bool dequeued = false;
while (s->reader->pending_head && s->head) {
SfChunk *c = s->head;
s->head = c->next;
if (!s->head) s->tail = NULL;
s->queued--;
dequeued = true;
sf_reader_fulfill_one(s->reader, false, c->value);
sf_chunk_release(c);
}
sf_stream_finish_close(s);
if (s->error) {
sf_reader_reject_all(s->reader, s->error);
} else if (s->closed) {
while (s->reader->pending_head) {
sf_reader_fulfill_one(s->reader, true, NULL);
}
}
/* Consuming the last queued chunk raises desiredSize above zero even
* when no read request remains. That transition is fresh demand and
* must replenish the queue up to its default high-water mark. */
if (dequeued && !s->enqueue_draining && !s->head &&
!s->close_requested && !s->closed && !s->error) {
sf_stream_pull(s);
}
sf_response_resume_if_ready(s);
sf_response_discard_if_unobserved(s);
return;
}
sf_stream_finish_close(s);
sf_response_resume_if_ready(s);
}
static void sf_stream_enqueue_value(SfStream *s, ScrDyn *value) {
if (s->close_requested || s->closed || s->error) {
sf_type_error("Invalid state: the ReadableStream is already closed");
return;
}
SfChunk *c = calloc(1, sizeof *c);
if (!c) sf_oom();
ScrDyn *stored = value;
if (value->kind == SCR_DYN_TYPED_REF) {
for (SfTypedRef **at = &s->typed_refs; *at;) {
SfTypedRef *ref = *at;
/* A typed ReadableStream.from(array) retains the source array for the
* stream's lifetime, so a source object can recur after an arbitrary
* number of pulls and must still denote the same JS object. Keep its
* capsule as long as that source is live. Callback-fed streams have no
* such bounded owner; drop an otherwise-unobserved capsule there so a
* long-running producer does not retain every object it ever emitted. */
if (!s->from_array && ref->value->rc == 1) {
*at = ref->next;
scr_dyn_release(ref->value);
free(ref);
continue;
}
if (scr_dyn_strict_eq(ref->value, value)) {
stored = ref->value;
break;
}
at = &ref->next;
}
if (stored == value) {
SfTypedRef *ref = malloc(sizeof *ref);
if (!ref) sf_oom();
ref->value = scr_dyn_retain(value);
ref->next = s->typed_refs;
s->typed_refs = ref;
}
}
c->value = scr_dyn_retain(stored);
if (s->tail) s->tail->next = c;
else s->head = c;
s->tail = c;
s->queued++;
bool was_enqueue_draining = s->enqueue_draining;
s->enqueue_draining = true;
sf_stream_drain(s);
s->enqueue_draining = was_enqueue_draining;
/* Enqueue is the event that can create fresh demand after a deferred
* pull. Merely having a waiting reader/request is not: looping on that
* state re-enters pull synchronously and starves timers. */
if (
s->started &&
!s->close_requested &&
!s->closed &&
!s->error &&
(s->request_owner || s->request_discarding ||
(s->reader && s->reader->pending_head) || !s->head)
) {
sf_stream_pull(s);
}
sf_response_pause_if_full(s);
}
static void sf_stream_enqueue_bytes(SfStream *s, ScrBytes *bytes) {
ScrDyn *value = scr_dyn_new_bytes(bytes);
sf_stream_enqueue_value(s, value);
scr_dyn_release(value);
}
static void sf_stream_close(SfStream *s) {
if (s->close_requested || s->closed || s->error) return;
s->close_requested = true;
/* pull can never run again once close is requested. Keep cancel until
* the queued tail drains because cancel() may still invoke it. */
sf_stream_drop_source_callbacks(s, false);
sf_stream_drain(s);
sf_stream_finish_close(s);
}
static void sf_stream_error(SfStream *s, ScrDyn *reason) {
if (s->closed || s->error) return;
s->error = scr_dyn_retain(reason);
sf_stream_drop_chunks(s);
sf_stream_drain(s);
if (s->reader) {
sf_reject_promise_reason(s->reader->closed, reason);
}
if (s->error_watch) s->error_watch(s->error_watch_ctx, reason);
if (s->request_owner) {
sf_transfer_stream_error(s->request_owner, reason);
}
sf_stream_drop_source_callbacks(s, true);
}
/* Web Streams adopts promise-like results from start/pull/cancel through
* PromiseResolve, not merely native Promise instances. Keep that adoption
* local to this surface: the checked-dynamic language intentionally does
* not otherwise assimilate arbitrary objects carrying a `then` member. */
static SfThenState *sf_then_state_retain(SfThenState *state) {
state->rc++;
return state;
}
static void sf_then_state_release(SfThenState *state) {
if (--state->rc > 0) return;
scr_promise_release(state->promise);
free(state);
}
static void *sf_then_state_retain_v(void *ptr) {
return sf_then_state_retain((SfThenState *)ptr);
}
static void sf_then_state_release_v(void *ptr) {
sf_then_state_release((SfThenState *)ptr);
}
static ScrBox *sf_then_state_box(SfThenState *state) {
ScrBox *box = scr_box_new_obj(
&sf_then_state_retain_v, &sf_then_state_release_v, NULL);
scr_box_set_ref(box, sf_then_state_retain(state));
return box;
}
static void sf_then_adopt(ScrPromise *target, ScrDyn *value);
static ScrDyn *sf_then_resolve_thunk(
ScrClosure *cb, ScrDyn *const *args, size_t argc) {
SfThenState *state = (SfThenState *)scr_box_get_ref(cb->caps[0]);
if (!state->called) {
state->called = true;
sf_then_adopt(
state->promise, argc ? args[0] : scr_dyn_undefined());
}
sf_then_state_release(state);
return scr_dyn_retain(scr_dyn_undefined());
}
static ScrDyn *sf_then_reject_thunk(
ScrClosure *cb, ScrDyn *const *args, size_t argc) {
SfThenState *state = (SfThenState *)scr_box_get_ref(cb->caps[0]);
if (!state->called) {
state->called = true;
sf_reject_promise_reason(
state->promise, argc ? args[0] : scr_dyn_undefined());
}
sf_then_state_release(state);
return scr_dyn_retain(scr_dyn_undefined());
}
static void sf_then_call_task(ScrClosure *cb) {
SfThenState *state = (SfThenState *)scr_box_get_ref(cb->caps[0]);
ScrDyn *thenable = (ScrDyn *)scr_box_get_ref(cb->caps[1]);
ScrDyn *then_fn = (ScrDyn *)scr_box_get_ref(cb->caps[2]);
ScrClosure *resolve_cb =
scr_closure_new((void *)&sf_then_resolve_thunk, 1);
resolve_cb->caps[0] = sf_then_state_box(state);
ScrDyn *resolve = scr_dyn_new_func(
resolve_cb, &sf_then_resolve_thunk, 1, "(value)", "resolve");
ScrClosure *reject_cb =
scr_closure_new((void *)&sf_then_reject_thunk, 1);
reject_cb->caps[0] = sf_then_state_box(state);
ScrDyn *reject = scr_dyn_new_func(
reject_cb, &sf_then_reject_thunk, 1, "(reason)", "reject");
ScrDyn *args[2] = {resolve, reject};
scr_dyn_this_push_dyn(thenable);
ScrDyn *result =
scr_dyn_call(then_fn, args, 2, "thenable.then");
scr_dyn_this_pop();
if (result) {
scr_dyn_release(result);
} else if (!state->called) {
state->called = true;
scr_promise_reject_pending(state->promise);
} else {
/* A throw after either resolving function ran is ignored. */
scr_exc_clear();
}
scr_dyn_release(resolve);
scr_dyn_release(reject);
scr_dyn_release(then_fn);
scr_dyn_release(thenable);
sf_then_state_release(state);
}
static void sf_then_schedule(
ScrPromise *target, ScrDyn *thenable, ScrDyn *then_fn) {
SfThenState *state = calloc(1, sizeof *state);
if (!state) sf_oom();
state->rc = 1;
state->promise = scr_promise_retain(target);
ScrClosure *task =
scr_closure_new((void *)&sf_then_call_task, 3);
task->caps[0] = sf_then_state_box(state);
task->caps[1] =
scr_box_new_obj(&scr_dyn_retain_v, &scr_dyn_release_v, NULL);
scr_box_set_ref(task->caps[1], scr_dyn_retain(thenable));
task->caps[2] =
scr_box_new_obj(&scr_dyn_retain_v, &scr_dyn_release_v, NULL);
scr_box_set_ref(task->caps[2], scr_dyn_retain(then_fn));
sf_then_state_release(state);
scr_queue_microtask(task);
}
static void sf_then_adopt(ScrPromise *target, ScrDyn *value) {
if (value->kind == SCR_DYN_PROMISE) {
scr_promise_race_add(
target, value->v.promise, &scr_promise_adapt_copy);
return;
}
if (value->kind == SCR_DYN_OBJ) {
const ScrDyn *then_fn = scr_dyn_obj_get(value, "then", 4);
if (then_fn && then_fn->kind == SCR_DYN_FUNC) {
sf_then_schedule(target, value, (ScrDyn *)then_fn);
return;
}
}
scr_promise_fulfill_ref(
target, scr_dyn_retain(value), &scr_dyn_retain_v,
&scr_dyn_release_v, NULL);
}
/* +1 when the callback result must be awaited; NULL for a plain value. */
static ScrPromise *sf_stream_callback_promise(ScrDyn *value) {
if (value->kind == SCR_DYN_PROMISE) {
return scr_promise_retain(value->v.promise);
}
if (value->kind != SCR_DYN_OBJ) return NULL;
const ScrDyn *then_fn = scr_dyn_obj_get(value, "then", 4);
if (!then_fn || then_fn->kind != SCR_DYN_FUNC) return NULL;
ScrPromise *promise = scr_promise_new();
sf_then_schedule(promise, value, (ScrDyn *)then_fn);
return promise;
}
static void sf_stream_pull_wait_entry(ScrFiber *self, void *arg) {
(void)self;
SfPullWait *wait = arg;
SfStream *s = wait->stream;
ScrDyn *value = scr_await_dyn(wait->promise);
bool rejected = scr_exc_pending();
ScrCaught *caught = rejected ? scr_exc_take() : NULL;
scr_dyn_release(value);
scr_promise_release(wait->promise);
free(wait);
s->pulling = false;
if (rejected) {
s->pull_again = false;
ScrDyn *reason = scr_caught_to_dyn(caught);
sf_stream_error(s, reason);
scr_dyn_release(reason);
} else {
bool again = s->pull_again;
s->pull_again = false;
if (again && !s->close_requested && !s->closed && !s->error) {
sf_stream_pull(s);
}
}
scr_caught_release(caught);
sf_stream_release(s);
}
static void sf_stream_start_wait_entry(ScrFiber *self, void *arg) {
(void)self;
SfStartWait *wait = arg;
SfStream *s = wait->stream;
ScrDyn *value = scr_await_dyn(wait->promise);
bool rejected = scr_exc_pending();
ScrCaught *caught = rejected ? scr_exc_take() : NULL;
scr_dyn_release(value);
scr_promise_release(wait->promise);
free(wait);
if (rejected) {
ScrDyn *reason = scr_caught_to_dyn(caught);
sf_stream_error(s, reason);
scr_dyn_release(reason);
} else {
s->started = true;
sf_stream_drain(s);
if (!s->close_requested && !s->closed && !s->error &&
s->queued == 0) {
sf_stream_pull(s);
sf_stream_drain(s);
}
}
scr_caught_release(caught);
sf_stream_release(s);
}
static ScrDyn *sf_controller_box(SfStream *s) {
return scr_dyn_new_handle(s, SCR_DYNH_WEB_CONTROLLER);
}
static bool sf_stream_has_from_source(const SfStream *s) {
return s->from_dyn || s->from_array || s->from_bytes || s->from_string;
}
static void sf_stream_pull_from_source(SfStream *s) {
ScrDyn *value = NULL;
if (s->from_array) {
if (s->from_index >= scr_arr_len(s->from_array)) {
sf_stream_close(s);
return;
}
value = s->from_array_item(s->from_array, s->from_index++);
} else if (s->from_bytes) {
if (s->from_index >= scr_bytes_len(s->from_bytes)) {
sf_stream_close(s);
return;
}
value = scr_dyn_new_num(
scr_bytes_get(s->from_bytes, s->from_index++));
} else if (s->from_string) {
double len = scr_str_utf16_len(s->from_string);
if (s->from_index >= len) {
sf_stream_close(s);
return;
}
ScrStr *cp = scr_str_cp_at(s->from_string, s->from_index);
s->from_index += scr_str_utf16_len(cp);
value = scr_dyn_new_str(cp);
scr_str_release(cp);
} else if (s->from_dyn->kind == SCR_DYN_ARR) {
if (s->from_index >= (double)s->from_dyn->v.arr.len) {
sf_stream_close(s);
return;
}
value = scr_dyn_retain(
s->from_dyn->v.arr.items[(size_t)s->from_index++]);
} else if (s->from_dyn->kind == SCR_DYN_BYTES) {
if (s->from_index >= scr_bytes_len(s->from_dyn->v.bytes)) {
sf_stream_close(s);
return;
}
value = scr_dyn_new_num(
scr_bytes_get(s->from_dyn->v.bytes, s->from_index++));
} else {
double len = scr_str_utf16_len(s->from_dyn->v.str);
if (s->from_index >= len) {
sf_stream_close(s);
return;
}
ScrStr *cp = scr_str_cp_at(s->from_dyn->v.str, s->from_index);
s->from_index += scr_str_utf16_len(cp);
value = scr_dyn_new_str(cp);
scr_str_release(cp);
}
sf_stream_enqueue_value(s, value);
scr_dyn_release(value);
}
static void sf_stream_initial_pull_task(ScrClosure *cb) {
SfStream *s = (SfStream *)scr_box_get_ref(cb->caps[0]);
if (!s) return;
s->initial_pull_pending = false;
if (s->started && !s->close_requested && !s->closed && !s->error &&
s->queued == 0) {
sf_stream_pull(s);
sf_stream_drain(s);
}
sf_stream_release(s);
}
static void sf_stream_schedule_initial_pull(SfStream *s) {
if ((!s->pull_cb && !sf_stream_has_from_source(s)) ||
s->initial_pull_pending || s->close_requested || s->closed ||
s->error) {
return;
}
s->initial_pull_pending = true;
ScrClosure *cb =
scr_closure_new((void *)&sf_stream_initial_pull_task, 1);
ScrBox *box =
scr_box_new_obj(&sf_stream_retain_v, &sf_stream_release_v, NULL);
scr_box_set_ref(box, sf_stream_retain(s));
cb->caps[0] = box;
scr_queue_microtask(cb);
}
/* Web Streams wraps an underlying source's plain pull return in a resolved
* promise. Its fulfillment step therefore runs as a promise job, never in
* the pull callback's stack. In particular, enqueueing into a pending read
* wakes that read's awaiter before a desiredSize-driven repull: the awaiter
* can cancel or release the reader first. Keep pulling=true through this
* microtask so demand raised by enqueue/read records pull_again exactly as
* it does while awaiting a genuinely asynchronous pull result. */
static void sf_stream_pull_complete_task(ScrClosure *cb) {
SfStream *s = (SfStream *)scr_box_get_ref(cb->caps[0]);
if (!s) return;
s->pulling = false;
bool again = s->pull_again;
s->pull_again = false;
if (again && !s->close_requested && !s->closed && !s->error) {
sf_stream_pull(s);
}
sf_stream_release(s);
}
static void sf_stream_schedule_pull_complete(SfStream *s) {
ScrClosure *cb =
scr_closure_new((void *)&sf_stream_pull_complete_task, 1);
ScrBox *box =
scr_box_new_obj(&sf_stream_retain_v, &sf_stream_release_v, NULL);
scr_box_set_ref(box, sf_stream_retain(s));
cb->caps[0] = box;
scr_queue_microtask(cb);
}
/* A consumer waiting for the stream's next chunk: a pending read, a body
* collector, or a fetch upload (fetch keeps reading the request body,
* including after an aborted transfer stops writing it). */
static bool sf_stream_has_consumer(const SfStream *s) {
return s->collector || (s->reader && s->reader->pending_head) || s->request_owner ||
s->request_discarding;
}
static void sf_stream_pull(SfStream *s) {
if (!s->started ||
(!s->pull_cb && !sf_stream_has_from_source(s)) ||
s->initial_pull_pending || s->close_requested || s->closed ||
s->error) {
return;
}
/* ReadableStreamDefaultControllerShouldCallPull: with no room in the
* queue, only a consumer asks for a chunk: a pending read, a body
* collector, or a fetch upload, whose reader is always waiting. */
if (s->hwm_zero && !sf_stream_has_consumer(s)) return;
if (s->pulling) {
s->pull_again = true;
return;
}
if (sf_stream_has_from_source(s)) {
s->pulling = true;
sf_stream_pull_from_source(s);
sf_stream_schedule_pull_complete(s);
return;
}
s->pulling = true;
ScrDyn *controller = sf_controller_box(s);
ScrDyn *args[1] = {controller};
ScrDyn *pull_cb = scr_dyn_retain(s->pull_cb);
scr_dyn_this_push_dyn(s->source_this);
ScrDyn *r =
scr_dyn_call(pull_cb, args, 1, "underlyingSource.pull");
scr_dyn_this_pop();
scr_dyn_release(pull_cb);
scr_dyn_release(controller);
if (!r) {
s->pulling = false;
s->pull_again = false;
ScrCaught *caught = scr_exc_take();
ScrDyn *reason = scr_caught_to_dyn(caught);
sf_stream_error(s, reason);
scr_dyn_release(reason);
scr_caught_release(caught);
return;
}
ScrPromise *callback_promise = sf_stream_callback_promise(r);
if (callback_promise) {
SfPullWait *wait = malloc(sizeof *wait);
if (!wait) sf_oom();
wait->stream = sf_stream_retain(s);
wait->promise = callback_promise;
ScrPromise *watcher =
scr_async_spawn(&sf_stream_pull_wait_entry, wait);
scr_promise_release(watcher);
scr_dyn_release(r);
return;
}
scr_dyn_release(r);
sf_stream_schedule_pull_complete(s);
}
static SfReader *sf_stream_get_reader(SfStream *s) {
if (s->reader || s->internal_lock) {
sf_type_error("Invalid state: ReadableStream is locked");
return NULL;
}
SfReader *r = calloc(1, sizeof *r);
if (!r) sf_oom();
r->rc = 1;
r->stream = s;
r->closed = scr_promise_new();
/* A reader's internal closed capability never reports as an unhandled
* rejection merely because user code does not read the property. */
scr_promise_mark_handled(r->closed);
if (s->closed) scr_promise_fulfill_void(r->closed);
else if (s->error) sf_reject_promise_reason(r->closed, s->error);
s->reader = sf_reader_retain(r);
return r;
}
/* readableStreamDefaultReaderRead: queue the request, then serve it from
* the queue or a pull. false (TypeError pending) for a released reader. */
static bool sf_reader_read_request(SfReader *r, ScrPromise *p, const ScrFetchReadSink *sink,
void *sink_ctx) {
SfStream *s = r->stream;
if (!s || s->reader != r) {
sf_type_error("This reader has been released");
return false;
}
s->disturbed = true;
SfReadRequest *request = calloc(1, sizeof *request);
if (!request) sf_oom();
request->promise = p ? scr_promise_retain(p) : NULL;
request->sink = sink;
request->sink_ctx = sink_ctx;
if (r->pending_tail) r->pending_tail->next = request;
else r->pending_head = request;
r->pending_tail = request;
sf_stream_drain(s);
if (r->pending_head && !s->head && !s->closed && !s->error) {
sf_stream_pull(s);
sf_stream_drain(s);
}
return true;
}
static ScrPromise *sf_reader_read(SfReader *r) {
ScrPromise *p = scr_promise_new();
if (!sf_reader_read_request(r, p, NULL, NULL)) scr_promise_reject_pending(p);
return p;
}
static void sf_reader_release_lock(SfReader *r) {
SfStream *s = r->stream;
if (!s || s->reader != r) return;
ScrPromise *old_closed = r->closed;
if (!s->closed && !s->error) {
sf_type_error("Invalid state: Reader released");
scr_promise_reject_pending(old_closed);
} else {
r->closed = scr_promise_new();
scr_promise_mark_handled(r->closed);
sf_type_error("Invalid state: Reader released");
scr_promise_reject_pending(r->closed);
scr_promise_release(old_closed);
}
sf_reader_reject_message_all(r, "Invalid state: Releasing reader");
size_t handle_refs = r->handle_refs;
s->reader = NULL;
r->stream = NULL;
sf_reader_release(r); /* stream's ownership */
while (handle_refs-- > 0) sf_stream_release(s);
}
static void sf_stream_cancel_wait_entry(ScrFiber *self, void *arg) {
(void)self;
SfCancelWait *wait = arg;
ScrDyn *value = scr_await_dyn(wait->source);
bool rejected = scr_exc_pending();
ScrCaught *caught = rejected ? scr_exc_take() : NULL;
scr_dyn_release(value);
if (rejected) {
ScrDyn *reason = scr_caught_to_dyn(caught);
sf_reject_promise_reason(wait->result, reason);
scr_dyn_release(reason);
} else {
scr_promise_fulfill_void(wait->result);
}
scr_caught_release(caught);
scr_promise_release(wait->source);
scr_promise_release(wait->result);
free(wait);
}
static ScrPromise *sf_stream_cancel(SfStream *s, ScrDyn *reason,
bool through_reader) {
ScrPromise *p = scr_promise_new();
if (!through_reader && (s->reader || s->internal_lock)) {
sf_type_error("Invalid state: cannot cancel a locked ReadableStream");
scr_promise_reject_pending(p);
return p;
}
/*
* ReadableStreamCancel disturbs the stream before inspecting its state.
* This remains observable through Response.bodyUsed even when the
* stream had already closed or errored.
*/
s->disturbed = true;
if (s->closed) {
/* WHATWG cancel is a no-op once the stream is closed. */
scr_promise_fulfill_void(p);
return p;
}
if (s->error) {
/* An errored stream preserves and rejects with its stored reason. */
sf_reject_promise_reason(p, s->error);
return p;
}
ScrDyn *cancel_cb =
s->cancel_cb ? scr_dyn_retain(s->cancel_cb) : NULL;
ScrDyn *cancel_this =
cancel_cb ? scr_dyn_retain(s->source_this) : NULL;
sf_stream_drop_chunks(s);
SfTransfer *response_owner = s->response_owner;
if (response_owner && response_owner->client) {
scr_http_client_destroy(response_owner->client);
}
if (s->close_requested) sf_stream_finish_close(s);
else sf_stream_close(s);
if (response_owner && !response_owner->done) sf_settle(response_owner);
if (cancel_cb) {
ScrDyn *args[1] = {reason ? reason : scr_dyn_undefined()};
scr_dyn_this_push_dyn(cancel_this);
ScrDyn *r =
scr_dyn_call(cancel_cb, args, 1, "underlyingSource.cancel");
scr_dyn_this_pop();
scr_dyn_release(cancel_cb);
scr_dyn_release(cancel_this);
if (!r) {
scr_promise_reject_pending(p);
return p;
}
ScrPromise *callback_promise = sf_stream_callback_promise(r);
if (callback_promise) {
SfCancelWait *wait = malloc(sizeof *wait);
if (!wait) sf_oom();
wait->source = callback_promise;
wait->result = scr_promise_retain(p);
ScrPromise *watcher =
scr_async_spawn(&sf_stream_cancel_wait_entry, wait);
scr_promise_release(watcher);
scr_dyn_release(r);
return p;
}
scr_dyn_release(r);
} else {
scr_dyn_release(cancel_cb);
}
scr_promise_fulfill_void(p);
return p;
}
static ScrPromise *sf_stream_collect(SfStream *s, int mode) {
ScrPromise *p = scr_promise_new();
if (s->disturbed || s->reader || s->internal_lock) {
sf_type_error("Body is unusable: Body has already been read");
scr_promise_reject_pending(p);
return p;
}
s->disturbed = true;
s->internal_lock = true;
SfCollector *c = calloc(1, sizeof *c);
if (!c) sf_oom();
c->promise = scr_promise_retain(p);
c->held_stream = sf_stream_retain(s);
c->mode = mode;
s->collector = c;
sf_stream_drain(s);
if (s->hwm_zero && !s->head) sf_stream_pull(s);
return p;
}
/* ── ReadableStreamDefaultTee ────────────────────────────────────── */
/*
* The streams spec's ReadableStreamDefaultTee over a native stream: the
* tee locks the source with a reader and feeds two native branches whose
* pull and cancel algorithms are native functions. A pull on either branch
* reads one chunk from the source and enqueues it into every branch that
* was not canceled; canceling both branches cancels the source with both
* reasons. Response.clone and Request.clone use it (undici's cloneBody).
*
* Ownership: the tee owns the source, its reader and both branches; each
* branch's callbacks and every pending read own the tee. Closing, erroring
* or canceling a branch drops its callbacks (sf_stream_drop_source_callbacks),
* and fetch teardown drops the callbacks of branches nobody finished.
*/
typedef struct SfTee {
size_t rc;
SfStream *source;
SfReader *reader;
SfStream *branch[2];
bool reading;
bool read_again;
bool canceled[2];
ScrDyn *reason[2];
ScrPromise *cancel_promise;
} SfTee;
static SfTee *sf_tee_retain(SfTee *t) {
t->rc++;
return t;
}
static void sf_tee_release(SfTee *t) {
if (!t || --t->rc > 0) return;
if (t->source->error_watch_ctx == t) {
t->source->error_watch = NULL;
t->source->error_watch_ctx = NULL;
}
sf_stream_release(t->branch[0]);
sf_stream_release(t->branch[1]);
sf_reader_release(t->reader);
sf_stream_release(t->source);
scr_dyn_release(t->reason[0]);
scr_dyn_release(t->reason[1]);
scr_promise_release(t->cancel_promise);
free(t);
}
static void *sf_tee_retain_v(void *p) { return sf_tee_retain((SfTee *)p); }
static void sf_tee_release_v(void *p) { sf_tee_release((SfTee *)p); }
static ScrBox *sf_tee_box(SfTee *t) {
ScrBox *box = scr_box_new_obj(&sf_tee_retain_v, &sf_tee_release_v, NULL);
scr_box_set_ref(box, sf_tee_retain(t));
return box;
}
static void sf_tee_pull(SfTee *t);
/* The read request's chunk steps run in a microtask (spec: "queue a
* microtask"), after the read that produced the chunk settles. */
static void sf_tee_chunk_task(ScrClosure *cb) {
SfTee *t = (SfTee *)scr_box_get_ref(cb->caps[0]);
ScrDyn *chunk = (ScrDyn *)scr_box_get_ref(cb->caps[1]);
t->read_again = false;
for (int i = 0; i < 2; i++) {
if (t->canceled[i]) continue;
SfStream *b = t->branch[i];
if (b->close_requested || b->closed || b->error) continue;
sf_stream_enqueue_value(b, chunk);
if (scr_exc_pending()) scr_exc_clear();
}
t->reading = false;
if (t->read_again) sf_tee_pull(t);
scr_dyn_release(chunk);
sf_tee_release(t);
}
static void sf_tee_sink_chunk(void *ctx, ScrDyn *value) {
SfTee *t = ctx;
ScrClosure *cb = scr_closure_new((void *)&sf_tee_chunk_task, 2);
cb->caps[0] = sf_tee_box(t);
cb->caps[1] = scr_box_new_obj(&scr_dyn_retain_v, &scr_dyn_release_v, NULL);
scr_box_set_ref(cb->caps[1], scr_dyn_retain(value));
scr_queue_microtask(cb);
sf_tee_release(t); /* the read request's reference */
}
static void sf_tee_sink_close(void *ctx) {
SfTee *t = ctx;
t->reading = false;
for (int i = 0; i < 2; i++) {
if (!t->canceled[i]) sf_stream_close(t->branch[i]);
}
if (!t->canceled[0] || !t->canceled[1]) {
scr_promise_fulfill_void(t->cancel_promise);
}
sf_tee_release(t);
}
/* The source reader's closed promise rejected (spec: upon its rejection
* with r, error both branches, and resolve cancelPromise unless both
* branches were canceled), as a promise reaction: in a microtask. */
static void sf_tee_source_errored_task(ScrClosure *cb) {
SfTee *t = (SfTee *)scr_box_get_ref(cb->caps[0]);
ScrDyn *reason = (ScrDyn *)scr_box_get_ref(cb->caps[1]);
sf_stream_error(t->branch[0], reason);
sf_stream_error(t->branch[1], reason);
if (!t->canceled[0] || !t->canceled[1]) scr_promise_fulfill_void(t->cancel_promise);
scr_dyn_release(reason);
sf_tee_release(t);
}
/* The source errored, with or without a read of it pending. */
static void sf_tee_source_errored(void *ctx, ScrDyn *reason) {
SfTee *t = ctx;
t->source->error_watch = NULL;
t->source->error_watch_ctx = NULL;
ScrClosure *cb = scr_closure_new((void *)&sf_tee_source_errored_task, 2);
cb->caps[0] = sf_tee_box(t);
cb->caps[1] = scr_box_new_obj(&scr_dyn_retain_v, &scr_dyn_release_v, NULL);
scr_box_set_ref(cb->caps[1], scr_dyn_retain(reason));
scr_queue_microtask(cb);
}
/* A read request's error steps only end the read: the source's error
* reaches the branches through sf_tee_source_errored. */
static void sf_tee_sink_error(void *ctx, ScrDyn *reason) {
(void)reason;
SfTee *t = ctx;
t->reading = false;
sf_tee_release(t);
}
static const ScrFetchReadSink sf_tee_sink = {
&sf_tee_sink_chunk, &sf_tee_sink_close, &sf_tee_sink_error};
static void sf_tee_pull(SfTee *t) {
if (t->reading) {
t->read_again = true;
return;
}
t->reading = true;
sf_tee_retain(t); /* owned by the read request */
if (!sf_reader_read_request(t->reader, NULL, &sf_tee_sink, t)) {
scr_exc_clear();
t->reading = false;
sf_tee_release(t);
}
}
static ScrDyn *sf_tee_pull_thunk(ScrClosure *cb, ScrDyn *const *args,
size_t argc) {
(void)args;
(void)argc;
SfTee *t = (SfTee *)scr_box_get_ref(cb->caps[0]);
sf_tee_pull(t);
sf_tee_release(t);
return scr_dyn_retain(scr_dyn_undefined());
}
static ScrDyn *sf_tee_cancel(SfTee *t, int i, ScrDyn *reason) {
t->canceled[i] = true;
scr_dyn_release(t->reason[i]);
t->reason[i] = scr_dyn_retain(reason);
if (t->canceled[0] && t->canceled[1]) {
ScrDyn *composite = scr_dyn_new_arr();
scr_dyn_arr_push(composite, scr_dyn_retain(t->reason[0]));
scr_dyn_arr_push(composite, scr_dyn_retain(t->reason[1]));
ScrPromise *result = sf_stream_cancel(t->source, composite, true);
scr_dyn_release(composite);
ScrDyn *boxed = scr_dyn_new_promise(result);
scr_promise_release(result);
sf_then_adopt(t->cancel_promise, boxed);
scr_dyn_release(boxed);
}
return scr_dyn_new_promise(t->cancel_promise);
}
static ScrDyn *sf_tee_cancel_thunk_0(ScrClosure *cb, ScrDyn *const *args,
size_t argc) {
SfTee *t = (SfTee *)scr_box_get_ref(cb->caps[0]);
ScrDyn *out = sf_tee_cancel(t, 0, argc ? args[0] : scr_dyn_undefined());
sf_tee_release(t);
return out;
}
static ScrDyn *sf_tee_cancel_thunk_1(ScrClosure *cb, ScrDyn *const *args,
size_t argc) {
SfTee *t = (SfTee *)scr_box_get_ref(cb->caps[0]);
ScrDyn *out = sf_tee_cancel(t, 1, argc ? args[0] : scr_dyn_undefined());
sf_tee_release(t);
return out;
}
static ScrDyn *sf_tee_func(SfTee *t, ScrDynThunk thunk, const char *name) {
ScrClosure *cb = scr_closure_new((void *)thunk, 1);
cb->caps[0] = sf_tee_box(t);
return scr_dyn_new_func(cb, thunk, 1, "(controller)", name);
}
/* Tees an unlocked, undisturbed stream: +1 branches in out[0] and out[1].
* The source stays locked by the tee. */
static void sf_stream_tee(SfStream *s, SfStream *out[2]) {
SfTee *t = calloc(1, sizeof *t);
if (!t) sf_oom();
t->rc = 1;
t->source = sf_stream_retain(s);
t->reader = sf_stream_get_reader(s);
s->error_watch = &sf_tee_source_errored;
s->error_watch_ctx = t;
t->cancel_promise = scr_promise_new();
scr_promise_mark_handled(t->cancel_promise);
for (int i = 0; i < 2; i++) {
SfStream *b = sf_stream_new_native();
b->pull_cb = sf_tee_func(t, &sf_tee_pull_thunk, "pull");
b->cancel_cb = sf_tee_func(
t, i == 0 ? &sf_tee_cancel_thunk_0 : &sf_tee_cancel_thunk_1, "cancel");
b->source_this = scr_dyn_retain(scr_dyn_undefined());
sf_stream_track_callbacks(b);
t->branch[i] = sf_stream_retain(b);
out[i] = b;
}
/* An errored source's closed promise has already rejected. */
if (s->error) sf_tee_source_errored(t, s->error);
/* The branches start at once; a branch with room then pulls. */
sf_stream_schedule_initial_pull(out[0]);
sf_stream_schedule_initial_pull(out[1]);
sf_tee_release(t);
}
ScrDyn *scr_fetch_stream_new(ScrDyn *source) {
ScrDyn *source_view = source;
ScrDyn *materialized = NULL;
if (source && source->kind == SCR_DYN_TYPED_REF) {
materialized = scr_dyn_typed_ref_materialize(source);
source_view = materialized;
}
if (source_view && source_view->kind != SCR_DYN_UNDEF &&
source_view->kind != SCR_DYN_OBJ) {
scr_dyn_release(materialized);
sf_type_error("ReadableStream source must be an object");
return NULL;
}
SfStream *s = sf_stream_new_native();
if (source_view && source_view->kind == SCR_DYN_OBJ) {
const ScrDyn *type = scr_dyn_obj_get(source_view, "type", 4);
if (type && type->kind != SCR_DYN_UNDEF) {
sf_stream_release(s);
scr_dyn_release(materialized);
sf_type_error("byte streams are not supported by static ReadableStream");
return NULL;
}
const ScrDyn *pull = scr_dyn_obj_get(source_view, "pull", 4);
const ScrDyn *cancel = scr_dyn_obj_get(source_view, "cancel", 6);
const ScrDyn *start = scr_dyn_obj_get(source_view, "start", 5);
if (pull && pull->kind != SCR_DYN_UNDEF) {
if (pull->kind != SCR_DYN_FUNC) {
sf_stream_release(s);
scr_dyn_release(materialized);
sf_type_error("ReadableStream source.pull must be a function");
return NULL;
}
s->pull_cb = scr_dyn_retain((ScrDyn *)pull);
}
if (cancel && cancel->kind != SCR_DYN_UNDEF) {
if (cancel->kind != SCR_DYN_FUNC) {
sf_stream_release(s);
scr_dyn_release(materialized);
sf_type_error("ReadableStream source.cancel must be a function");
return NULL;
}
s->cancel_cb = scr_dyn_retain((ScrDyn *)cancel);
}
if (s->pull_cb || s->cancel_cb) {
s->source_this = scr_dyn_retain(source);
}
sf_stream_track_callbacks(s);
if (start && start->kind != SCR_DYN_UNDEF) {
if (start->kind != SCR_DYN_FUNC) {
sf_stream_release(s);
scr_dyn_release(materialized);
sf_type_error("ReadableStream source.start must be a function");
return NULL;
}
s->started = false;
/* `this` may be a live typed capsule. A property write from start()
* refreshes its cached object and releases the old field snapshot,
* so retain the borrowed callback across that refresh. */
ScrDyn *start_cb = scr_dyn_retain((ScrDyn *)start);
ScrDyn *controller = sf_controller_box(s);
ScrDyn *args[1] = {controller};
scr_dyn_this_push_dyn(source);
ScrDyn *r =
scr_dyn_call(start_cb, args, 1, "underlyingSource.start");
scr_dyn_this_pop();
scr_dyn_release(start_cb);
scr_dyn_release(controller);
if (!r) {
sf_stream_release(s);
scr_dyn_release(materialized);
return NULL;
}
ScrPromise *callback_promise = sf_stream_callback_promise(r);
if (callback_promise) {
SfStartWait *wait = malloc(sizeof *wait);
if (!wait) sf_oom();
wait->stream = sf_stream_retain(s);
wait->promise = callback_promise;
ScrPromise *watcher =
scr_async_spawn(&sf_stream_start_wait_entry, wait);
scr_promise_release(watcher);
} else {
s->started = true;
}
scr_dyn_release(r);
}
}
if (s->started) sf_stream_schedule_initial_pull(s);
ScrDyn *out = scr_dyn_new_handle(s, SCR_DYNH_WEB_STREAM);
sf_stream_release(s);
scr_dyn_release(materialized);
return out;
}
ScrDyn *scr_fetch_stream_from(ScrDyn *iterable) {
if (!iterable) {
sf_type_error("ReadableStream.from requires an iterable");
return NULL;
}
if (iterable->kind == SCR_DYN_UNDEF) {
sf_type_error(
"Cannot read properties of undefined (reading 'Symbol(Symbol.asyncIterator)')");
return NULL;
}
if (iterable->kind != SCR_DYN_ARR && iterable->kind != SCR_DYN_BYTES && iterable->kind != SCR_DYN_STR) {
/* Reuse the checked-dynamic iterable error wording. */
ScrDyn *packed = scr_dyn_iter_pack(iterable, NULL);
scr_dyn_release(packed);
return NULL;
}
ScrDyn *source = iterable;
SfStream *s = sf_stream_new_native();
s->from_dyn = scr_dyn_retain(source);
sf_stream_schedule_initial_pull(s);
ScrDyn *out = scr_dyn_new_handle(s, SCR_DYNH_WEB_STREAM);
sf_stream_release(s);
if (source != iterable) scr_dyn_release(source);
return out;
}
ScrDyn *scr_fetch_stream_from_array(
ScrArr *iterable, ScrDyn *(*item)(ScrArr *, double)) {
SfStream *s = sf_stream_new_native();
s->from_array = scr_arr_retain(iterable);
s->from_array_item = item;
sf_stream_schedule_initial_pull(s);
ScrDyn *out = scr_dyn_new_handle(s, SCR_DYNH_WEB_STREAM);
sf_stream_release(s);
return out;
}
ScrDyn *scr_fetch_stream_from_bytes(ScrBytes *iterable) {
SfStream *s = sf_stream_new_native();
s->from_bytes = scr_bytes_retain(iterable);
sf_stream_schedule_initial_pull(s);
ScrDyn *out = scr_dyn_new_handle(s, SCR_DYNH_WEB_STREAM);
sf_stream_release(s);
return out;
}
ScrDyn *scr_fetch_stream_from_string(ScrStr *iterable) {
SfStream *s = sf_stream_new_native();
s->from_string = scr_str_retain(iterable);
sf_stream_schedule_initial_pull(s);
ScrDyn *out = scr_dyn_new_handle(s, SCR_DYNH_WEB_STREAM);
sf_stream_release(s);
return out;
}
bool scr_fetch_reader_read_into(ScrDyn *reader, const ScrFetchReadSink *sink, void *ctx) {
if (!reader || reader->kind != SCR_DYN_HANDLE ||
reader->v.handle.tag != SCR_DYNH_WEB_READER) {
sf_type_error("Expected a ReadableStreamDefaultReader");
return false;
}
return sf_reader_read_request((SfReader *)reader->v.handle.ptr, NULL, sink, ctx);
}
ScrPromise *scr_fetch_reader_read(ScrDyn *reader) {
if (!reader || reader->kind != SCR_DYN_HANDLE ||
reader->v.handle.tag != SCR_DYNH_WEB_READER) {
sf_type_error("Expected a ReadableStreamDefaultReader");
ScrPromise *p = scr_promise_new();
scr_promise_reject_pending(p);
return p;
}
return sf_reader_read((SfReader *)reader->v.handle.ptr);
}
static SfStream *sf_stream_of(const ScrDyn *d) {
if (!d || d->kind != SCR_DYN_HANDLE ||
d->v.handle.tag != SCR_DYNH_WEB_STREAM) {
sf_type_error("Expected a ReadableStream");
return NULL;
}
return (SfStream *)d->v.handle.ptr;
}
static ScrDyn *sf_stream_invoke(void *ptr, ScrDyn *self, const char *method,
ScrDyn *const *args, size_t argc,
const char *what) {
SfStream *s = ptr;
if (strcmp(method, "getReader") == 0) {
ScrDyn *options = argc ? args[0] : NULL;
ScrDyn *materialized = NULL;
ScrDyn *mode = NULL;
if (options && options->kind == SCR_DYN_TYPED_REF) {
materialized = scr_dyn_typed_ref_materialize(options);
options = materialized;
}
if (options && options->kind == SCR_DYN_OBJ) {
mode = scr_dyn_obj_get(options, "mode", 4);
}
bool byob = mode && mode->kind != SCR_DYN_UNDEF;
scr_dyn_release(materialized);
if (scr_exc_pending()) return NULL;
if (byob) {
sf_type_error("BYOB readers are not supported");
return NULL;
}
SfReader *r = sf_stream_get_reader(s);
if (!r) return NULL;
ScrDyn *out = scr_dyn_new_handle(r, SCR_DYNH_WEB_READER);
sf_reader_release(r);
return out;
}
if (strcmp(method, "cancel") == 0) {
ScrPromise *p =
sf_stream_cancel(s, argc ? args[0] : scr_dyn_undefined(), false);
ScrDyn *out = scr_dyn_new_promise(p);
scr_promise_release(p);
return out;
}
(void)self;
sf_not_function(what);
return NULL;
}
static ScrDyn *sf_stream_get(void *ptr, const char *key, size_t len) {
SfStream *s = ptr;
if (sf_name(key, len, "locked")) {
return scr_dyn_new_bool(s->reader != NULL || s->internal_lock);
}
return NULL;
}
static ScrDyn *sf_reader_invoke(void *ptr, ScrDyn *self, const char *method,
ScrDyn *const *args, size_t argc,
const char *what) {
SfReader *r = ptr;
if (strcmp(method, "read") == 0) {
ScrPromise *p = sf_reader_read(r);
ScrDyn *out = scr_dyn_new_promise(p);
scr_promise_release(p);
return out;
}
if (strcmp(method, "cancel") == 0) {
ScrPromise *p;
if (!r->stream) {
sf_type_error("This reader has been released");
p = scr_promise_new();
scr_promise_reject_pending(p);
} else {
p = sf_stream_cancel(
r->stream, argc ? args[0] : scr_dyn_undefined(), true);
}
ScrDyn *out = scr_dyn_new_promise(p);
scr_promise_release(p);
return out;
}
if (strcmp(method, "releaseLock") == 0) {
sf_reader_release_lock(r);
if (scr_exc_pending()) return NULL;
return scr_dyn_retain(scr_dyn_undefined());
}
(void)self;
sf_not_function(what);
return NULL;
}
static ScrDyn *sf_reader_get(void *ptr, const char *key, size_t len) {
SfReader *r = ptr;
if (sf_name(key, len, "closed")) return scr_dyn_new_promise(r->closed);
return NULL;
}
static ScrDyn *sf_controller_invoke(void *ptr, ScrDyn *self,
const char *method,
ScrDyn *const *args, size_t argc,
const char *what) {
SfStream *s = ptr;
if (strcmp(method, "enqueue") == 0) {
sf_stream_enqueue_value(
s, argc ? args[0] : scr_dyn_undefined());
if (scr_exc_pending()) return NULL;
return scr_dyn_retain(scr_dyn_undefined());
}
if (strcmp(method, "close") == 0) {
if (s->close_requested || s->closed || s->error) {
sf_type_error("Invalid state: Controller is already closed");
return NULL;
}
sf_stream_close(s);
return scr_dyn_retain(scr_dyn_undefined());
}
if (strcmp(method, "error") == 0) {
sf_stream_error(s, argc ? args[0] : scr_dyn_undefined());
return scr_dyn_retain(scr_dyn_undefined());
}
(void)self;
sf_not_function(what);
return NULL;
}
static ScrDyn *sf_controller_get(void *ptr, const char *key, size_t len) {
SfStream *s = ptr;
if (sf_name(key, len, "desiredSize")) {
if (s->error) return scr_dyn_new_null();
if (s->closed) return scr_dyn_new_num(0);
return scr_dyn_new_num(1.0 - (double)s->queued);
}
return NULL;
}
/* ── Headers ─────────────────────────────────────────────────────── */
static bool sf_header_name_ok(const char *s, size_t len);
/* A borrowed element of a runtime-built header pair array. These arrays are
* only ever appended to or rewritten whole, so they are dense. */
static inline ScrStr *sf_pair_at(ScrArr *pairs, size_t i) {
if (i < pairs->filled) return (ScrStr *)(uintptr_t)scr_arr_dense_get(pairs, i);
return (ScrStr *)scr_arr_peek_ref(pairs, (double)i);
}
static ScrStr *sf_header_name_lower(const char *data, size_t len);
/* `name` in lowercase, +1: the same string when it has no uppercase byte. */
static ScrStr *sf_header_name_lower_str(ScrStr *name) {
for (size_t i = 0; i < name->len; i++) {
char c = name->data[i];
if (c >= 'A' && c <= 'Z') return sf_header_name_lower(name->data, name->len);
}
return scr_str_retain(name);
}
static ScrStr *sf_header_name_lower(const char *data, size_t len) {
char *lower = malloc(len ? len : 1);
if (!lower) sf_oom();
for (size_t i = 0; i < len; i++) {
char c = data[i];
lower[i] = c >= 'A' && c <= 'Z' ? (char)(c - 'A' + 'a') : c;
}
ScrStr *out = scr_str_new(lower, len);
free(lower);
return out;
}
/* HTTP field values are ByteStrings: each wire octet becomes the same
* U+00XX code point in the JS string. ScrStr stores UTF-8, so obs-text
* bytes need a real Latin-1→UTF-8 expansion before they enter a Headers
* handle (and before string methods assume the storage invariant). */
static ScrStr *sf_latin1_to_utf8(const ScrStr *raw) {
size_t extra = 0;
for (size_t i = 0; i < raw->len; i++) {
if ((unsigned char)raw->data[i] >= 0x80) extra++;
}
if (extra == 0) return scr_str_retain((ScrStr *)raw);
char *utf8 = malloc(raw->len + extra);
if (!utf8) sf_oom();
size_t at = 0;
for (size_t i = 0; i < raw->len; i++) {
unsigned char byte = (unsigned char)raw->data[i];
if (byte < 0x80) {
utf8[at++] = (char)byte;
} else {
utf8[at++] = (char)(0xc0 | (byte >> 6));
utf8[at++] = (char)(0x80 | (byte & 0x3f));
}
}
ScrStr *out = scr_str_new(utf8, at);
free(utf8);
return out;
}
/*
* Redirect Location values are byte sequences until Fetch's header-value
* extraction decodes them as UTF-8. Invalid wire sequences become U+FFFD;
* treating the bytes as an already-valid ScrStr instead percent-encodes
* each invalid octet and can follow a different resource.
*/
static ScrStr *sf_location_to_utf8(const ScrStr *raw) {
ScrBytes *bytes = scr_bytes_new(SCR_BYTES_U8, (double)raw->len);
if (raw->len > 0) memcpy(bytes->data, raw->data, raw->len);
ScrStr *encoding = scr_str_new("utf8", 4);
ScrStr *out = scr_bytes_to_str(bytes, encoding);
scr_str_release(encoding);
scr_bytes_release(bytes);
return out;
}
static SfHeaders *sf_headers_new(ScrArr *pairs, bool immutable) {
SfHeaders *h = calloc(1, sizeof *h);
if (!h) sf_oom();
h->rc = 1;
h->pairs = pairs;
h->immutable = immutable;
return h;
}
static SfHeaders *sf_headers_new_response(ScrArr *raw_pairs) {
size_t n = (size_t)scr_arr_len(raw_pairs);
ScrArr *pairs = scr_arr_new(SCR_ELEM_STR, n);
for (size_t i = 0; i + 1 < n; i += 2) {
scr_arr_push_ref(pairs, sf_header_name_lower_str(sf_pair_at(raw_pairs, i)));
scr_arr_push_ref(pairs, sf_latin1_to_utf8(sf_pair_at(raw_pairs, i + 1)));
}
scr_arr_release(raw_pairs);
return sf_headers_new(pairs, true);
}
static SfHeaders *sf_headers_retain(SfHeaders *h) {
h->rc++;
return h;
}
static void sf_headers_release(SfHeaders *h) {
if (!h || --h->rc > 0) return;
scr_arr_release(h->pairs);
if (h->sorted) scr_arr_release(h->sorted);
free(h);
}
/* Every change to h->pairs goes through here first. */
static void sf_headers_changed(SfHeaders *h) {
if (!h->sorted) return;
scr_arr_release(h->sorted);
h->sorted = NULL;
}
static void *sf_headers_retain_v(void *p) {
return sf_headers_retain((SfHeaders *)p);
}
static void sf_headers_release_v(void *p) {
sf_headers_release((SfHeaders *)p);
}
static bool sf_header_value_ok(const ScrStr *value);
static ScrStr *sf_header_value_bytestring(ScrStr *value);
static ScrStr *sf_trim_header_value(const ScrStr *value);
static void sf_headers_invalid(
const char *method, const ScrStr *value, const char *kind) {
size_t cap = strlen(method) + value->len + strlen(kind) + 48;
char *message = malloc(cap);
if (!message) sf_oom();
int head = snprintf(message, cap, "Headers.%s: \"", method);
if (head < 0 || (size_t)head >= cap) sf_oom();
size_t at = (size_t)head;
memcpy(message + at, value->data, value->len);
at += value->len;
int tail = snprintf(
message + at, cap - at,
"\" is an invalid header %s.", kind);
if (tail < 0 || (size_t)tail >= cap - at) sf_oom();
scr_throw_error_msg(SCR_ERR_TYPE, message, at + (size_t)tail);
free(message);
}
static ScrStr *sf_headers_name(const ScrDyn *value, const char *method) {
ScrStr *raw =
scr_dyn_string_coerce_js(value ? value : scr_dyn_undefined());
if (!raw) return NULL;
if (!sf_header_name_ok(raw->data, raw->len)) {
sf_headers_invalid(method, raw, "name");
scr_str_release(raw);
return NULL;
}
ScrStr *out = sf_header_name_lower_str(raw);
scr_str_release(raw);
return out;
}
static bool sf_headers_key_eq(const ScrStr *left, const ScrStr *right) {
return left->len == right->len &&
memcmp(left->data, right->data, left->len) == 0;
}
static ScrStr *sf_headers_get_value(SfHeaders *h, const ScrStr *name) {
ScrArr *pairs = h->pairs;
size_t n = (size_t)scr_arr_len(pairs);
size_t count = 0;
size_t total = 0;
ScrStr *first = NULL;
for (size_t i = 0; i + 1 < n; i += 2) {
if (!sf_headers_key_eq(sf_pair_at(pairs, i), name)) continue;
ScrStr *value = sf_pair_at(pairs, i + 1);
if (count > 0) total += 2;
else first = value;
total += value->len;
count++;
}
if (count == 0) return NULL;
if (count == 1) return scr_str_retain(first);
char *joined = malloc(total > 0 ? total : 1);
if (!joined) sf_oom();
size_t at = 0;
size_t emitted = 0;
for (size_t i = 0; i + 1 < n; i += 2) {
if (!sf_headers_key_eq(sf_pair_at(pairs, i), name)) continue;
ScrStr *value = sf_pair_at(pairs, i + 1);
if (emitted > 0) {
joined[at++] = ',';
joined[at++] = ' ';
}
memcpy(joined + at, value->data, value->len);
at += value->len;
emitted++;
}
ScrStr *out = scr_str_new(joined, total);
free(joined);
return out;
}
static void sf_headers_delete_name(SfHeaders *h, const ScrStr *name) {
ScrArr *old = h->pairs;
size_t n = (size_t)scr_arr_len(old);
bool present = false;
for (size_t i = 0; i + 1 < n && !present; i += 2)
present = sf_headers_key_eq(sf_pair_at(old, i), name);
if (!present) return;
sf_headers_changed(h);
ScrArr *next = scr_arr_new(SCR_ELEM_STR, n);
for (size_t i = 0; i + 1 < n; i += 2) {
ScrStr *key = sf_pair_at(old, i);
if (sf_headers_key_eq(key, name)) continue;
scr_arr_push_ref(next, scr_str_retain(key));
scr_arr_push_ref(next, scr_str_retain(sf_pair_at(old, i + 1)));
}
h->pairs = next;
scr_arr_release(old);
}
static bool sf_headers_convert_pair(
const ScrDyn *name_value, const ScrDyn *value_value,
const char *method, ScrStr **name_out, ScrStr **value_out) {
ScrStr *name = sf_headers_name(name_value, method);
if (!name) return false;
ScrStr *raw = scr_dyn_string_coerce_js(
value_value ? value_value : scr_dyn_undefined());
ScrStr *bytes = raw ? sf_header_value_bytestring(raw) : NULL;
scr_str_release(raw);
if (!bytes) {
scr_str_release(name);
return false;
}
if (!sf_header_value_ok(bytes)) {
ScrStr *display = sf_latin1_to_utf8(bytes);
sf_headers_invalid(method, display, "value");
scr_str_release(display);
scr_str_release(bytes);
scr_str_release(name);
return false;
}
ScrStr *trimmed = sf_trim_header_value(bytes);
scr_str_release(bytes);
ScrStr *normalized = sf_latin1_to_utf8(trimmed);
scr_str_release(trimmed);
*name_out = name;
*value_out = normalized;
return true;
}
/* Consumes a normalized lowercase name and UTF-8 value. Headers stores one
* joined value for ordinary fields, separate values for Set-Cookie, and the
* Fetch-specific semicolon separator for Cookie. */
static void sf_headers_append_pair(
SfHeaders *h, ScrStr *name, ScrStr *value) {
sf_headers_changed(h);
if (!(name->len == 10 && memcmp(name->data, "set-cookie", 10) == 0)) {
size_t n = (size_t)scr_arr_len(h->pairs);
for (size_t i = 0; i + 1 < n; i += 2) {
if (!sf_headers_key_eq(sf_pair_at(h->pairs, i), name)) continue;
ScrStr *previous = sf_pair_at(h->pairs, i + 1);
const char *separator =
name->len == 6 && memcmp(name->data, "cookie", 6) == 0
? "; "
: ", ";
size_t joined_len = previous->len + 2 + value->len;
char *joined_data = malloc(joined_len ? joined_len : 1);
if (!joined_data) sf_oom();
memcpy(joined_data, previous->data, previous->len);
memcpy(joined_data + previous->len, separator, 2);
memcpy(joined_data + previous->len + 2, value->data, value->len);
ScrStr *joined = scr_str_new(joined_data, joined_len);
free(joined_data);
/* Appending an empty value leaves the separator's space at the end of
* the combined value, which a copy trims again: the pairs are no
* longer in their canonical, trimmed form. */
if (value->len == 0) h->canonical = false;
scr_str_release(name);
scr_str_release(value);
scr_arr_set_ref(h->pairs, (double)(i + 1), joined);
return;
}
}
scr_arr_push_ref(h->pairs, name);
scr_arr_push_ref(h->pairs, value);
}
static int sf_headers_name_cmp(const void *left, const void *right) {
const ScrStr *a = *(ScrStr *const *)left;
const ScrStr *b = *(ScrStr *const *)right;
size_t shared = a->len < b->len ? a->len : b->len;
int cmp = memcmp(a->data, b->data, shared);
if (cmp != 0) return cmp;
return a->len < b->len ? -1 : a->len > b->len ? 1 : 0;
}
static ScrStr **sf_headers_sorted_names(SfHeaders *h, size_t *count_out) {
size_t n = (size_t)scr_arr_len(h->pairs);
size_t capacity = n / 2;
ScrStr **names = capacity ? malloc(capacity * sizeof *names) : NULL;
if (capacity && !names) sf_oom();
size_t count = 0;
for (size_t i = 0; i + 1 < n; i += 2) {
ScrStr *key = sf_pair_at(h->pairs, i);
bool seen = false;
for (size_t j = 0; j < count; j++) {
if (sf_headers_key_eq(names[j], key)) {
seen = true;
break;
}
}
if (!seen) names[count++] = key;
}
if (count > 1) {
qsort(names, count, sizeof *names, &sf_headers_name_cmp);
}
*count_out = count;
return names;
}
static bool sf_headers_is_set_cookie(const ScrStr *name) {
return name->len == 10 && memcmp(name->data, "set-cookie", 10) == 0;
}
/* Fetch's "sort and combine" list as flat name/value pairs: names in byte
* order, each with its combined value, except that every set-cookie value
* is its own entry (in insertion order), as Node's Headers iterator yields
* them. Cached until the next mutation; borrowed. */
static ScrArr *sf_headers_sorted(SfHeaders *h) {
if (h->sorted) return h->sorted;
size_t count = 0;
ScrStr **names = sf_headers_sorted_names(h, &count); /* borrowed names */
size_t n = (size_t)scr_arr_len(h->pairs);
ScrArr *sorted = scr_arr_new(SCR_ELEM_STR, n);
for (size_t i = 0; i < count; i++) {
if (!sf_headers_is_set_cookie(names[i])) {
scr_arr_push_ref(sorted, scr_str_retain(names[i]));
scr_arr_push_ref(sorted, sf_headers_get_value(h, names[i]));
continue;
}
for (size_t j = 0; j + 1 < n; j += 2) {
if (!sf_headers_is_set_cookie(sf_pair_at(h->pairs, j))) continue;
scr_arr_push_ref(sorted, scr_str_retain(names[i]));
scr_arr_push_ref(sorted, scr_str_retain(sf_pair_at(h->pairs, j + 1)));
}
}
free(names);
h->sorted = sorted;
return sorted;
}
/* The `index`th entry of the sort-and-combine list (sf_headers_sorted).
* False past the end. Outputs are +1. */
static bool sf_headers_sorted_entry(SfHeaders *h, size_t index,
ScrStr **name_out,
ScrStr **value_out) {
ScrArr *sorted = sf_headers_sorted(h);
size_t entries = (size_t)scr_arr_len(sorted) / 2;
if (index >= entries) return false;
*name_out = scr_str_retain(sf_pair_at(sorted, 2 * index));
*value_out = scr_str_retain(sf_pair_at(sorted, 2 * index + 1));
return true;
}
/* One step of a live Headers iterator (entries, keys or values): the
* `*index`th entry of the current sorted list, as Node's iterator re-reads
* the list on every step. The result is { value, done }. */
static ScrDyn *sf_headers_step(void *ptr, double *index, unsigned selection) {
ScrStr *name = NULL;
ScrStr *value = NULL;
bool found = sf_headers_sorted_entry(ptr, (size_t)*index, &name, &value);
ScrDyn *item;
if (found) {
(*index)++;
if (selection == 2) {
item = scr_dyn_new_arr();
scr_dyn_arr_push(item, scr_dyn_new_str(name));
scr_dyn_arr_push(item, scr_dyn_new_str(value));
} else {
item = scr_dyn_new_str(selection == 1 ? name : value);
}
scr_str_release(name);
scr_str_release(value);
} else {
item = scr_dyn_retain(scr_dyn_undefined());
}
ScrDyn *result = scr_dyn_new_obj();
scr_dyn_obj_set(result, "value", 5, item);
scr_dyn_obj_set(result, "done", 4, scr_dyn_new_bool(!found));
return result;
}
static ScrDyn *sf_headers_invoke(void *ptr, ScrDyn *self,
const char *method,
ScrDyn *const *args, size_t argc,
const char *what) {
SfHeaders *h = ptr;
if (strcmp(method, "entries") == 0 || strcmp(method, "keys") == 0 ||
strcmp(method, "values") == 0) {
return scr_dyn_native_handle_iterator(
self, strcmp(method, "entries") == 0 ? 2 : strcmp(method, "keys") == 0 ? 1 : 0);
}
if (strcmp(method, "get") == 0 || strcmp(method, "has") == 0) {
if (argc < 1) {
sf_type_error("Headers.get/has requires a header name");
return NULL;
}
ScrStr *name = sf_headers_name(args[0], method);
if (!name) return NULL;
ScrStr *value = sf_headers_get_value(h, name);
scr_str_release(name);
if (strcmp(method, "has") == 0) {
bool present = value != NULL;
scr_str_release(value);
return scr_dyn_new_bool(present);
}
if (!value) return scr_dyn_new_null();
ScrDyn *out = scr_dyn_new_str(value);
scr_str_release(value);
return out;
}
if (strcmp(method, "getSetCookie") == 0) {
ScrDyn *out = scr_dyn_new_arr();
size_t n = (size_t)scr_arr_len(h->pairs);
for (size_t i = 0; i + 1 < n; i += 2) {
if (sf_headers_is_set_cookie(sf_pair_at(h->pairs, i)))
scr_dyn_arr_push(out, scr_dyn_new_str(sf_pair_at(h->pairs, i + 1)));
}
return out;
}
if (strcmp(method, "forEach") == 0) {
if (argc < 1 || args[0]->kind != SCR_DYN_FUNC) {
sf_type_error("Headers.forEach requires a callback function");
return NULL;
}
ScrDyn *callback = args[0];
const ScrDyn *this_arg =
argc > 1 ? args[1] : scr_dyn_undefined();
/* Headers iteration is live. Re-sort the current list for each numeric
* index so callback mutations have the same effects as Node: deleting
* a future name skips it, inserting a later name visits it, and
* inserting before the current index can make the current name appear
* again. The old one-time snapshot was only safe while every native
* Headers handle was immutable. Set-Cookie values are visited one by
* one (sf_headers_sorted_entry). */
for (size_t i = 0;; i++) {
ScrStr *name = NULL;
ScrStr *value = NULL;
if (!sf_headers_sorted_entry(h, i, &name, &value)) break;
ScrDyn *boxed_value = scr_dyn_new_str(value);
ScrDyn *boxed_name = scr_dyn_new_str(name);
scr_str_release(name);
ScrDyn *call_args[3] = {boxed_value, boxed_name, self};
scr_dyn_this_push_dyn(this_arg);
ScrDyn *result =
scr_dyn_call(callback, call_args, 3, "Headers.forEach callback");
scr_dyn_this_pop();
scr_dyn_release(boxed_value);
scr_dyn_release(boxed_name);
scr_str_release(value);
if (!result) {
return NULL;
}
scr_dyn_release(result);
}
return scr_dyn_retain(scr_dyn_undefined());
}
if (strcmp(method, "append") == 0 || strcmp(method, "delete") == 0 ||
strcmp(method, "set") == 0) {
size_t required = strcmp(method, "delete") == 0 ? 1 : 2;
if (argc < required) {
sf_type_error("Headers mutation requires its declared arguments");
return NULL;
}
if (strcmp(method, "delete") == 0) {
ScrStr *name = sf_headers_name(args[0], method);
if (!name) return NULL;
if (h->immutable) {
scr_str_release(name);
sf_type_error("immutable");
return NULL;
}
sf_headers_delete_name(h, name);
scr_str_release(name);
return scr_dyn_retain(scr_dyn_undefined());
}
ScrStr *name = NULL;
ScrStr *value = NULL;
if (!sf_headers_convert_pair(
args[0], args[1], method, &name, &value)) {
return NULL;
}
if (h->immutable) {
scr_str_release(name);
scr_str_release(value);
sf_type_error("immutable");
return NULL;
}
if (strcmp(method, "set") == 0) sf_headers_delete_name(h, name);
sf_headers_append_pair(h, name, value);
return scr_dyn_retain(scr_dyn_undefined());
}
sf_not_function(what);
return NULL;
}
static ScrDyn *sf_headers_get(void *ptr, const char *key, size_t len) {
(void)ptr;
(void)key;
(void)len;
return NULL;
}
/* ── TransformStream / WritableStream ────────────────────────────── */
/*
* The identity TransformStream (`new TransformStream()`): a native
* readable side (an SfStream with a zero high-water mark, fed by the
* transform) and a writable side implemented here with the streams spec's
* WritableStream state machine (default high-water mark 1, chunk size 1).
* The writable side has no other sink, so the TransformStream object, its
* WritableStream and the sink share one SfTransform; the handles differ
* only by tag. Promise reactions the spec chains on already-settled
* promises run after the same number of microtask turns as in Node's
* implementation (lib/internal/webstreams), so interleavings with user
* code match.
*
* Ownership: the transform owns its readable side and its writer; the
* readable side's pull/cancel callbacks own the transform until that side
* closes or errors (fetch teardown drops the callbacks of transforms
* nobody finished). A writer handle retains the transform while bound,
* like a reader handle retains its stream.
*/
enum { SFW_WRITABLE, SFW_ERRORING, SFW_ERRORED, SFW_CLOSED };
typedef struct SfTransform SfTransform;
typedef struct SfWriteReq {
ScrDyn *chunk;
ScrPromise *promise;
struct SfWriteReq *next;
} SfWriteReq;
typedef struct SfWriter {
size_t rc;
size_t handle_refs;
SfTransform *stream; /* weak; handle refs retain it while bound */
ScrPromise *ready;
ScrPromise *closed;
} SfWriter;
struct SfTransform {
size_t rc;
SfStream *readable;
/* WritableStream */
bool started; /* the start algorithm's promise settled (one turn) */
int state;
ScrDyn *stored_error;
SfWriter *writer; /* owned while locked */
SfWriteReq *head; /* queued writes (chunk + request promise) */
SfWriteReq *tail;
SfWriteReq *in_flight;
size_t queue_size; /* queueTotalSize: queued chunks + the in-flight one */
bool close_queued; /* the close sentinel is in the queue */
ScrPromise *close_request;
ScrPromise *in_flight_close;
bool has_pending_abort;
ScrPromise *pending_abort;
/* The sink abort found a finish promise from a cancel or close, which
* settles the abort instead (Node returns that promise). */
bool abort_joins_finish;
/* The abort request's outcome between SFT_ABORT_DONE and
* SFT_ABORT_SETTLE: a rejection reason (+1), or NULL to fulfill. */
ScrDyn *abort_settle_error;
ScrDyn *pending_abort_reason;
bool pending_abort_was_erroring;
bool backpressure;
/* TransformStream */
bool t_backpressure;
ScrPromise *bp_change;
bool write_waiting; /* the in-flight write waits for bp_change */
ScrDyn *write_error; /* the in-flight write's failure, if any */
ScrDyn *cancel_reason; /* the readable side's cancel reason, until it reacts */
ScrPromise *finish; /* finishPromise of close/abort/cancel */
};
static SfTransform *sf_transform_retain(SfTransform *t) {
t->rc++;
return t;
}
static void sf_write_req_free(SfWriteReq *w) {
if (!w) return;
scr_dyn_release(w->chunk);
scr_promise_release(w->promise);
free(w);
}
static void sf_writer_release(SfWriter *w) {
if (!w || --w->rc > 0) return;
scr_promise_release(w->ready);
scr_promise_release(w->closed);
free(w);
}
static void sf_transform_release(SfTransform *t) {
if (!t || --t->rc > 0) return;
sf_stream_release(t->readable);
scr_dyn_release(t->stored_error);
if (t->writer) {
t->writer->stream = NULL;
sf_writer_release(t->writer);
}
while (t->head) {
SfWriteReq *w = t->head;
t->head = w->next;
sf_write_req_free(w);
}
sf_write_req_free(t->in_flight);
scr_promise_release(t->close_request);
scr_promise_release(t->in_flight_close);
scr_promise_release(t->pending_abort);
scr_dyn_release(t->pending_abort_reason);
scr_dyn_release(t->abort_settle_error);
scr_promise_release(t->bp_change);
scr_promise_release(t->finish);
scr_dyn_release(t->write_error);
scr_dyn_release(t->cancel_reason);
free(t);
}
static void *sf_transform_retain_v(void *p) {
return sf_transform_retain((SfTransform *)p);
}
static void sf_transform_release_v(void *p) {
sf_transform_release((SfTransform *)p);
}
static void *sf_writer_handle_retain_v(void *p) {
SfWriter *w = p;
w->rc++;
w->handle_refs++;
if (w->stream) sf_transform_retain(w->stream);
return w;
}
static void sf_writer_handle_release_v(void *p) {
SfWriter *w = p;
SfTransform *t = w->stream;
if (w->handle_refs > 0) w->handle_refs--;
sf_writer_release(w);
sf_transform_release(t);
}
static ScrBox *sf_transform_box(SfTransform *t) {
ScrBox *box = scr_box_new_obj(&sf_transform_retain_v,
&sf_transform_release_v, NULL);
scr_box_set_ref(box, sf_transform_retain(t));
return box;
}
static void sf_invalid_state(const char *message) {
char buf[96];
int n = snprintf(buf, sizeof buf, "Invalid state: %s", message);
scr_throw_error_msg_code(SCR_ERR_TYPE, buf, (size_t)n, "ERR_INVALID_STATE");
}
static ScrPromise *sf_promise_invalid_state(const char *message) {
ScrPromise *p = scr_promise_new();
sf_invalid_state(message);
scr_promise_reject_pending(p);
return p;
}
static ScrPromise *sf_promise_rejected_with(ScrDyn *reason) {
ScrPromise *p = scr_promise_new();
sf_reject_promise_reason(p, reason);
return p;
}
static ScrPromise *sf_promise_resolved(void) {
ScrPromise *p = scr_promise_new();
scr_promise_fulfill_void(p);
return p;
}
/* Deferred steps: the reaction `op` after `ticks` microtask turns. */
enum {
SFT_WRITE_DONE,
SFT_WRITE_CONTINUE,
SFT_CLOSE_DONE,
SFT_ABORT_DONE,
SFT_START,
SFT_CANCEL_DONE,
SFT_ABORT_SETTLE,
};
static void sf_transform_step(SfTransform *t, int op);
typedef struct SfTransformTask {
size_t rc;
SfTransform *t;
int op;
int ticks;
} SfTransformTask;
static void *sf_transform_task_retain_v(void *p) {
((SfTransformTask *)p)->rc++;
return p;
}
static void sf_transform_task_release_v(void *p) {
SfTransformTask *task = p;
if (--task->rc > 0) return;
sf_transform_release(task->t);
free(task);
}
static void sf_transform_tick_task(ScrClosure *cb) {
SfTransformTask *task = (SfTransformTask *)scr_box_get_ref(cb->caps[0]);
if (--task->ticks > 0) {
ScrClosure *next = scr_closure_new((void *)&sf_transform_tick_task, 1);
next->caps[0] = scr_box_new_obj(&sf_transform_task_retain_v,
&sf_transform_task_release_v, NULL);
scr_box_set_ref(next->caps[0], sf_transform_task_retain_v(task));
scr_queue_microtask(next);
} else {
sf_transform_step(task->t, task->op);
}
sf_transform_task_release_v(task);
}
static void sf_transform_defer(SfTransform *t, int op, int ticks) {
SfTransformTask *task = calloc(1, sizeof *task);
if (!task) sf_oom();
task->rc = 1;
task->t = sf_transform_retain(t);
task->op = op;
task->ticks = ticks;
ScrClosure *cb = scr_closure_new((void *)&sf_transform_tick_task, 1);
cb->caps[0] = scr_box_new_obj(&sf_transform_task_retain_v,
&sf_transform_task_release_v, NULL);
scr_box_set_ref(cb->caps[0], task); /* the box takes the reference */
scr_queue_microtask(cb);
}
/* ── WritableStream abstract operations ── */
static bool sfw_close_queued_or_in_flight(const SfTransform *t) {
return t->close_request || t->in_flight_close;
}
static bool sfw_has_operation_in_flight(const SfTransform *t) {
return t->in_flight || t->in_flight_close;
}
static void sfw_writer_ensure_ready_rejected(SfWriter *w, ScrDyn *reason) {
if (!scr_promise_is_pending(w->ready)) {
scr_promise_release(w->ready);
w->ready = scr_promise_new();
}
sf_reject_promise_reason(w->ready, reason);
scr_promise_mark_handled(w->ready);
}
static void sfw_writer_ensure_closed_rejected(SfWriter *w, ScrDyn *reason) {
if (!scr_promise_is_pending(w->closed)) {
scr_promise_release(w->closed);
w->closed = scr_promise_new();
}
sf_reject_promise_reason(w->closed, reason);
scr_promise_mark_handled(w->closed);
}
static void sfw_update_backpressure(SfTransform *t, bool backpressure) {
SfWriter *w = t->writer;
if (w && backpressure != t->backpressure) {
if (backpressure) {
scr_promise_release(w->ready);
w->ready = scr_promise_new();
scr_promise_mark_handled(w->ready);
} else {
scr_promise_fulfill_void(w->ready);
}
}
t->backpressure = backpressure;
}
static void sfw_reject_close_and_closed(SfTransform *t) {
if (t->close_request) {
sf_reject_promise_reason(t->close_request, t->stored_error);
scr_promise_release(t->close_request);
t->close_request = NULL;
}
if (t->writer) {
sf_reject_promise_reason(t->writer->closed, t->stored_error);
scr_promise_mark_handled(t->writer->closed);
}
}
static void sft_abort_algorithm(SfTransform *t, ScrDyn *reason);
static void sfw_finish_erroring(SfTransform *t) {
t->state = SFW_ERRORED;
/* The controller's queue resets; every queued write rejects. */
while (t->head) {
SfWriteReq *w = t->head;
t->head = w->next;
sf_reject_promise_reason(w->promise, t->stored_error);
sf_write_req_free(w);
}
t->tail = NULL;
t->queue_size = 0;
t->close_queued = false;
if (!t->has_pending_abort) {
sfw_reject_close_and_closed(t);
return;
}
if (t->pending_abort_was_erroring) {
t->has_pending_abort = false;
sf_reject_promise_reason(t->pending_abort, t->stored_error);
scr_promise_release(t->pending_abort);
t->pending_abort = NULL;
scr_dyn_release(t->pending_abort_reason);
t->pending_abort_reason = NULL;
sfw_reject_close_and_closed(t);
return;
}
/* The sink's abort runs; its promise settles the abort request. */
sft_abort_algorithm(t, t->pending_abort_reason);
}
static void sfw_start_erroring(SfTransform *t, ScrDyn *reason) {
t->state = SFW_ERRORING;
scr_dyn_release(t->stored_error);
t->stored_error = scr_dyn_retain(reason);
if (t->writer) sfw_writer_ensure_ready_rejected(t->writer, reason);
if (!sfw_has_operation_in_flight(t) && t->started) sfw_finish_erroring(t);
}
static void sfw_deal_with_rejection(SfTransform *t, ScrDyn *error) {
if (t->state == SFW_WRITABLE) {
sfw_start_erroring(t, error);
return;
}
sfw_finish_erroring(t);
}
static void sfw_error_if_needed(SfTransform *t, ScrDyn *error) {
if (t->state == SFW_WRITABLE) sfw_start_erroring(t, error);
}
static void sft_set_backpressure(SfTransform *t, bool backpressure);
static void sft_sink_write(SfTransform *t);
static void sft_sink_close(SfTransform *t);
static void sfw_advance_queue(SfTransform *t) {
if (!t->started || t->in_flight || t->in_flight_close) return;
if (t->state == SFW_ERRORING) {
sfw_finish_erroring(t);
return;
}
if (t->head) {
SfWriteReq *w = t->head;
t->head = w->next;
if (!t->head) t->tail = NULL;
w->next = NULL;
t->in_flight = w;
sft_sink_write(t);
return;
}
if (t->close_queued) {
t->close_queued = false;
t->in_flight_close = t->close_request;
t->close_request = NULL;
sft_sink_close(t);
}
}
static void sfw_finish_write(SfTransform *t, ScrDyn *error) {
SfWriteReq *w = t->in_flight;
t->in_flight = NULL;
if (error) {
sf_reject_promise_reason(w->promise, error);
sf_write_req_free(w);
sfw_deal_with_rejection(t, error);
return;
}
scr_promise_fulfill_void(w->promise);
sf_write_req_free(w);
if (t->queue_size) t->queue_size--;
if (!sfw_close_queued_or_in_flight(t) && t->state == SFW_WRITABLE) {
sfw_update_backpressure(t, t->queue_size >= 1);
}
sfw_advance_queue(t);
}
static void sfw_finish_close(SfTransform *t, ScrDyn *error) {
ScrPromise *p = t->in_flight_close;
t->in_flight_close = NULL;
if (error) {
sf_reject_promise_reason(p, error);
scr_promise_release(p);
if (t->has_pending_abort) {
t->has_pending_abort = false;
sf_reject_promise_reason(t->pending_abort, error);
scr_promise_release(t->pending_abort);
t->pending_abort = NULL;
scr_dyn_release(t->pending_abort_reason);
t->pending_abort_reason = NULL;
}
sfw_deal_with_rejection(t, error);
return;
}
scr_promise_fulfill_void(p);
scr_promise_release(p);
if (t->state == SFW_ERRORING) {
scr_dyn_release(t->stored_error);
t->stored_error = NULL;
if (t->has_pending_abort) {
t->has_pending_abort = false;
scr_promise_fulfill_void(t->pending_abort);
scr_promise_release(t->pending_abort);
t->pending_abort = NULL;
scr_dyn_release(t->pending_abort_reason);
t->pending_abort_reason = NULL;
}
}
t->state = SFW_CLOSED;
if (t->writer) scr_promise_fulfill_void(t->writer->closed);
}
/* WritableStreamAbort: +1 promise. */
static ScrPromise *sfw_abort(SfTransform *t, ScrDyn *reason) {
if (t->state == SFW_CLOSED || t->state == SFW_ERRORED) {
return sf_promise_resolved();
}
if (t->has_pending_abort) return scr_promise_retain(t->pending_abort);
bool was_erroring = t->state == SFW_ERRORING;
ScrPromise *p = scr_promise_new();
t->has_pending_abort = true;
t->pending_abort = scr_promise_retain(p);
t->pending_abort_reason =
scr_dyn_retain(was_erroring ? scr_dyn_undefined() : reason);
t->pending_abort_was_erroring = was_erroring;
if (!was_erroring) sfw_start_erroring(t, reason);
return p;
}
/* WritableStreamClose: +1 promise. */
static ScrPromise *sfw_close(SfTransform *t) {
if (t->state == SFW_CLOSED || t->state == SFW_ERRORED) {
char message[64];
snprintf(message, sizeof message, "WritableStream is %s",
t->state == SFW_CLOSED ? "closed" : "errored");
return sf_promise_invalid_state(message);
}
ScrPromise *p = scr_promise_new();
t->close_request = scr_promise_retain(p);
if (t->writer && t->backpressure && t->state == SFW_WRITABLE) {
scr_promise_fulfill_void(t->writer->ready);
}
t->close_queued = true;
sfw_advance_queue(t);
return p;
}
/* WritableStreamDefaultWriterWrite: +1 promise. */
static ScrPromise *sfw_writer_write(SfWriter *w, ScrDyn *chunk) {
SfTransform *t = w->stream;
if (!t) return sf_promise_invalid_state("Writer is not bound to a WritableStream");
if (t->state == SFW_ERRORED || t->state == SFW_ERRORING) {
return sf_promise_rejected_with(t->stored_error);
}
if (sfw_close_queued_or_in_flight(t) || t->state == SFW_CLOSED) {
return sf_promise_invalid_state("WritableStream is closed");
}
SfWriteReq *req = calloc(1, sizeof *req);
if (!req) sf_oom();
req->chunk = scr_dyn_retain(chunk);
req->promise = scr_promise_new();
ScrPromise *p = scr_promise_retain(req->promise);
if (t->tail) t->tail->next = req;
else t->head = req;
t->tail = req;
t->queue_size++;
sfw_update_backpressure(t, t->queue_size >= 1);
sfw_advance_queue(t);
return p;
}
static SfWriter *sfw_get_writer(SfTransform *t) {
if (t->writer) {
sf_invalid_state("WritableStream is locked");
return NULL;
}
SfWriter *w = calloc(1, sizeof *w);
if (!w) sf_oom();
w->rc = 1;
w->stream = t;
w->ready = scr_promise_new();
w->closed = scr_promise_new();
scr_promise_mark_handled(w->ready);
scr_promise_mark_handled(w->closed);
switch (t->state) {
case SFW_WRITABLE:
if (sfw_close_queued_or_in_flight(t) || !t->backpressure) {
scr_promise_fulfill_void(w->ready);
}
break;
case SFW_ERRORING:
sf_reject_promise_reason(w->ready, t->stored_error);
break;
case SFW_CLOSED:
scr_promise_fulfill_void(w->ready);
scr_promise_fulfill_void(w->closed);
break;
default:
sf_reject_promise_reason(w->ready, t->stored_error);
sf_reject_promise_reason(w->closed, t->stored_error);
break;
}
t->writer = w;
w->rc++; /* the stream's ownership */
return w;
}
static void sfw_writer_release_lock(SfWriter *w) {
SfTransform *t = w->stream;
if (!t) return;
sf_invalid_state("Writer has been released");
ScrCaught *caught = scr_exc_take();
ScrDyn *released = scr_caught_to_dyn(caught);
scr_caught_release(caught);
sfw_writer_ensure_ready_rejected(w, released);
sfw_writer_ensure_closed_rejected(w, released);
scr_dyn_release(released);
size_t handle_refs = w->handle_refs;
t->writer = NULL;
w->stream = NULL;
sf_writer_release(w); /* the stream's ownership */
while (handle_refs-- > 0) sf_transform_release(t);
}
/* ── TransformStream algorithms (identity transformer) ── */
static void sft_set_backpressure(SfTransform *t, bool backpressure) {
if (t->bp_change) {
scr_promise_fulfill_void(t->bp_change);
scr_promise_release(t->bp_change);
}
t->bp_change = scr_promise_new();
t->t_backpressure = backpressure;
if (t->write_waiting) {
t->write_waiting = false;
sf_transform_defer(t, SFT_WRITE_CONTINUE, 1);
}
}
/* After an enqueue: whether a read is still waiting. A fetch upload's read
* was just satisfied by the chunk, and it reads again through the next
* pull, as a reader's next read() does. */
static bool sft_readable_wants_chunk(const SfStream *r) {
return r->collector || (r->reader && r->reader->pending_head);
}
/* The readable side is closed or errored: a pull still waiting on
* bp_change can no longer receive a chunk, so complete it. */
static void sft_finish_pull(SfTransform *t) {
if (t->bp_change && scr_promise_is_pending(t->bp_change)) scr_promise_fulfill_void(t->bp_change);
}
static bool sft_readable_can_enqueue(const SfStream *r) {
return !r->close_requested && !r->closed && !r->error;
}
/* TransformStreamErrorWritableAndUnblockWrite */
static void sft_error_writable(SfTransform *t, ScrDyn *error) {
sfw_error_if_needed(t, error);
if (t->t_backpressure) sft_set_backpressure(t, false);
}
/* The identity transform's enqueue: NULL on success, else +1 error. */
static ScrDyn *sft_perform_transform(SfTransform *t, ScrDyn *chunk) {
SfStream *r = t->readable;
if (!sft_readable_can_enqueue(r)) {
sf_invalid_state("Unable to enqueue");
ScrCaught *caught = scr_exc_take();
ScrDyn *error = scr_caught_to_dyn(caught);
scr_caught_release(caught);
sft_error_writable(t, error);
return error;
}
sf_stream_enqueue_value(r, chunk);
if (scr_exc_pending()) {
ScrCaught *caught = scr_exc_take();
ScrDyn *error = scr_caught_to_dyn(caught);
scr_caught_release(caught);
sf_stream_error(r, error);
sft_error_writable(t, error);
return error;
}
bool backpressure = !sft_readable_wants_chunk(r);
if (backpressure != t->t_backpressure) sft_set_backpressure(t, true);
return NULL;
}
static void sft_sink_write(SfTransform *t) {
if (t->t_backpressure) {
t->write_waiting = true;
return;
}
ScrDyn *error = sft_perform_transform(t, t->in_flight->chunk);
if (error) {
scr_dyn_release(t->write_error);
t->write_error = error;
}
sf_transform_defer(t, SFT_WRITE_DONE, 2);
}
static void sft_sink_close(SfTransform *t) {
if (!t->finish) {
t->finish = scr_promise_new();
scr_promise_mark_handled(t->finish);
sf_transform_defer(t, SFT_CLOSE_DONE, 1);
return;
}
/* A cancel or abort already finished the transform. */
sf_transform_defer(t, SFT_CLOSE_DONE, 1);
}
static void sft_abort_algorithm(SfTransform *t, ScrDyn *reason) {
(void)reason;
t->abort_joins_finish = t->finish != NULL;
if (!t->finish) {
t->finish = scr_promise_new();
scr_promise_mark_handled(t->finish);
}
sf_transform_defer(t, SFT_ABORT_DONE, 1);
}
static void sf_transform_step(SfTransform *t, int op) {
switch (op) {
case SFT_WRITE_CONTINUE: {
if (!t->in_flight) return;
if (t->state == SFW_ERRORING) {
ScrDyn *error = scr_dyn_retain(t->stored_error);
scr_dyn_release(t->write_error);
t->write_error = error;
} else {
ScrDyn *error = sft_perform_transform(t, t->in_flight->chunk);
if (error) {
scr_dyn_release(t->write_error);
t->write_error = error;
}
}
sf_transform_defer(t, SFT_WRITE_DONE, 3);
return;
}
case SFT_WRITE_DONE: {
if (!t->in_flight) return;
ScrDyn *error = t->write_error;
t->write_error = NULL;
sfw_finish_write(t, error);
scr_dyn_release(error);
return;
}
case SFT_CLOSE_DONE: {
SfStream *r = t->readable;
ScrDyn *error = NULL;
if (r->error) {
error = scr_dyn_retain(r->error);
} else {
sf_stream_close(r);
}
sft_finish_pull(t);
if (error) sf_reject_promise_reason(t->finish, error);
else scr_promise_fulfill_void(t->finish);
if (t->in_flight_close) sfw_finish_close(t, error);
scr_dyn_release(error);
return;
}
case SFT_ABORT_DONE: {
SfStream *r = t->readable;
scr_dyn_release(t->abort_settle_error);
t->abort_settle_error = NULL;
if (t->abort_joins_finish && !scr_promise_is_pending(t->finish)) {
/* The cancel or close that created the finish promise settled
* it; the abort request takes its result. */
if (scr_promise_is_rejected(t->finish))
t->abort_settle_error = scr_promise_reason_dyn(t->finish);
} else {
ScrDyn *reason = t->pending_abort_reason
? scr_dyn_retain(t->pending_abort_reason)
: scr_dyn_retain(scr_dyn_undefined());
bool readable_errored = r->error != NULL;
if (!readable_errored) sf_stream_error(r, reason);
sft_finish_pull(t);
/* The sink abort algorithm's finish promise, which a
* readable.cancel() after the abort answers too: rejected with the
* readable side's stored error when it had already errored, else
* resolved. */
if (t->finish && scr_promise_is_pending(t->finish)) {
if (readable_errored) sf_reject_promise_reason(t->finish, r->error);
else scr_promise_fulfill_void(t->finish);
}
if (readable_errored) t->abort_settle_error = scr_dyn_retain(r->error);
scr_dyn_release(reason);
}
/* WritableStreamFinishErroring settles the abort request in a
* reaction to the sink abort's promise, two turns after it. */
sf_transform_defer(t, SFT_ABORT_SETTLE, 2);
return;
}
case SFT_ABORT_SETTLE: {
ScrDyn *error = t->abort_settle_error;
t->abort_settle_error = NULL;
if (t->has_pending_abort) {
t->has_pending_abort = false;
if (error) sf_reject_promise_reason(t->pending_abort, error);
else scr_promise_fulfill_void(t->pending_abort);
scr_promise_release(t->pending_abort);
t->pending_abort = NULL;
scr_dyn_release(t->pending_abort_reason);
t->pending_abort_reason = NULL;
}
sfw_reject_close_and_closed(t);
scr_dyn_release(error);
return;
}
case SFT_CANCEL_DONE: {
ScrDyn *reason = t->cancel_reason;
t->cancel_reason = NULL;
if (t->state == SFW_ERRORED) {
sf_reject_promise_reason(t->finish, t->stored_error);
} else {
sfw_error_if_needed(t, reason ? reason : scr_dyn_undefined());
if (t->t_backpressure) sft_set_backpressure(t, false);
sft_finish_pull(t);
scr_promise_fulfill_void(t->finish);
}
scr_dyn_release(reason);
return;
}
case SFT_START: {
/* Both sides start one turn after construction, the writable side
* first (Node registers its start reaction first). */
t->started = true;
sfw_advance_queue(t);
SfStream *r = t->readable;
r->started = true;
if (!r->close_requested && !r->closed && !r->error) {
sf_stream_drain(r);
sf_stream_pull(r);
}
return;
}
}
}
/* The readable side's pull: release backpressure; the pull completes when
* backpressure changes again. */
static ScrDyn *sft_pull_thunk(ScrClosure *cb, ScrDyn *const *args,
size_t argc) {
(void)args;
(void)argc;
SfTransform *t = (SfTransform *)scr_box_get_ref(cb->caps[0]);
sft_set_backpressure(t, false);
ScrDyn *out = scr_dyn_new_promise(t->bp_change);
sf_transform_release(t);
return out;
}
/* The readable side's cancel (TransformStreamDefaultSourceCancelAlgorithm):
* errors the writable side with the reason and unblocks a waiting write. */
static ScrDyn *sft_cancel_thunk(ScrClosure *cb, ScrDyn *const *args,
size_t argc) {
SfTransform *t = (SfTransform *)scr_box_get_ref(cb->caps[0]);
ScrDyn *reason = argc ? args[0] : scr_dyn_undefined();
if (!t->finish) {
t->finish = scr_promise_new();
scr_promise_mark_handled(t->finish);
scr_dyn_release(t->cancel_reason);
t->cancel_reason = scr_dyn_retain(reason);
/* The (identity) cancel algorithm's resolved promise reacts a turn
* later. */
sf_transform_defer(t, SFT_CANCEL_DONE, 1);
}
ScrDyn *out = scr_dyn_new_promise(t->finish);
sf_transform_release(t);
return out;
}
static ScrDyn *sft_func(SfTransform *t, ScrDynThunk thunk, const char *name) {
ScrClosure *cb = scr_closure_new((void *)thunk, 1);
cb->caps[0] = sf_transform_box(t);
return scr_dyn_new_func(cb, thunk, 1, "(controller)", name);
}
ScrDyn *scr_fetch_transform_new(void) {
SfTransform *t = calloc(1, sizeof *t);
if (!t) sf_oom();
t->rc = 1;
t->state = SFW_WRITABLE;
SfStream *r = sf_stream_new_native();
r->started = false;
r->hwm_zero = true;
r->pull_cb = sft_func(t, &sft_pull_thunk, "pull");
r->cancel_cb = sft_func(t, &sft_cancel_thunk, "cancel");
r->source_this = scr_dyn_retain(scr_dyn_undefined());
sf_stream_track_callbacks(r);
t->readable = r;
/* The writable side's high-water mark of 1 leaves room: no backpressure.
* The readable side's 0 does: writes wait for a read. */
t->backpressure = false;
sft_set_backpressure(t, true);
sf_transform_defer(t, SFT_START, 1);
ScrDyn *out = scr_dyn_new_handle(t, SCR_DYNH_WEB_TRANSFORM);
sf_transform_release(t);
return out;
}
static ScrDyn *sf_transform_get(void *ptr, const char *key, size_t len) {
SfTransform *t = ptr;
if (sf_name(key, len, "readable")) {
return scr_dyn_new_handle(t->readable, SCR_DYNH_WEB_STREAM);
}
if (sf_name(key, len, "writable")) {
return scr_dyn_new_handle(t, SCR_DYNH_WEB_WRITABLE);
}
return NULL;
}
static ScrDyn *sf_transform_invoke(void *ptr, ScrDyn *self,
const char *method, ScrDyn *const *args,
size_t argc, const char *what) {
(void)ptr;
(void)self;
(void)args;
(void)argc;
(void)method;
sf_not_function(what);
return NULL;
}
static ScrDyn *sf_promise_dyn(ScrPromise *p) {
ScrDyn *out = scr_dyn_new_promise(p);
scr_promise_release(p);
return out;
}
static ScrDyn *sf_writable_invoke(void *ptr, ScrDyn *self,
const char *method, ScrDyn *const *args,
size_t argc, const char *what) {
SfTransform *t = ptr;
(void)self;
if (strcmp(method, "getWriter") == 0) {
SfWriter *w = sfw_get_writer(t);
if (!w) return NULL;
ScrDyn *out = scr_dyn_new_handle(w, SCR_DYNH_WEB_WRITER);
sf_writer_release(w);
return out;
}
if (strcmp(method, "abort") == 0) {
if (t->writer) {
return sf_promise_dyn(
sf_promise_invalid_state("WritableStream is locked"));
}
return sf_promise_dyn(sfw_abort(t, argc ? args[0] : scr_dyn_undefined()));
}
if (strcmp(method, "close") == 0) {
if (t->writer) {
return sf_promise_dyn(
sf_promise_invalid_state("WritableStream is locked"));
}
if (sfw_close_queued_or_in_flight(t)) {
return sf_promise_dyn(
sf_promise_invalid_state("WritableStream is closed"));
}
return sf_promise_dyn(sfw_close(t));
}
sf_not_function(what);
return NULL;
}
static ScrDyn *sf_writable_get(void *ptr, const char *key, size_t len) {
SfTransform *t = ptr;
if (sf_name(key, len, "locked")) return scr_dyn_new_bool(t->writer != NULL);
return NULL;
}
static ScrDyn *sf_writer_invoke(void *ptr, ScrDyn *self, const char *method,
ScrDyn *const *args, size_t argc,
const char *what) {
SfWriter *w = ptr;
(void)self;
if (strcmp(method, "write") == 0) {
return sf_promise_dyn(
sfw_writer_write(w, argc ? args[0] : scr_dyn_undefined()));
}
if (strcmp(method, "close") == 0) {
SfTransform *t = w->stream;
if (!t) {
return sf_promise_dyn(sf_promise_invalid_state(
"Writer is not bound to a WritableStream"));
}
if (sfw_close_queued_or_in_flight(t)) {
return sf_promise_dyn(
sf_promise_invalid_state("WritableStream is closed"));
}
return sf_promise_dyn(sfw_close(t));
}
if (strcmp(method, "abort") == 0) {
SfTransform *t = w->stream;
if (!t) {
return sf_promise_dyn(sf_promise_invalid_state(
"Writer is not bound to a WritableStream"));
}
return sf_promise_dyn(sfw_abort(t, argc ? args[0] : scr_dyn_undefined()));
}
if (strcmp(method, "releaseLock") == 0) {
sfw_writer_release_lock(w);
return scr_dyn_retain(scr_dyn_undefined());
}
sf_not_function(what);
return NULL;
}
static ScrDyn *sf_writer_get(void *ptr, const char *key, size_t len) {
SfWriter *w = ptr;
if (sf_name(key, len, "ready")) return scr_dyn_new_promise(w->ready);
if (sf_name(key, len, "closed")) return scr_dyn_new_promise(w->closed);
if (sf_name(key, len, "desiredSize")) {
SfTransform *t = w->stream;
if (!t) {
sf_invalid_state("Writer is not bound to a WritableStream");
return NULL;
}
if (t->state == SFW_ERRORED || t->state == SFW_ERRORING) {
return scr_dyn_new_null();
}
if (t->state == SFW_CLOSED) return scr_dyn_new_num(0);
return scr_dyn_new_num(1.0 - (double)t->queue_size);
}
return NULL;
}
/* ── Response ────────────────────────────────────────────────────── */
static SfResponse *sf_response_retain(SfResponse *r) {
r->rc++;
return r;
}
static void sf_response_release(SfResponse *r) {
if (!r || --r->rc > 0) return;
scr_weak_dispose_inline(r);
sf_stream_release(r->body);
sf_headers_release(r->headers);
scr_str_release(r->url);
scr_str_release(r->status_text);
scr_arr_release(r->light_record);
free(r);
}
static void *sf_response_retain_v(void *p) {
return sf_response_retain((SfResponse *)p);
}
static void sf_response_release_v(void *p) {
sf_response_release((SfResponse *)p);
}
/* A read the package's lightweight Response answers from its native
* response materializes it; status, ok and headers keep it light, and a
* headers read turns the header form into a Headers object. */
static void sf_response_materialize(SfResponse *r) {
r->light = 0;
}
static ScrPromise *sf_response_collect(SfResponse *r, int mode) {
if (!r->null_body) return sf_stream_collect(r->body, mode);
/*
* A null body is not a disturbed empty stream: body stays null,
* bodyUsed stays false, and the body readers may be called repeatedly.
* Each call therefore consumes its own empty payload.
*/
ScrPromise *p = scr_promise_new();
ScrDyn *value = NULL;
if (mode == SF_COLLECT_JSON) {
ScrStr *empty = scr_str_new("", 0);
value = scr_json_parse(empty);
scr_str_release(empty);
} else if (mode == SF_COLLECT_TEXT) {
ScrStr *empty = scr_str_new("", 0);
value = scr_dyn_new_str(empty);
scr_str_release(empty);
} else {
ScrBytes *empty = scr_bytes_new(SCR_BYTES_U8, 0);
value = mode == SF_COLLECT_ARRAY_BUFFER ? scr_array_buffer_from_bytes(empty) : scr_dyn_new_bytes(empty);
scr_bytes_release(empty);
}
if (scr_exc_pending()) {
scr_dyn_release(value);
scr_promise_reject_pending(p);
} else {
scr_promise_fulfill_ref(p, value, &scr_dyn_retain_v,
&scr_dyn_release_v, NULL);
}
return p;
}
/* The body and metadata copy of Response.clone and Request.clone (undici
* cloneResponse/cloneBody): the body tees, so the original and the clone
* each keep one branch, and the clone's headers are a copy with the same
* guard. NULL with a TypeError pending when the body is unusable. */
static SfResponse *sf_response_clone(SfResponse *r, const char *unusable) {
if (!r->null_body &&
(r->body->disturbed || r->body->reader || r->body->internal_lock)) {
sf_type_error(unusable);
return NULL;
}
SfResponse *c = calloc(1, sizeof *c);
if (!c) sf_oom();
c->rc = 1;
if (r->null_body) {
c->body = sf_stream_new_native();
sf_stream_close(c->body);
} else {
SfStream *branches[2];
sf_stream_tee(r->body, branches);
sf_stream_release(r->body);
r->body = branches[0];
c->body = branches[1];
}
c->headers = sf_headers_new(scr_arr_slice(r->headers->pairs, 0, INFINITY),
r->headers->immutable);
c->url = scr_str_retain(r->url);
c->status_text = scr_str_retain(r->status_text);
c->status = r->status;
c->redirected = r->redirected;
c->null_body = r->null_body;
c->light_length = r->light_length;
return c;
}
static ScrDyn *sf_response_invoke(void *ptr, ScrDyn *self,
const char *method,
ScrDyn *const *args, size_t argc,
const char *what) {
SfResponse *r = ptr;
sf_response_materialize(r);
if (strcmp(method, "clone") == 0) {
SfResponse *c = sf_response_clone(
r, "Response.clone: Body has already been consumed.");
if (!c) return NULL;
ScrDyn *out = scr_dyn_new_handle(c, SCR_DYNH_FETCH_RESPONSE);
sf_response_release(c);
return out;
}
int mode = -1;
if (strcmp(method, "json") == 0) mode = SF_COLLECT_JSON;
else if (strcmp(method, "text") == 0) mode = SF_COLLECT_TEXT;
else if (strcmp(method, "bytes") == 0) mode = SF_COLLECT_BYTES;
else if (strcmp(method, "arrayBuffer") == 0) mode = SF_COLLECT_ARRAY_BUFFER;
if (mode >= 0) {
ScrPromise *p = sf_response_collect(r, mode);
ScrDyn *out = scr_dyn_new_promise(p);
scr_promise_release(p);
return out;
}
(void)args;
(void)self;
sf_not_function(what);
return NULL;
}
static ScrDyn *sf_response_get(void *ptr, const char *key, size_t len) {
SfResponse *r = ptr;
if (sf_name(key, len, "status")) return scr_dyn_new_num((double)r->status);
if (sf_name(key, len, "ok")) {
return scr_dyn_new_bool(r->status >= 200 && r->status <= 299);
}
if (sf_name(key, len, "headers")) {
if (r->light == 1) {
r->light = 2;
r->light_default_ct = !r->null_body;
} else if (r->light == 3) {
r->light = 2;
}
return scr_dyn_new_handle(r->headers, SCR_DYNH_FETCH_HEADERS);
}
if (sf_name(key, len, "body") || sf_name(key, len, "bodyUsed") ||
sf_name(key, len, "url") || sf_name(key, len, "redirected") ||
sf_name(key, len, "statusText"))
sf_response_materialize(r);
if (sf_name(key, len, "bodyUsed")) {
return scr_dyn_new_bool(!r->null_body && r->body->disturbed);
}
if (sf_name(key, len, "body")) {
return r->null_body
? scr_dyn_new_null()
: scr_dyn_new_handle(r->body, SCR_DYNH_WEB_STREAM);
}
if (sf_name(key, len, "url")) return scr_dyn_new_str(r->url);
if (sf_name(key, len, "redirected")) {
return scr_dyn_new_bool(r->redirected);
}
if (sf_name(key, len, "statusText")) {
return scr_dyn_new_str(r->status_text);
}
return NULL;
}
static ScrPromise *sf_response_collect_dyn(ScrDyn *response, int mode) {
if (!response || response->kind != SCR_DYN_HANDLE ||
response->v.handle.tag != SCR_DYNH_FETCH_RESPONSE) {
sf_type_error("Illegal invocation");
return scr_promise_settled_ref(NULL, &scr_dyn_retain_v,
&scr_dyn_release_v, NULL);
}
SfResponse *r = response->v.handle.ptr;
sf_response_materialize(r);
return sf_response_collect(r, mode);
}
ScrPromise *scr_fetch_response_json(ScrDyn *response) {
return sf_response_collect_dyn(response, SF_COLLECT_JSON);
}
ScrPromise *scr_fetch_response_text(ScrDyn *response) {
return sf_response_collect_dyn(response, SF_COLLECT_TEXT);
}
ScrPromise *scr_fetch_response_array_buffer(ScrDyn *response) {
return sf_response_collect_dyn(response, SF_COLLECT_ARRAY_BUFFER);
}
ScrPromise *scr_fetch_response_bytes(ScrDyn *response) {
return sf_response_collect_dyn(response, SF_COLLECT_BYTES);
}
/* ── fetch transfer ──────────────────────────────────────────────── */
static SfTransfer *sf_retain(SfTransfer *t) {
t->rc++;
return t;
}
static void sf_release(SfTransfer *t) {
if (--t->rc > 0) return;
scr_promise_release(t->promise);
scr_url_release(t->url);
scr_str_release(t->method);
scr_arr_release(t->headers);
scr_dyn_release(t->body);
if (t->client) scr_http_client_release(t->client);
scr_net_sock_release(t->response_socket);
scr_bytes_release(t->inflate_pending);
if (t->inflating) inflateEnd(&t->zs);
sf_watch_free(t->signal_watch);
sf_signal_release(t->signal);
sf_stream_release(t->request_stream);
sf_stream_release(t->response_stream);
free(t);
}
static void *sf_retain_v(void *p) { return sf_retain((SfTransfer *)p); }
static void sf_release_v(void *p) { sf_release((SfTransfer *)p); }
static ScrClosure *sf_closure(SfTransfer *t, void *fn) {
ScrClosure *cb = scr_closure_new(fn, 1);
ScrBox *box = scr_box_new_obj(&sf_retain_v, &sf_release_v, NULL);
scr_box_set_ref(box, sf_retain(t));
cb->caps[0] = box;
return cb;
}
static SfTransfer *sf_from(ScrClosure *cb) {
return (SfTransfer *)scr_box_get_ref(cb->caps[0]);
}
static void sf_settle(SfTransfer *t) {
if (t->done) return;
t->done = true;
if (t->signal_watch) {
sf_watch_free(t->signal_watch);
t->signal_watch = NULL;
}
if (t->request_stream && t->request_stream->request_owner == t) {
t->request_stream->request_owner = NULL;
}
if (t->response_stream && t->response_stream->response_owner == t) {
t->response_stream->response_owner = NULL;
}
if (t->client) {
scr_http_client_release(t->client);
t->client = NULL;
}
if (t->response_socket) {
scr_net_sock_release(t->response_socket);
t->response_socket = NULL;
t->response_paused = false;
}
for (SfTransfer **link = &sf_live; *link; link = &(*link)->next) {
if (*link == t) {
*link = t->next;
sf_release(t); /* registry reference */
return;
}
}
}
static void sf_reject(SfTransfer *t, const char *message) {
if (t->done) return;
sf_type_error(message);
scr_promise_reject_pending(t->promise);
sf_settle(t);
}
static void sf_reject_reason(SfTransfer *t, ScrDyn *reason) {
if (t->done) return;
sf_reject_promise_reason(t->promise, reason);
sf_settle(t);
}
static void sf_transfer_abort_watch(SfSignalWatch *w, SfSignal *signal) {
SfTransfer *t = w->owner;
if (t->done) return;
ScrDyn *reason = sf_signal_reason_ref(signal);
if (t->request_stream && t->request_stream->request_owner == t) {
SfStream *request = t->request_stream;
request->request_owner = NULL;
if (!request->close_requested && !request->closed && !request->error) {
request->request_discarding = true;
sf_stream_drain(request);
sf_stream_pull(request);
}
}
if (t->client) scr_http_client_destroy(t->client);
if (t->response_sent && t->response_stream) {
sf_stream_error(t->response_stream, reason);
sf_settle(t);
} else {
sf_reject_reason(t, reason);
}
scr_dyn_release(reason);
}
static void sf_transfer_stream_error(SfTransfer *t, ScrDyn *reason) {
if (!t || t->done) return;
if (t->client) scr_http_client_destroy(t->client);
if (t->response_sent && t->response_stream) {
sf_stream_error(t->response_stream, reason);
sf_settle(t);
} else {
sf_reject(t, "fetch failed");
}
}
static bool sf_eq_ci(const ScrStr *s, const char *lit) {
size_t n = strlen(lit);
if (s->len != n) return false;
for (size_t i = 0; i < n; i++) {
char c = s->data[i];
if (c >= 'A' && c <= 'Z') c = (char)(c - 'A' + 'a');
if (c != lit[i]) return false;
}
return true;
}
static ScrStr *sf_bare_host(const ScrStr *host) {
if (host->len >= 2 && host->data[0] == '[' &&
host->data[host->len - 1] == ']') {
return scr_str_new(host->data + 1, host->len - 2);
}
return scr_str_retain((ScrStr *)host);
}
static int sf_port(const ScrUrl *u, int dflt) {
if (u->port->len == 0) return dflt;
int p = 0;
for (size_t i = 0; i < u->port->len; i++) {
p = p * 10 + (u->port->data[i] - '0');
}
return p;
}
static bool sf_bad_port(int port) {
static const int ports[] = {SCR_FETCH_BAD_PORTS};
for (size_t i = 0; i < sizeof ports / sizeof ports[0]; i++) {
if (port == ports[i]) return true;
}
return false;
}
static ScrStr *sf_path(const ScrUrl *u) {
size_t plen = (u->path->len > 0 ? u->path->len : 1) + u->query->len;
char *buf = malloc(plen);
if (!buf) sf_oom();
size_t n = 0;
if (u->path->len > 0) {
memcpy(buf, u->path->data, u->path->len);
n = u->path->len;
} else {
buf[n++] = '/';
}
memcpy(buf + n, u->query->data, u->query->len);
n += u->query->len;
ScrStr *out = scr_str_new(buf, n);
free(buf);
return out;
}
static ScrStr *sf_url_serialize(const ScrUrl *u) {
size_t cap = u->scheme->len + u->userinfo->len + u->host->len +
u->port->len + u->path->len + u->query->len + 8;
char *buf = malloc(cap);
if (!buf) sf_oom();
size_t n = 0;
memcpy(buf + n, u->scheme->data, u->scheme->len);
n += u->scheme->len;
buf[n++] = ':';
if (u->has_authority) {
buf[n++] = '/';
buf[n++] = '/';
if (u->userinfo->len > 0) {
memcpy(buf + n, u->userinfo->data, u->userinfo->len);
n += u->userinfo->len;
buf[n++] = '@';
}
memcpy(buf + n, u->host->data, u->host->len);
n += u->host->len;
if (u->port->len > 0) {
buf[n++] = ':';
memcpy(buf + n, u->port->data, u->port->len);
n += u->port->len;
}
}
if (u->path->len > 0) {
memcpy(buf + n, u->path->data, u->path->len);
n += u->path->len;
} else {
buf[n++] = '/';
}
memcpy(buf + n, u->query->data, u->query->len);
n += u->query->len;
ScrStr *out = scr_str_new(buf, n);
free(buf);
return out;
}
static ScrUrl *sf_url_parse_quiet(ScrStr *text) {
ScrUrl *url = scr_url_new(text);
if (!url) scr_exc_clear();
return url;
}
static bool sf_proxy_enabled;
static char *sf_http_proxy;
static char *sf_https_proxy;
static char *sf_all_proxy;
static char *sf_no_proxy;
static char *sf_env_copy(const char *lower, const char *upper) {
const char *value = scr_getenv(lower);
if (!value || value[0] == '\0') value = scr_getenv(upper);
if (!value || value[0] == '\0') return NULL;
size_t len = strlen(value);
char *copy = malloc(len + 1);
if (!copy) sf_oom();
memcpy(copy, value, len + 1);
return copy;
}
static void sf_proxy_snapshot(void) {
const char *optin = scr_getenv("NODE_USE_ENV_PROXY");
sf_proxy_enabled = optin && strcmp(optin, "1") == 0;
if (!sf_proxy_enabled) return;
sf_http_proxy = sf_env_copy("http_proxy", "HTTP_PROXY");
sf_https_proxy = sf_env_copy("https_proxy", "HTTPS_PROXY");
sf_all_proxy = sf_env_copy("all_proxy", "ALL_PROXY");
sf_no_proxy = sf_env_copy("no_proxy", "NO_PROXY");
}
static void sf_proxy_snapshot_free(void) {
free(sf_http_proxy);
free(sf_https_proxy);
free(sf_all_proxy);
free(sf_no_proxy);
sf_http_proxy = sf_https_proxy = sf_all_proxy = sf_no_proxy = NULL;
}
static int sf_hex_value(char c) {
if (c >= '0' && c <= '9') return c - '0';
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
if (c >= 'A' && c <= 'F') return c - 'A' + 10;
return -1;
}
/* EnvHttpProxyAgent sends Basic proxy authentication only when the proxy
* URL has both a username and a password. */
static bool sf_proxy_has_credentials(const ScrStr *userinfo) {
const char *colon = memchr(userinfo->data, ':', userinfo->len);
return colon != NULL && colon != userinfo->data &&
colon + 1 < userinfo->data + userinfo->len;
}
/* EnvHttpProxyAgent derives Basic proxy authentication from the proxy URL's
* decoded username/password. ScrUrl keeps the encoded userinfo as one string,
* so decode it here. */
static ScrStr *sf_proxy_authorization(const ScrStr *userinfo) {
char *plain = malloc(userinfo->len + 1);
if (!plain) sf_oom();
size_t plain_len = 0;
for (size_t i = 0; i < userinfo->len;) {
char c = userinfo->data[i];
if (c == '%' && i + 2 < userinfo->len) {
int hi = sf_hex_value(userinfo->data[i + 1]);
int lo = sf_hex_value(userinfo->data[i + 2]);
if (hi >= 0 && lo >= 0) {
plain[plain_len++] = (char)((hi << 4) | lo);
i += 3;
continue;
}
}
plain[plain_len++] = c;
i++;
}
static const char b64[] =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
size_t encoded_len = ((plain_len + 2) / 3) * 4;
char *header = malloc(6 + encoded_len);
if (!header) sf_oom();
memcpy(header, "Basic ", 6);
size_t out = 6;
for (size_t i = 0; i < plain_len; i += 3) {
unsigned b0 = (unsigned char)plain[i];
unsigned b1 = i + 1 < plain_len ? (unsigned char)plain[i + 1] : 0;
unsigned b2 = i + 2 < plain_len ? (unsigned char)plain[i + 2] : 0;
unsigned triple = (b0 << 16) | (b1 << 8) | b2;
header[out++] = b64[(triple >> 18) & 63];
header[out++] = b64[(triple >> 12) & 63];
header[out++] = i + 1 < plain_len ? b64[(triple >> 6) & 63] : '=';
header[out++] = i + 2 < plain_len ? b64[triple & 63] : '=';
}
free(plain);
ScrStr *result = scr_str_new(header, out);
free(header);
return result;
}
static bool sf_no_proxy_match(const char *list, const ScrStr *host,
int port) {
const char *p = list;
while (*p) {
while (*p == ',' || *p == ' ' || *p == '\t') p++;
const char *start = p;
while (*p && *p != ',' && *p != ' ' && *p != '\t') p++;
size_t len = (size_t)(p - start);
if (len == 0) continue;
if (len == 1 && start[0] == '*') return true;
size_t host_len = len;
long entry_port = -1;
if (start[0] == '[') {
size_t close = 1;
while (close < len && start[close] != ']') close++;
if (close < len && close + 1 < len && start[close + 1] == ':') {
host_len = close + 1;
entry_port = strtol(start + close + 2, NULL, 10);
}
} else {
for (size_t i = 0; i < len; i++) {
if (start[i] == ':') {
host_len = i;
entry_port = strtol(start + i + 1, NULL, 10);
break;
}
}
}
if (host_len >= 2 && start[0] == '*' && start[1] == '.') {
start += 2;
host_len -= 2;
} else if (host_len > 0 && start[0] == '.') {
start++;
host_len--;
}
if (host_len == 0 || (entry_port >= 0 && entry_port != port)) {
continue;
}
size_t offset = host->len >= host_len ? host->len - host_len : 0;
bool boundary =
host->len == host_len ||
(host->len > host_len && host->data[offset - 1] == '.');
if (!boundary) continue;
bool equal = true;
for (size_t i = 0; i < host_len; i++) {
char a = host->data[offset + i];
char b = start[i];
if (a >= 'A' && a <= 'Z') a = (char)(a - 'A' + 'a');
if (b >= 'A' && b <= 'Z') b = (char)(b - 'A' + 'a');
if (a != b) {
equal = false;
break;
}
}
if (equal) return true;
}
return false;
}
static ScrUrl *sf_proxy_for(const ScrUrl *target, bool https,
int target_port, bool *invalid) {
*invalid = false;
if (!sf_proxy_enabled) return NULL;
const char *proxy = https ? sf_https_proxy : sf_http_proxy;
if (!proxy) proxy = sf_all_proxy;
if (!proxy) return NULL;
if (sf_no_proxy &&
sf_no_proxy_match(sf_no_proxy, target->host, target_port)) {
return NULL;
}
ScrStr *text = scr_str_new(proxy, strlen(proxy));
ScrUrl *url = sf_url_parse_quiet(text);
scr_str_release(text);
*invalid = url == NULL;
return url;
}
static bool sf_same_scheme_relative(const ScrUrl *base,
const ScrStr *location) {
if (location->len <= base->scheme->len ||
location->data[base->scheme->len] != ':') {
return false;
}
for (size_t i = 0; i < base->scheme->len; i++) {
char a = location->data[i];
char b = base->scheme->data[i];
if (a >= 'A' && a <= 'Z') a = (char)(a - 'A' + 'a');
if (b >= 'A' && b <= 'Z') b = (char)(b - 'A' + 'a');
if (a != b) return false;
}
size_t tail = base->scheme->len + 1;
return location->len - tail < 2 ||
!((location->data[tail] == '/' ||
location->data[tail] == '\\') &&
(location->data[tail + 1] == '/' ||
location->data[tail + 1] == '\\'));
}
static ScrUrl *sf_resolve_url(const ScrUrl *base, const ScrStr *location) {
bool same_scheme_relative = sf_same_scheme_relative(base, location);
const char *ref_data =
location->data +
(same_scheme_relative ? base->scheme->len + 1 : 0);
size_t ref_len =
location->len -
(same_scheme_relative ? base->scheme->len + 1 : 0);
if (!same_scheme_relative) {
ScrStr *absolute = scr_str_new(location->data, location->len);
ScrUrl *parsed = sf_url_parse_quiet(absolute);
scr_str_release(absolute);
if (parsed) return parsed;
}
/*
* Empty and fragment-only references preserve the complete base path
* and query. The request serializer drops the resulting fragment, but
* resolving it first is significant: `Location: #next` re-requests
* `/a/start`, not its parent `/a/`.
*/
if (ref_len == 0 || ref_data[0] == '#') {
ScrStr *base_text = sf_url_serialize(base);
size_t len = base_text->len + ref_len;
char *joined = malloc(len);
if (!joined) sf_oom();
memcpy(joined, base_text->data, base_text->len);
memcpy(joined + base_text->len, ref_data, ref_len);
ScrStr *text = scr_str_new(joined, len);
free(joined);
scr_str_release(base_text);
ScrUrl *out = sf_url_parse_quiet(text);
scr_str_release(text);
return out;
}
size_t cap = base->scheme->len + base->userinfo->len + base->host->len +
base->port->len + base->path->len + base->query->len +
ref_len + 16;
char *buf = malloc(cap);
if (!buf) sf_oom();
size_t n = 0;
memcpy(buf + n, base->scheme->data, base->scheme->len);
n += base->scheme->len;
buf[n++] = ':';
if (ref_len >= 2 &&
(ref_data[0] == '/' || ref_data[0] == '\\') &&
(ref_data[1] == '/' || ref_data[1] == '\\')) {
memcpy(buf + n, ref_data, ref_len);
n += ref_len;
} else {
buf[n++] = '/';
buf[n++] = '/';
if (base->userinfo->len > 0) {
memcpy(buf + n, base->userinfo->data, base->userinfo->len);
n += base->userinfo->len;
buf[n++] = '@';
}
memcpy(buf + n, base->host->data, base->host->len);
n += base->host->len;
if (base->port->len > 0) {
buf[n++] = ':';
memcpy(buf + n, base->port->data, base->port->len);
n += base->port->len;
}
if (ref_len > 0 &&
(ref_data[0] == '/' || ref_data[0] == '\\')) {
memcpy(buf + n, ref_data, ref_len);
n += ref_len;
} else if (ref_len > 0 && ref_data[0] == '?') {
memcpy(buf + n, base->path->data, base->path->len);
n += base->path->len;
memcpy(buf + n, ref_data, ref_len);
n += ref_len;
} else {
size_t keep = 0;
for (size_t i = 0; i < base->path->len; i++) {
if (base->path->data[i] == '/') keep = i + 1;
}
memcpy(buf + n, base->path->data, keep);
n += keep;
if (keep == 0) buf[n++] = '/';
memcpy(buf + n, ref_data, ref_len);
n += ref_len;
}
}
ScrStr *text = scr_str_new(buf, n);
free(buf);
ScrUrl *out = sf_url_parse_quiet(text);
scr_str_release(text);
return out;
}
static bool sf_pairs_have(ScrArr *pairs, const char *name) {
size_t n = (size_t)scr_arr_len(pairs);
size_t want = strlen(name);
for (size_t i = 0; i + 1 < n; i += 2) {
ScrStr *key = sf_pair_at(pairs, i);
bool same = key->len == want;
for (size_t j = 0; j < want && same; j++) {
char c = key->data[j];
if (c >= 'A' && c <= 'Z') c = (char)(c - 'A' + 'a');
same = c == name[j];
}
if (same) return true;
}
return false;
}
static bool sf_header_name_ok(const char *s, size_t len) {
if (len == 0) return false;
for (size_t i = 0; i < len; i++) {
unsigned char c = (unsigned char)s[i];
if (!((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
(c >= '0' && c <= '9') ||
strchr("!#$%&'*+-.^_`|~", c) != NULL)) {
return false;
}
}
return true;
}
static bool sf_header_value_ok(const ScrStr *value) {
for (size_t i = 0; i < value->len; i++) {
unsigned char c = (unsigned char)value->data[i];
if ((c < 0x20 && c != '\t') || c == 0x7f) return false;
}
return true;
}
static ScrStr *sf_header_value_bytestring(ScrStr *value) {
/* An ASCII value is its own ByteString: each byte is one UTF-16 unit. */
size_t ascii = 0;
while (ascii < value->len && (unsigned char)value->data[ascii] < 0x80) ascii++;
if (ascii == value->len) return scr_str_retain(value);
size_t len = (size_t)scr_str_utf16_len(value);
char *bytes = malloc(len ? len : 1);
if (!bytes) sf_oom();
for (size_t i = 0; i < len; i++) {
double code = scr_str_char_code_at(value, (double)i);
if (code > 255) {
char message[192];
int message_len = snprintf(
message, sizeof message,
"Cannot convert argument to a ByteString because the character "
"at index %zu has a value of %.0f which is greater than 255.",
i, code);
free(bytes);
if (message_len < 0 || (size_t)message_len >= sizeof message) {
sf_oom();
}
sf_type_error(message);
return NULL;
}
bytes[i] = (char)(unsigned char)code;
}
ScrStr *out = scr_str_new(bytes, len);
free(bytes);
return out;
}
static bool sf_header_name_ci(
const char *name, size_t name_len, const char *want) {
size_t want_len = strlen(want);
if (name_len != want_len) return false;
for (size_t i = 0; i < name_len; i++) {
char c = name[i];
if (c >= 'A' && c <= 'Z') c = (char)(c - 'A' + 'a');
if (c != want[i]) return false;
}
return true;
}
/* Undici's connection rule: a comma list whose trimmed entries are all
* HTTP tokens. Answers whether one of them is "close" in *close. */
static bool sf_connection_tokens(const ScrStr *value, bool *close) {
*close = false;
size_t start = 0;
for (size_t i = 0; i <= value->len; i++) {
if (i < value->len && value->data[i] != ',') continue;
size_t a = start, b = i;
while (a < b && (value->data[a] == ' ' || value->data[a] == '\t')) a++;
while (b > a && (value->data[b - 1] == ' ' || value->data[b - 1] == '\t')) b--;
if (!sf_header_name_ok(value->data + a, b - a)) return false;
if (sf_header_name_ci(value->data + a, b - a, "close")) *close = true;
start = i + 1;
}
return true;
}
static bool sf_request_header_ok(
const char *name, size_t name_len, const ScrStr *value) {
if (sf_header_name_ci(name, name_len, "connection")) {
bool close;
return sf_connection_tokens(value, &close);
}
return
!sf_header_name_ci(name, name_len, "transfer-encoding") &&
!sf_header_name_ci(name, name_len, "keep-alive") &&
!sf_header_name_ci(name, name_len, "upgrade") &&
!sf_header_name_ci(name, name_len, "expect");
}
/* Whether every value in `pairs` is already trimmed (sf_trim_header_value
* would keep it): the form a copy can take unchanged (SfHeaders.canonical).
* Combining duplicate names with an empty value leaves the separator's
* space at the end of the combined value, which a copy trims again. */
static bool sf_header_values_trimmed(ScrArr *pairs) {
size_t n = (size_t)scr_arr_len(pairs);
for (size_t i = 1; i < n; i += 2) {
const ScrStr *value = sf_pair_at(pairs, i);
if (value->len == 0) continue;
char first = value->data[0], last = value->data[value->len - 1];
if (first == ' ' || first == '\t' || last == ' ' || last == '\t') return false;
}
return true;
}
static ScrStr *sf_trim_header_value(const ScrStr *value) {
size_t start = 0;
size_t end = value->len;
while (start < end &&
(value->data[start] == ' ' || value->data[start] == '\t')) {
start++;
}
while (end > start &&
(value->data[end - 1] == ' ' || value->data[end - 1] == '\t')) {
end--;
}
if (start == 0 && end == value->len) return scr_str_retain((ScrStr *)value);
return scr_str_new(value->data + start, end - start);
}
static bool sf_push_header(ScrArr *pairs, ScrStr *name_str,
ScrStr *value, bool request_guard) {
const char *name = name_str->data;
size_t name_len = name_str->len;
if (!sf_header_name_ok(name, name_len)) {
ScrStr *display = scr_str_new(name, name_len);
sf_headers_invalid("append", display, "name");
scr_str_release(display);
return false;
}
ScrStr *byte_value = sf_header_value_bytestring(value);
if (!byte_value) return false;
if (!sf_header_value_ok(byte_value)) {
ScrStr *display = sf_latin1_to_utf8(byte_value);
sf_headers_invalid("append", display, "value");
scr_str_release(display);
scr_str_release(byte_value);
return false;
}
ScrStr *normalized = sf_trim_header_value(byte_value);
scr_str_release(byte_value);
if (!request_guard) {
ScrStr *utf8 = sf_latin1_to_utf8(normalized);
scr_str_release(normalized);
normalized = utf8;
}
if (!sf_header_name_ci(name, name_len, "set-cookie")) {
size_t n = (size_t)scr_arr_len(pairs);
for (size_t i = 0; i + 1 < n; i += 2) {
ScrStr *key = sf_pair_at(pairs, i);
bool match = key->len == name_len;
for (size_t j = 0; j < name_len && match; j++) {
unsigned char a = (unsigned char)key->data[j];
unsigned char b = (unsigned char)name[j];
if (a >= 'A' && a <= 'Z') a = (unsigned char)(a - 'A' + 'a');
if (b >= 'A' && b <= 'Z') b = (unsigned char)(b - 'A' + 'a');
match = a == b;
}
if (!match) continue;
ScrStr *previous = sf_pair_at(pairs, i + 1);
const char *separator =
sf_header_name_ci(name, name_len, "cookie") ? "; " : ", ";
size_t separator_len = 2;
size_t joined_len =
previous->len + separator_len + normalized->len;
char *joined_data = malloc(joined_len ? joined_len : 1);
if (!joined_data) sf_oom();
memcpy(joined_data, previous->data, previous->len);
memcpy(joined_data + previous->len, separator, separator_len);
memcpy(joined_data + previous->len + separator_len,
normalized->data, normalized->len);
ScrStr *joined = scr_str_new(joined_data, joined_len);
free(joined_data);
scr_str_release(normalized);
if (request_guard && !sf_request_header_ok(name, name_len, joined)) {
scr_str_release(joined);
sf_type_error("fetch failed");
return false;
}
scr_arr_set_ref(pairs, (double)(i + 1), joined);
return true;
}
}
if (request_guard && !sf_request_header_ok(name, name_len, normalized)) {
scr_str_release(normalized);
sf_type_error("fetch failed");
return false;
}
scr_arr_push_ref(
pairs,
request_guard
? scr_str_retain(name_str)
: sf_header_name_lower_str(name_str));
scr_arr_push_ref(pairs, normalized);
return true;
}
static void sf_headers_constructor_type_error(void) {
static const char message[] =
"Headers constructor: Argument 1 could not be converted to one of: "
"sequence<sequence<ByteString>>, record<ByteString, ByteString>.";
sf_type_error(message);
}
static void sf_headers_pair_length_error(size_t len) {
char message[96];
int n = snprintf(
message, sizeof message,
"Headers constructor: expected name/value pair to be length 2, "
"found %zu.", len);
if (n < 0 || (size_t)n >= sizeof message) sf_oom();
sf_type_error(message);
}
/* The WebIDL HeadersInit conversion phase. It performs the observable
* sequence/record walk and every ByteString coercion, but deliberately
* leaves header-name/value syntax validation for initialization. Response
* construction converts this snapshot before status/statusText and only
* validates it after body extraction, matching Undici's two phases. */
/* *canonical_out reports that the result copies a canonical Headers
* object's pairs (SfHeaders.canonical). */
static ScrArr *sf_headers_convert_init(const ScrDyn *headers, bool *canonical_out) {
*canonical_out = false;
if (headers && headers->kind == SCR_DYN_TYPED_REF) {
ScrDyn *materialized = scr_dyn_typed_ref_materialize(headers);
ScrArr *converted = sf_headers_convert_init(materialized, canonical_out);
scr_dyn_release(materialized);
return converted;
}
ScrArr *converted = scr_arr_new(SCR_ELEM_STR, 8);
if (!headers || headers->kind == SCR_DYN_UNDEF) {
return converted;
}
if (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 *raw_name = scr_str_new(e->key, e->key_len);
ScrStr *name_bytes = sf_header_value_bytestring(raw_name);
ScrStr *name = name_bytes ? sf_latin1_to_utf8(name_bytes) : NULL;
scr_str_release(name_bytes);
scr_str_release(raw_name);
ScrDyn *source = scr_dyn_retain(e->value);
ScrStr *raw_value = name ? scr_dyn_string_coerce_js(source) : NULL;
scr_dyn_release(source);
ScrStr *value_bytes = raw_value
? sf_header_value_bytestring(raw_value)
: NULL;
ScrStr *value = value_bytes ? sf_latin1_to_utf8(value_bytes) : NULL;
scr_str_release(value_bytes);
scr_str_release(raw_value);
if (!name || !value) {
scr_str_release(name);
scr_str_release(value);
goto fail;
}
scr_arr_push_ref(converted, name);
scr_arr_push_ref(converted, value);
}
return converted;
}
if (headers->kind == SCR_DYN_ARR) {
for (size_t i = 0; i < headers->v.arr.len; i++) {
ScrDyn *pair = scr_dyn_retain(headers->v.arr.items[i]);
if (pair->kind == SCR_DYN_TYPED_REF) {
ScrDyn *materialized = scr_dyn_typed_ref_materialize(pair);
scr_dyn_release(pair);
pair = materialized;
}
if (pair->kind != SCR_DYN_ARR || pair->v.arr.len != 2) {
bool sequence = pair->kind == SCR_DYN_ARR;
size_t pair_len = pair->kind == SCR_DYN_ARR ? pair->v.arr.len : 0;
scr_dyn_release(pair);
if (!scr_exc_pending()) {
if (sequence) {
sf_headers_pair_length_error(pair_len);
} else {
sf_headers_constructor_type_error();
}
}
goto fail;
}
ScrDyn *name_source = scr_dyn_retain(pair->v.arr.items[0]);
ScrDyn *value_source = scr_dyn_retain(pair->v.arr.items[1]);
scr_dyn_release(pair);
ScrStr *raw_name = scr_dyn_string_coerce_js(name_source);
scr_dyn_release(name_source);
ScrStr *name_bytes = raw_name
? sf_header_value_bytestring(raw_name)
: NULL;
ScrStr *name = name_bytes ? sf_latin1_to_utf8(name_bytes) : NULL;
scr_str_release(name_bytes);
scr_str_release(raw_name);
ScrStr *raw_value = name
? scr_dyn_string_coerce_js(value_source)
: NULL;
scr_dyn_release(value_source);
ScrStr *value_bytes = raw_value
? sf_header_value_bytestring(raw_value)
: NULL;
ScrStr *value = value_bytes ? sf_latin1_to_utf8(value_bytes) : NULL;
scr_str_release(value_bytes);
scr_str_release(raw_value);
if (!name || !value) {
scr_str_release(name);
scr_str_release(value);
goto fail;
}
scr_arr_push_ref(converted, name);
scr_arr_push_ref(converted, value);
}
return converted;
}
if (headers->kind == SCR_DYN_HANDLE &&
headers->v.handle.tag == SCR_DYNH_FETCH_HEADERS) {
SfHeaders *source = (SfHeaders *)headers->v.handle.ptr;
size_t n = (size_t)scr_arr_len(source->pairs);
for (size_t i = 0; i < n - n % 2; i++)
scr_arr_push_ref(converted, scr_str_retain(sf_pair_at(source->pairs, i)));
*canonical_out = source->canonical;
return converted;
}
/* Functions are WebIDL objects and an ordinary function has no
* enumerable HeadersInit entries. The checked-dynamic function model's
* expando properties are non-enumerable by construction. */
if (headers->kind == SCR_DYN_FUNC) return converted;
sf_headers_constructor_type_error();
fail:
scr_arr_release(converted);
return NULL;
}
static bool sf_add_converted_headers(
ScrArr *pairs, ScrArr *converted, bool request_guard, bool canonical) {
size_t n = (size_t)scr_arr_len(converted);
/* Pushing canonical pairs into an empty list one by one through
* sf_push_header would reproduce them exactly. */
if (canonical && !request_guard && scr_arr_len(pairs) == 0) {
for (size_t i = 0; i < n; i++)
scr_arr_push_ref(pairs, scr_str_retain(sf_pair_at(converted, i)));
return true;
}
for (size_t i = 0; i + 1 < n; i += 2) {
if (!sf_push_header(pairs, sf_pair_at(converted, i),
sf_pair_at(converted, i + 1), request_guard))
return false;
}
return true;
}
static bool sf_add_headers(
ScrArr *pairs, const ScrDyn *headers, bool request_guard) {
bool canonical = false;
ScrArr *converted = sf_headers_convert_init(headers, &canonical);
if (!converted) return false;
bool ok = sf_add_converted_headers(pairs, converted, request_guard, canonical);
scr_arr_release(converted);
return ok;
}
static void sf_push_header_text(ScrArr *pairs, const char *name,
const char *value, size_t value_len) {
scr_arr_push_ref(pairs, scr_str_new(name, strlen(name)));
scr_arr_push_ref(pairs, scr_str_new(value, value_len));
}
static void sf_strip_header(ScrArr **headers, const char *name) {
ScrArr *old = *headers;
size_t n = (size_t)scr_arr_len(old);
ScrArr *out = scr_arr_new(SCR_ELEM_STR, n);
for (size_t i = 0; i + 1 < n; i += 2) {
ScrStr *key = (ScrStr *)scr_arr_get_ref(old, (double)i);
if (sf_eq_ci(key, name)) {
scr_str_release(key);
continue;
}
scr_arr_push_ref(out, key);
scr_arr_push_ref(out, scr_arr_get_ref(old, (double)(i + 1)));
}
scr_arr_release(old);
*headers = out;
}
static bool sf_content_length(ScrArr *headers, bool *present_out,
size_t *length_out) {
bool present = false;
size_t length = 0;
size_t n = (size_t)scr_arr_len(headers);
for (size_t i = 0; i + 1 < n; i += 2) {
ScrStr *name = scr_arr_get_ref(headers, (double)i);
ScrStr *value = scr_arr_get_ref(headers, (double)(i + 1));
if (!sf_eq_ci(name, "content-length")) {
scr_str_release(name);
scr_str_release(value);
continue;
}
if (present) {
scr_str_release(name);
scr_str_release(value);
sf_type_error("fetch failed");
return false;
}
size_t start = 0;
size_t end = value->len;
while (start < end &&
(value->data[start] == ' ' || value->data[start] == '\t')) {
start++;
}
while (end > start &&
(value->data[end - 1] == ' ' ||
value->data[end - 1] == '\t')) {
end--;
}
bool valid = start < end;
size_t parsed = 0;
for (size_t j = start; j < end && valid; j++) {
unsigned char c = (unsigned char)value->data[j];
if (c < '0' || c > '9') {
valid = false;
break;
}
size_t digit = (size_t)(c - '0');
if (parsed > (SIZE_MAX - digit) / 10) {
valid = false;
break;
}
parsed = parsed * 10 + digit;
}
scr_str_release(name);
scr_str_release(value);
if (!valid) {
sf_type_error("fetch failed");
return false;
}
present = true;
length = parsed;
}
*present_out = present;
*length_out = length;
return true;
}
/* RequestInit dictionary conversion coerces every member before Request's
* method validation runs. Keep ToString separate so later members
* (redirect and signal) are still observed when the resulting method is
* syntactically invalid or forbidden. */
static ScrStr *sf_method_value(const ScrDyn *init) {
const ScrDyn *value =
init && init->kind == SCR_DYN_OBJ
? scr_dyn_obj_get(init, "method", 6)
: NULL;
if (!value || value->kind == SCR_DYN_UNDEF) return scr_str_new("GET", 3);
return scr_dyn_string_coerce_js(value);
}
static bool sf_method_validate(ScrStr **method_out) {
ScrStr *method = *method_out;
static const char *normalized[] = {
"DELETE", "GET", "HEAD", "OPTIONS", "POST", "PUT"};
for (size_t i = 0; i < sizeof normalized / sizeof normalized[0]; i++) {
const char *candidate = normalized[i];
size_t n = strlen(candidate);
if (method->len != n) continue;
bool same = true;
for (size_t j = 0; j < n && same; j++) {
char c = method->data[j];
if (c >= 'a' && c <= 'z') c = (char)(c - 'a' + 'A');
same = c == candidate[j];
}
if (same) {
scr_str_release(method);
*method_out = scr_str_new(candidate, n);
return true;
}
}
bool valid = method->len > 0;
for (size_t i = 0; i < method->len && valid; i++) {
unsigned char c = (unsigned char)method->data[i];
valid = (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
(c >= '0' && c <= '9') ||
strchr("!#$%&'*+-.^_`|~", c) != NULL;
}
if (!valid) {
sf_type_error("fetch failed");
return false;
}
if (sf_eq_ci(method, "connect") || sf_eq_ci(method, "trace") ||
sf_eq_ci(method, "track")) {
size_t message_len = method->len + 31;
char *message = malloc(message_len + 1);
if (!message) sf_oom();
int len = snprintf(message, message_len + 1, "'%.*s' HTTP method is unsupported.",
(int)method->len, method->data);
if (len < 0 || (size_t)len > message_len) sf_oom();
sf_type_error(message);
free(message);
return false;
}
return true;
}
static bool sf_redirect_mode(const ScrDyn *init, int *out) {
const ScrDyn *value =
init && init->kind == SCR_DYN_OBJ
? scr_dyn_obj_get(init, "redirect", 8)
: NULL;
if (!value || value->kind == SCR_DYN_UNDEF) {
*out = SF_REDIRECT_FOLLOW;
return true;
}
ScrStr *mode = scr_dyn_string_coerce_js(value);
if (!mode) return false;
if (mode->len == 6 && memcmp(mode->data, "follow", 6) == 0) {
*out = SF_REDIRECT_FOLLOW;
scr_str_release(mode);
return true;
}
if (mode->len == 5 && memcmp(mode->data, "error", 5) == 0) {
*out = SF_REDIRECT_ERROR;
scr_str_release(mode);
return true;
}
if (mode->len == 6 && memcmp(mode->data, "manual", 6) == 0) {
*out = SF_REDIRECT_MANUAL;
scr_str_release(mode);
return true;
}
static const char prefix[] = "undefined: ";
static const char suffix[] =
" is not an accepted type. Expected one of follow, manual, error.";
size_t message_len =
sizeof prefix - 1 + mode->len + sizeof suffix - 1;
char *message = malloc(message_len + 1);
if (!message) sf_oom();
memcpy(message, prefix, sizeof prefix - 1);
memcpy(message + sizeof prefix - 1, mode->data, mode->len);
memcpy(message + sizeof prefix - 1 + mode->len,
suffix, sizeof suffix - 1);
message[message_len] = '\0';
scr_str_release(mode);
sf_type_error(message);
free(message);
return false;
}
static ScrPromise *sf_reject_now(ScrPromise *promise, const char *message) {
if (!scr_exc_pending()) sf_type_error(message);
scr_promise_reject_pending(promise);
return promise;
}
static void sf_request_length_mismatch(SfTransfer *t) {
if (t->client) scr_http_client_destroy(t->client);
sf_reject(t, "fetch failed");
}
static void sf_stream_request_flush(SfStream *s) {
SfTransfer *t = s->request_owner;
if (!t || t->done || !t->client) return;
while (s->head) {
SfChunk *c = s->head;
s->head = c->next;
if (!s->head) s->tail = NULL;
s->queued--;
ScrBytes *bytes = sf_chunk_bytes(c->value);
if (!bytes) {
sf_chunk_release(c);
sf_stream_drop_chunks(s);
ScrCaught *caught = scr_exc_take();
ScrDyn *reason = scr_caught_to_dyn(caught);
sf_transfer_stream_error(t, reason);
scr_dyn_release(reason);
scr_caught_release(caught);
return;
}
if (t->request_has_content_length &&
(t->request_body_sent > t->request_content_length ||
bytes->len >
t->request_content_length - t->request_body_sent)) {
scr_bytes_release(bytes);
sf_chunk_release(c);
sf_stream_drop_chunks(s);
sf_request_length_mismatch(t);
return;
}
t->request_body_sent += bytes->len;
scr_http_client_write_bytes(t->client, bytes);
scr_bytes_release(bytes);
sf_chunk_release(c);
}
/* A response body reused as an upload may have paused its source socket
* before fetch attached this consumer. Draining its queue creates demand
* just as a reader.read() does, so let the source transport deliver more. */
sf_response_resume_if_ready(s);
if (s->error) {
sf_transfer_stream_error(t, s->error);
} else if (s->close_requested) {
if (t->request_has_content_length &&
t->request_body_sent != t->request_content_length) {
sf_request_length_mismatch(t);
return;
}
sf_stream_finish_close(s);
scr_http_client_end(t->client);
}
}
static bool sf_start_hop(SfTransfer *t);
static bool sf_redirect(SfTransfer *t, int status, const ScrStr *location);
static void sf_response_terminate(SfTransfer *t) {
if (!t || t->done || !t->response_stream) return;
if (t->client) scr_http_client_destroy(t->client);
ScrError *e = NULL;
ScrDyn *reason = sf_type_reason("terminated", &e);
sf_stream_error(t->response_stream, reason);
scr_dyn_release(reason);
scr_error_release(e);
sf_settle(t);
}
static void sf_inflate_append(
SfTransfer *t, const unsigned char *data, size_t len) {
if (len == 0) return;
size_t prior = t->inflate_pending ? t->inflate_pending->len : 0;
if (len > SIZE_MAX - prior) sf_oom();
ScrBytes *combined =
scr_bytes_new(SCR_BYTES_U8, (double)(prior + len));
if (prior > 0) {
memcpy(combined->data, t->inflate_pending->data, prior);
}
memcpy(combined->data + prior, data, len);
scr_bytes_release(t->inflate_pending);
t->inflate_pending = combined;
}
/*
* Inflate only while the decoded stream has demand. zlib keeps a small
* amount of output internally, while the unconsumed compressed suffix lives
* in inflate_pending so the network callback's borrowed chunk can return.
*/
static void sf_inflate_process(SfTransfer *t) {
if (!t || t->done || t->inflate_draining || !t->response_stream ||
t->response_stream->discarded ||
(!t->inflate_pending && !t->inflate_needs_drain) ||
t->response_stream->queued >= 1) {
return;
}
t->inflate_draining = true;
ScrBytes *input = t->inflate_pending;
t->inflate_pending = NULL;
t->zs.next_in = input ? (Bytef *)input->data : Z_NULL;
t->zs.avail_in = input ? (uInt)input->len : 0;
bool drain_internal = t->inflate_needs_drain;
t->inflate_needs_drain = false;
unsigned char out[16384];
while (!t->done && !t->response_stream->discarded &&
t->response_stream->queued < 1 &&
(t->zs.avail_in > 0 || drain_internal)) {
if (t->inflate_member_end) {
if (t->zs.avail_in == 0) break;
if (inflateReset(&t->zs) != Z_OK) {
sf_response_terminate(t);
break;
}
t->inflate_member_end = false;
}
t->zs.next_out = out;
t->zs.avail_out = sizeof out;
int result = inflate(&t->zs, Z_NO_FLUSH);
size_t produced = sizeof out - t->zs.avail_out;
drain_internal = false;
if (produced > 0) {
ScrBytes *decoded = scr_bytes_new(SCR_BYTES_U8, (double)produced);
memcpy(decoded->data, out, produced);
sf_stream_enqueue_bytes(t->response_stream, decoded);
scr_bytes_release(decoded);
}
if (result == Z_STREAM_END) {
/* RFC 1952 permits concatenated gzip members. */
t->inflate_member_end = true;
if (t->zs.avail_in == 0) break;
continue;
}
if (result != Z_OK && result != Z_BUF_ERROR) {
sf_response_terminate(t);
break;
}
if (t->done || t->response_stream->discarded) break;
if (t->response_stream->queued >= 1) {
if (result == Z_OK && t->zs.avail_out == 0) {
t->inflate_needs_drain = true;
}
break;
}
if (result == Z_BUF_ERROR && produced == 0) break;
if (t->zs.avail_in == 0) {
if (result == Z_OK && t->zs.avail_out == 0) {
drain_internal = true;
continue;
}
break;
}
}
if (!t->done && !t->response_stream->discarded &&
t->zs.avail_in > 0) {
sf_inflate_append(t, t->zs.next_in, t->zs.avail_in);
}
t->zs.next_in = Z_NULL;
t->zs.avail_in = 0;
if (t->response_stream->discarded) {
scr_bytes_release(t->inflate_pending);
t->inflate_pending = NULL;
t->inflate_needs_drain = false;
}
scr_bytes_release(input);
t->inflate_draining = false;
}
static void sf_inflate_finish_if_ended(SfTransfer *t) {
if (!t || t->done || !t->response_ended || t->inflate_draining ||
!t->response_stream) {
return;
}
if (t->response_stream->discarded) {
sf_stream_close(t->response_stream);
sf_settle(t);
return;
}
if (t->inflate_pending || t->inflate_needs_drain) {
if (t->response_stream->queued >= 1) return;
sf_inflate_process(t);
if (t->done) return;
if (t->inflate_pending || t->inflate_needs_drain) {
/*
* Node fetch accepts a clean HTTP EOF even when the compressed
* representation ends before zlib reaches Z_STREAM_END, exposing the
* decoded prefix. Discard the suffix that still needs wire input.
*/
scr_bytes_release(t->inflate_pending);
t->inflate_pending = NULL;
t->inflate_needs_drain = false;
}
}
sf_stream_close(t->response_stream);
sf_settle(t);
}
static void sf_inflate_resume(SfTransfer *t) {
if (!t || t->done || t->inflate_draining) return;
sf_retain(t);
sf_inflate_process(t);
sf_inflate_finish_if_ended(t);
if (!t->done && t->response_paused && t->response_socket &&
t->response_stream && t->response_stream->queued < 1 &&
!t->response_stream->close_requested &&
!t->response_stream->closed && !t->response_stream->error) {
t->response_paused = false;
scr_net_sock_release(scr_net_sock_resume(t->response_socket));
}
sf_release(t);
}
static void sf_on_data(ScrClosure *cb, ScrBytes *chunk) {
SfTransfer *t = sf_from(cb);
if (!t) return;
if (t->done || !t->response_stream) {
sf_release(t);
return;
}
/*
* Fetch exposes no body for HEAD/204/205/304 responses. The shared HTTP
* parser still drains an invalid payload on 205, so discard those bytes
* here instead of queueing them into an inaccessible stream that would
* apply backpressure forever.
*/
if (t->response_null_body) {
sf_release(t);
return;
}
if (t->response_stream->discarded) {
sf_release(t);
return;
}
if (!t->inflating) {
sf_stream_enqueue_bytes(t->response_stream, chunk);
sf_release(t);
return;
}
SCR_BYTES_SNAPSHOT(chunk);
sf_inflate_append(t, chunk->data, chunk->len);
sf_inflate_process(t);
sf_release(t);
}
static void sf_on_end(ScrClosure *cb) {
SfTransfer *t = sf_from(cb);
if (!t) return;
if (!t->done) {
if (t->inflating && t->response_stream &&
!t->response_stream->discarded) {
t->response_ended = true;
sf_inflate_finish_if_ended(t);
} else {
if (t->response_stream) sf_stream_close(t->response_stream);
sf_settle(t);
}
}
sf_release(t);
}
static void sf_on_res_error(ScrClosure *cb, ScrStr *message) {
(void)message;
SfTransfer *t = sf_from(cb);
if (!t) return;
if (!t->done && t->response_stream) {
sf_response_terminate(t);
} else {
sf_reject(t, "fetch failed");
}
sf_release(t);
}
static bool sf_response_has_null_body(const SfTransfer *t, int status) {
return sf_eq_ci(t->method, "head") || status == 101 ||
status == 204 || status == 205 || status == 304;
}
static void sf_on_response(ScrClosure *cb, ScrHttpReq *res) {
SfTransfer *t = sf_from(cb);
if (!t) {
scr_http_req_release(res);
return;
}
if (t->done) {
scr_http_req_release(res);
sf_release(t);
return;
}
int status = (int)scr_http_req_status(res);
if (status == 301 || status == 302 || status == 303 ||
status == 307 || status == 308) {
ScrStr *name = scr_str_new("location", 8);
ScrStr *location = scr_http_req_header(res, name);
scr_str_release(name);
if (location) {
if (t->redirect_mode == SF_REDIRECT_MANUAL) {
scr_str_release(location);
} else {
ScrHttpClientReq *old = t->client;
t->client = NULL;
if (old) {
scr_http_client_destroy(old);
scr_http_client_release(old);
}
if (t->redirect_mode == SF_REDIRECT_ERROR) {
sf_reject(t, "fetch failed");
scr_str_release(location);
scr_http_req_release(res);
sf_release(t);
return;
}
ScrStr *decoded_location = sf_location_to_utf8(location);
bool follow = sf_redirect(t, status, decoded_location);
scr_str_release(decoded_location);
scr_str_release(location);
if (follow) sf_start_hop(t);
scr_http_req_release(res);
sf_release(t);
return;
}
}
}
bool null_body = sf_response_has_null_body(t, status);
t->response_null_body = null_body;
if (!null_body) {
ScrStr *name = scr_str_new("content-encoding", 16);
ScrStr *encoding = scr_http_req_header(res, name);
scr_str_release(name);
if (encoding) {
if (sf_eq_ci(encoding, "gzip") || sf_eq_ci(encoding, "x-gzip") ||
sf_eq_ci(encoding, "deflate")) {
memset(&t->zs, 0, sizeof t->zs);
if (inflateInit2(&t->zs, 15 + 32) != Z_OK) sf_oom();
t->inflating = true;
}
scr_str_release(encoding);
}
}
SfStream *body = sf_stream_new_native();
body->response_owner = t;
t->response_stream = sf_stream_retain(body);
t->response_socket = scr_http_req_socket(res);
SfResponse *response = calloc(1, sizeof *response);
if (!response) sf_oom();
response->rc = 1;
response->body = sf_stream_retain(body);
response->headers =
sf_headers_new_response(scr_http_req_header_lines(res));
response->url = sf_url_serialize(t->url);
ScrStr *raw_status_text = scr_http_req_status_message(res);
response->status_text =
raw_status_text ? sf_latin1_to_utf8(raw_status_text)
: scr_str_new("", 0);
scr_str_release(raw_status_text);
response->status = status;
response->redirected = t->redirected;
response->null_body = null_body;
ScrDyn *boxed = scr_dyn_new_handle(response, SCR_DYNH_FETCH_RESPONSE);
sf_response_release(response);
sf_stream_release(body);
scr_http_req_on_data(res, sf_closure(t, (void *)&sf_on_data),
&sf_on_data, false);
scr_http_req_on_end(res, sf_closure(t, (void *)&sf_on_end), false);
scr_http_req_on_error(res, sf_closure(t, (void *)&sf_on_res_error),
&sf_on_res_error, false);
t->response_sent = true;
ScrPromise *promise = t->promise;
t->promise = NULL;
scr_promise_fulfill_ref(promise, boxed, &scr_dyn_retain_v,
&scr_dyn_release_v, NULL);
scr_promise_release(promise);
scr_http_req_release(res);
sf_release(t);
}
typedef struct { int value; const char *name; } SfUvError;
#define SF_UV_ERROR(name, message) { SCR_NODE_UV__##name, #name },
static const SfUvError sf_uv_errors[] = { SCR_NODE_UV_ERRNO_MAP(SF_UV_ERROR) };
#undef SF_UV_ERROR
/* libuv's errno value for a code name; getaddrinfo's ENOTFOUND is
* EAI_NONAME. 0 when the name is not a libuv code. */
static int sf_uv_errno(const char *name) {
if (strcmp(name, "ENOTFOUND") == 0) return SCR_NODE_UV__EAI_NONAME;
for (size_t i = 0; i < sizeof sf_uv_errors / sizeof sf_uv_errors[0]; i++) {
if (strcmp(sf_uv_errors[i].name, name) == 0) return sf_uv_errors[i].value;
}
return 0;
}
static const char sf_tunnel_prefix[] = "Proxy response (";
static const char sf_not_http_prefix[] = "Response does not match the HTTP/1.1 protocol";
/* The cause Node's fetch gives a transport failure, built from the
* runtime's failure message. "<syscall> <CODE>[ <detail>]" messages
* become Node's system error with code, errno and syscall (plus address
* and port for connect, hostname for getaddrinfo). A tunnel the proxy
* refused becomes undici's DOMException "Request was cancelled." whose
* cause is the AbortError carrying the proxy status, and a proxy reply
* that is not HTTP becomes the HTTPParserError. NULL (no cause) for
* every other message. +1. */
static ScrDyn *sf_failure_cause(const ScrStr *message) {
if (message->len > sizeof sf_tunnel_prefix - 1 &&
memcmp(message->data, sf_tunnel_prefix, sizeof sf_tunnel_prefix - 1) == 0) {
ScrError *aborted = scr_error_new(SCR_ERR_ERROR, (ScrStr *)message);
scr_str_release(aborted->name);
aborted->name = scr_str_new("AbortError", 10);
aborted->name_present = true;
scr_error_set_code(aborted, "UND_ERR_ABORTED");
ScrDyn *inner = scr_dyn_from_error(aborted);
scr_error_release(aborted);
ScrDyn *options = scr_dyn_new_obj();
ScrStr *name = scr_str_new("Error", 5);
scr_dyn_obj_set(options, "name", 4, scr_dyn_new_str(name));
scr_str_release(name);
scr_dyn_obj_set(options, "cause", 5, inner);
ScrStr *text = scr_str_new("Request was cancelled.", 22);
ScrDyn *m = scr_dyn_new_str(text);
scr_str_release(text);
ScrError *dom = scr_domex_new(m, options);
scr_dyn_release(m);
scr_dyn_release(options);
ScrDyn *cause = scr_dyn_from_error(dom);
scr_error_release(dom);
return cause;
}
if (message->len >= sizeof sf_not_http_prefix - 1 &&
memcmp(message->data, sf_not_http_prefix, sizeof sf_not_http_prefix - 1) == 0) {
ScrError *parser = scr_error_new(SCR_ERR_ERROR, (ScrStr *)message);
scr_str_release(parser->name);
parser->name = scr_str_new("HTTPParserError", 15);
parser->name_present = true;
ScrDyn *cause = scr_dyn_from_error(parser);
scr_error_release(parser);
return cause;
}
const char *data = message->data;
const char *space = memchr(data, ' ', message->len);
if (space == NULL || space == data) return NULL;
size_t syscall_len = (size_t)(space - data);
const char *code = space + 1;
const char *end = data + message->len;
const char *code_end = code;
while (code_end < end && *code_end != ' ') code_end++;
char code_buf[32];
size_t code_len = (size_t)(code_end - code);
if (code_len < 2 || code_len >= sizeof code_buf || code[0] != 'E') return NULL;
for (size_t i = 0; i < code_len; i++) {
char c = code[i];
if (!((c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_')) return NULL;
}
memcpy(code_buf, code, code_len);
code_buf[code_len] = '\0';
int uv = sf_uv_errno(code_buf);
if (uv == 0) return NULL;
ScrError *e = scr_error_new(SCR_ERR_ERROR, (ScrStr *)message);
scr_error_set_code(e, code_buf);
e->system_errno = uv;
e->system_call = scr_str_new(data, syscall_len);
ScrDyn *cause = scr_dyn_from_error(e);
scr_error_release(e);
const char *detail = code_end < end ? code_end + 1 : end;
size_t detail_len = (size_t)(end - detail);
if (syscall_len == 7 && memcmp(data, "connect", 7) == 0 && detail_len > 0) {
const char *colon = NULL;
for (const char *q = detail; q < end; q++) {
if (*q == ':') colon = q;
}
if (colon != NULL && colon > detail && colon + 1 < end) {
ScrStr *address = scr_str_new(detail, (size_t)(colon - detail));
scr_dyn_obj_set(cause, "address", 7, scr_dyn_new_str(address));
scr_str_release(address);
scr_dyn_obj_set(cause, "port", 4, scr_dyn_new_num(strtod(colon + 1, NULL)));
}
} else if (syscall_len == 11 && memcmp(data, "getaddrinfo", 11) == 0 && detail_len > 0) {
ScrStr *hostname = scr_str_new(detail, detail_len);
scr_dyn_obj_set(cause, "hostname", 8, scr_dyn_new_str(hostname));
scr_str_release(hostname);
}
return cause;
}
/* A plain Error cause. +1. */
static ScrDyn *sf_plain_cause(const char *message) {
ScrStr *text = scr_str_new(message, strlen(message));
ScrError *e = scr_error_new(SCR_ERR_ERROR, text);
scr_str_release(text);
ScrDyn *cause = scr_dyn_from_error(e);
scr_error_release(e);
return cause;
}
/* Rejects with TypeError "fetch failed" whose cause is `cause` (moves;
* NULL rejects without a cause). */
static void sf_reject_cause(SfTransfer *t, ScrDyn *cause) {
if (t->done || cause == NULL) {
scr_dyn_release(cause);
sf_reject(t, "fetch failed");
return;
}
ScrError *e = NULL;
ScrDyn *reason = sf_type_reason("fetch failed", &e);
scr_error_set_cause(e, cause);
scr_dyn_release(cause);
scr_error_release(e);
sf_reject_reason(t, reason);
scr_dyn_release(reason);
}
/* Rejects with TypeError "fetch failed", with Node's cause when the
* failure message has one. */
static void sf_reject_failure(SfTransfer *t, const ScrStr *message) {
sf_reject_cause(t, message != NULL ? sf_failure_cause(message) : NULL);
}
static void sf_on_client_error(ScrClosure *cb, ScrStr *message) {
SfTransfer *t = sf_from(cb);
if (!t) return;
if (!t->done) {
if (t->response_sent && t->response_stream) {
sf_response_terminate(t);
} else {
sf_reject_failure(t, message);
}
}
sf_release(t);
}
static void sf_on_upgrade(
ScrClosure *cb, ScrHttpReq *res, ScrNetSocket *socket,
ScrBytes *head) {
SfTransfer *t = sf_from(cb);
scr_http_req_release(res);
scr_bytes_release(head);
scr_net_sock_destroy(socket);
scr_net_sock_release(socket);
if (!t) return;
if (!t->done) sf_reject(t, "fetch failed");
sf_release(t);
}
static bool sf_body_is_stream(const ScrDyn *body) {
return body &&
body->kind == SCR_DYN_HANDLE &&
body->v.handle.tag == SCR_DYNH_WEB_STREAM;
}
static bool sf_redirect(SfTransfer *t, int status,
const ScrStr *location) {
if (t->hops >= 20) {
sf_reject(t, "fetch failed");
return false;
}
ScrUrl *next = sf_resolve_url(t->url, location);
if (!next ||
!(sf_eq_ci(next->scheme, "http") ||
sf_eq_ci(next->scheme, "https")) ||
next->userinfo->len > 0) {
scr_url_release(next);
sf_reject(t, "fetch failed");
return false;
}
bool is_get = sf_eq_ci(t->method, "get");
bool is_head = sf_eq_ci(t->method, "head");
bool rewrite =
(status == 303 && !is_get && !is_head) ||
((status == 301 || status == 302) &&
sf_eq_ci(t->method, "post"));
if (sf_body_is_stream(t->body) && status != 303) {
scr_url_release(next);
sf_reject(t, "fetch failed");
return false;
}
if (rewrite) {
scr_str_release(t->method);
t->method = scr_str_new("GET", 3);
scr_dyn_release(t->body);
t->body = NULL;
sf_strip_header(&t->headers, "content-type");
sf_strip_header(&t->headers, "content-length");
sf_strip_header(&t->headers, "content-encoding");
sf_strip_header(&t->headers, "content-language");
sf_strip_header(&t->headers, "content-location");
}
bool same_origin =
t->url->host->len == next->host->len &&
memcmp(t->url->host->data, next->host->data, next->host->len) == 0 &&
sf_port(t->url, 0) == sf_port(next, 0) &&
t->url->scheme->len == next->scheme->len &&
memcmp(t->url->scheme->data, next->scheme->data,
next->scheme->len) == 0;
if (!same_origin) {
sf_strip_header(&t->headers, "authorization");
sf_strip_header(&t->headers, "proxy-authorization");
sf_strip_header(&t->headers, "cookie");
}
scr_url_release(t->url);
t->url = next;
t->hops++;
t->redirected = true;
return true;
}
/* A CONNECT tunnel for one request: the head sent to the proxy, and the
* target's TLS client context when the target is https. */
typedef struct {
char *head;
size_t head_len;
void *tls_ctx;
} SfTunnel;
/* undici's CONNECT head: the target as host:port, its Host value, and
* the proxy credentials from the proxy URL. Returns malloc'd storage. */
static char *sf_tunnel_head(const ScrUrl *url, int port, const char *authority,
size_t authority_len, const ScrUrl *proxy, size_t *len_out) {
ScrStr *authorization = sf_proxy_has_credentials(proxy->userinfo)
? sf_proxy_authorization(proxy->userinfo)
: NULL;
size_t cap = url->host->len + authority_len + 160 + (authorization ? authorization->len : 0);
char *head = malloc(cap);
if (!head) sf_oom();
int n = snprintf(head, cap, "CONNECT %.*s:%d HTTP/1.1\r\nhost: %.*s\r\nconnection: close\r\n",
(int)url->host->len, url->host->data, port, (int)authority_len, authority);
if (authorization) {
n += snprintf(head + n, cap - (size_t)n, "proxy-authorization: %.*s\r\n",
(int)authorization->len, authorization->data);
scr_str_release(authorization);
}
n += snprintf(head + n, cap - (size_t)n, "proxy-connection: keep-alive\r\n\r\n");
*len_out = (size_t)n;
return head;
}
static SfTunnel *sf_tunnel_new(const ScrUrl *url, int port, const char *authority,
size_t authority_len, const ScrUrl *proxy, void *tls_ctx) {
SfTunnel *tunnel = malloc(sizeof *tunnel);
if (!tunnel) sf_oom();
tunnel->head = sf_tunnel_head(url, port, authority, authority_len, proxy, &tunnel->head_len);
tunnel->tls_ctx = tls_ctx;
return tunnel;
}
/* The client's wrap hook for a tunneled request: the CONNECT exchange
* runs on the proxy socket, then the target's TLS session (if any) runs
* inside the tunnel. */
static void sf_tunnel_wrap(ScrNetSocket *sock, void *ctx) {
SfTunnel *tunnel = ctx;
scr_net_sock_set_tunnel(sock, tunnel->head, tunnel->head_len);
if (tunnel->tls_ctx) scr_tls_fetch_client_wrap(sock, tunnel->tls_ctx);
free(tunnel->head);
free(tunnel);
}
static bool sf_start_hop(SfTransfer *t) {
ScrUrl *url = t->url;
bool https = sf_eq_ci(url->scheme, "https");
int default_port = https ? 443 : 80;
int port = sf_port(url, default_port);
if (sf_bad_port(port)) {
sf_reject_cause(t, sf_plain_cause("bad port"));
return false;
}
bool invalid_proxy = false;
ScrUrl *proxy = sf_proxy_for(url, https, port, &invalid_proxy);
bool proxy_http = proxy && sf_eq_ci(proxy->scheme, "http");
bool proxy_https = proxy && sf_eq_ci(proxy->scheme, "https");
if (invalid_proxy || (proxy && !proxy_http && !proxy_https)) {
scr_url_release(proxy);
sf_reject(t, "fetch failed");
return false;
}
/* A proxy carries every request through a CONNECT tunnel, as undici's
* ProxyAgent does; an https:// proxy runs the tunnel inside a TLS
* session to the proxy. */
size_t user_len = (size_t)scr_arr_len(t->headers);
ScrArr *headers = scr_arr_new(SCR_ELEM_STR, user_len + 16);
char authority[384];
int authority_len =
port == default_port
? snprintf(authority, sizeof authority, "%.*s",
(int)url->host->len, url->host->data)
: snprintf(authority, sizeof authority, "%.*s:%d",
(int)url->host->len, url->host->data, port);
if (authority_len < 0 ||
(size_t)authority_len >= sizeof authority) {
scr_arr_release(headers);
scr_url_release(proxy);
sf_reject(t, "fetch failed");
return false;
}
sf_push_header_text(headers, "host", authority,
(size_t)authority_len);
/* The user's connection header only chooses close over keep-alive (a
* "close" token); the transport writes its own value after host. */
bool connection_close = false;
for (size_t i = 0; i + 1 < user_len; i += 2) {
ScrStr *name = scr_arr_get_ref(t->headers, (double)i);
if (sf_eq_ci(name, "connection")) {
ScrStr *value = scr_arr_get_ref(t->headers, (double)(i + 1));
bool close;
if (sf_connection_tokens(value, &close) && close) connection_close = true;
scr_str_release(value);
}
scr_str_release(name);
}
if (connection_close) sf_push_header_text(headers, "connection", "close", 5);
else sf_push_header_text(headers, "connection", "keep-alive", 10);
/* Headers keeps each Set-Cookie value separately, but undici sends its
* header list's combined value: one line, the values joined by ", ",
* at the position of the first one. */
bool set_cookie_sent = false;
for (size_t i = 0; i + 1 < user_len; i += 2) {
ScrStr *name = scr_arr_get_ref(t->headers, (double)i);
ScrStr *value = scr_arr_get_ref(t->headers, (double)(i + 1));
if (sf_eq_ci(name, "host") || sf_eq_ci(name, "connection") ||
(set_cookie_sent && sf_eq_ci(name, "set-cookie"))) {
scr_str_release(name);
scr_str_release(value);
continue;
}
if (sf_eq_ci(name, "set-cookie")) {
set_cookie_sent = true;
for (size_t j = i + 2; j + 1 < user_len; j += 2) {
ScrStr *later = scr_arr_get_ref(t->headers, (double)j);
bool match = sf_eq_ci(later, "set-cookie");
scr_str_release(later);
if (!match) continue;
ScrStr *next = scr_arr_get_ref(t->headers, (double)(j + 1));
size_t joined_len = value->len + 2 + next->len;
char *joined = malloc(joined_len ? joined_len : 1);
if (!joined) sf_oom();
memcpy(joined, value->data, value->len);
memcpy(joined + value->len, ", ", 2);
memcpy(joined + value->len + 2, next->data, next->len);
scr_str_release(next);
scr_str_release(value);
value = scr_str_new(joined, joined_len);
free(joined);
}
}
scr_arr_push_ref(headers, name);
scr_arr_push_ref(headers, value);
}
if (!sf_pairs_have(t->headers, "accept")) {
sf_push_header_text(headers, "accept", "*/*", 3);
}
if (!sf_pairs_have(t->headers, "accept-language")) {
sf_push_header_text(headers, "accept-language", "*", 1);
}
if (!sf_pairs_have(t->headers, "sec-fetch-mode")) {
sf_push_header_text(headers, "sec-fetch-mode", "cors", 4);
}
if (!sf_pairs_have(t->headers, "user-agent")) {
sf_push_header_text(headers, "user-agent", "node", 4);
}
if (!sf_pairs_have(t->headers, "accept-encoding")) {
sf_push_header_text(headers, "accept-encoding", "gzip, deflate", 13);
}
ScrStr *bare =
sf_bare_host(proxy ? proxy->host : url->host);
ScrStr *dial = scr_net_blocking_lookup(bare);
ScrStr *path = sf_path(url);
int dial_port =
proxy ? sf_port(proxy, proxy_https ? 443 : 80) : port;
ScrStr *sni = https ? sf_bare_host(url->host) : NULL;
void (*wrap)(ScrNetSocket *, void *) = NULL;
void *wrap_ctx = NULL;
if (proxy_https) {
size_t head_len = 0;
char *head = sf_tunnel_head(url, port, authority, (size_t)authority_len, proxy, &head_len);
wrap_ctx = scr_tls_fetch_tunnel_ctx(bare, sni, head, head_len);
wrap = &scr_tls_fetch_tunnel_wrap;
free(head);
} else if (proxy_http) {
void *tls_ctx = sni ? scr_tls_fetch_client_ctx(sni, true) : NULL;
wrap_ctx = sf_tunnel_new(url, port, authority, (size_t)authority_len, proxy, tls_ctx);
wrap = &sf_tunnel_wrap;
} else if (sni) {
wrap_ctx = scr_tls_fetch_client_ctx(sni, true);
wrap = &scr_tls_fetch_client_wrap;
}
ScrHttpClientReq *client = scr_http_request_ex(
dial, dial_port, path, t->method, 0, headers, false, NULL, NULL,
default_port, wrap, wrap_ctx);
scr_arr_release(headers);
scr_str_release(path);
scr_str_release(dial);
scr_str_release(bare);
scr_str_release(sni);
scr_url_release(proxy);
if (!client) {
sf_reject_now(t->promise, "fetch failed");
sf_settle(t);
return false;
}
t->client = client;
scr_http_client_on_response(
client, sf_closure(t, (void *)&sf_on_response),
&sf_on_response, true);
scr_http_client_on_upgrade(
client, sf_closure(t, (void *)&sf_on_upgrade),
&sf_on_upgrade, true);
scr_http_client_on_error(
client, sf_closure(t, (void *)&sf_on_client_error),
&sf_on_client_error, false);
ScrDyn *body = t->body;
if (body && body->kind == SCR_DYN_STR) {
scr_http_client_end_str(client, body->v.str);
} else if (body && body->kind == SCR_DYN_BYTES) {
scr_http_client_end_bytes(client, body->v.bytes);
} else if (sf_body_is_stream(body)) {
SfStream *stream = NULL;
ScrDyn *seeded = NULL;
if (body->kind == SCR_DYN_ARR) {
seeded = scr_fetch_stream_from(body);
if (!seeded) {
if (t->client) scr_http_client_destroy(t->client);
sf_reject_now(t->promise, "fetch failed");
sf_settle(t);
return false;
}
stream = sf_stream_of(seeded);
} else {
stream = sf_stream_of(body);
}
if (!stream || stream->reader || stream->internal_lock ||
stream->disturbed) {
scr_dyn_release(seeded);
if (t->client) scr_http_client_destroy(t->client);
sf_reject(t,
"Response body object should not be disturbed or locked");
return false;
}
stream->disturbed = true;
stream->internal_lock = true;
stream->request_owner = t;
t->request_stream = sf_stream_retain(stream);
sf_stream_request_flush(stream);
if (stream->started && !stream->close_requested && !stream->closed &&
!stream->error && !t->done && t->client) {
sf_stream_pull(stream);
}
scr_dyn_release(seeded);
} else {
scr_http_client_end(client);
}
return true;
}
static bool sf_response_init_object_like(const ScrDyn *init) {
if (!init) return false;
switch (init->kind) {
case SCR_DYN_OBJ:
case SCR_DYN_ARR:
case SCR_DYN_BYTES:
case SCR_DYN_FUNC:
case SCR_DYN_HANDLE:
case SCR_DYN_PROMISE:
case SCR_DYN_TYPED_REF:
return true;
default:
return false;
}
}
/* Owned property read for the checked-dynamic object kinds that can carry
* ResponseInit dictionary members. Arrays/bytes/native handles/promises
* have no such expandos in the static model and therefore answer absent. */
static ScrDyn *sf_response_init_get(
const ScrDyn *init, const char *key, size_t len) {
if (!init) return NULL;
if (init->kind == SCR_DYN_OBJ) {
ScrDyn *value = scr_dyn_obj_get(init, key, len);
return value ? scr_dyn_retain(value) : NULL;
}
if (init->kind == SCR_DYN_FUNC) {
return scr_dyn_fn_get(init, key, len);
}
if (init->kind == SCR_DYN_HANDLE &&
init->v.handle.tag == SCR_DYNH_FETCH_RESPONSE) {
return sf_response_get(init->v.handle.ptr, key, len);
}
if (init->kind == SCR_DYN_TYPED_REF) {
ScrDyn *materialized = scr_dyn_typed_ref_materialize(init);
ScrDyn *value = sf_response_init_get(materialized, key, len);
scr_dyn_release(materialized);
return value;
}
return NULL;
}
/* The Response constructor, and Response.json(data, init) when json_data
* is non-NULL: undici converts ResponseInit, serializes the data (TypeError
* "Value is not JSON serializable" when JSON.stringify answers undefined),
* then initializes the response exactly as the constructor does, with
* "application/json" as the default content type. */
typedef ScrDyn *(*SfJsonSerialize)(const ScrDyn *value);
/* The JSON serializer reaches the constructor only through Response.json's
* entry point, so programs that never call it link no JSON walker. */
static ScrDyn *sf_response_json_serialize(const ScrDyn *value) {
ScrStr *gap = scr_str_new("", 0);
ScrDyn *serialized = scr_json_stringify_replacer(value, scr_dyn_undefined(), gap);
scr_str_release(gap);
return serialized;
}
/* The lightweight header form of a constructor's init headers (see
* SfResponse.light): absent headers are form 1 (Response.json's plain
* {"content-type": "application/json"} record is form 3), a Headers object
* or pair list form 2, and a plain record form 3, whose names and values
* *record receives in order (an array value contributes one entry per
* element, as writeHead writes it). Response.json with init headers is
* form 2 whatever their shape: the package's Response.json passes them on
* as a Headers object, adding the JSON content type when they lack one.
* 0 for any other init. */
static int8_t sf_light_header_form(const ScrDyn *headers, bool json_body,
ScrArr **record) {
if (!headers || headers->kind == SCR_DYN_UNDEF) {
if (!json_body) return 1;
*record = scr_arr_new(SCR_ELEM_STR, 2);
sf_push_header_text(*record, "content-type", "application/json", 16);
return 3;
}
if (json_body) return 2;
if (headers->kind == SCR_DYN_ARR ||
(headers->kind == SCR_DYN_HANDLE &&
headers->v.handle.tag == SCR_DYNH_FETCH_HEADERS))
return 2;
ScrDyn *materialized = NULL;
if (headers->kind == SCR_DYN_TYPED_REF) {
materialized = scr_dyn_typed_ref_materialize(headers);
if (!materialized) return 0;
headers = materialized;
}
int8_t form = 0;
if (headers->kind == SCR_DYN_OBJ) {
ScrArr *out = scr_arr_new(SCR_ELEM_STR, 2 * headers->v.obj.len);
form = 3;
for (size_t i = 0; i < headers->v.obj.len && form == 3; i++) {
const ScrDynEntry *e = &headers->v.obj.entries[i];
const ScrDyn *value = e->value;
size_t count = value->kind == SCR_DYN_ARR ? value->v.arr.len : 1;
for (size_t j = 0; j < count; j++) {
const ScrDyn *item = value->kind == SCR_DYN_ARR ? value->v.arr.items[j] : value;
if (item->kind != SCR_DYN_STR && item->kind != SCR_DYN_NUM &&
item->kind != SCR_DYN_BOOL) {
form = 0;
break;
}
ScrStr *text = scr_dyn_string_coerce_js(item);
if (!text) {
form = 0;
break;
}
scr_arr_push_ref(out, scr_str_new(e->key, e->key_len));
scr_arr_push_ref(out, text);
}
}
if (form == 3) *record = out;
else scr_arr_release(out);
}
scr_dyn_release(materialized);
return form;
}
static ScrDyn *sf_response_construct(ScrDyn *body, ScrDyn *init,
const ScrDyn *json_data,
SfJsonSerialize serialize) {
ScrArr *header_pairs = NULL;
ScrArr *converted_headers = NULL;
bool converted_canonical = false;
ScrBytes *body_bytes = NULL;
ScrStr *status_text = NULL;
SfStream *stream = NULL;
bool json_body = json_data != NULL;
bool null_body = !json_body &&
(!body || body->kind == SCR_DYN_UNDEF || body->kind == SCR_DYN_NULL);
bool text_body = false;
int status = 200;
bool status_range_error = false;
/* The lightweight-response classification (see SfResponse.light): a
* Response init qualifies only while it is itself still light. */
bool light_ok = !(init && init->kind == SCR_DYN_HANDLE &&
init->v.handle.tag == SCR_DYNH_FETCH_RESPONSE &&
((SfResponse *)init->v.handle.ptr)->light == 0);
if (!json_body && !null_body) {
if (body->kind == SCR_DYN_BYTES)
light_ok = light_ok && body->v.bytes->elem == SCR_BYTES_U8 &&
!body->v.bytes->is_data_view;
else if (!(body->kind == SCR_DYN_STR ||
(body->kind == SCR_DYN_HANDLE &&
body->v.handle.tag == SCR_DYNH_WEB_STREAM)))
light_ok = false;
}
int8_t light_form = 0;
ScrArr *light_record = NULL;
size_t light_length_bytes = 0;
/* WebIDL converts constructor arguments from left to right. BodyInit's
* string arm therefore runs before ResponseInit's dictionary conversion,
* while a ReadableStream is only brand-converted here — its locked/
* disturbed state belongs to the later body-extraction phase. */
if (!null_body && !json_body &&
!(body->kind == SCR_DYN_HANDLE &&
body->v.handle.tag == SCR_DYNH_WEB_STREAM)) {
if (body->kind == SCR_DYN_BYTES) {
ScrBytes *raw = scr_bytes_raw_view(body->v.bytes);
body_bytes = scr_bytes_copy(raw);
scr_bytes_release(raw);
} else {
ScrStr *text =
body->kind == SCR_DYN_STR
? scr_str_retain(body->v.str)
: scr_dyn_string_coerce_js(body);
if (!text) goto fail;
ScrStr *encoding = scr_str_new("utf8", 4);
body_bytes = scr_bytes_from_str(text, encoding);
scr_str_release(encoding);
scr_str_release(text);
text_body = true;
}
}
if (init && init->kind != SCR_DYN_UNDEF && init->kind != SCR_DYN_NULL &&
!sf_response_init_object_like(init)) {
sf_type_error("Response constructor init must be an object");
goto fail;
}
/* ResponseInit's WebIDL dictionary conversion is lexicographic and the
* nested HeadersInit conversion is observable: headers' ByteString
* coercions precede status and statusText. Header syntax validation is a
* later response-initialization step, after body extraction. */
ScrDyn *init_headers = sf_response_init_get(init, "headers", 7);
if (light_ok) light_form = sf_light_header_form(init_headers, json_body, &light_record);
converted_headers = sf_headers_convert_init(init_headers, &converted_canonical);
scr_dyn_release(init_headers);
if (!converted_headers) goto fail;
if (init && sf_response_init_object_like(init)) {
ScrDyn *status_value = sf_response_init_get(init, "status", 6);
if (status_value && status_value->kind != SCR_DYN_UNDEF) {
double converted;
if (!scr_dyn_number_coerce_js(status_value, &converted)) {
scr_dyn_release(status_value);
goto fail;
}
converted = trunc(converted);
if (!isfinite(converted) || converted < 0.0) {
status_range_error = true;
} else {
if (converted > 65535.0) converted = fmod(converted, 65536.0);
status = (int)converted;
}
}
scr_dyn_release(status_value);
ScrDyn *status_text_value =
sf_response_init_get(init, "statusText", 10);
if (status_text_value && status_text_value->kind != SCR_DYN_UNDEF) {
ScrStr *raw = scr_dyn_string_coerce_js(status_text_value);
ScrStr *bytes = raw ? sf_header_value_bytestring(raw) : NULL;
scr_str_release(raw);
status_text = bytes ? sf_latin1_to_utf8(bytes) : NULL;
scr_str_release(bytes);
if (!status_text) {
scr_dyn_release(status_text_value);
goto fail;
}
}
scr_dyn_release(status_text_value);
}
if (!status_text) status_text = scr_str_new("", 0);
/* Extract BodyInit before validating the response metadata. This makes a
* locked/disturbed stream win over the later status range, statusText HTTP
* reason-phrase, and HeadersInit validation exactly as in pinned Node. */
if (json_body) {
ScrDyn *serialized = serialize(json_data);
if (!serialized) goto fail;
if (serialized->kind != SCR_DYN_STR) {
static const char unserializable[30] = "Value is not JSON serializable";
scr_dyn_release(serialized);
scr_throw_error_msg(SCR_ERR_TYPE, unserializable, sizeof unserializable);
goto fail;
}
ScrStr *encoding = scr_str_new("utf8", 4);
body_bytes = scr_bytes_from_str(serialized->v.str, encoding);
scr_str_release(encoding);
scr_dyn_release(serialized);
stream = sf_stream_new_native();
light_length_bytes = body_bytes->len;
sf_stream_enqueue_bytes(stream, body_bytes);
sf_stream_close(stream);
scr_bytes_release(body_bytes);
body_bytes = NULL;
} else if (null_body) {
stream = sf_stream_new_native();
sf_stream_close(stream);
} else if (body->kind == SCR_DYN_HANDLE &&
body->v.handle.tag == SCR_DYNH_WEB_STREAM) {
stream = (SfStream *)body->v.handle.ptr;
if (stream->reader || stream->internal_lock || stream->disturbed) {
stream = NULL; /* borrowed until the successful retain below */
sf_type_error("Response body object should not be disturbed or locked");
goto fail;
}
sf_stream_retain(stream);
} else {
stream = sf_stream_new_native();
light_length_bytes = body_bytes->len;
sf_stream_enqueue_bytes(stream, body_bytes);
sf_stream_close(stream);
scr_bytes_release(body_bytes);
body_bytes = NULL;
}
if (status_range_error || status < 200 || status > 599) {
static const char message[] =
"init[\"status\"] must be in the range of 200 to 599, inclusive.";
scr_throw_error_msg(SCR_ERR_RANGE, message, sizeof message - 1);
goto fail;
}
if (!sf_header_value_ok(status_text)) {
sf_type_error("Invalid statusText");
goto fail;
}
header_pairs = scr_arr_new(SCR_ELEM_STR, 8);
if (!sf_add_converted_headers(
header_pairs, converted_headers, false, converted_canonical)) goto fail;
scr_arr_release(converted_headers);
converted_headers = NULL;
if (!null_body &&
(status == 204 || status == 205 || status == 304)) {
char message[80];
int len = snprintf(message, sizeof message,
"Response constructor: Invalid response status code %d",
status);
if (len < 0 || (size_t)len >= sizeof message) sf_oom();
sf_type_error(message);
goto fail;
}
bool auto_ct = false;
if (text_body && !sf_pairs_have(header_pairs, "content-type")) {
sf_push_header_text(header_pairs, "content-type",
"text/plain;charset=UTF-8", 24);
auto_ct = true;
}
if (json_body && !sf_pairs_have(header_pairs, "content-type")) {
static const char json_type[16] = "application/json";
scr_arr_push_ref(header_pairs, scr_str_new("content-type", 12));
scr_arr_push_ref(header_pairs, scr_str_new(json_type, sizeof json_type));
}
SfResponse *response = calloc(1, sizeof *response);
if (!response) sf_oom();
response->rc = 1;
response->body = stream;
response->headers = sf_headers_new(header_pairs, false);
response->headers->canonical = sf_header_values_trimmed(header_pairs);
response->url = scr_str_new("", 0);
response->status_text = status_text;
response->status = status;
response->redirected = false;
response->null_body = null_body;
response->light = light_form;
response->auto_ct = auto_ct;
response->light_record = light_record;
light_record = NULL;
response->light_length =
null_body ? -1.0
: (!json_body && body->kind == SCR_DYN_HANDLE) ? -2.0
: (double)light_length_bytes;
ScrDyn *out =
scr_dyn_new_handle(response, SCR_DYNH_FETCH_RESPONSE);
sf_response_release(response);
return out;
fail:
sf_stream_release(stream);
scr_bytes_release(body_bytes);
scr_str_release(status_text);
scr_arr_release(header_pairs);
scr_arr_release(converted_headers);
scr_arr_release(light_record);
return NULL;
}
/* @hono/node-server's built-in implementation reads a Response's
* lightweight state to write it as the package does: [form, flags,
* byte length, ...raw record name/value pairs], with form as in
* SfResponse.light ("0" alone when the response is not light), flags "d"
* (the package's Headers hold its default content type) and "a" (the
* constructor added the text content type), and the length -1 for a null
* body or -2 for a stream. */
ScrArr *scr_fetch_response_light(ScrDyn *response) {
ScrArr *out = scr_arr_new(SCR_ELEM_STR, 4);
if (!response || response->kind != SCR_DYN_HANDLE ||
response->v.handle.tag != SCR_DYNH_FETCH_RESPONSE) {
scr_arr_push_ref(out, scr_str_new("0", 1));
return out;
}
SfResponse *r = response->v.handle.ptr;
char buf[32];
int n = snprintf(buf, sizeof buf, "%d", (int)r->light);
scr_arr_push_ref(out, scr_str_new(buf, (size_t)n));
if (r->light == 0) return out;
char flags[3];
size_t f = 0;
if (r->light_default_ct) flags[f++] = 'd';
if (r->auto_ct) flags[f++] = 'a';
scr_arr_push_ref(out, scr_str_new(flags, f));
n = snprintf(buf, sizeof buf, "%.0f", r->light_length);
scr_arr_push_ref(out, scr_str_new(buf, (size_t)n));
if (r->light == 3 && r->light_record) {
for (size_t i = 0; i < r->light_record->len; i++)
scr_arr_push_ref(out, scr_arr_get_ref(r->light_record, (double)i));
}
return out;
}
ScrDyn *scr_fetch_response_new(ScrDyn *body, ScrDyn *init) {
return sf_response_construct(body, init, NULL, NULL);
}
ScrDyn *scr_fetch_response_json_static(ScrDyn *data, ScrDyn *init) {
return sf_response_construct(NULL, init, data ? data : scr_dyn_undefined(),
&sf_response_json_serialize);
}
/* Request uses the same body stream and Headers ownership as Response. */
typedef struct SfRequest {
size_t rc;
SfResponse *payload;
ScrStr *url;
ScrStr *method;
ScrStr *redirect;
SfSignal *signal;
} SfRequest;
static void *sf_request_retain_v(void *ptr) {
SfRequest *r = ptr; r->rc++; return r;
}
static void sf_request_release_v(void *ptr) {
SfRequest *r = ptr;
if (--r->rc) return;
scr_weak_dispose_inline(r);
sf_response_release(r->payload);
scr_str_release(r->url); scr_str_release(r->method); scr_str_release(r->redirect);
sf_signal_release(r->signal); free(r);
}
static ScrDyn *sf_request_get(void *ptr, const char *key, size_t len) {
SfRequest *r = ptr;
if (sf_name(key, len, "url")) return scr_dyn_new_str(r->url);
if (sf_name(key, len, "method")) return scr_dyn_new_str(r->method);
if (sf_name(key, len, "redirect")) return scr_dyn_new_str(r->redirect);
if (sf_name(key, len, "signal")) return scr_dyn_new_handle(r->signal, SCR_DYNH_ABORT_SIGNAL);
if (sf_name(key, len, "duplex")) { ScrStr *s = scr_str_new("half", 4); ScrDyn *v = scr_dyn_new_str(s); scr_str_release(s); return v; }
const char *default_value = NULL;
if (sf_name(key, len, "destination") || sf_name(key, len, "integrity") || sf_name(key, len, "referrerPolicy")) default_value = "";
if (sf_name(key, len, "mode")) default_value = "cors";
if (sf_name(key, len, "credentials")) default_value = "same-origin";
if (sf_name(key, len, "cache")) default_value = "default";
if (sf_name(key, len, "referrer")) default_value = "about:client";
if (default_value) { ScrStr *s = scr_str_new(default_value, strlen(default_value)); ScrDyn *v = scr_dyn_new_str(s); scr_str_release(s); return v; }
if (sf_name(key, len, "keepalive") || sf_name(key, len, "isReloadNavigation") || sf_name(key, len, "isHistoryNavigation")) return scr_dyn_new_bool(false);
if (sf_name(key, len, "body") || sf_name(key, len, "bodyUsed") || sf_name(key, len, "headers")) return sf_response_get(r->payload, key, len);
return NULL;
}
/* Request.clone: the payload clones as a Response does, and the clone's
* signal follows the original's (undici: an AbortController aborted with
* the original signal's reason, at once when it already aborted). */
static ScrDyn *sf_request_clone(SfRequest *r) {
SfResponse *payload = sf_response_clone(r->payload, "unusable");
if (!payload) return NULL;
ScrDyn *sources = scr_dyn_new_arr();
scr_dyn_arr_push(sources,
scr_dyn_new_handle(r->signal, SCR_DYNH_ABORT_SIGNAL));
ScrDyn *follower = scr_fetch_abort_any(sources);
scr_dyn_release(sources);
if (!follower) {
sf_response_release(payload);
return NULL;
}
SfRequest *c = calloc(1, sizeof *c);
if (!c) sf_oom();
c->rc = 1;
c->payload = payload;
c->url = scr_str_retain(r->url);
c->method = scr_str_retain(r->method);
c->redirect = scr_str_retain(r->redirect);
c->signal = sf_signal_retain((SfSignal *)follower->v.handle.ptr);
scr_dyn_release(follower);
ScrDyn *out = scr_dyn_new_handle(c, SCR_DYNH_FETCH_REQUEST);
sf_request_release_v(c);
return out;
}
static ScrDyn *sf_request_invoke(void *ptr, ScrDyn *self, const char *method, ScrDyn *const *args, size_t argc, const char *what) {
SfRequest *r = ptr;
if (strcmp(method, "clone") == 0) return sf_request_clone(r);
return sf_response_invoke(r->payload, self, method, args, argc, what);
}
ScrDyn *scr_fetch_headers_new(ScrDyn *init) {
bool canonical = false;
ScrArr *converted = sf_headers_convert_init(init, &canonical);
if (!converted) return NULL;
ScrArr *pairs = scr_arr_new(SCR_ELEM_STR, 8);
bool ok = sf_add_converted_headers(pairs, converted, false, canonical);
scr_arr_release(converted);
if (!ok) { scr_arr_release(pairs); return NULL; }
SfHeaders *headers = sf_headers_new(pairs, false);
headers->canonical = sf_header_values_trimmed(pairs);
ScrDyn *result = scr_dyn_new_handle(headers, SCR_DYNH_FETCH_HEADERS);
sf_headers_release(headers);
return result;
}
ScrDyn *scr_fetch_request_new(ScrDyn *input, ScrDyn *init) {
SfRequest *source = input && input->kind == SCR_DYN_HANDLE && input->v.handle.tag == SCR_DYNH_FETCH_REQUEST ? input->v.handle.ptr : NULL;
if (init && init->kind != SCR_DYN_UNDEF && init->kind != SCR_DYN_NULL && !sf_response_init_object_like(init)) {
sf_type_error("Request constructor init must be an object"); return NULL;
}
ScrStr *url_text = source ? scr_str_retain(source->url) : scr_dyn_string_coerce_js(input);
if (!url_text) return NULL;
ScrUrl *url = scr_url_new(url_text);
if (!url) sf_throw_url_parse_failure(url_text);
scr_str_release(url_text);
if (!url) return NULL;
if (url->userinfo->len) { scr_url_release(url); sf_type_error("Request cannot be constructed from a URL that includes credentials"); return NULL; }
ScrDyn *dictionary = scr_dyn_new_obj();
ScrDyn *method = sf_response_init_get(init, "method", 6);
if ((!method || method->kind == SCR_DYN_UNDEF) && source) { scr_dyn_release(method); method = scr_dyn_new_str(source->method); }
if (method) scr_dyn_obj_set(dictionary, "method", 6, method);
ScrStr *method_text = sf_method_value(dictionary);
scr_dyn_release(dictionary);
if (!method_text || !sf_method_validate(&method_text)) { scr_str_release(method_text); scr_url_release(url); return NULL; }
ScrDyn *body = sf_response_init_get(init, "body", 4);
bool supplied = body && body->kind != SCR_DYN_UNDEF && body->kind != SCR_DYN_NULL;
if (!supplied && source && !source->payload->null_body) {
if (source->payload->body->disturbed || source->payload->body->reader || source->payload->body->internal_lock) {
scr_dyn_release(body); scr_str_release(method_text); scr_url_release(url);
sf_type_error("Cannot construct a Request with a Request object that has already been used."); return NULL;
}
scr_dyn_release(body); body = scr_dyn_new_handle(source->payload->body, SCR_DYNH_WEB_STREAM);
}
bool has_body = body && body->kind != SCR_DYN_UNDEF && body->kind != SCR_DYN_NULL;
if (has_body && (sf_name(method_text->data, method_text->len, "GET") || sf_name(method_text->data, method_text->len, "HEAD"))) {
scr_dyn_release(body); scr_str_release(method_text); scr_url_release(url); sf_type_error("Request with GET/HEAD method cannot have body."); return NULL;
}
if (supplied && body->kind == SCR_DYN_HANDLE && body->v.handle.tag == SCR_DYNH_WEB_STREAM) {
ScrDyn *duplex = sf_response_init_get(init, "duplex", 6);
ScrStr *text = duplex && duplex->kind != SCR_DYN_UNDEF ? scr_dyn_string_coerce_js(duplex) : NULL;
scr_dyn_release(duplex);
bool half = text && sf_name(text->data, text->len, "half"); scr_str_release(text);
if (!half) { scr_dyn_release(body); scr_str_release(method_text); scr_url_release(url); sf_type_error("RequestInit: duplex option is required when sending a body."); return NULL; }
}
ScrDyn *headers = sf_response_init_get(init, "headers", 7);
if ((!headers || headers->kind == SCR_DYN_UNDEF) && source) { scr_dyn_release(headers); headers = scr_dyn_new_handle(source->payload->headers, SCR_DYNH_FETCH_HEADERS); }
ScrDyn *payload_init = scr_dyn_new_obj();
if (headers) scr_dyn_obj_set(payload_init, "headers", 7, headers);
ScrDyn *payload = scr_fetch_response_new(body, payload_init);
scr_dyn_release(payload_init); scr_dyn_release(body);
if (!payload) { scr_str_release(method_text); scr_url_release(url); return NULL; }
ScrDyn *signal = sf_response_init_get(init, "signal", 6);
if ((!signal || signal->kind == SCR_DYN_UNDEF) && source) { scr_dyn_release(signal); signal = scr_dyn_new_handle(source->signal, SCR_DYNH_ABORT_SIGNAL); }
SfSignal *following = NULL;
if (signal && signal->kind != SCR_DYN_UNDEF && signal->kind != SCR_DYN_NULL) {
ScrDyn *signals = scr_dyn_new_arr(); scr_dyn_arr_push(signals, scr_dyn_retain(signal));
ScrDyn *dependent = scr_fetch_abort_any(signals); scr_dyn_release(signals);
if (dependent) { following = sf_signal_retain(dependent->v.handle.ptr); scr_dyn_release(dependent); }
} else following = sf_signal_new();
scr_dyn_release(signal);
if (!following) { scr_dyn_release(payload); scr_str_release(method_text); scr_url_release(url); return NULL; }
ScrDyn *redirect = sf_response_init_get(init, "redirect", 8);
ScrStr *redirect_text = redirect && redirect->kind != SCR_DYN_UNDEF ? scr_dyn_string_coerce_js(redirect) : source ? scr_str_retain(source->redirect) : scr_str_new("follow", 6);
scr_dyn_release(redirect);
if (!redirect_text || !(sf_name(redirect_text->data, redirect_text->len, "follow") || sf_name(redirect_text->data, redirect_text->len, "manual") || sf_name(redirect_text->data, redirect_text->len, "error"))) {
scr_str_release(redirect_text); sf_signal_release(following); scr_dyn_release(payload); scr_str_release(method_text); scr_url_release(url); sf_type_error("Invalid Request redirect value"); return NULL;
}
SfRequest *r = calloc(1, sizeof *r); if (!r) sf_oom();
r->rc = 1; r->payload = sf_response_retain(payload->v.handle.ptr); scr_dyn_release(payload);
r->url = scr_url_href(url); scr_url_release(url); r->method = method_text; r->redirect = redirect_text; r->signal = following;
ScrDyn *result = scr_dyn_new_handle(r, SCR_DYNH_FETCH_REQUEST); sf_request_release_v(r); return result;
}
ScrPromise *scr_fetch_input(ScrDyn *input, ScrDyn *init) {
if (init && init->kind == SCR_DYN_TYPED_REF) {
ScrDyn *view = scr_dyn_typed_ref_materialize(init);
if (scr_exc_pending()) { scr_dyn_release(view); ScrPromise *p = scr_promise_new(); scr_promise_reject_pending(p); return p; }
ScrPromise *result = scr_fetch_input(input, view);
scr_dyn_release(view);
return result;
}
if (input && input->kind == SCR_DYN_HANDLE && input->v.handle.tag == SCR_DYNH_FETCH_REQUEST) {
ScrDyn *converted = scr_fetch_request_new(input, init);
if (!converted) { ScrPromise *p = scr_promise_new(); scr_promise_reject_pending(p); return p; }
SfRequest *r = converted->v.handle.ptr;
ScrDyn *dictionary = scr_dyn_new_obj();
scr_dyn_obj_set(dictionary, "method", 6, scr_dyn_new_str(r->method));
scr_dyn_obj_set(dictionary, "headers", 7, scr_dyn_new_handle(r->payload->headers, SCR_DYNH_FETCH_HEADERS));
scr_dyn_obj_set(dictionary, "signal", 6, scr_dyn_new_handle(r->signal, SCR_DYNH_ABORT_SIGNAL));
scr_dyn_obj_set(dictionary, "redirect", 8, scr_dyn_new_str(r->redirect));
if (!r->payload->null_body) {
scr_dyn_obj_set(dictionary, "body", 4, scr_dyn_new_handle(r->payload->body, SCR_DYNH_WEB_STREAM));
ScrStr *half = scr_str_new("half", 4); scr_dyn_obj_set(dictionary, "duplex", 6, scr_dyn_new_str(half)); scr_str_release(half);
}
ScrPromise *result = scr_fetch_static(r->url, dictionary);
scr_dyn_release(dictionary); scr_dyn_release(converted); return result;
}
ScrStr *url = scr_dyn_string_coerce_js(input);
if (!url) { ScrPromise *p = scr_promise_new(); scr_promise_reject_pending(p); return p; }
ScrPromise *result = scr_fetch_static(url, init); scr_str_release(url); return result;
}
static ScrDyn *sf_fetch_function_call(ScrClosure *closure, ScrDyn *const *args, size_t argc) {
(void)closure;
ScrPromise *p = scr_fetch_input(argc ? args[0] : scr_dyn_undefined(), argc > 1 ? args[1] : scr_dyn_undefined());
ScrDyn *result = scr_dyn_new_promise(p); scr_promise_release(p); return result;
}
static ScrDyn *sf_fetch_function_value;
static void sf_fetch_function_cleanup(void) {
ScrDyn *value = sf_fetch_function_value;
sf_fetch_function_value = NULL;
scr_dyn_release(value);
}
bool scr_fetch_web_is(const ScrDyn *value, const ScrStr *name) {
if (value->kind != SCR_DYN_HANDLE) return false;
if (sf_name(name->data, name->len, "Response")) return value->v.handle.tag == SCR_DYNH_FETCH_RESPONSE;
if (sf_name(name->data, name->len, "Request")) return value->v.handle.tag == SCR_DYNH_FETCH_REQUEST;
return sf_name(name->data, name->len, "Headers") && value->v.handle.tag == SCR_DYNH_FETCH_HEADERS;
}
bool scr_fetch_stream_is(const ScrDyn *value) {
return value && value->kind == SCR_DYN_HANDLE && value->v.handle.tag == SCR_DYNH_WEB_STREAM;
}
ScrDyn *scr_fetch_function(void) {
if (!sf_fetch_function_value) {
sf_fetch_function_value = scr_dyn_new_func(scr_closure_new(NULL, 0), sf_fetch_function_call, 1, "fetch", "fetch");
scr_atexit(sf_fetch_function_cleanup);
}
return scr_dyn_retain(sf_fetch_function_value);
}
static int sf_data_hex(unsigned char ch) {
if (ch >= '0' && ch <= '9') return ch - '0';
if (ch >= 'a' && ch <= 'f') return ch - 'a' + 10;
if (ch >= 'A' && ch <= 'F') return ch - 'A' + 10;
return -1;
}
static ScrDyn *sf_data_response(ScrStr *url) {
size_t end = url->len;
for (size_t i = 5; i < end; i++) if (url->data[i] == '#') { end = i; break; }
size_t comma = 5;
while (comma < end && url->data[comma] != ',') comma++;
if (comma == end) { sf_type_error("fetch failed"); return NULL; }
size_t meta_end = comma;
bool base64 = meta_end >= 12 && strncasecmp(url->data + meta_end - 7, ";base64", 7) == 0;
if (base64) meta_end -= 7;
ScrBytes *bytes = scr_bytes_new(SCR_BYTES_U8, (double)(end - comma - 1));
size_t n = 0;
for (size_t i = comma + 1; i < end; i++) {
int a, b;
if (url->data[i] == '%' && i + 2 < end && (a = sf_data_hex(url->data[i + 1])) >= 0 && (b = sf_data_hex(url->data[i + 2])) >= 0) {
bytes->data[n++] = (unsigned char)((a << 4) | b); i += 2;
} else bytes->data[n++] = (unsigned char)url->data[i];
}
bytes->len = n;
if (base64) {
ScrStr *encoded = scr_bytes_string(bytes, 0, bytes->len);
ScrStr *encoding = scr_str_new("base64", 6);
ScrBytes *decoded = scr_bytes_from_str(encoded, encoding);
scr_str_release(encoded); scr_str_release(encoding); scr_bytes_release(bytes); bytes = decoded;
}
ScrDyn *body = scr_dyn_new_bytes(bytes); scr_bytes_release(bytes);
ScrStr *mime = meta_end == 5 ? scr_str_new("text/plain;charset=US-ASCII", 26) : scr_str_new(url->data + 5, meta_end - 5);
if (mime->len && mime->data[0] == ';') {
ScrJsonBuf b; scr_jb_init(&b); scr_jb_puts(&b, "text/plain"); scr_jb_write(&b, mime->data, mime->len);
scr_str_release(mime); mime = scr_jb_finish(&b);
}
ScrDyn *headers = scr_dyn_new_obj(); scr_dyn_obj_set(headers, "content-type", 12, scr_dyn_new_str(mime)); scr_str_release(mime);
ScrDyn *init = scr_dyn_new_obj(); scr_dyn_obj_set(init, "headers", 7, headers);
ScrStr *ok = scr_str_new("OK", 2); scr_dyn_obj_set(init, "statusText", 10, scr_dyn_new_str(ok)); scr_str_release(ok);
ScrDyn *result = scr_fetch_response_new(body, init); scr_dyn_release(body); scr_dyn_release(init);
if (result) {
SfResponse *response = result->v.handle.ptr;
scr_str_release(response->url); response->url = scr_str_new(url->data, end);
response->headers->immutable = true;
}
return result;
}
ScrPromise *scr_fetch_static(ScrStr *url, ScrDyn *init) {
if (init && init->kind == SCR_DYN_TYPED_REF) {
ScrDyn *view = scr_dyn_typed_ref_materialize(init);
if (scr_exc_pending()) {
scr_dyn_release(view);
ScrPromise *promise = scr_promise_new();
scr_promise_reject_pending(promise);
return promise;
}
ScrPromise *result = scr_fetch_static(url, view);
scr_dyn_release(view);
return result;
}
ScrPromise *promise = scr_promise_new();
if (init && init->kind != SCR_DYN_OBJ &&
init->kind != SCR_DYN_UNDEF && init->kind != SCR_DYN_NULL) {
return sf_reject_now(promise, "fetch failed");
}
if (init && init->kind == SCR_DYN_OBJ) {
for (size_t i = 0; i < init->v.obj.len; i++) {
const ScrDynEntry *entry = &init->v.obj.entries[i];
bool supported = sf_name(entry->key, entry->key_len, "method") ||
sf_name(entry->key, entry->key_len, "headers") ||
sf_name(entry->key, entry->key_len, "body") ||
sf_name(entry->key, entry->key_len, "duplex") ||
sf_name(entry->key, entry->key_len, "redirect") ||
sf_name(entry->key, entry->key_len, "signal");
bool recognized_unsupported =
sf_name(entry->key, entry->key_len, "cache") ||
sf_name(entry->key, entry->key_len, "credentials") ||
sf_name(entry->key, entry->key_len, "dispatcher") ||
sf_name(entry->key, entry->key_len, "integrity") ||
sf_name(entry->key, entry->key_len, "keepalive") ||
sf_name(entry->key, entry->key_len, "mode") ||
sf_name(entry->key, entry->key_len, "priority") ||
sf_name(entry->key, entry->key_len, "referrer") ||
sf_name(entry->key, entry->key_len, "referrerPolicy") ||
sf_name(entry->key, entry->key_len, "window");
if (!supported && recognized_unsupported && entry->value &&
entry->value->kind != SCR_DYN_UNDEF) {
return sf_reject_now(promise, "fetch failed");
}
}
}
/* WebIDL converts dictionary members by name, not object insertion
* order: body, duplex, headers, method, redirect, signal. Every
* observable conversion completes before Request validates the URL,
* method/body combination, or an already-aborted signal. */
const ScrDyn *body =
init && init->kind == SCR_DYN_OBJ
? scr_dyn_obj_get(init, "body", 4)
: NULL;
bool body_present =
body && body->kind != SCR_DYN_UNDEF && body->kind != SCR_DYN_NULL;
bool body_stream =
body_present &&
body->kind == SCR_DYN_HANDLE &&
body->v.handle.tag == SCR_DYNH_WEB_STREAM;
/* BodyInit's USVString fallback applies to every ordinary value that
* is not already bytes or a ReadableStream. This includes scalars,
* arrays, and plain objects (with the ordinary JS object coercion
* protocol), while null and undefined continue to mean no body. */
ScrDyn *coerced_body = NULL;
/* A URLSearchParams body is its serialization, sent as
* application/x-www-form-urlencoded;charset=UTF-8 (extract a body). */
bool form_body = false;
if (body_present && body->kind == SCR_DYN_HANDLE &&
body->v.handle.tag == SCR_DYNH_SEARCH_PARAMS) {
coerced_body = scr_dyn_handle_ops_of(body)->invoke(body->v.handle.ptr, (ScrDyn *)body,
"toString", NULL, 0, "URLSearchParams");
if (!coerced_body || coerced_body->kind != SCR_DYN_STR) {
scr_dyn_release(coerced_body);
return sf_reject_now(promise, "fetch failed");
}
body = coerced_body;
form_body = true;
}
if (body_present && !form_body && body->kind != SCR_DYN_STR &&
body->kind != SCR_DYN_BYTES && !body_stream) {
ScrStr *text = scr_dyn_string_coerce_js(body);
if (!text) {
return sf_reject_now(promise, "fetch failed");
}
coerced_body = scr_dyn_new_str(text);
scr_str_release(text);
body = coerced_body;
}
const ScrDyn *duplex =
init && init->kind == SCR_DYN_OBJ
? scr_dyn_obj_get(init, "duplex", 6)
: NULL;
bool duplex_present =
duplex && duplex->kind != SCR_DYN_UNDEF;
bool duplex_half = false;
if (duplex_present) {
ScrStr *duplex_mode = scr_dyn_string_coerce_js(duplex);
if (!duplex_mode) {
scr_dyn_release(coerced_body);
return sf_reject_now(promise, "fetch failed");
}
duplex_half =
duplex_mode->len == 4 &&
memcmp(duplex_mode->data, "half", 4) == 0;
scr_str_release(duplex_mode);
}
if (duplex_present && !duplex_half) {
scr_dyn_release(coerced_body);
return sf_reject_now(
promise, "RequestInit.duplex must be 'half'");
}
ScrArr *headers = scr_arr_new(SCR_ELEM_STR, 16);
const ScrDyn *init_headers =
init && init->kind == SCR_DYN_OBJ
? scr_dyn_obj_get(init, "headers", 7)
: NULL;
if (!sf_add_headers(headers, init_headers, true)) {
scr_dyn_release(coerced_body);
scr_arr_release(headers);
return sf_reject_now(promise, "fetch failed");
}
ScrStr *method = sf_method_value(init);
if (!method) {
scr_dyn_release(coerced_body);
scr_arr_release(headers);
return sf_reject_now(promise, "fetch failed");
}
int redirect_mode;
if (!sf_redirect_mode(init, &redirect_mode)) {
scr_dyn_release(coerced_body);
scr_arr_release(headers);
scr_str_release(method);
return sf_reject_now(promise, "fetch failed");
}
const ScrDyn *signal_dyn =
init && init->kind == SCR_DYN_OBJ
? scr_dyn_obj_get(init, "signal", 6)
: NULL;
SfSignal *signal = NULL;
if (signal_dyn && signal_dyn->kind != SCR_DYN_UNDEF &&
signal_dyn->kind != SCR_DYN_NULL) {
signal = sf_signal_of(signal_dyn,
"Request init.signal must be an AbortSignal");
if (!signal) {
scr_dyn_release(coerced_body);
scr_arr_release(headers);
scr_str_release(method);
return sf_reject_now(promise, "fetch failed");
}
}
ScrUrl *u = scr_url_new(url);
if (!u) {
sf_throw_url_parse_failure(url);
scr_dyn_release(coerced_body);
scr_arr_release(headers);
scr_str_release(method);
scr_promise_reject_pending(promise);
return promise;
}
if (u->userinfo->len > 0) {
static const char prefix[] =
"Request cannot be constructed from a URL that includes credentials: ";
size_t message_len = sizeof prefix - 1 + url->len;
char *message = malloc(message_len + 1);
if (!message) sf_oom();
memcpy(message, prefix, sizeof prefix - 1);
memcpy(message + sizeof prefix - 1, url->data, url->len);
message[message_len] = '\0';
sf_type_error(message);
free(message);
scr_dyn_release(coerced_body);
scr_arr_release(headers);
scr_str_release(method);
scr_url_release(u);
return sf_reject_now(promise, "fetch failed");
}
bool https = sf_eq_ci(u->scheme, "https");
bool data_url = sf_eq_ci(u->scheme, "data");
if (!https && !sf_eq_ci(u->scheme, "http") && !data_url) {
scr_dyn_release(coerced_body);
scr_arr_release(headers);
scr_str_release(method);
scr_url_release(u);
return sf_reject_now(promise, "fetch failed");
}
if (!sf_method_validate(&method)) {
scr_dyn_release(coerced_body);
scr_arr_release(headers);
scr_str_release(method);
scr_url_release(u);
return sf_reject_now(promise, "fetch failed");
}
if (body_stream && !duplex_half) {
scr_dyn_release(coerced_body);
scr_arr_release(headers);
scr_str_release(method);
scr_url_release(u);
return sf_reject_now(
promise,
"RequestInit: duplex option is required when sending a body.");
}
if (body_present &&
((method->len == 3 && memcmp(method->data, "GET", 3) == 0) ||
(method->len == 4 && memcmp(method->data, "HEAD", 4) == 0))) {
scr_dyn_release(coerced_body);
scr_arr_release(headers);
scr_str_release(method);
scr_url_release(u);
return sf_reject_now(
promise, "Request with GET/HEAD method cannot have body.");
}
/* Fetch Metadata is controlled by the fetch implementation. Undici
* replaces a caller-provided sec-fetch-mode with "cors". */
sf_strip_header(&headers, "sec-fetch-mode");
if (form_body && !sf_pairs_have(headers, "content-type")) {
sf_push_header_text(headers, "content-type",
"application/x-www-form-urlencoded;charset=UTF-8", 47);
} else if (body && body->kind == SCR_DYN_STR &&
!sf_pairs_have(headers, "content-type")) {
sf_push_header_text(headers, "content-type",
"text/plain;charset=UTF-8", 24);
}
bool has_content_length = false;
size_t content_length = 0;
if (!sf_content_length(headers, &has_content_length, &content_length)) {
scr_dyn_release(coerced_body);
scr_arr_release(headers);
scr_str_release(method);
scr_url_release(u);
return sf_reject_now(promise, "fetch failed");
}
if (has_content_length && !body_stream) {
size_t body_length =
!body_present
? 0
: body->kind == SCR_DYN_STR
? body->v.str->len
: body->v.bytes->len;
if (body_length != content_length) {
scr_dyn_release(coerced_body);
scr_arr_release(headers);
scr_str_release(method);
scr_url_release(u);
return sf_reject_now(promise, "fetch failed");
}
}
/*
* Request construction validates URL/method/body/headers before fetch
* observes an already-aborted signal. Undici does the same: abort wins
* over starting I/O, but never masks a RequestInit validation error.
*/
if (signal && signal->aborted) {
scr_dyn_release(coerced_body);
scr_arr_release(headers);
scr_str_release(method);
scr_url_release(u);
ScrDyn *reason = sf_signal_reason_ref(signal);
sf_reject_promise_reason(promise, reason);
scr_dyn_release(reason);
return promise;
}
if (data_url) {
ScrDyn *response = sf_data_response(url);
scr_dyn_release(coerced_body); scr_arr_release(headers); scr_str_release(method); scr_url_release(u);
if (response) { scr_promise_resolve_dyn(promise, response); }
else scr_promise_reject_pending(promise);
return promise;
}
SfTransfer *t = calloc(1, sizeof *t);
if (!t) sf_oom();
t->rc = 1; /* registry */
t->promise = scr_promise_retain(promise);
t->url = u;
t->method = method;
t->headers = headers;
t->body = body_present ? scr_dyn_retain((ScrDyn *)body) : NULL;
scr_dyn_release(coerced_body);
t->redirect_mode = redirect_mode;
t->request_has_content_length = has_content_length;
t->request_content_length = content_length;
if (signal) {
t->signal = sf_signal_retain(signal);
t->signal_watch =
sf_signal_watch(signal, t, &sf_transfer_abort_watch);
}
t->next = sf_live;
sf_live = t;
sf_start_hop(t);
return promise;
}
/* ── handle dispatch tables + teardown ───────────────────────────── */
static bool sf_no_set(void *ptr, const char *key, size_t len,
const ScrDyn *value) {
(void)ptr;
(void)key;
(void)len;
(void)value;
return false;
}
/* The members invoke answers, as values (ScrDynHandleMethod), with Node's
* lengths and the brand check each one runs on another receiver. The
* signal's EventTarget methods are EventTarget's own (sf_signal_get). */
static const ScrDynHandleMethod sf_signal_methods[] = {
{"throwIfAborted", 0, false}, {NULL, 0, false}};
static const ScrDynHandleMethod sf_stream_methods[] = {
{"cancel", 0, true}, {"getReader", 0, false}, {NULL, 0, false}};
static const ScrDynHandleMethod sf_reader_methods[] = {
{"read", 0, true}, {"releaseLock", 0, false}, {"cancel", 0, true},
{NULL, 0, false}};
static const ScrDynHandleMethod sf_body_methods[] = {
{"json", 0, true}, {"text", 0, true}, {"bytes", 0, true},
{"arrayBuffer", 0, true}, {"clone", 0, false}, {NULL, 0, false}};
static const ScrDynHandleMethod sf_headers_methods[] = {
{"append", 2, false}, {"delete", 1, false}, {"entries", 0, false},
{"forEach", 1, false}, {"get", 1, false}, {"getSetCookie", 0, false},
{"has", 1, false}, {"keys", 0, false}, {"set", 2, false},
{"values", 0, false}, {NULL, 0, false}};
static const ScrDynHandleMethod sf_writable_methods[] = {
{"abort", 0, true}, {"close", 0, true}, {"getWriter", 0, false},
{NULL, 0, false}};
static const ScrDynHandleMethod sf_writer_methods[] = {
{"abort", 0, true}, {"close", 0, true}, {"releaseLock", 0, false},
{"write", 0, true}, {NULL, 0, false}};
static ScrDyn *sf_rejected(void) {
ScrPromise *p = scr_promise_settled_ref(NULL, &scr_dyn_retain_v,
&scr_dyn_release_v, NULL);
ScrDyn *out = scr_dyn_new_promise(p);
scr_promise_release(p);
return out;
}
static const ScrDynHandleOps sf_signal_ops = {
"AbortSignal", &sf_signal_retain_v, &sf_signal_release_v,
&sf_signal_invoke, &sf_signal_get, &sf_signal_set, NULL,
.methods = sf_signal_methods,
.invalid_this = "Value of \"this\" must be of type AbortSignal"};
static const ScrDynHandleOps sf_abort_controller_ops = {
"AbortController", &sf_signal_retain_v, &sf_signal_release_v,
&sf_abort_controller_invoke, &sf_abort_controller_get, &sf_no_set, NULL};
static const ScrDynHandleOps sf_stream_ops = {
"ReadableStream", &sf_stream_retain_v, &sf_stream_release_v,
&sf_stream_invoke, &sf_stream_get, &sf_no_set, NULL,
.methods = sf_stream_methods,
.invalid_this = "Value of \"this\" must be of type ReadableStream",
.rejected = &sf_rejected};
static const ScrDynHandleOps sf_reader_ops = {
"ReadableStreamDefaultReader", &sf_reader_handle_retain_v,
&sf_reader_handle_release_v, &sf_reader_invoke, &sf_reader_get,
&sf_no_set, NULL, .methods = sf_reader_methods,
.invalid_this =
"Value of \"this\" must be of type ReadableStreamDefaultReader",
.rejected = &sf_rejected};
static const ScrDynHandleOps sf_controller_ops = {
"ReadableStreamDefaultController", &sf_stream_retain_v,
&sf_stream_release_v, &sf_controller_invoke, &sf_controller_get,
&sf_no_set, NULL};
static const ScrDynHandleOps sf_request_ops = {
"Request", &sf_request_retain_v, &sf_request_release_v,
&sf_request_invoke, &sf_request_get, &sf_no_set, NULL,
.methods = sf_body_methods, .rejected = &sf_rejected};
static const ScrDynHandleOps sf_response_ops = {
"Response", &sf_response_retain_v, &sf_response_release_v,
&sf_response_invoke, &sf_response_get, &sf_no_set, NULL,
.methods = sf_body_methods, .rejected = &sf_rejected};
static const ScrDynHandleOps sf_headers_ops = {
"Headers", &sf_headers_retain_v, &sf_headers_release_v,
&sf_headers_invoke, &sf_headers_get, &sf_no_set, NULL, NULL,
&sf_headers_step, .methods = sf_headers_methods};
static const ScrDynHandleOps sf_transform_ops = {
"TransformStream", &sf_transform_retain_v, &sf_transform_release_v,
&sf_transform_invoke, &sf_transform_get, &sf_no_set, NULL};
static const ScrDynHandleOps sf_writable_ops = {
"WritableStream", &sf_transform_retain_v, &sf_transform_release_v,
&sf_writable_invoke, &sf_writable_get, &sf_no_set, NULL,
.methods = sf_writable_methods,
.invalid_this = "Value of \"this\" must be of type WritableStream",
.rejected = &sf_rejected};
static const ScrDynHandleOps sf_writer_ops = {
"WritableStreamDefaultWriter", &sf_writer_handle_retain_v,
&sf_writer_handle_release_v, &sf_writer_invoke, &sf_writer_get,
&sf_no_set, NULL, .methods = sf_writer_methods,
.invalid_this =
"Value of \"this\" must be of type WritableStreamDefaultWriter",
.rejected = &sf_rejected};
static void sf_teardown(void) {
while (sf_live) {
SfTransfer *t = sf_live;
if (t->client) scr_http_client_destroy(t->client);
if (t->response_stream) sf_stream_close(t->response_stream);
sf_settle(t);
}
/*
* Dyn function boxes deliberately do not participate in trial deletion.
* At process/session teardown no Web object remains observable, so sever
* every native callback edge before the collector and RC audit run.
* Releasing a callback may destroy and unlink another tracked owner.
*/
while (sf_callback_streams) {
sf_stream_drop_source_callbacks(sf_callback_streams, true);
}
while (sf_reason_signals) {
sf_signal_drop_reason(sf_reason_signals);
}
sf_proxy_snapshot_free();
}
void scr_fetch_install(void) {
static SCR_TL bool installed;
if (installed) return;
installed = true;
scr_tls_ca_install();
scr_global_fetch_install(scr_fetch_function);
sf_proxy_snapshot();
scr_net_install();
scr_dyn_handle_install(SCR_DYNH_ABORT_SIGNAL, &sf_signal_ops);
scr_dyn_handle_install(SCR_DYNH_ABORT_CONTROLLER,
&sf_abort_controller_ops);
scr_dyn_handle_install(SCR_DYNH_WEB_STREAM, &sf_stream_ops);
scr_dyn_handle_install(SCR_DYNH_WEB_READER, &sf_reader_ops);
scr_dyn_handle_install(SCR_DYNH_WEB_CONTROLLER, &sf_controller_ops);
scr_dyn_handle_install(SCR_DYNH_FETCH_REQUEST, &sf_request_ops);
scr_dyn_handle_install(SCR_DYNH_FETCH_RESPONSE, &sf_response_ops);
scr_dyn_handle_install(SCR_DYNH_FETCH_HEADERS, &sf_headers_ops);
scr_dyn_handle_install(SCR_DYNH_WEB_TRANSFORM, &sf_transform_ops);
scr_dyn_handle_install(SCR_DYNH_WEB_WRITABLE, &sf_writable_ops);
scr_dyn_handle_install(SCR_DYNH_WEB_WRITER, &sf_writer_ops);
scr_et_signal_core_fn = sf_signal_core_of;
scr_atexit(sf_teardown);
}
#else /* SCR_FETCH_SIGNAL_ONLY */
static bool sf_no_set(void *ptr, const char *key, size_t len, const ScrDyn *value) {
(void)ptr; (void)key; (void)len; (void)value;
return false;
}
static const ScrDynHandleMethod sf_signal_methods[] = {
{"throwIfAborted", 0, false}, {NULL, 0, false}};
static const ScrDynHandleOps sf_signal_ops = {
"AbortSignal", &sf_signal_retain_v, &sf_signal_release_v,
&sf_signal_invoke, &sf_signal_get, &sf_signal_set, NULL,
.methods = sf_signal_methods,
.invalid_this = "Value of \"this\" must be of type AbortSignal" };
static const ScrDynHandleOps sf_abort_controller_ops = {
"AbortController", &sf_signal_retain_v, &sf_signal_release_v,
&sf_abort_controller_invoke, &sf_abort_controller_get, &sf_no_set, NULL };
static void sf_signal_teardown(void) {
while (sf_reason_signals) sf_signal_drop_reason(sf_reason_signals);
}
static void sf_signal_install(void) {
static SCR_TL bool installed;
if (installed) return;
installed = true;
scr_dyn_handle_install(SCR_DYNH_ABORT_SIGNAL, &sf_signal_ops);
scr_dyn_handle_install(SCR_DYNH_ABORT_CONTROLLER, &sf_abort_controller_ops);
scr_et_signal_core_fn = sf_signal_core_of;
scr_atexit(sf_signal_teardown);
}
#endif /* SCR_FETCH_SIGNAL_ONLY */
#undef SCR_FETCH_BAD_PORTS