Read Web Headers pairs without copying them, and cache their sorted list

Headers kept its name/value pairs in a string array and read every element
through scr_arr_get_ref (index checks, retain, release). forEach and the
iterators re-sorted and rescanned the whole list for every step, and every
Response or Headers built from another Headers object re-validated and
re-normalized each copied pair. The Hono fixture spends 12-15% of a request
there.

- Pair loops read borrowed elements of the dense array (sf_pair_at).
- The sort-and-combine list is cached on the Headers object and dropped by
  every mutation, so live iteration keeps Node's semantics.
- get() of a single value returns the stored string.
- Copying a Headers object whose pairs are canonical (built by the
  constructor or a Response) into an empty list copies the pairs; wire
  response headers keep the per-pair path because they may repeat names.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Malte Ubl
2026-10-23 08:03:59 -07:00
co-authored by Claude Opus 5.5
parent 8476f68136
commit 056dda3633
4 changed files with 236 additions and 124 deletions
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/fetch-headers-copy-cache.ts"
],
"diags": []
}
+144 -124
View File
@@ -172,7 +172,14 @@ struct SfStream {
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 {
@@ -2164,6 +2171,24 @@ static ScrDyn *sf_controller_get(void *ptr, const char *key, size_t len) {
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();
@@ -2232,13 +2257,8 @@ 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) {
ScrStr *raw_name = scr_arr_get_ref(raw_pairs, (double)i);
scr_arr_push_ref(
pairs, sf_header_name_lower(raw_name->data, raw_name->len));
scr_str_release(raw_name);
ScrStr *raw = scr_arr_get_ref(raw_pairs, (double)(i + 1));
scr_arr_push_ref(pairs, sf_latin1_to_utf8(raw));
scr_str_release(raw);
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);
@@ -2252,9 +2272,17 @@ static SfHeaders *sf_headers_retain(SfHeaders *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);
}
@@ -2294,7 +2322,7 @@ static ScrStr *sf_headers_name(const ScrDyn *value, const char *method) {
scr_str_release(raw);
return NULL;
}
ScrStr *out = sf_header_name_lower(raw->data, raw->len);
ScrStr *out = sf_header_name_lower_str(raw);
scr_str_release(raw);
return out;
}
@@ -2305,40 +2333,36 @@ static bool sf_headers_key_eq(const ScrStr *left, const ScrStr *right) {
}
static ScrStr *sf_headers_get_value(SfHeaders *h, const ScrStr *name) {
size_t n = (size_t)scr_arr_len(h->pairs);
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) {
ScrStr *key = scr_arr_get_ref(h->pairs, (double)i);
ScrStr *value = scr_arr_get_ref(h->pairs, (double)(i + 1));
if (sf_headers_key_eq(key, name)) {
if (count > 0) total += 2;
total += value->len;
count++;
}
scr_str_release(key);
scr_str_release(value);
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) {
ScrStr *key = scr_arr_get_ref(h->pairs, (double)i);
ScrStr *value = scr_arr_get_ref(h->pairs, (double)(i + 1));
if (sf_headers_key_eq(key, name)) {
if (emitted > 0) {
joined[at++] = ',';
joined[at++] = ' ';
}
memcpy(joined + at, value->data, value->len);
at += value->len;
emitted++;
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++] = ' ';
}
scr_str_release(key);
scr_str_release(value);
memcpy(joined + at, value->data, value->len);
at += value->len;
emitted++;
}
ScrStr *out = scr_str_new(joined, total);
free(joined);
@@ -2348,17 +2372,17 @@ static ScrStr *sf_headers_get_value(SfHeaders *h, const ScrStr *name) {
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 = scr_arr_get_ref(old, (double)i);
ScrStr *value = scr_arr_get_ref(old, (double)(i + 1));
if (sf_headers_key_eq(key, name)) {
scr_str_release(key);
scr_str_release(value);
continue;
}
scr_arr_push_ref(next, key);
scr_arr_push_ref(next, value);
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);
@@ -2399,15 +2423,13 @@ static bool sf_headers_convert_pair(
* 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) {
ScrStr *key = scr_arr_get_ref(h->pairs, (double)i);
bool match = sf_headers_key_eq(key, name);
scr_str_release(key);
if (!match) continue;
if (!sf_headers_key_eq(sf_pair_at(h->pairs, i), name)) continue;
ScrStr *previous = scr_arr_get_ref(h->pairs, (double)(i + 1));
ScrStr *previous = sf_pair_at(h->pairs, i + 1);
const char *separator =
name->len == 6 && memcmp(name->data, "cookie", 6) == 0
? "; "
@@ -2420,7 +2442,6 @@ static void sf_headers_append_pair(
memcpy(joined_data + previous->len + 2, value->data, value->len);
ScrStr *joined = scr_str_new(joined_data, joined_len);
free(joined_data);
scr_str_release(previous);
scr_str_release(name);
scr_str_release(value);
scr_arr_set_ref(h->pairs, (double)(i + 1), joined);
@@ -2447,7 +2468,7 @@ static ScrStr **sf_headers_sorted_names(SfHeaders *h, size_t *count_out) {
if (capacity && !names) sf_oom();
size_t count = 0;
for (size_t i = 0; i + 1 < n; i += 2) {
ScrStr *key = scr_arr_get_ref(h->pairs, (double)i);
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)) {
@@ -2455,8 +2476,7 @@ static ScrStr **sf_headers_sorted_names(SfHeaders *h, size_t *count_out) {
break;
}
}
if (seen) scr_str_release(key);
else names[count++] = key;
if (!seen) names[count++] = key;
}
if (count > 1) {
qsort(names, count, sizeof *names, &sf_headers_name_cmp);
@@ -2469,44 +2489,44 @@ static bool sf_headers_is_set_cookie(const ScrStr *name) {
return name->len == 10 && memcmp(name->data, "set-cookie", 10) == 0;
}
/* The `index`th entry of Fetch's "sort and combine" list: 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. False past the end. Outputs are +1. */
/* 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) {
size_t count = 0;
ScrStr **names = sf_headers_sorted_names(h, &count);
bool found = false;
size_t at = 0;
for (size_t i = 0; i < count && !found; i++) {
if (!sf_headers_is_set_cookie(names[i])) {
if (at == index) {
*name_out = scr_str_retain(names[i]);
*value_out = sf_headers_get_value(h, names[i]);
found = true;
}
at++;
continue;
}
size_t n = (size_t)scr_arr_len(h->pairs);
for (size_t j = 0; j + 1 < n && !found; j += 2) {
ScrStr *key = scr_arr_get_ref(h->pairs, (double)j);
bool match = sf_headers_is_set_cookie(key);
scr_str_release(key);
if (!match) continue;
if (at == index) {
*name_out = scr_str_retain(names[i]);
*value_out = scr_arr_get_ref(h->pairs, (double)(j + 1));
found = true;
}
at++;
}
}
for (size_t j = 0; j < count; j++) scr_str_release(names[j]);
free(names);
return found;
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
@@ -2570,14 +2590,8 @@ static ScrDyn *sf_headers_invoke(void *ptr, ScrDyn *self,
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) {
ScrStr *key = scr_arr_get_ref(h->pairs, (double)i);
ScrStr *value = scr_arr_get_ref(h->pairs, (double)(i + 1));
if (key->len == 10 &&
memcmp(key->data, "set-cookie", 10) == 0) {
scr_dyn_arr_push(out, scr_dyn_new_str(value));
}
scr_str_release(key);
scr_str_release(value);
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;
}
@@ -3323,14 +3337,13 @@ 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 = (ScrStr *)scr_arr_get_ref(pairs, (double)i);
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];
}
scr_str_release(key);
if (same) return true;
}
return false;
@@ -3437,11 +3450,14 @@ static ScrStr *sf_trim_header_value(const ScrStr *value) {
(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, const char *name, size_t name_len,
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");
@@ -3467,7 +3483,7 @@ static bool sf_push_header(ScrArr *pairs, const char *name, size_t name_len,
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 = scr_arr_get_ref(pairs, (double)i);
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];
@@ -3476,11 +3492,9 @@ static bool sf_push_header(ScrArr *pairs, const char *name, size_t name_len,
if (b >= 'A' && b <= 'Z') b = (unsigned char)(b - 'A' + 'a');
match = a == b;
}
scr_str_release(key);
if (!match) continue;
ScrStr *previous =
scr_arr_get_ref(pairs, (double)(i + 1));
ScrStr *previous = sf_pair_at(pairs, i + 1);
const char *separator =
sf_header_name_ci(name, name_len, "cookie") ? "; " : ", ";
size_t separator_len = 2;
@@ -3494,7 +3508,6 @@ static bool sf_push_header(ScrArr *pairs, const char *name, size_t name_len,
normalized->data, normalized->len);
ScrStr *joined = scr_str_new(joined_data, joined_len);
free(joined_data);
scr_str_release(previous);
scr_str_release(normalized);
if (request_guard && !sf_request_header_ok(name, name_len, joined)) {
scr_str_release(joined);
@@ -3513,8 +3526,8 @@ static bool sf_push_header(ScrArr *pairs, const char *name, size_t name_len,
scr_arr_push_ref(
pairs,
request_guard
? scr_str_new(name, name_len)
: sf_header_name_lower(name, name_len));
? scr_str_retain(name_str)
: sf_header_name_lower_str(name_str));
scr_arr_push_ref(pairs, normalized);
return true;
}
@@ -3541,10 +3554,13 @@ static void sf_headers_pair_length_error(size_t len) {
* 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. */
static ScrArr *sf_headers_convert_init(const ScrDyn *headers) {
/* *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);
ScrArr *converted = sf_headers_convert_init(materialized, canonical_out);
scr_dyn_release(materialized);
return converted;
}
@@ -3635,13 +3651,9 @@ static ScrArr *sf_headers_convert_init(const ScrDyn *headers) {
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 + 1 < n; i += 2) {
ScrStr *name = scr_arr_get_ref(source->pairs, (double)i);
ScrStr *value =
scr_arr_get_ref(source->pairs, (double)(i + 1));
scr_arr_push_ref(converted, name);
scr_arr_push_ref(converted, value);
}
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
@@ -3656,25 +3668,29 @@ fail:
}
static bool sf_add_converted_headers(
ScrArr *pairs, ScrArr *converted, bool request_guard) {
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) {
ScrStr *name = scr_arr_get_ref(converted, (double)i);
ScrStr *value = scr_arr_get_ref(converted, (double)(i + 1));
bool ok = sf_push_header(
pairs, name->data, name->len, value, request_guard);
scr_str_release(name);
scr_str_release(value);
if (!ok) return false;
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) {
ScrArr *converted = sf_headers_convert_init(headers);
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);
bool ok = sf_add_converted_headers(pairs, converted, request_guard, canonical);
scr_arr_release(converted);
return ok;
}
@@ -4854,6 +4870,7 @@ static ScrDyn *sf_response_construct(ScrDyn *body, ScrDyn *init,
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;
@@ -4918,7 +4935,7 @@ static ScrDyn *sf_response_construct(ScrDyn *body, ScrDyn *init,
* 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_headers = sf_headers_convert_init(init_headers, &converted_canonical);
scr_dyn_release(init_headers);
if (!converted_headers) goto fail;
@@ -5013,7 +5030,7 @@ static ScrDyn *sf_response_construct(ScrDyn *body, ScrDyn *init,
header_pairs = scr_arr_new(SCR_ELEM_STR, 8);
if (!sf_add_converted_headers(
header_pairs, converted_headers, false)) goto fail;
header_pairs, converted_headers, false, converted_canonical)) goto fail;
scr_arr_release(converted_headers);
converted_headers = NULL;
@@ -5044,6 +5061,7 @@ static ScrDyn *sf_response_construct(ScrDyn *body, ScrDyn *init,
response->rc = 1;
response->body = stream;
response->headers = sf_headers_new(header_pairs, false);
response->headers->canonical = true;
response->url = scr_str_new("", 0);
response->status_text = status_text;
response->status = status;
@@ -5159,13 +5177,15 @@ static ScrDyn *sf_request_invoke(void *ptr, ScrDyn *self, const char *method, Sc
}
ScrDyn *scr_fetch_headers_new(ScrDyn *init) {
ScrArr *converted = sf_headers_convert_init(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);
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 = true;
ScrDyn *result = scr_dyn_new_handle(headers, SCR_DYNH_FETCH_HEADERS);
sf_headers_release(headers);
return result;
+85
View File
@@ -0,0 +1,85 @@
// Headers copies and the cached sort-and-combine list: copies are
// independent of their source, reads see every mutation, and live
// iterators re-read the current list after a change.
function dump(label: string, h: Headers): void {
const parts: string[] = [];
h.forEach((value, name) => parts.push(`${name}=${value}`));
console.log(`${label}: ${parts.join(" | ")}`);
}
// The corpus lib has no DOM iterable typings for Headers, so iterators
// are reached through an untyped view.
const iter = (h: Headers): any => h;
const source = new Headers({ "X-B": " two ", "x-a": "one", Accept: "text/html" });
source.append("accept", "application/json");
source.append("Set-Cookie", "a=1");
source.append("set-cookie", "b=2");
dump("source", source);
// A Headers copy keeps the combined values and stays separate.
const copy = new Headers(source);
source.set("x-a", "changed");
copy.append("x-c", "three");
dump("source after set", source);
dump("copy after append", copy);
console.log(JSON.stringify(copy.get("accept")), JSON.stringify(copy.get("set-cookie")));
console.log(JSON.stringify(copy.getSetCookie()));
// Copying into an empty Response and a Request, then appending a repeat.
const response = new Response("body", { headers: copy, status: 201 });
response.headers.append("accept", "*/*");
response.headers.append("cookie", "c=3");
response.headers.append("Cookie", "d=4");
dump("response", response.headers);
dump("copy unchanged", copy);
const clone = new Response(response.body, response);
dump("response clone", clone.headers);
const request = new Request("http://example.test/", { headers: copy });
console.log("request x-b", JSON.stringify(request.headers.get("X-B")));
// Every read after a mutation sees it: get, has, iteration, spread.
const h = new Headers([["x-z", "z"], ["x-m", "m"]]);
console.log([...iter(h).entries()].map(([n, v]) => `${n}:${v}`).join(","));
h.delete("x-z");
console.log([...iter(h).keys()].join(","), h.has("x-z"));
h.delete("x-missing");
console.log([...iter(h).values()].join(","));
h.append("x-a", "first");
console.log([...iter(h).entries()].map(([n, v]) => `${n}:${v}`).join(","));
h.set("X-M", "again");
console.log(JSON.stringify(h.get("x-m")), [...iter(h).keys()].join(","));
// A live iterator: mutations between steps change what comes next.
const it = iter(h).entries();
console.log(JSON.stringify(it.next().value));
h.append("x-0", "before");
console.log(JSON.stringify(it.next().value));
h.delete("x-m");
h.append("x-zz", "after");
console.log(JSON.stringify(it.next()), JSON.stringify(it.next()));
// forEach that mutates as it walks.
const walk = new Headers([["b", "2"], ["d", "4"]]);
const seen: string[] = [];
walk.forEach((value, name) => {
seen.push(`${name}=${value}`);
if (name === "b") {
walk.append("c", "3");
walk.append("a", "1");
}
if (name === "c") walk.set("d", "four");
});
console.log(seen.join(" | "));
// Invalid input in a copy source is still rejected (plain object source).
try {
new Response("x", { headers: { "bad name": "v" } });
} catch (error) {
console.log((error as Error).name, (error as Error).message);
}
try {
new Headers(source).append("x", "a\nb");
} catch (error) {
console.log((error as Error).name, (error as Error).message);
}
@@ -0,0 +1 @@
{ "plain": 6400, "san": 9200 }