]> git.kaiwu.me - njs.git/commitdiff
Fetch: centralize Headers mutations
authorDmitry Volyntsev <xeioex@nginx.com>
Thu, 13 Aug 2026 00:22:31 +0000 (17:22 -0700)
committerDmitry Volyntsev <xeioexception@gmail.com>
Thu, 13 Aug 2026 14:00:21 +0000 (07:00 -0700)
Previously, Headers.set() validated its arguments only when the header
did not already exist.  Existing values were overwritten without
trimming or validating the new value and without checking the immutable
guard.  It also detached duplicate values only from the first header's
chain, while leaving the corresponding list entries active.  The stale
values could reappear when the Headers object was copied or serialized.

The fix is to move append(), set(), and delete() validation and list
updates into engine-independent helpers, deactivate every duplicate on
replacement, reject empty names, and enforce immutable guards
consistently.  Content-Type presence is now derived from the header list
instead of a separate cached pointer.

Reported by vladimirelitokarev@gmail.com.

nginx/ngx_js_fetch.c
nginx/ngx_js_http.c
nginx/ngx_js_http.h
nginx/ngx_qjs_fetch.c
nginx/t/js_fetch_objects.t

index 554f14f418540cc4b228bdfcd903fe0b0604b79e..e2ce03207a58a870f7c8f95f18660030312ef6ca 100644 (file)
@@ -55,6 +55,8 @@ static ngx_int_t ngx_js_fetch_append_headers(ngx_js_http_t *http,
     ngx_js_headers_t *headers, u_char *name, size_t len, u_char *value,
     size_t vlen);
 static void ngx_js_fetch_process_done(ngx_js_http_t *http);
+static njs_int_t ngx_js_headers_result(njs_vm_t *vm,
+    ngx_js_headers_rc_t rc);
 static njs_int_t ngx_js_headers_append(njs_vm_t *vm, ngx_js_headers_t *headers,
     u_char *name, size_t len, u_char *value, size_t vlen);
 
@@ -1309,7 +1311,6 @@ ngx_js_request_constructor(njs_vm_t *vm, ngx_js_request_t *request,
      *  request->cache_mode = CACHE_MODE_DEFAULT;
      *  request->credentials = CREDENTIALS_SAME_ORIGIN;
      *  request->mode = MODE_NO_CORS;
-     *  request->headers.content_type = NULL;
      */
 
     ngx_memzero(request, sizeof(ngx_js_request_t));
@@ -1473,7 +1474,9 @@ ngx_js_request_constructor(njs_vm_t *vm, ngx_js_request_t *request,
                 return NJS_ERROR;
             }
 
-            if (request->headers.content_type == NULL
+            if (!ngx_js_headers_has(&request->headers,
+                                    (u_char *) "Content-Type",
+                                    njs_length("Content-Type"))
                 && njs_value_is_string(value))
             {
                 ret = ngx_js_headers_append(vm, &request->headers,
@@ -1526,86 +1529,32 @@ ngx_js_fetch_process_done(ngx_js_http_t *http)
 
 
 static njs_int_t
-ngx_js_headers_append(njs_vm_t *vm, ngx_js_headers_t *headers,
-    u_char *name, size_t len, u_char *value, size_t vlen)
+ngx_js_headers_result(njs_vm_t *vm, ngx_js_headers_rc_t rc)
 {
-    ngx_int_t         ret;
-    ngx_uint_t        i;
-    ngx_js_tb_elt_t  *h, **ph;
-    ngx_list_part_t  *part;
-
-    ngx_js_http_trim_ows(&value, &vlen);
-
-    ret = ngx_js_check_header_name(name, len);
-    if (ret != NGX_OK) {
-        njs_vm_type_error(vm, "invalid header name");
-        return NJS_ERROR;
-    }
-
-    ret = ngx_js_check_header_value(value, vlen);
-    if (ret != NGX_OK) {
-        njs_vm_type_error(vm, "invalid header value");
-        return NJS_ERROR;
-    }
-
-    if (headers->guard == GUARD_IMMUTABLE) {
-        njs_vm_type_error(vm, "cannot append to immutable object");
-        return NJS_ERROR;
+    if (rc == NGX_JS_HEADERS_OK) {
+        return NJS_OK;
     }
 
-    ph = NULL;
-    part = &headers->header_list.part;
-    h = part->elts;
-
-    for (i = 0; /* void */; i++) {
-
-        if (i >= part->nelts) {
-            if (part->next == NULL) {
-                break;
-            }
-
-            part = part->next;
-            h = part->elts;
-            i = 0;
-        }
-
-        if (h[i].hash == 0) {
-            continue;
-        }
+    if (rc == NGX_JS_HEADERS_NOMEM) {
+        njs_vm_memory_error(vm);
 
-        if (len == h[i].key.len
-            && (njs_strncasecmp(name, h[i].key.data, len) == 0))
-        {
-            ph = &h[i].next;
-            while (*ph) { ph = &(*ph)->next; }
-            break;
-        }
+    } else {
+        njs_vm_type_error(vm, "%s", ngx_js_headers_error(rc));
     }
 
-    h = ngx_list_push(&headers->header_list);
-    if (h == NULL) {
-        njs_vm_memory_error(vm);
-        return NJS_ERROR;
-    }
+    return NJS_ERROR;
+}
 
-    if (ph != NULL) {
-        *ph = h;
-    }
 
-    h->hash = 1;
-    h->key.data = name;
-    h->key.len = len;
-    h->value.data = value;
-    h->value.len = vlen;
-    h->next = NULL;
+static njs_int_t
+ngx_js_headers_append(njs_vm_t *vm, ngx_js_headers_t *headers,
+    u_char *name, size_t len, u_char *value, size_t vlen)
+{
+    ngx_js_headers_rc_t  rc;
 
-    if (len == njs_strlen("Content-Type")
-        && ngx_strncasecmp(name, (u_char *) "Content-Type", len) == 0)
-    {
-        headers->content_type = h;
-    }
+    rc = ngx_js_headers_modify(headers, name, len, value, vlen, 0);
 
-    return NJS_OK;
+    return ngx_js_headers_result(vm, rc);
 }
 
 
@@ -1749,12 +1698,10 @@ static njs_int_t
 ngx_headers_js_ext_delete(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
     njs_index_t unused, njs_value_t *retval)
 {
-    njs_int_t          ret;
-    njs_str_t          name;
-    ngx_uint_t         i;
-    ngx_list_part_t   *part;
-    ngx_js_tb_elt_t   *h;
-    ngx_js_headers_t  *headers;
+    njs_int_t            ret;
+    njs_str_t            name;
+    ngx_js_headers_t    *headers;
+    ngx_js_headers_rc_t  rc;
 
     headers = njs_vm_external(vm, ngx_http_js_fetch_headers_proto_id,
                               njs_argument(args, 0));
@@ -1768,37 +1715,9 @@ ngx_headers_js_ext_delete(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
         return NJS_ERROR;
     }
 
-    part = &headers->header_list.part;
-    h = part->elts;
-
-    for (i = 0; /* void */; i++) {
-
-        if (i >= part->nelts) {
-            if (part->next == NULL) {
-                break;
-            }
-
-            part = part->next;
-            h = part->elts;
-            i = 0;
-        }
-
-        if (h[i].hash == 0) {
-            continue;
-        }
-
-        if (name.length == h[i].key.len
-            && (njs_strncasecmp(name.start, h[i].key.data, name.length) == 0))
-        {
-            h[i].hash = 0;
-        }
-    }
-
-    if (name.length == njs_strlen("Content-Type")
-        && ngx_strncasecmp(name.start, (u_char *) "Content-Type", name.length)
-           == 0)
-    {
-        headers->content_type = NULL;
+    rc = ngx_js_headers_remove(headers, name.start, name.length);
+    if (ngx_js_headers_result(vm, rc) != NJS_OK) {
+        return NJS_ERROR;
     }
 
     njs_value_undefined_set(retval);
@@ -2049,12 +1968,10 @@ static njs_int_t
 ngx_headers_js_ext_set(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
     njs_index_t unused, njs_value_t *retval)
 {
-    njs_int_t          ret;
-    njs_str_t          name, value;
-    ngx_uint_t         i;
-    ngx_list_part_t   *part;
-    ngx_js_tb_elt_t   *h, **ph, **pp;
-    ngx_js_headers_t  *headers;
+    njs_int_t            ret;
+    njs_str_t            name, value;
+    ngx_js_headers_t    *headers;
+    ngx_js_headers_rc_t  rc;
 
     headers = njs_vm_external(vm, ngx_http_js_fetch_headers_proto_id,
                               njs_argument(args, 0));
@@ -2073,51 +1990,12 @@ ngx_headers_js_ext_set(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
         return NJS_ERROR;
     }
 
-    part = &headers->header_list.part;
-    h = part->elts;
-
-    for (i = 0; /* void */; i++) {
-
-        if (i >= part->nelts) {
-            if (part->next == NULL) {
-                break;
-            }
-
-            part = part->next;
-            h = part->elts;
-            i = 0;
-        }
-
-        if (h[i].hash == 0) {
-            continue;
-        }
-
-        if (name.length == h[i].key.len
-            && (njs_strncasecmp(name.start, h[i].key.data, name.length) == 0))
-        {
-            h[i].value.len = value.length;
-            h[i].value.data = value.start;
-
-            ph = &h[i].next;
-
-            while (*ph) {
-                pp = ph;
-                ph = &(*ph)->next;
-                *pp = NULL;
-            }
-
-            goto done;
-        }
-    }
-
-    ret = ngx_js_headers_append(vm, headers, name.start, name.length,
-                                value.start, value.length);
-    if (ret != NJS_OK) {
+    rc = ngx_js_headers_modify(headers, name.start, name.length, value.start,
+                               value.length, 1);
+    if (ngx_js_headers_result(vm, rc) != NJS_OK) {
         return NJS_ERROR;
     }
 
-done:
-
     njs_value_undefined_set(retval);
 
     return NJS_OK;
index e5d367e9754d3d8fe33ba8efe724c1890bc9cf36..db8b0732fc66f97e5be2fa2b48b7cb472881beee 100644 (file)
@@ -1755,6 +1755,10 @@ ngx_js_check_header_name(u_char *name, size_t len)
 {
     u_char  *p, *end;
 
+    if (len == 0) {
+        return NGX_ERROR;
+    }
+
     p = name;
     end = p + len;
 
@@ -1810,6 +1814,193 @@ ngx_js_check_header_value(u_char *value, size_t len)
 }
 
 
+const char *
+ngx_js_headers_error(ngx_js_headers_rc_t rc)
+{
+    switch (rc) {
+    case NGX_JS_HEADERS_INVALID_NAME:
+        return "invalid header name";
+
+    case NGX_JS_HEADERS_INVALID_VALUE:
+        return "invalid header value";
+
+    case NGX_JS_HEADERS_IMMUTABLE:
+        return "cannot modify immutable object";
+
+    default:
+        return "unknown headers error";
+    }
+}
+
+
+ngx_js_headers_rc_t
+ngx_js_headers_modify(ngx_js_headers_t *headers, u_char *name, size_t len,
+    u_char *value, size_t vlen, njs_bool_t replace)
+{
+    ngx_uint_t        i;
+    ngx_list_part_t  *part;
+    ngx_js_tb_elt_t  *first, *h, **ph;
+
+    ngx_js_http_trim_ows(&value, &vlen);
+
+    if (ngx_js_check_header_name(name, len) != NGX_OK) {
+        return NGX_JS_HEADERS_INVALID_NAME;
+    }
+
+    if (ngx_js_check_header_value(value, vlen) != NGX_OK) {
+        return NGX_JS_HEADERS_INVALID_VALUE;
+    }
+
+    if (headers->guard == GUARD_IMMUTABLE) {
+        return NGX_JS_HEADERS_IMMUTABLE;
+    }
+
+    first = NULL;
+    part = &headers->header_list.part;
+    h = part->elts;
+
+    for (i = 0; /* void */; i++) {
+
+        if (i >= part->nelts) {
+            if (part->next == NULL) {
+                break;
+            }
+
+            part = part->next;
+            h = part->elts;
+            i = 0;
+        }
+
+        if (h[i].hash == 0 || len != h[i].key.len
+            || ngx_strncasecmp(name, h[i].key.data, len) != 0)
+        {
+            continue;
+        }
+
+        if (first == NULL) {
+            first = &h[i];
+
+            if (replace) {
+                first->value.data = value;
+                first->value.len = vlen;
+            }
+
+            continue;
+        }
+
+        if (replace) {
+            h[i].hash = 0;
+            h[i].next = NULL;
+        }
+    }
+
+    if (replace && first != NULL) {
+        first->next = NULL;
+        return NGX_JS_HEADERS_OK;
+    }
+
+    ph = NULL;
+
+    if (first != NULL) {
+        ph = &first->next;
+        while (*ph) { ph = &(*ph)->next; }
+    }
+
+    h = ngx_list_push(&headers->header_list);
+    if (h == NULL) {
+        return NGX_JS_HEADERS_NOMEM;
+    }
+
+    if (ph != NULL) {
+        *ph = h;
+    }
+
+    h->hash = 1;
+    h->key.data = name;
+    h->key.len = len;
+    h->value.data = value;
+    h->value.len = vlen;
+    h->next = NULL;
+
+    return NGX_JS_HEADERS_OK;
+}
+
+
+ngx_js_headers_rc_t
+ngx_js_headers_remove(ngx_js_headers_t *headers, u_char *name, size_t len)
+{
+    ngx_uint_t        i;
+    ngx_list_part_t  *part;
+    ngx_js_tb_elt_t  *h;
+
+    if (ngx_js_check_header_name(name, len) != NGX_OK) {
+        return NGX_JS_HEADERS_INVALID_NAME;
+    }
+
+    if (headers->guard == GUARD_IMMUTABLE) {
+        return NGX_JS_HEADERS_IMMUTABLE;
+    }
+
+    part = &headers->header_list.part;
+    h = part->elts;
+
+    for (i = 0; /* void */; i++) {
+
+        if (i >= part->nelts) {
+            if (part->next == NULL) {
+                break;
+            }
+
+            part = part->next;
+            h = part->elts;
+            i = 0;
+        }
+
+        if (h[i].hash != 0 && len == h[i].key.len
+            && ngx_strncasecmp(name, h[i].key.data, len) == 0)
+        {
+            h[i].hash = 0;
+            h[i].next = NULL;
+        }
+    }
+
+    return NGX_JS_HEADERS_OK;
+}
+
+
+njs_bool_t
+ngx_js_headers_has(ngx_js_headers_t *headers, u_char *name, size_t len)
+{
+    ngx_uint_t        i;
+    ngx_list_part_t  *part;
+    ngx_js_tb_elt_t  *h;
+
+    part = &headers->header_list.part;
+    h = part->elts;
+
+    for (i = 0; /* void */; i++) {
+
+        if (i >= part->nelts) {
+            if (part->next == NULL) {
+                break;
+            }
+
+            part = part->next;
+            h = part->elts;
+            i = 0;
+        }
+
+        if (h[i].hash != 0 && len == h[i].key.len
+            && ngx_strncasecmp(name, h[i].key.data, len) == 0)
+        {
+            return 1;
+        }
+    }
+
+    return 0;
+}
+
+
 static ngx_int_t
 ngx_js_http_get_keepalive_connection(ngx_js_http_t *http)
 {
index b9e4666c469bb5e64ad462efa3822dab5c1b003e..fc4276d03ff1c8760d8da33607081823ed79d932 100644 (file)
@@ -63,10 +63,18 @@ typedef struct {
         GUARD_RESPONSE,
     }                              guard;
     ngx_list_t                     header_list;
-    ngx_js_tb_elt_t               *content_type;
 } ngx_js_headers_t;
 
 
+typedef enum {
+    NGX_JS_HEADERS_OK = 0,
+    NGX_JS_HEADERS_INVALID_NAME,
+    NGX_JS_HEADERS_INVALID_VALUE,
+    NGX_JS_HEADERS_IMMUTABLE,
+    NGX_JS_HEADERS_NOMEM,
+} ngx_js_headers_rc_t;
+
+
 typedef struct {
     enum {
         CACHE_MODE_DEFAULT = 0,
@@ -184,6 +192,13 @@ void ngx_js_http_trim_ows(u_char **value, size_t *len);
 ngx_int_t ngx_js_check_header_name(u_char *name, size_t len);
 ngx_int_t ngx_js_check_request_line_component(u_char *value, size_t len);
 ngx_int_t ngx_js_check_header_value(u_char *value, size_t len);
+const char *ngx_js_headers_error(ngx_js_headers_rc_t rc);
+ngx_js_headers_rc_t ngx_js_headers_modify(ngx_js_headers_t *headers,
+    u_char *name, size_t len, u_char *value, size_t vlen, njs_bool_t replace);
+ngx_js_headers_rc_t ngx_js_headers_remove(ngx_js_headers_t *headers,
+    u_char *name, size_t len);
+njs_bool_t ngx_js_headers_has(ngx_js_headers_t *headers, u_char *name,
+    size_t len);
 
 ngx_buf_t *ngx_js_chain_to_buf(ngx_pool_t *pool, njs_chb_t *chain);
 
index 693fb48d1cb8c602142a202b11d62f9f95d8f89e..1e6f551b67bbff7d2ec6b663d3f4c82c88a8a10a 100644 (file)
@@ -48,6 +48,8 @@ static ngx_int_t ngx_qjs_fetch_append_headers(ngx_js_http_t *http,
     ngx_js_headers_t *headers, u_char *name, size_t len, u_char *value,
     size_t vlen);
 static void ngx_qjs_fetch_process_done(ngx_js_http_t *http);
+static ngx_int_t ngx_qjs_headers_result(JSContext *cx,
+    ngx_js_headers_rc_t rc);
 static ngx_int_t ngx_qjs_headers_append(JSContext *cx,
     ngx_js_headers_t *headers, u_char *name, size_t len, u_char *value,
     size_t vlen);
@@ -538,7 +540,6 @@ ngx_qjs_request_ctor(JSContext *cx, ngx_js_request_t *request,
      *  request->cache_mode = CACHE_MODE_DEFAULT;
      *  request->credentials = CREDENTIALS_SAME_ORIGIN;
      *  request->mode = MODE_NO_CORS;
-     *  request->headers.content_type = NULL;
      */
 
     ngx_memzero(request, sizeof(ngx_js_request_t));
@@ -725,7 +726,11 @@ ngx_qjs_request_ctor(JSContext *cx, ngx_js_request_t *request,
                 return NGX_ERROR;
             }
 
-            if (request->headers.content_type == NULL && JS_IsString(value)) {
+            if (!ngx_js_headers_has(&request->headers,
+                                    (u_char *) "Content-Type",
+                                    sizeof("Content-Type") - 1)
+                && JS_IsString(value))
+            {
                 rc = ngx_qjs_headers_append(cx, &request->headers,
                                         (u_char *) "Content-Type",
                                         sizeof("Content-Type") - 1,
@@ -1356,86 +1361,32 @@ ngx_qjs_fetch_process_done(ngx_js_http_t *http)
 
 
 static ngx_int_t
-ngx_qjs_headers_append(JSContext *cx, ngx_js_headers_t *headers,
-    u_char *name, size_t len, u_char *value, size_t vlen)
+ngx_qjs_headers_result(JSContext *cx, ngx_js_headers_rc_t rc)
 {
-    ngx_int_t         ret;
-    ngx_uint_t        i;
-    ngx_list_part_t  *part;
-    ngx_js_tb_elt_t  *h, **ph;
-
-    ngx_js_http_trim_ows(&value, &vlen);
-
-    ret = ngx_js_check_header_name(name, len);
-    if (ret != NGX_OK) {
-        JS_ThrowTypeError(cx, "invalid header name");
-        return NGX_ERROR;
-    }
-
-    ret = ngx_js_check_header_value(value, vlen);
-    if (ret != NGX_OK) {
-        JS_ThrowTypeError(cx, "invalid header value");
-        return NGX_ERROR;
-    }
-
-    if (headers->guard == GUARD_IMMUTABLE) {
-        JS_ThrowTypeError(cx, "cannot append to immutable object");
-        return NGX_ERROR;
+    if (rc == NGX_JS_HEADERS_OK) {
+        return NGX_OK;
     }
 
-    ph = NULL;
-    part = &headers->header_list.part;
-    h = part->elts;
-
-    for (i = 0; /* void */; i++) {
-
-        if (i >= part->nelts) {
-            if (part->next == NULL) {
-                break;
-            }
-
-            part = part->next;
-            h = part->elts;
-            i = 0;
-        }
-
-        if (h[i].hash == 0) {
-            continue;
-        }
+    if (rc == NGX_JS_HEADERS_NOMEM) {
+        JS_ThrowOutOfMemory(cx);
 
-        if (len == h[i].key.len
-            && (ngx_strncasecmp(name, h[i].key.data, len) == 0))
-        {
-            ph = &h[i].next;
-            while (*ph) { ph = &(*ph)->next; }
-            break;
-        }
+    } else {
+        JS_ThrowTypeError(cx, "%s", ngx_js_headers_error(rc));
     }
 
-    h = ngx_list_push(&headers->header_list);
-    if (h == NULL) {
-        JS_ThrowOutOfMemory(cx);
-        return NGX_ERROR;
-    }
+    return NGX_ERROR;
+}
 
-    if (ph != NULL) {
-        *ph = h;
-    }
 
-    h->hash = 1;
-    h->key.data = name;
-    h->key.len = len;
-    h->value.data = value;
-    h->value.len = vlen;
-    h->next = NULL;
+static ngx_int_t
+ngx_qjs_headers_append(JSContext *cx, ngx_js_headers_t *headers,
+    u_char *name, size_t len, u_char *value, size_t vlen)
+{
+    ngx_js_headers_rc_t  rc;
 
-    if (len == (sizeof("Content-Type") - 1)
-        && ngx_strncasecmp(name, (u_char *) "Content-Type", len) == 0)
-    {
-        headers->content_type = h;
-    }
+    rc = ngx_js_headers_modify(headers, name, len, value, vlen, 0);
 
-    return NGX_OK;
+    return ngx_qjs_headers_result(cx, rc);
 }
 
 
@@ -1785,11 +1736,9 @@ static JSValue
 ngx_qjs_ext_fetch_headers_delete(JSContext *cx, JSValueConst this_val,
     int argc, JSValueConst *argv)
 {
-    ngx_str_t          name;
-    ngx_uint_t         i;
-    ngx_list_part_t   *part;
-    ngx_js_tb_elt_t   *h;
-    ngx_js_headers_t  *headers;
+    ngx_str_t            name;
+    ngx_js_headers_t    *headers;
+    ngx_js_headers_rc_t  rc;
 
     headers = JS_GetOpaque(this_val, NGX_QJS_CLASS_ID_FETCH_HEADERS);
     if (headers == NULL) {
@@ -1802,40 +1751,11 @@ ngx_qjs_ext_fetch_headers_delete(JSContext *cx, JSValueConst this_val,
         return JS_EXCEPTION;
     }
 
-    part = &headers->header_list.part;
-    h = part->elts;
-
-    for (i = 0; /* void */; i++) {
-
-        if (i >= part->nelts) {
-            if (part->next == NULL) {
-                break;
-            }
-
-            part = part->next;
-            h = part->elts;
-            i = 0;
-        }
-
-        if (h[i].hash == 0) {
-            continue;
-        }
-
-        if (name.len == h[i].key.len
-            && (ngx_strncasecmp(name.data, h[i].key.data, name.len) == 0))
-        {
-            h[i].hash = 0;
-        }
-    }
-
-    if (name.len == (sizeof("Content-Type") - 1)
-        && ngx_strncasecmp(name.data, (u_char *) "Content-Type", name.len)
-           == 0)
-    {
-        headers->content_type = NULL;
-    }
-
+    rc = ngx_js_headers_remove(headers, name.data, name.len);
     JS_FreeCString(cx, (const char *) name.data);
+    if (ngx_qjs_headers_result(cx, rc) != NGX_OK) {
+        return JS_EXCEPTION;
+    }
 
     return JS_UNDEFINED;
 }
@@ -1964,13 +1884,11 @@ static JSValue
 ngx_qjs_ext_fetch_headers_set(JSContext *cx, JSValueConst this_val,
     int argc, JSValueConst *argv)
 {
-    ngx_int_t          rc;
-    ngx_str_t          name, value;
-    ngx_uint_t         i;
-    ngx_pool_t        *pool;
-    ngx_list_part_t   *part;
-    ngx_js_tb_elt_t   *h, **ph, **pp;
-    ngx_js_headers_t  *headers;
+    ngx_int_t            ret;
+    ngx_str_t            name, value;
+    ngx_pool_t          *pool;
+    ngx_js_headers_t    *headers;
+    ngx_js_headers_rc_t  rc;
 
     headers = JS_GetOpaque(this_val, NGX_QJS_CLASS_ID_FETCH_HEADERS);
     if (headers == NULL) {
@@ -1985,55 +1903,21 @@ ngx_qjs_ext_fetch_headers_set(JSContext *cx, JSValueConst this_val,
 
     pool = ngx_qjs_external_pool(cx, JS_GetContextOpaque(cx));
 
-    rc = ngx_qjs_string(cx, pool, argv[1], &value);
-    if (rc != NGX_OK) {
+    ret = ngx_qjs_string(cx, pool, argv[1], &value);
+    if (ret != NGX_OK) {
         JS_FreeCString(cx, (const char *) name.data);
-        return JS_EXCEPTION;
-    }
-
-    part = &headers->header_list.part;
-    h = part->elts;
-
-    for (i = 0; /* void */; i++) {
-
-        if (i >= part->nelts) {
-            if (part->next == NULL) {
-                break;
-            }
 
-            part = part->next;
-            h = part->elts;
-            i = 0;
-        }
-
-        if (h[i].hash == 0) {
-            continue;
+        if (!JS_HasException(cx)) {
+            JS_ThrowOutOfMemory(cx);
         }
 
-        if (name.len == h[i].key.len
-            && (ngx_strncasecmp(name.data, h[i].key.data, name.len) == 0))
-        {
-            h[i].value.len = value.len;
-            h[i].value.data = value.data;
-
-            ph = &h[i].next;
-
-            while (*ph) {
-                pp = ph;
-                ph = &(*ph)->next;
-                *pp = NULL;
-            }
-
-            JS_FreeCString(cx, (const char *) name.data);
-
-            return JS_UNDEFINED;
-        }
+        return JS_EXCEPTION;
     }
 
-    rc = ngx_qjs_headers_append(cx, headers, name.data, name.len,
-                                 value.data, value.len);
+    rc = ngx_js_headers_modify(headers, name.data, name.len, value.data,
+                               value.len, 1);
     JS_FreeCString(cx, (const char *) name.data);
-    if (rc != NGX_OK) {
+    if (ngx_qjs_headers_result(cx, rc) != NGX_OK) {
         return JS_EXCEPTION;
     }
 
index 8a6806ac8d3c4df75eb9cb0566774172e00bde84..1e39d1c4cb8693d03b6abb066d0a550858f6073c 100644 (file)
@@ -255,15 +255,51 @@ $t->write_file('test.js', <<EOF);
                 return 'OK';
              }, 'OK'],
             ['set', () => {
-                var h = new Headers([['A', 'x'], ['a', 'y'], ['a', 'z']]);
-                h.set('a', '#');
-                return h.get('a');
-             }, '#'],
-             ['set on empty', () => {
+                var h = new Headers();
+
+                for (var i = 0; i < 10; i++) {
+                    h.append(i == 0 ? 'A' : 'a', String(i));
+                }
+
+                h.set('a', ' \t#\t ');
+                var h2 = new Headers(h);
+                var key;
+                h.forEach((k) => key = k);
+                return `\${key} \${JSON.stringify(h.getAll('a'))} `
+                       + JSON.stringify(h2.getAll('a'));
+             }, 'A ["#"] ["#"]'],
+            ['set invalid value', () => {
+                var h = new Headers({a: 'x'});
+                h.set('a', 'a\\r\\nb');
+             }, 'invalid header value'],
+            ['set on empty', () => {
                 var h = new Headers([]);
                 h.set('x-test', '1234');
                 return h.get('x-test');
              }, '1234'],
+            ['invalid empty name', () => {
+                var h = new Headers({a: 'x'});
+                var tests = [
+                    () => new Headers({'': 'x'}),
+                    () => h.append('', 'x'),
+                    () => h.delete(''),
+                    () => h.set('', 'x'),
+                ];
+
+                for (var i = 0; i < tests.length; i++) {
+                    try {
+                        tests[i]();
+                        throw new Error('no error');
+
+                    } catch (e) {
+                        if (e.message != 'invalid header name') {
+                            throw e;
+                        }
+                    }
+                }
+
+                return 'OK';
+             }, 'OK'],
         ];
 
         run(r, tests);
@@ -629,6 +665,35 @@ $t->write_file('test.js', <<EOF);
                 var body = await r.text();
                 return `\${r.status} \${body}`;
              }, '200 /target%0D%0A?q=%00%20'],
+            ['immutable headers', async () => {
+                var r = await ngx.fetch('http://127.0.0.1:$p0/method');
+                var errors = [];
+
+                try {
+                    r.headers.append('X-Test', 'test');
+
+                } catch (e) {
+                    errors.push(e.message);
+                }
+
+                try {
+                    r.headers.set('Content-Type', 'text/html');
+
+                } catch (e) {
+                    errors.push(e.message);
+                }
+
+                try {
+                    r.headers.delete('Content-Type');
+
+                } catch (e) {
+                    errors.push(e.message);
+                }
+
+                return errors.join('|');
+             }, 'cannot modify immutable object|'
+                + 'cannot modify immutable object|'
+                + 'cannot modify immutable object'],
         ];
 
         run(r, tests);
@@ -645,6 +710,14 @@ $t->write_file('test.js', <<EOF);
                 var body = await r.text();
                 return `\${r.url}: \${r.status} \${body}`;
              }, 'http://127.0.0.1:$p0/header: 200 X, Z'],
+            ['set request multi header', async () => {
+                var h = new Headers([['a', 'X'], ['a', 'Y'], ['a', 'Z']]);
+                h.set('a', '#');
+                var r = await ngx.fetch('http://127.0.0.1:$p0/header',
+                                        {headers: h});
+                var body = await r.text();
+                return `\${r.url}: \${r.status} \${body}`;
+             }, 'http://127.0.0.1:$p0/header: 200 #'],
         ];
 
         run(r, tests);