diff --git a/nginx/config b/nginx/config
index 2edf7a3d7..17672b7bf 100644
--- a/nginx/config
+++ b/nginx/config
@@ -150,6 +150,7 @@ NJS_ENGINE_LIB="$ngx_addon_dir/../build/libnjs.a"
 if [ "$NJS_HAVE_QUICKJS" = "YES" ];  then
     NJS_ENGINE_DEP="$ngx_addon_dir/../build/libqjs.a"
     NJS_ENGINE_LIB="$ngx_addon_dir/../build/libnjs.a $ngx_addon_dir/../build/libqjs.a"
+    QJS_SRCS="$QJS_SRCS $ngx_addon_dir/ngx_qjs_fetch.c"
 fi
 
 if [ $HTTP != NO ]; then
diff --git a/nginx/ngx_http_js_module.c b/nginx/ngx_http_js_module.c
index 0c8215c56..c6455c551 100644
--- a/nginx/ngx_http_js_module.c
+++ b/nginx/ngx_http_js_module.c
@@ -1133,6 +1133,7 @@ static JSClassDef ngx_http_qjs_headers_out_class = {
 qjs_module_t *njs_http_qjs_addon_modules[] = {
     &ngx_qjs_ngx_module,
     &ngx_qjs_ngx_shared_dict_module,
+    &ngx_qjs_ngx_fetch_module,
     /*
      * Shared addons should be in the same order and the same positions
      * in all nginx modules.
diff --git a/nginx/ngx_js.c b/nginx/ngx_js.c
index c91a55308..0c4575b0f 100644
--- a/nginx/ngx_js.c
+++ b/nginx/ngx_js.c
@@ -436,6 +436,7 @@ static const JSCFunctionListEntry ngx_qjs_ext_ngx[] = {
     JS_CGETSET_MAGIC_DEF("ERR", ngx_qjs_ext_constant_integer, NULL,
                          NGX_LOG_ERR),
     JS_CGETSET_DEF("error_log_path", ngx_qjs_ext_error_log_path, NULL),
+    JS_CFUNC_DEF("fetch", 2, ngx_qjs_ext_fetch),
     JS_CGETSET_MAGIC_DEF("INFO", ngx_qjs_ext_constant_integer, NULL,
                          NGX_LOG_INFO),
     JS_CFUNC_MAGIC_DEF("log", 1, ngx_qjs_ext_log, 0),
@@ -2184,6 +2185,31 @@ ngx_qjs_core_init(JSContext *cx, const char *name)
     return m;
 }
 
+
+int
+ngx_qjs_array_length(JSContext *cx, uint32_t *plen, JSValueConst arr)
+{
+    int       ret;
+    JSValue   value;
+    uint32_t  len;
+
+    value = JS_GetPropertyStr(cx, arr, "length");
+    if (JS_IsException(value)) {
+        return -1;
+    }
+
+    ret = JS_ToUint32(cx, &len, value);
+    JS_FreeValue(cx, value);
+
+    if (ret) {
+        return -1;
+    }
+
+    *plen = len;
+
+    return 0;
+}
+
 #endif
 
 
diff --git a/nginx/ngx_js.h b/nginx/ngx_js.h
index ba4cbdf96..ea2adc88b 100644
--- a/nginx/ngx_js.h
+++ b/nginx/ngx_js.h
@@ -63,6 +63,9 @@
 #define NGX_QJS_CLASS_ID_SHARED (NGX_QJS_CLASS_ID_OFFSET + 11)
 #define NGX_QJS_CLASS_ID_SHARED_DICT (NGX_QJS_CLASS_ID_OFFSET + 12)
 #define NGX_QJS_CLASS_ID_SHARED_DICT_ERROR (NGX_QJS_CLASS_ID_OFFSET + 13)
+#define NGX_QJS_CLASS_ID_FETCH_HEADERS (NGX_QJS_CLASS_ID_OFFSET + 14)
+#define NGX_QJS_CLASS_ID_FETCH_REQUEST (NGX_QJS_CLASS_ID_OFFSET + 15)
+#define NGX_QJS_CLASS_ID_FETCH_RESPONSE (NGX_QJS_CLASS_ID_OFFSET + 16)
 
 
 typedef struct ngx_js_loc_conf_s ngx_js_loc_conf_t;
@@ -345,6 +348,10 @@ ngx_int_t ngx_qjs_call(JSContext *cx, JSValue function, JSValue *argv,
 ngx_int_t ngx_qjs_exception(JSContext *cx, ngx_str_t *s);
 ngx_int_t ngx_qjs_integer(JSContext *cx, JSValueConst val, ngx_int_t *n);
 ngx_int_t ngx_qjs_string(JSContext *cx, JSValueConst val, ngx_str_t *str);
+int ngx_qjs_array_length(JSContext *cx, uint32_t *plen, JSValueConst arr);
+
+JSValue ngx_qjs_ext_fetch(JSContext *cx, JSValueConst this_val, int argc,
+    JSValueConst *argv);
 
 #define ngx_qjs_prop(cx, type, start, len)                                   \
     ((type == NGX_JS_STRING) ? qjs_string_create(cx, start, len)             \
@@ -381,6 +388,7 @@ extern qjs_module_t  qjs_webcrypto_module;
 extern qjs_module_t  qjs_zlib_module;
 extern qjs_module_t  ngx_qjs_ngx_module;
 extern qjs_module_t  ngx_qjs_ngx_shared_dict_module;
+extern qjs_module_t  ngx_qjs_ngx_fetch_module;
 
 #endif
 
diff --git a/nginx/ngx_qjs_fetch.c b/nginx/ngx_qjs_fetch.c
new file mode 100644
index 000000000..a5da5e0a9
--- /dev/null
+++ b/nginx/ngx_qjs_fetch.c
@@ -0,0 +1,3903 @@
+
+/*
+ * Copyright (C) Dmitry Volyntsev
+ * Copyright (C) hongzhidao
+ * Copyright (C) Antoine Bonavita
+ * Copyright (C) NGINX, Inc.
+ */
+
+
+#include <ngx_config.h>
+#include <ngx_core.h>
+#include <ngx_event.h>
+#include <ngx_event_connect.h>
+#include "ngx_js.h"
+
+
+typedef struct ngx_qjs_tb_elt_s  ngx_qjs_tb_elt_t;
+
+struct ngx_qjs_tb_elt_s {
+    ngx_uint_t         hash;
+    ngx_str_t          key;
+    ngx_str_t          value;
+    ngx_qjs_tb_elt_t  *next;
+};
+
+
+typedef struct {
+    enum {
+        GUARD_NONE = 0,
+        GUARD_REQUEST,
+        GUARD_IMMUTABLE,
+        GUARD_RESPONSE,
+    }                              guard;
+    ngx_list_t                     header_list;
+    ngx_qjs_tb_elt_t              *content_type;
+} ngx_qjs_headers_t;
+
+
+typedef struct {
+    enum {
+        CACHE_MODE_DEFAULT = 0,
+        CACHE_MODE_NO_STORE,
+        CACHE_MODE_RELOAD,
+        CACHE_MODE_NO_CACHE,
+        CACHE_MODE_FORCE_CACHE,
+        CACHE_MODE_ONLY_IF_CACHED,
+    }                              cache_mode;
+    enum {
+        CREDENTIALS_SAME_ORIGIN = 0,
+        CREDENTIALS_INCLUDE,
+        CREDENTIALS_OMIT,
+    }                              credentials;
+    enum {
+        MODE_NO_CORS = 0,
+        MODE_SAME_ORIGIN,
+        MODE_CORS,
+        MODE_NAVIGATE,
+        MODE_WEBSOCKET,
+    }                              mode;
+    ngx_str_t                      url;
+    ngx_str_t                      method;
+    u_char                         m[8];
+    uint8_t                        body_used;
+    ngx_str_t                      body;
+    ngx_qjs_headers_t              headers;
+    JSValue                        header_value;
+} ngx_qjs_request_t;
+
+
+typedef struct {
+    ngx_str_t                      url;
+    ngx_int_t                      code;
+    ngx_str_t                      status_text;
+    uint8_t                        body_used;
+    njs_chb_t                      chain;
+    ngx_qjs_headers_t              headers;
+    JSValue                        header_value;
+} ngx_qjs_response_t;
+
+
+typedef struct {
+    njs_str_t       name;
+    njs_int_t       value;
+} ngx_qjs_entry_t;
+
+
+typedef struct {
+    ngx_uint_t                     state;
+    ngx_uint_t                     code;
+    u_char                        *status_text;
+    u_char                        *status_text_end;
+    ngx_uint_t                     count;
+    ngx_flag_t                     chunked;
+    off_t                          content_length_n;
+
+    u_char                        *header_name_start;
+    u_char                        *header_name_end;
+    u_char                        *header_start;
+    u_char                        *header_end;
+} ngx_qjs_http_parse_t;
+
+
+typedef struct {
+    u_char                        *pos;
+    uint64_t                       chunk_size;
+    uint8_t                        state;
+    uint8_t                        last;
+} ngx_qjs_http_chunk_parse_t;
+
+
+typedef struct ngx_qjs_http_s  ngx_qjs_http_t;
+
+struct ngx_qjs_http_s {
+    ngx_log_t                     *log;
+    ngx_pool_t                    *pool;
+
+    JSContext                     *cx;
+    ngx_qjs_event_t               *event;
+
+    ngx_resolver_ctx_t            *ctx;
+    ngx_addr_t                     addr;
+    ngx_addr_t                    *addrs;
+    ngx_uint_t                     naddrs;
+    ngx_uint_t                     naddr;
+    in_port_t                      port;
+
+    ngx_peer_connection_t          peer;
+    ngx_msec_t                     timeout;
+
+    ngx_int_t                      buffer_size;
+    ngx_int_t                      max_response_body_size;
+
+    unsigned                       header_only;
+
+#if (NGX_SSL)
+    ngx_str_t                      tls_name;
+    ngx_ssl_t                     *ssl;
+    njs_bool_t                     ssl_verify;
+#endif
+
+    ngx_buf_t                     *buffer;
+    ngx_buf_t                     *chunk;
+    njs_chb_t                      chain;
+
+    ngx_qjs_response_t             response;
+    JSValue                        response_value;
+
+    JSValue                        promise;
+    JSValue                        promise_callbacks[2];
+
+    uint8_t                        done;
+    ngx_qjs_http_parse_t           http_parse;
+    ngx_qjs_http_chunk_parse_t     http_chunk_parse;
+    ngx_int_t                    (*process)(ngx_qjs_http_t *http);
+};
+
+
+static njs_int_t ngx_qjs_headers_fill_header_free(JSContext *cx,
+    ngx_qjs_headers_t *headers, JSValue prop_name, JSValue prop_value);
+static njs_int_t ngx_qjs_headers_append(JSContext *cx,
+    ngx_qjs_headers_t *headers, u_char *name, size_t len, u_char *value,
+    size_t vlen);
+static JSValue ngx_qjs_headers_get(JSContext *cx, JSValue this_val,
+    ngx_str_t *name, njs_bool_t as_array);
+static JSValue ngx_qjs_fetch_flag(JSContext *cx, const ngx_qjs_entry_t *entries,
+    njs_int_t value);
+static njs_int_t ngx_qjs_request_constructor(JSContext *cx,
+    ngx_qjs_request_t *request, ngx_url_t *u, int argc, JSValueConst *argv);
+static void ngx_qjs_http_connect(ngx_qjs_http_t *http);
+static void ngx_qjs_http_close_connection(ngx_connection_t *c);
+static void ngx_qjs_http_fetch_done(ngx_qjs_http_t *http, JSValue retval,
+    njs_int_t rc);
+
+#if (NGX_SSL)
+static void ngx_qjs_http_ssl_init_connection(ngx_qjs_http_t *http);
+static void ngx_qjs_http_ssl_handshake_handler(ngx_connection_t *c);
+static void ngx_qjs_http_ssl_handshake(ngx_qjs_http_t *http);
+static njs_int_t ngx_qjs_http_ssl_name(ngx_qjs_http_t *http);
+#endif
+
+
+static const ngx_qjs_entry_t  ngx_qjs_fetch_cache_modes[] = {
+    { njs_str("default"), CACHE_MODE_DEFAULT },
+    { njs_str("no-store"), CACHE_MODE_NO_STORE },
+    { njs_str("reload"), CACHE_MODE_RELOAD },
+    { njs_str("no-cache"), CACHE_MODE_NO_CACHE },
+    { njs_str("force-cache"), CACHE_MODE_FORCE_CACHE },
+    { njs_str("only-if-cached"), CACHE_MODE_ONLY_IF_CACHED },
+    { njs_null_str, 0 },
+};
+
+static const ngx_qjs_entry_t  ngx_qjs_fetch_credentials[] = {
+    { njs_str("same-origin"), CREDENTIALS_SAME_ORIGIN },
+    { njs_str("omit"), CREDENTIALS_OMIT },
+    { njs_str("include"), CREDENTIALS_INCLUDE },
+    { njs_null_str, 0 },
+};
+
+static const ngx_qjs_entry_t  ngx_qjs_fetch_modes[] = {
+    { njs_str("no-cors"), MODE_NO_CORS },
+    { njs_str("cors"), MODE_CORS },
+    { njs_str("same-origin"), MODE_SAME_ORIGIN },
+    { njs_str("navigate"), MODE_NAVIGATE },
+    { njs_str("websocket"), MODE_WEBSOCKET },
+    { njs_null_str, 0 },
+};
+
+
+#define NGX_QJS_BODY_ARRAY_BUFFER   0
+#define NGX_QJS_BODY_JSON           1
+#define NGX_QJS_BODY_TEXT           2
+
+
+static void
+njs_qjs_http_destructor(ngx_qjs_event_t *event)
+{
+    JSContext       *cx;
+    ngx_qjs_http_t  *http;
+
+    cx = event->ctx;
+    http = event->data;
+
+    ngx_log_debug1(NGX_LOG_DEBUG_EVENT, http->log, 0, "qjs fetch destructor:%p",
+                   http);
+
+    if (http->ctx != NULL) {
+        ngx_resolve_name_done(http->ctx);
+        http->ctx = NULL;
+    }
+
+    if (http->peer.connection != NULL) {
+        ngx_qjs_http_close_connection(http->peer.connection);
+        http->peer.connection = NULL;
+    }
+
+    njs_chb_destroy(&http->chain);
+
+    JS_FreeValue(cx, http->promise_callbacks[0]);
+    JS_FreeValue(cx, http->promise_callbacks[1]);
+    JS_FreeValue(cx, http->promise);
+    JS_FreeValue(cx, http->response_value);
+}
+
+
+static ngx_qjs_http_t *
+ngx_qjs_http_alloc(JSContext *cx, ngx_pool_t *pool, ngx_log_t *log)
+{
+    ngx_js_ctx_t     *ctx;
+    ngx_qjs_http_t   *http;
+    ngx_qjs_event_t  *event;
+
+    http = ngx_pcalloc(pool, sizeof(ngx_qjs_http_t));
+    if (http == NULL) {
+        return NULL;
+    }
+
+    http->pool = pool;
+    http->log = log;
+    http->cx = cx;
+
+    http->response.header_value = JS_UNDEFINED;
+
+    http->timeout = 10000;
+
+    http->http_parse.content_length_n = -1;
+
+    http->promise = JS_NewPromiseCapability(cx, http->promise_callbacks);
+    if (JS_IsException(http->promise)) {
+        return NULL;
+    }
+
+    event = ngx_palloc(pool, sizeof(ngx_qjs_event_t));
+    if (njs_slow_path(event == NULL)) {
+        goto fail;
+    }
+
+    ctx = ngx_qjs_external_ctx(cx, JS_GetContextOpaque(cx));
+
+    event->ctx = cx;
+    event->destructor = njs_qjs_http_destructor;
+    event->fd = ctx->event_id++;
+    event->data = http;
+
+    ngx_js_add_event(ctx, event);
+
+    http->event = event;
+
+    ngx_log_debug1(NGX_LOG_DEBUG_EVENT, log, 0, "qjs fetch alloc:%p", http);
+
+    return http;
+
+fail:
+
+    JS_FreeValue(cx, http->promise);
+    JS_FreeValue(cx, http->promise_callbacks[0]);
+    JS_FreeValue(cx, http->promise_callbacks[1]);
+
+    JS_ThrowInternalError(cx, "internal error");
+
+    return NULL;
+}
+
+
+#define ngx_qjs_http_error(http, err, fmt, ...)                                \
+    do {                                                                       \
+        JS_ThrowInternalError((http)->cx, fmt, ##__VA_ARGS__);                 \
+        (http)->response_value = JS_GetException((http)->cx);                  \
+        ngx_qjs_http_fetch_done(http, (http)->response_value, NJS_ERROR);      \
+    } while (0)
+
+static void
+ngx_qjs_http_next(ngx_qjs_http_t *http)
+{
+    ngx_log_debug0(NGX_LOG_DEBUG_EVENT, http->log, 0, "qjs fetch next addr");
+
+    if (++http->naddr >= http->naddrs) {
+        ngx_qjs_http_error(http, 0, "connect failed");
+        return;
+    }
+
+    if (http->peer.connection != NULL) {
+        ngx_qjs_http_close_connection(http->peer.connection);
+        http->peer.connection = NULL;
+    }
+
+    http->buffer = NULL;
+
+    ngx_qjs_http_connect(http);
+}
+
+
+static void
+ngx_qjs_http_dummy_handler(ngx_event_t *ev)
+{
+    ngx_log_debug0(NGX_LOG_DEBUG_EVENT, ev->log, 0, "qjs fetch dummy handler");
+}
+
+
+static void
+ngx_qjs_http_write_handler(ngx_event_t *wev)
+{
+    ssize_t            n, size;
+    ngx_buf_t         *b;
+    ngx_qjs_http_t    *http;
+    ngx_connection_t  *c;
+
+    c = wev->data;
+    http = c->data;
+
+    ngx_log_debug0(NGX_LOG_DEBUG_EVENT, wev->log, 0, "qjs fetch write handler");
+
+    if (wev->timedout) {
+        ngx_qjs_http_error(http, NGX_ETIMEDOUT, "write timed out");
+        return;
+    }
+
+#if (NGX_SSL)
+    if (http->ssl != NULL && http->peer.connection->ssl == NULL) {
+        ngx_qjs_http_ssl_init_connection(http);
+        return;
+    }
+#endif
+
+    b = http->buffer;
+
+    if (b == NULL) {
+        size = njs_chb_size(&http->chain);
+        if (size < 0) {
+            ngx_qjs_http_error(http, 0, "memory error");
+            return;
+        }
+
+        b = ngx_create_temp_buf(http->pool, size);
+        if (b == NULL) {
+            ngx_qjs_http_error(http, 0, "memory error");
+            return;
+        }
+
+        njs_chb_join_to(&http->chain, b->last);
+        b->last += size;
+
+        http->buffer = b;
+    }
+
+    size = b->last - b->pos;
+
+    n = c->send(c, b->pos, size);
+
+    if (n == NGX_ERROR) {
+        ngx_qjs_http_next(http);
+        return;
+    }
+
+    if (n > 0) {
+        b->pos += n;
+
+        if (n == size) {
+            wev->handler = ngx_qjs_http_dummy_handler;
+
+            http->buffer = NULL;
+
+            if (wev->timer_set) {
+                ngx_del_timer(wev);
+            }
+
+            if (ngx_handle_write_event(wev, 0) != NGX_OK) {
+                ngx_qjs_http_error(http, 0, "write failed");
+            }
+
+            return;
+        }
+    }
+
+    if (!wev->timer_set) {
+        ngx_add_timer(wev, http->timeout);
+    }
+}
+
+
+static void
+ngx_qjs_http_read_handler(ngx_event_t *rev)
+{
+    ssize_t            n, size;
+    ngx_int_t          rc;
+    ngx_buf_t         *b;
+    ngx_qjs_http_t    *http;
+    ngx_connection_t  *c;
+
+    c = rev->data;
+    http = c->data;
+
+    ngx_log_debug0(NGX_LOG_DEBUG_EVENT, rev->log, 0, "qjs fetch read handler");
+
+    if (rev->timedout) {
+        ngx_qjs_http_error(http, NGX_ETIMEDOUT, "read timed out");
+        return;
+    }
+
+    if (http->buffer == NULL) {
+        b = ngx_create_temp_buf(http->pool, http->buffer_size);
+        if (b == NULL) {
+            ngx_qjs_http_error(http, 0, "memory error");
+            return;
+        }
+
+        http->buffer = b;
+    }
+
+    for ( ;; ) {
+        b = http->buffer;
+        size = b->end - b->last;
+
+        n = c->recv(c, b->last, size);
+
+        if (n > 0) {
+            b->last += n;
+
+            rc = http->process(http);
+
+            if (rc == NGX_ERROR) {
+                return;
+            }
+
+            continue;
+        }
+
+        if (n == NGX_AGAIN) {
+            if (ngx_handle_read_event(rev, 0) != NGX_OK) {
+                ngx_qjs_http_error(http, 0, "read failed");
+            }
+
+            return;
+        }
+
+        if (n == NGX_ERROR) {
+            ngx_qjs_http_next(http);
+            return;
+        }
+
+        break;
+    }
+
+    http->done = 1;
+
+    rc = http->process(http);
+
+    if (rc == NGX_DONE) {
+        /* handler was called */
+        return;
+    }
+
+    if (rc == NGX_AGAIN) {
+        ngx_qjs_http_error(http, 0, "prematurely closed connection");
+    }
+}
+
+
+static ngx_int_t
+ngx_qjs_http_parse_status_line(ngx_qjs_http_parse_t *hp, ngx_buf_t *b)
+{
+    u_char   ch;
+    u_char  *p;
+    enum {
+        sw_start = 0,
+        sw_H,
+        sw_HT,
+        sw_HTT,
+        sw_HTTP,
+        sw_first_major_digit,
+        sw_major_digit,
+        sw_first_minor_digit,
+        sw_minor_digit,
+        sw_status,
+        sw_space_after_status,
+        sw_status_text,
+        sw_almost_done
+    } state;
+
+    state = hp->state;
+
+    for (p = b->pos; p < b->last; p++) {
+        ch = *p;
+
+        switch (state) {
+
+        /* "HTTP/" */
+        case sw_start:
+            switch (ch) {
+            case 'H':
+                state = sw_H;
+                break;
+            default:
+                return NGX_ERROR;
+            }
+            break;
+
+        case sw_H:
+            switch (ch) {
+            case 'T':
+                state = sw_HT;
+                break;
+            default:
+                return NGX_ERROR;
+            }
+            break;
+
+        case sw_HT:
+            switch (ch) {
+            case 'T':
+                state = sw_HTT;
+                break;
+            default:
+                return NGX_ERROR;
+            }
+            break;
+
+        case sw_HTT:
+            switch (ch) {
+            case 'P':
+                state = sw_HTTP;
+                break;
+            default:
+                return NGX_ERROR;
+            }
+            break;
+
+        case sw_HTTP:
+            switch (ch) {
+            case '/':
+                state = sw_first_major_digit;
+                break;
+            default:
+                return NGX_ERROR;
+            }
+            break;
+
+        /* the first digit of major HTTP version */
+        case sw_first_major_digit:
+            if (ch < '1' || ch > '9') {
+                return NGX_ERROR;
+            }
+
+            state = sw_major_digit;
+            break;
+
+        /* the major HTTP version or dot */
+        case sw_major_digit:
+            if (ch == '.') {
+                state = sw_first_minor_digit;
+                break;
+            }
+
+            if (ch < '0' || ch > '9') {
+                return NGX_ERROR;
+            }
+
+            break;
+
+        /* the first digit of minor HTTP version */
+        case sw_first_minor_digit:
+            if (ch < '0' || ch > '9') {
+                return NGX_ERROR;
+            }
+
+            state = sw_minor_digit;
+            break;
+
+        /* the minor HTTP version or the end of the request line */
+        case sw_minor_digit:
+            if (ch == ' ') {
+                state = sw_status;
+                break;
+            }
+
+            if (ch < '0' || ch > '9') {
+                return NGX_ERROR;
+            }
+
+            break;
+
+        /* HTTP status code */
+        case sw_status:
+            if (ch == ' ') {
+                break;
+            }
+
+            if (ch < '0' || ch > '9') {
+                return NGX_ERROR;
+            }
+
+            hp->code = hp->code * 10 + (ch - '0');
+
+            if (++hp->count == 3) {
+                state = sw_space_after_status;
+            }
+
+            break;
+
+        /* space or end of line */
+        case sw_space_after_status:
+            switch (ch) {
+            case ' ':
+                state = sw_status_text;
+                break;
+            case '.':                    /* IIS may send 403.1, 403.2, etc */
+                state = sw_status_text;
+                break;
+            case CR:
+                break;
+            case LF:
+                goto done;
+            default:
+                return NGX_ERROR;
+            }
+            break;
+
+        /* any text until end of line */
+        case sw_status_text:
+            switch (ch) {
+            case CR:
+                hp->status_text_end = p;
+                state = sw_almost_done;
+                break;
+            case LF:
+                hp->status_text_end = p;
+                goto done;
+            }
+
+            if (hp->status_text == NULL) {
+                hp->status_text = p;
+            }
+
+            break;
+
+        /* end of status line */
+        case sw_almost_done:
+            switch (ch) {
+            case LF:
+                goto done;
+            default:
+                return NGX_ERROR;
+            }
+        }
+    }
+
+    b->pos = p;
+    hp->state = state;
+
+    return NGX_AGAIN;
+
+done:
+
+    b->pos = p + 1;
+    hp->state = sw_start;
+
+    return NGX_OK;
+}
+
+
+static ngx_int_t
+ngx_qjs_http_parse_header_line(ngx_qjs_http_parse_t *hp, ngx_buf_t *b)
+{
+    u_char  c, ch, *p;
+    enum {
+        sw_start = 0,
+        sw_name,
+        sw_space_before_value,
+        sw_value,
+        sw_space_after_value,
+        sw_almost_done,
+        sw_header_almost_done
+    } state;
+
+    state = hp->state;
+
+    for (p = b->pos; p < b->last; p++) {
+        ch = *p;
+
+        switch (state) {
+
+        /* first char */
+        case sw_start:
+
+            switch (ch) {
+            case CR:
+                hp->header_end = p;
+                state = sw_header_almost_done;
+                break;
+            case LF:
+                hp->header_end = p;
+                goto header_done;
+            default:
+                state = sw_name;
+                hp->header_name_start = p;
+
+                c = (u_char) (ch | 0x20);
+                if (c >= 'a' && c <= 'z') {
+                    break;
+                }
+
+                if (ch >= '0' && ch <= '9') {
+                    break;
+                }
+
+                return NGX_ERROR;
+            }
+            break;
+
+        /* header name */
+        case sw_name:
+            c = (u_char) (ch | 0x20);
+            if (c >= 'a' && c <= 'z') {
+                break;
+            }
+
+            if (ch == ':') {
+                hp->header_name_end = p;
+                state = sw_space_before_value;
+                break;
+            }
+
+            if (ch == '-' || ch == '_') {
+                break;
+            }
+
+            if (ch >= '0' && ch <= '9') {
+                break;
+            }
+
+            if (ch == CR) {
+                hp->header_name_end = p;
+                hp->header_start = p;
+                hp->header_end = p;
+                state = sw_almost_done;
+                break;
+            }
+
+            if (ch == LF) {
+                hp->header_name_end = p;
+                hp->header_start = p;
+                hp->header_end = p;
+                goto done;
+            }
+
+            return NGX_ERROR;
+
+        /* space* before header value */
+        case sw_space_before_value:
+            switch (ch) {
+            case ' ':
+                break;
+            case CR:
+                hp->header_start = p;
+                hp->header_end = p;
+                state = sw_almost_done;
+                break;
+            case LF:
+                hp->header_start = p;
+                hp->header_end = p;
+                goto done;
+            default:
+                hp->header_start = p;
+                state = sw_value;
+                break;
+            }
+            break;
+
+        /* header value */
+        case sw_value:
+            switch (ch) {
+            case ' ':
+                hp->header_end = p;
+                state = sw_space_after_value;
+                break;
+            case CR:
+                hp->header_end = p;
+                state = sw_almost_done;
+                break;
+            case LF:
+                hp->header_end = p;
+                goto done;
+            }
+            break;
+
+        /* space* before end of header line */
+        case sw_space_after_value:
+            switch (ch) {
+            case ' ':
+                break;
+            case CR:
+                state = sw_almost_done;
+                break;
+            case LF:
+                goto done;
+            default:
+                state = sw_value;
+                break;
+            }
+            break;
+
+        /* end of header line */
+        case sw_almost_done:
+            switch (ch) {
+            case LF:
+                goto done;
+            default:
+                return NGX_ERROR;
+            }
+
+        /* end of header */
+        case sw_header_almost_done:
+            switch (ch) {
+            case LF:
+                goto header_done;
+            default:
+                return NGX_ERROR;
+            }
+        }
+    }
+
+    b->pos = p;
+    hp->state = state;
+
+    return NGX_AGAIN;
+
+done:
+
+    b->pos = p + 1;
+    hp->state = sw_start;
+
+    return NGX_OK;
+
+header_done:
+
+    b->pos = p + 1;
+    hp->state = sw_start;
+
+    return NGX_DONE;
+}
+
+
+#define                                                                       \
+ngx_size_is_sufficient(cs)                                                    \
+    (cs < ((__typeof__(cs)) 1 << (sizeof(cs) * 8 - 4)))
+
+
+#define NGX_QJS_HTTP_CHUNK_MIDDLE     0
+#define NGX_QJS_HTTP_CHUNK_ON_BORDER  1
+#define NGX_QJS_HTTP_CHUNK_END        2
+
+
+static ngx_int_t
+ngx_qjs_http_chunk_buffer(ngx_qjs_http_chunk_parse_t *hcp, ngx_buf_t *b,
+    njs_chb_t *chain)
+{
+    size_t  size;
+
+    size = b->last - hcp->pos;
+
+    if (hcp->chunk_size < size) {
+        njs_chb_append(chain, hcp->pos, hcp->chunk_size);
+        hcp->pos += hcp->chunk_size;
+
+        return NGX_QJS_HTTP_CHUNK_END;
+    }
+
+    njs_chb_append(chain, hcp->pos, size);
+    hcp->pos += size;
+
+    hcp->chunk_size -= size;
+
+    if (hcp->chunk_size == 0) {
+        return NGX_QJS_HTTP_CHUNK_ON_BORDER;
+    }
+
+    return NGX_QJS_HTTP_CHUNK_MIDDLE;
+}
+
+
+static ngx_int_t
+ngx_qjs_http_parse_chunked(ngx_qjs_http_chunk_parse_t *hcp, ngx_buf_t *b,
+    njs_chb_t *chain)
+{
+    u_char     c, ch;
+    ngx_int_t  rc;
+
+    enum {
+        sw_start = 0,
+        sw_chunk_size,
+        sw_chunk_size_linefeed,
+        sw_chunk_end_newline,
+        sw_chunk_end_linefeed,
+        sw_chunk,
+    } state;
+
+    state = hcp->state;
+
+    hcp->pos = b->pos;
+
+    while (hcp->pos < b->last) {
+        /*
+         * The sw_chunk state is tested outside the switch
+         * to preserve hcp->pos and to not touch memory.
+         */
+        if (state == sw_chunk) {
+            rc = ngx_qjs_http_chunk_buffer(hcp, b, chain);
+            if (rc == NGX_ERROR) {
+                return rc;
+            }
+
+            if (rc == NGX_QJS_HTTP_CHUNK_MIDDLE) {
+                break;
+            }
+
+            state = sw_chunk_end_newline;
+
+            if (rc == NGX_QJS_HTTP_CHUNK_ON_BORDER) {
+                break;
+            }
+
+            /* rc == NGX_JS_HTTP_CHUNK_END */
+        }
+
+        ch = *hcp->pos++;
+
+        switch (state) {
+
+        case sw_start:
+            state = sw_chunk_size;
+
+            c = ch - '0';
+
+            if (c <= 9) {
+                hcp->chunk_size = c;
+                continue;
+            }
+
+            c = (ch | 0x20) - 'a';
+
+            if (c <= 5) {
+                hcp->chunk_size = 0x0A + c;
+                continue;
+            }
+
+            return NGX_ERROR;
+
+        case sw_chunk_size:
+
+            c = ch - '0';
+
+            if (c > 9) {
+                c = (ch | 0x20) - 'a';
+
+                if (c <= 5) {
+                    c += 0x0A;
+
+                } else if (ch == '\r') {
+                    state = sw_chunk_size_linefeed;
+                    continue;
+
+                } else {
+                    return NGX_ERROR;
+                }
+            }
+
+            if (ngx_size_is_sufficient(hcp->chunk_size)) {
+                hcp->chunk_size = (hcp->chunk_size << 4) + c;
+                continue;
+            }
+
+            return NGX_ERROR;
+
+        case sw_chunk_size_linefeed:
+            if (ch == '\n') {
+
+                if (hcp->chunk_size != 0) {
+                    state = sw_chunk;
+                    continue;
+                }
+
+                hcp->last = 1;
+                state = sw_chunk_end_newline;
+                continue;
+            }
+
+            return NGX_ERROR;
+
+        case sw_chunk_end_newline:
+            if (ch == '\r') {
+                state = sw_chunk_end_linefeed;
+                continue;
+            }
+
+            return NGX_ERROR;
+
+        case sw_chunk_end_linefeed:
+            if (ch == '\n') {
+
+                if (!hcp->last) {
+                    state = sw_start;
+                    continue;
+                }
+
+                return NGX_OK;
+            }
+
+            return NGX_ERROR;
+
+        case sw_chunk:
+            /*
+             * This state is processed before the switch.
+             * It added here just to suppress a warning.
+             */
+            continue;
+        }
+    }
+
+    hcp->state = state;
+
+    return NGX_AGAIN;
+}
+
+
+static ngx_int_t
+ngx_qjs_http_process_body(ngx_qjs_http_t *http)
+{
+    ssize_t     size, chsize, need;
+    ngx_int_t   rc;
+    ngx_buf_t  *b;
+
+    ngx_log_debug1(NGX_LOG_DEBUG_EVENT, http->log, 0,
+                   "qjs fetch process body done:%ui", (ngx_uint_t) http->done);
+
+    if (http->done) {
+        size = njs_chb_size(&http->response.chain);
+        if (size < 0) {
+            ngx_qjs_http_error(http, 0, "memory error");
+            return NGX_ERROR;
+        }
+
+        if (!http->header_only
+            && http->http_parse.chunked
+            && http->http_parse.content_length_n == -1)
+        {
+            ngx_qjs_http_error(http, 0, "invalid fetch chunked response");
+            return NGX_ERROR;
+        }
+
+        if (http->header_only
+            || http->http_parse.content_length_n == -1
+            || size == http->http_parse.content_length_n)
+        {
+            http->response_value = JS_NewObjectClass(http->cx,
+                                              NGX_QJS_CLASS_ID_FETCH_RESPONSE);
+            if (JS_IsException(http->response_value)) {
+                return NGX_ERROR;
+            }
+
+            JS_SetOpaque(http->response_value, &http->response);
+
+            ngx_qjs_http_fetch_done(http, http->response_value, NJS_OK);
+            return NGX_DONE;
+        }
+
+        if (size < http->http_parse.content_length_n) {
+            return NGX_AGAIN;
+        }
+
+        ngx_qjs_http_error(http, 0, "fetch trailing data");
+        return NGX_ERROR;
+    }
+
+    b = http->buffer;
+
+    if (http->http_parse.chunked) {
+        rc = ngx_qjs_http_parse_chunked(&http->http_chunk_parse, b,
+                                        &http->response.chain);
+        if (rc == NGX_ERROR) {
+            ngx_qjs_http_error(http, 0, "invalid fetch chunked response");
+            return NGX_ERROR;
+        }
+
+        size = njs_chb_size(&http->response.chain);
+
+        if (rc == NGX_OK) {
+            http->http_parse.content_length_n = size;
+        }
+
+        if (size > http->max_response_body_size * 10) {
+            ngx_qjs_http_error(http, 0, "very large fetch chunked response");
+            return NGX_ERROR;
+        }
+
+        b->pos = http->http_chunk_parse.pos;
+
+    } else {
+        size = njs_chb_size(&http->response.chain);
+
+        if (http->header_only) {
+            need = 0;
+
+        } else  if (http->http_parse.content_length_n == -1) {
+            need = http->max_response_body_size - size;
+
+        } else {
+            need = http->http_parse.content_length_n - size;
+        }
+
+        chsize = ngx_min(need, b->last - b->pos);
+
+        if (size + chsize > http->max_response_body_size) {
+            ngx_qjs_http_error(http, 0, "fetch response body is too large");
+            return NGX_ERROR;
+        }
+
+        if (chsize > 0) {
+            njs_chb_append(&http->response.chain, b->pos, chsize);
+            b->pos += chsize;
+        }
+
+        rc = (need > chsize) ? NGX_AGAIN : NGX_DONE;
+    }
+
+    if (b->pos == b->end) {
+        if (http->chunk == NULL) {
+            b = ngx_create_temp_buf(http->pool, http->buffer_size);
+            if (b == NULL) {
+                ngx_qjs_http_error(http, 0, "memory error");
+                return NGX_ERROR;
+            }
+
+            http->buffer = b;
+            http->chunk = b;
+
+        } else {
+            b->last = b->start;
+            b->pos = b->start;
+        }
+    }
+
+    return rc;
+}
+
+
+static ngx_int_t
+ngx_qjs_http_process_headers(ngx_qjs_http_t *http)
+{
+    size_t                 len, vlen;
+    ngx_int_t              rc;
+    njs_int_t              ret;
+    ngx_qjs_http_parse_t  *hp;
+
+    ngx_log_debug0(NGX_LOG_DEBUG_EVENT, http->log, 0,
+                   "qjs fetch process headers");
+
+    hp = &http->http_parse;
+
+    if (http->response.headers.header_list.size == 0) {
+        rc = ngx_list_init(&http->response.headers.header_list, http->pool, 4,
+                           sizeof(ngx_qjs_tb_elt_t));
+        if (rc != NGX_OK) {
+            ngx_qjs_http_error(http, 0, "alloc failed");
+            return NGX_ERROR;
+        }
+    }
+
+    for ( ;; ) {
+        rc = ngx_qjs_http_parse_header_line(hp, http->buffer);
+
+        if (rc == NGX_OK) {
+            len = hp->header_name_end - hp->header_name_start;
+            vlen = hp->header_end - hp->header_start;
+
+            ret = ngx_qjs_headers_append(http->cx, &http->response.headers,
+                                         hp->header_name_start, len,
+                                         hp->header_start, vlen);
+
+            if (ret == NJS_ERROR) {
+                ngx_qjs_http_error(http, 0, "cannot add respose header");
+                return NGX_ERROR;
+            }
+
+            ngx_log_debug4(NGX_LOG_DEBUG_EVENT, http->log, 0,
+                           "qjs fetch header \"%*s: %*s\"",
+                           len, hp->header_name_start, vlen, hp->header_start);
+
+            if (len == njs_strlen("Transfer-Encoding")
+                && vlen == njs_strlen("chunked")
+                && ngx_strncasecmp(hp->header_name_start,
+                                   (u_char *) "Transfer-Encoding", len) == 0
+                && ngx_strncasecmp(hp->header_start, (u_char *) "chunked",
+                                   vlen) == 0)
+            {
+                hp->chunked = 1;
+            }
+
+            if (len == njs_strlen("Content-Length")
+                && ngx_strncasecmp(hp->header_name_start,
+                                   (u_char *) "Content-Length", len) == 0)
+            {
+                hp->content_length_n = ngx_atoof(hp->header_start, vlen);
+                if (hp->content_length_n == NGX_ERROR) {
+                    ngx_qjs_http_error(http, 0, "invalid fetch content length");
+                    return NGX_ERROR;
+                }
+
+                if (!http->header_only
+                    && hp->content_length_n
+                       > (off_t) http->max_response_body_size)
+                {
+                    ngx_qjs_http_error(http, 0,
+                                      "fetch content length is too large");
+                    return NGX_ERROR;
+                }
+            }
+
+            continue;
+        }
+
+        if (rc == NGX_DONE) {
+            http->response.headers.guard = GUARD_IMMUTABLE;
+            break;
+        }
+
+        if (rc == NGX_AGAIN) {
+            return NGX_AGAIN;
+        }
+
+        /* rc == NGX_ERROR */
+
+        ngx_qjs_http_error(http, 0, "invalid fetch header");
+
+        return NGX_ERROR;
+    }
+
+    http->process = ngx_qjs_http_process_body;
+
+    return http->process(http);
+}
+
+
+static ngx_int_t
+ngx_qjs_http_process_status_line(ngx_qjs_http_t *http)
+{
+    ngx_int_t              rc;
+    ngx_qjs_http_parse_t  *hp;
+
+    hp = &http->http_parse;
+
+    rc = ngx_qjs_http_parse_status_line(hp, http->buffer);
+
+    if (rc == NGX_OK) {
+        ngx_log_debug1(NGX_LOG_DEBUG_EVENT, http->log, 0,
+                       "qjs fetch status %ui", hp->code);
+
+        http->response.code = hp->code;
+        http->response.status_text.data = hp->status_text;
+        http->response.status_text.len = hp->status_text_end - hp->status_text;
+        http->process = ngx_qjs_http_process_headers;
+
+        return http->process(http);
+    }
+
+    if (rc == NGX_AGAIN) {
+        return NGX_AGAIN;
+    }
+
+    /* rc == NGX_ERROR */
+
+    ngx_qjs_http_error(http, 0, "invalid fetch status line");
+
+    return NGX_ERROR;
+}
+
+
+static void
+ngx_qjs_http_close_connection(ngx_connection_t *c)
+{
+    ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "qjs fetch close connection: %d", c->fd);
+
+#if (NGX_SSL)
+    if (c->ssl) {
+        c->ssl->no_wait_shutdown = 1;
+
+        if (ngx_ssl_shutdown(c) == NGX_AGAIN) {
+            c->ssl->handler = ngx_qjs_http_close_connection;
+            return;
+        }
+    }
+#endif
+
+    c->destroyed = 1;
+
+    ngx_close_connection(c);
+}
+
+
+static void
+ngx_qjs_http_connect(ngx_qjs_http_t *http)
+{
+    ngx_int_t    rc;
+    ngx_addr_t  *addr;
+
+    addr = &http->addrs[http->naddr];
+
+    ngx_log_debug2(NGX_LOG_DEBUG_EVENT, http->log, 0,
+                   "qjs fetch connect %ui/%ui", http->naddr, http->naddrs);
+
+    http->peer.sockaddr = addr->sockaddr;
+    http->peer.socklen = addr->socklen;
+    http->peer.name = &addr->name;
+    http->peer.get = ngx_event_get_peer;
+    http->peer.log = http->log;
+    http->peer.log_error = NGX_ERROR_ERR;
+
+    rc = ngx_event_connect_peer(&http->peer);
+
+    if (rc == NGX_ERROR) {
+        ngx_qjs_http_error(http, 0, "connect failed");
+        return;
+    }
+
+    if (rc == NGX_BUSY || rc == NGX_DECLINED) {
+        ngx_qjs_http_next(http);
+        return;
+    }
+
+    http->peer.connection->data = http;
+    http->peer.connection->pool = http->pool;
+
+    http->peer.connection->write->handler = ngx_qjs_http_write_handler;
+    http->peer.connection->read->handler = ngx_qjs_http_read_handler;
+
+    http->process = ngx_qjs_http_process_status_line;
+
+    ngx_add_timer(http->peer.connection->read, http->timeout);
+    ngx_add_timer(http->peer.connection->write, http->timeout);
+
+#if (NGX_SSL)
+    if (http->ssl != NULL && http->peer.connection->ssl == NULL) {
+        ngx_qjs_http_ssl_init_connection(http);
+        return;
+    }
+#endif
+
+    if (rc == NGX_OK) {
+        ngx_qjs_http_write_handler(http->peer.connection->write);
+    }
+}
+
+
+static void
+ngx_qjs_http_fetch_done(ngx_qjs_http_t *http, JSValue retval, njs_int_t rc)
+{
+    void             *external;
+    JSValue           action;
+    JSContext        *cx;
+    ngx_js_ctx_t     *ctx;
+    ngx_qjs_event_t  *event;
+
+    ngx_log_debug2(NGX_LOG_DEBUG_EVENT, http->log, 0,
+                   "qjs fetch done http:%p rc:%i", http, (ngx_int_t) rc);
+
+    if (http->peer.connection != NULL) {
+        ngx_qjs_http_close_connection(http->peer.connection);
+        http->peer.connection = NULL;
+    }
+
+    if (http->event != NULL) {
+        action = http->promise_callbacks[(rc != NJS_OK)];
+
+        cx = http->cx;
+        event = http->event;
+
+        rc = ngx_qjs_call(cx, action, &retval, 1);
+
+        external = JS_GetContextOpaque(cx);
+        ctx = ngx_qjs_external_ctx(cx, external);
+        ngx_js_del_event(ctx, event);
+
+        ngx_qjs_external_event_finalize(cx)(external, rc);
+    }
+}
+
+
+static void
+ngx_qjs_resolve_handler(ngx_resolver_ctx_t *ctx)
+{
+    u_char           *p;
+    size_t            len;
+    socklen_t         socklen;
+    ngx_uint_t        i;
+    ngx_qjs_http_t   *http;
+    struct sockaddr  *sockaddr;
+
+    http = ctx->data;
+
+    if (ctx->state) {
+        ngx_qjs_http_error(http, 0, "\"%.*s\" could not be resolved (%i: %s)",
+                          (int) ctx->name.len, ctx->name.data, (int) ctx->state,
+                          ngx_resolver_strerror(ctx->state));
+        return;
+    }
+
+    ngx_log_debug1(NGX_LOG_DEBUG_EVENT, http->log, 0,
+                   "http fetch resolved: \"%V\"", &ctx->name);
+
+#if (NGX_DEBUG)
+    {
+    u_char      text[NGX_SOCKADDR_STRLEN];
+    ngx_str_t   addr;
+    ngx_uint_t  i;
+
+    addr.data = text;
+
+    for (i = 0; i < ctx->naddrs; i++) {
+        addr.len = ngx_sock_ntop(ctx->addrs[i].sockaddr, ctx->addrs[i].socklen,
+                                 text, NGX_SOCKADDR_STRLEN, 0);
+
+        ngx_log_debug1(NGX_LOG_DEBUG_EVENT, http->log, 0,
+                       "name was resolved to \"%V\"", &addr);
+    }
+    }
+#endif
+
+    http->naddrs = ctx->naddrs;
+    http->addrs = ngx_pcalloc(http->pool, http->naddrs * sizeof(ngx_addr_t));
+
+    if (http->addrs == NULL) {
+        goto failed;
+    }
+
+    for (i = 0; i < ctx->naddrs; i++) {
+        socklen = ctx->addrs[i].socklen;
+
+        sockaddr = ngx_palloc(http->pool, socklen);
+        if (sockaddr == NULL) {
+            goto failed;
+        }
+
+        ngx_memcpy(sockaddr, ctx->addrs[i].sockaddr, socklen);
+        ngx_inet_set_port(sockaddr, http->port);
+
+        http->addrs[i].sockaddr = sockaddr;
+        http->addrs[i].socklen = socklen;
+
+        p = ngx_pnalloc(http->pool, NGX_SOCKADDR_STRLEN);
+        if (p == NULL) {
+            goto failed;
+        }
+
+        len = ngx_sock_ntop(sockaddr, socklen, p, NGX_SOCKADDR_STRLEN, 1);
+        http->addrs[i].name.len = len;
+        http->addrs[i].name.data = p;
+    }
+
+    ngx_resolve_name_done(ctx);
+    http->ctx = NULL;
+
+    ngx_qjs_http_connect(http);
+
+    return;
+
+failed:
+
+    ngx_qjs_http_error(http, 0, "memory error");
+}
+
+
+#if (NGX_SSL)
+
+static void
+ngx_qjs_http_ssl_init_connection(ngx_qjs_http_t *http)
+{
+    ngx_int_t          rc;
+    ngx_connection_t  *c;
+
+    c = http->peer.connection;
+
+    ngx_log_debug2(NGX_LOG_DEBUG_EVENT, http->log, 0,
+                   "qjs fetch secure connect %ui/%ui", http->naddr,
+                   http->naddrs);
+
+    if (ngx_ssl_create_connection(http->ssl, c, NGX_SSL_BUFFER|NGX_SSL_CLIENT)
+        != NGX_OK)
+    {
+        ngx_qjs_http_error(http, 0, "failed to create ssl connection");
+        return;
+    }
+
+    c->sendfile = 0;
+
+    if (ngx_qjs_http_ssl_name(http) != NGX_OK) {
+        ngx_qjs_http_error(http, 0, "failed to create ssl connection");
+        return;
+    }
+
+    c->log->action = "SSL handshaking to fetch target";
+
+    rc = ngx_ssl_handshake(c);
+
+    if (rc == NGX_AGAIN) {
+        c->data = http;
+        c->ssl->handler = ngx_qjs_http_ssl_handshake_handler;
+        return;
+    }
+
+    ngx_qjs_http_ssl_handshake(http);
+}
+
+
+static njs_int_t
+ngx_qjs_http_ssl_name(ngx_qjs_http_t *http)
+{
+#ifdef SSL_CTRL_SET_TLSEXT_HOSTNAME
+    u_char  *p;
+
+    /* as per RFC 6066, literal IPv4 and IPv6 addresses are not permitted */
+    ngx_str_t  *name = &http->tls_name;
+
+    if (name->len == 0 || *name->data == '[') {
+        goto done;
+    }
+
+    if (ngx_inet_addr(name->data, name->len) != INADDR_NONE) {
+        goto done;
+    }
+
+    /*
+     * SSL_set_tlsext_host_name() needs a null-terminated string,
+     * hence we explicitly null-terminate name here
+     */
+
+    p = ngx_pnalloc(http->pool, name->len + 1);
+    if (p == NULL) {
+        return NGX_ERROR;
+    }
+
+    (void) ngx_cpystrn(p, name->data, name->len + 1);
+
+    name->data = p;
+
+    ngx_log_debug1(NGX_LOG_DEBUG_EVENT, http->log, 0,
+                   "qjs fetch SSL server name: \"%s\"", name->data);
+
+    if (SSL_set_tlsext_host_name(http->peer.connection->ssl->connection,
+                                 (char *) name->data)
+        == 0)
+    {
+        ngx_ssl_error(NGX_LOG_ERR, http->log, 0,
+                      "SSL_set_tlsext_host_name(\"%s\") failed", name->data);
+        return NGX_ERROR;
+    }
+
+#endif
+
+done:
+
+    return NJS_OK;
+}
+
+
+static void
+ngx_qjs_http_ssl_handshake_handler(ngx_connection_t *c)
+{
+    ngx_qjs_http_t  *http;
+
+    http = c->data;
+
+    http->peer.connection->write->handler = ngx_qjs_http_write_handler;
+    http->peer.connection->read->handler = ngx_qjs_http_read_handler;
+
+    ngx_qjs_http_ssl_handshake(http);
+}
+
+
+static void
+ngx_qjs_http_ssl_handshake(ngx_qjs_http_t *http)
+{
+    long               rc;
+    ngx_connection_t  *c;
+
+    c = http->peer.connection;
+
+    if (c->ssl->handshaked) {
+        if (http->ssl_verify) {
+            rc = SSL_get_verify_result(c->ssl->connection);
+
+            if (rc != X509_V_OK) {
+                ngx_log_error(NGX_LOG_ERR, c->log, 0,
+                              "js fetch SSL certificate verify error: (%l:%s)",
+                              rc, X509_verify_cert_error_string(rc));
+                goto failed;
+            }
+
+            if (ngx_ssl_check_host(c, &http->tls_name) != NGX_OK) {
+                ngx_log_error(NGX_LOG_ERR, c->log, 0,
+                              "js fetch SSL certificate does not match \"%V\"",
+                              &http->tls_name);
+                goto failed;
+            }
+        }
+
+        c->write->handler = ngx_qjs_http_write_handler;
+        c->read->handler = ngx_qjs_http_read_handler;
+
+        if (c->read->ready) {
+            ngx_post_event(c->read, &ngx_posted_events);
+        }
+
+        http->process = ngx_qjs_http_process_status_line;
+        ngx_qjs_http_write_handler(c->write);
+
+        return;
+    }
+
+failed:
+
+    ngx_qjs_http_next(http);
+}
+
+#endif
+
+
+JSValue
+ngx_qjs_ext_fetch(JSContext *cx, JSValueConst this_val, int argc,
+    JSValueConst *argv)
+{
+    void                *external;
+    JSValue              init, value;
+    njs_int_t            ret;
+    njs_str_t            str;
+    ngx_url_t            u;
+    ngx_uint_t           i;
+    njs_bool_t           has_host;
+    ngx_pool_t          *pool;
+    ngx_qjs_http_t      *http;
+    ngx_list_part_t     *part;
+    ngx_qjs_tb_elt_t    *h;
+    ngx_connection_t    *c;
+    ngx_qjs_request_t    request;
+    ngx_resolver_ctx_t  *ctx;
+
+    external = JS_GetContextOpaque(cx);
+    c = ngx_qjs_external_connection(cx, external);
+    pool = ngx_qjs_external_pool(cx, external);
+
+    http = ngx_qjs_http_alloc(cx, pool, c->log);
+    if (http == NULL) {
+        return JS_ThrowOutOfMemory(cx);
+    }
+
+    ret = ngx_qjs_request_constructor(cx, &request, &u, argc, argv);
+    if (ret != NJS_OK) {
+        goto fail;
+    }
+
+    http->response.url = request.url;
+    http->timeout = ngx_qjs_external_fetch_timeout(cx, external);
+    http->buffer_size = ngx_qjs_external_buffer_size(cx, external);
+    http->max_response_body_size =
+                        ngx_qjs_external_max_response_buffer_size(cx, external);
+
+#if (NGX_SSL)
+    if (u.default_port == 443) {
+        http->ssl = ngx_qjs_external_ssl(cx, external);
+        http->ssl_verify = ngx_qjs_external_ssl_verify(cx, external);
+    }
+#endif
+
+    if (argc > 1 && JS_IsObject(argv[1])) {
+        init = argv[1];
+        value = JS_GetPropertyStr(cx, init, "buffer_size");
+        if (JS_IsException(value)) {
+            goto fail;
+        }
+
+        if (!JS_IsUndefined(value)) {
+            ret = JS_ToInt64(cx, &http->buffer_size, value);
+            JS_FreeValue(cx, value);
+
+            if (ret < 0) {
+                goto fail;
+            }
+        }
+
+        value = JS_GetPropertyStr(cx, init, "max_response_body_size");
+        if (JS_IsException(value)) {
+            goto fail;
+        }
+
+        if (!JS_IsUndefined(value)) {
+            ret = JS_ToInt64(cx, &http->max_response_body_size, value);
+            JS_FreeValue(cx, value);
+
+            if (ret < 0) {
+                goto fail;
+            }
+        }
+
+#if (NGX_SSL)
+        value = JS_GetPropertyStr(cx, init, "verify");
+        if (JS_IsException(value)) {
+            goto fail;
+        }
+
+        if (!JS_IsUndefined(value)) {
+            http->ssl_verify = JS_ToBool(cx, value);
+        }
+#endif
+    }
+
+    str.start = request.method.data;
+    str.length = request.method.len;
+    http->header_only = njs_strstr_eq(&str, &njs_str_value("HEAD"));
+
+    NJS_CHB_CTX_INIT(&http->chain, cx);
+    NJS_CHB_CTX_INIT(&http->response.chain, cx);
+
+    njs_chb_append(&http->chain, request.method.data, request.method.len);
+    njs_chb_append_literal(&http->chain, " ");
+
+    if (u.uri.len == 0 || u.uri.data[0] != '/') {
+        njs_chb_append_literal(&http->chain, "/");
+    }
+
+    njs_chb_append(&http->chain, u.uri.data, u.uri.len);
+    njs_chb_append_literal(&http->chain, " HTTP/1.1" CRLF);
+
+    has_host = 0;
+    part = &request.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 (h[i].key.len == 4
+            && ngx_strncasecmp(h[i].key.data, (u_char *) "Host", 4) == 0)
+        {
+            has_host = 1;
+            njs_chb_append_literal(&http->chain, "Host: ");
+            njs_chb_append(&http->chain, h[i].value.data, h[i].value.len);
+            njs_chb_append_literal(&http->chain, CRLF);
+            break;
+        }
+    }
+
+    if (!has_host) {
+        njs_chb_append_literal(&http->chain, "Host: ");
+        njs_chb_append(&http->chain, u.host.data, u.host.len);
+
+        if (!u.no_port) {
+            njs_chb_sprintf(&http->chain, 32, ":%d", u.port);
+        }
+
+        njs_chb_append_literal(&http->chain, CRLF);
+    }
+
+    part = &request.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 (h[i].key.len == 4
+            && ngx_strncasecmp(h[i].key.data, (u_char *) "Host", 4) == 0)
+        {
+            continue;
+        }
+
+        njs_chb_append(&http->chain, h[i].key.data, h[i].key.len);
+        njs_chb_append_literal(&http->chain, ": ");
+        njs_chb_append(&http->chain, h[i].value.data, h[i].value.len);
+        njs_chb_append_literal(&http->chain, CRLF);
+    }
+
+    njs_chb_append_literal(&http->chain, "Connection: close" CRLF);
+
+#if (NGX_SSL)
+    http->tls_name.data = u.host.data;
+    http->tls_name.len = u.host.len;
+#endif
+
+    if (request.body.len != 0) {
+        njs_chb_sprintf(&http->chain, 32, "Content-Length: %uz" CRLF CRLF,
+                        request.body.len);
+        njs_chb_append(&http->chain, request.body.data, request.body.len);
+
+    } else {
+        njs_chb_append_literal(&http->chain, CRLF);
+    }
+
+    if (u.addrs == NULL) {
+        ctx = ngx_resolve_start(ngx_qjs_external_resolver(cx, external), NULL);
+        if (ctx == NULL) {
+            return JS_ThrowOutOfMemory(cx);
+        }
+
+        if (ctx == NGX_NO_RESOLVER) {
+            JS_ThrowInternalError(cx, "no resolver defined");
+            goto fail;
+        }
+
+        http->ctx = ctx;
+        http->port = u.port;
+
+        ctx->name = u.host;
+        ctx->handler = ngx_qjs_resolve_handler;
+        ctx->data = http;
+        ctx->timeout = ngx_qjs_external_resolver_timeout(cx, external);
+
+        ret = ngx_resolve_name(http->ctx);
+        if (ret != NGX_OK) {
+            http->ctx = NULL;
+            return JS_ThrowOutOfMemory(cx);
+        }
+
+        return JS_DupValue(cx, http->promise);
+    }
+
+    http->naddrs = 1;
+    ngx_memcpy(&http->addr, &u.addrs[0], sizeof(ngx_addr_t));
+    http->addrs = &http->addr;
+
+    ngx_qjs_http_connect(http);
+
+    return JS_DupValue(cx, http->promise);
+
+fail:
+
+    http->response_value = JS_GetException(cx);
+
+    ngx_qjs_http_fetch_done(http, http->response_value, NJS_ERROR);
+
+    return JS_DupValue(cx, http->promise);
+}
+
+
+static int
+ngx_qjs_fetch_headers_own_property(JSContext *cx, JSPropertyDescriptor *desc,
+    JSValueConst obj, JSAtom prop)
+{
+    JSValue    value;
+    ngx_str_t  name;
+
+    name.data = (u_char *) JS_AtomToCString(cx, prop);
+    if (name.data == NULL) {
+        return -1;
+    }
+
+    name.len = ngx_strlen(name.data);
+
+    value = ngx_qjs_headers_get(cx, obj, &name, 0);
+    JS_FreeCString(cx, (char *) name.data);
+
+    if (JS_IsException(value)) {
+        return -1;
+    }
+
+    if (JS_IsNull(value)) {
+        return 0;
+    }
+
+    if (desc == NULL) {
+        JS_FreeValue(cx, value);
+
+    } else {
+        desc->flags = JS_PROP_ENUMERABLE;
+        desc->getter = JS_UNDEFINED;
+        desc->setter = JS_UNDEFINED;
+        desc->value = value;
+    }
+
+    return 1;
+}
+
+
+static int
+ngx_qjs_fetch_headers_own_property_names(JSContext *cx, JSPropertyEnum **ptab,
+    uint32_t *plen, JSValueConst obj)
+{
+    int                 ret;
+    JSAtom              key;
+    JSValue             keys;
+    ngx_uint_t          i;
+    ngx_list_part_t    *part;
+    ngx_qjs_tb_elt_t   *h;
+    ngx_qjs_headers_t  *headers;
+
+    headers = JS_GetOpaque2(cx, obj, NGX_QJS_CLASS_ID_FETCH_HEADERS);
+    if (headers == NULL) {
+        (void) JS_ThrowInternalError(cx, "\"this\" is not a Headers object");
+        return -1;
+    }
+
+    keys = JS_NewObject(cx);
+    if (JS_IsException(keys)) {
+        return -1;
+    }
+
+    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;
+        }
+
+        key = JS_NewAtomLen(cx, (const char *) h[i].key.data, h[i].key.len);
+        if (key == JS_ATOM_NULL) {
+            goto fail;
+        }
+
+        if (JS_DefinePropertyValue(cx, keys, key, JS_UNDEFINED,
+                                   JS_PROP_ENUMERABLE) < 0)
+        {
+            JS_FreeAtom(cx, key);
+            goto fail;
+        }
+
+        JS_FreeAtom(cx, key);
+    }
+
+    ret = JS_GetOwnPropertyNames(cx, ptab, plen, keys, JS_GPN_STRING_MASK | JS_GPN_ENUM_ONLY);
+
+    JS_FreeValue(cx, keys);
+
+    return ret;
+
+fail:
+
+    JS_FreeValue(cx, keys);
+
+    return -1;
+}
+
+
+static void
+ngx_qjs_fetch_request_finalizer(JSRuntime *rt, JSValue val)
+{
+    ngx_qjs_request_t  *request;
+
+    request = JS_GetOpaque(val, NGX_QJS_CLASS_ID_FETCH_REQUEST);
+
+    JS_FreeValueRT(rt, request->header_value);
+}
+
+
+static const JSClassDef  ngx_qjs_fetch_headers_class = {
+    "Headers",
+    .finalizer = NULL,
+    .exotic = & (JSClassExoticMethods) {
+        .get_own_property = ngx_qjs_fetch_headers_own_property,
+        .get_own_property_names = ngx_qjs_fetch_headers_own_property_names,
+    },
+};
+
+static const JSClassDef  ngx_qjs_fetch_request_class = {
+    "Request",
+    .finalizer = ngx_qjs_fetch_request_finalizer,
+};
+
+
+static void
+ngx_qjs_fetch_response_finalizer(JSRuntime *rt, JSValue val)
+{
+    ngx_qjs_response_t  *response;
+
+    response = JS_GetOpaque(val, NGX_QJS_CLASS_ID_FETCH_RESPONSE);
+
+    njs_chb_destroy(&response->chain);
+
+    JS_FreeValueRT(rt, response->header_value);
+}
+
+static const JSClassDef  ngx_qjs_fetch_response_class = {
+    "Response",
+    .finalizer = ngx_qjs_fetch_response_finalizer,
+};
+
+
+static JSValue
+ngx_qjs_headers_get(JSContext *cx, JSValue this_val, ngx_str_t *name,
+    njs_bool_t as_array)
+{
+    int                 ret;
+    JSValue             retval, value;
+    njs_chb_t           chain;
+    ngx_uint_t          i;
+    ngx_list_part_t    *part;
+    ngx_qjs_tb_elt_t   *h, *ph;
+    ngx_qjs_headers_t  *headers;
+
+    headers = JS_GetOpaque2(cx, this_val, NGX_QJS_CLASS_ID_FETCH_HEADERS);
+    if (headers == NULL) {
+        return JS_NULL;
+    }
+
+    part = &headers->header_list.part;
+    h = part->elts;
+    ph = NULL;
+
+    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 (h[i].key.len == name->len
+            && njs_strncasecmp(h[i].key.data, name->data, name->len) == 0)
+        {
+            ph = &h[i];
+            break;
+        }
+    }
+
+    if (as_array) {
+        retval = JS_NewArray(cx);
+        if (JS_IsException(retval)) {
+            return JS_EXCEPTION;
+        }
+
+        i = 0;
+        while (ph != NULL) {
+            value = JS_NewStringLen(cx, (const char *) ph->value.data,
+                                    ph->value.len);
+            if (JS_IsException(value)) {
+                JS_FreeValue(cx, retval);
+                return JS_EXCEPTION;
+            }
+
+            ret = JS_DefinePropertyValueUint32(cx, retval, i, value,
+                                               JS_PROP_C_W_E);
+            if (ret < 0) {
+                JS_FreeValue(cx, retval);
+                JS_FreeValue(cx, value);
+                return JS_EXCEPTION;
+            }
+
+            i++;
+            ph = ph->next;
+        }
+
+        return retval;
+    }
+
+    if (ph == NULL) {
+        return JS_NULL;
+    }
+
+    NJS_CHB_CTX_INIT(&chain, cx);
+
+    h = ph;
+
+    for ( ;; ) {
+        njs_chb_append(&chain, h->value.data, h->value.len);
+
+        if (h->next == NULL) {
+            break;
+        }
+
+        njs_chb_append_literal(&chain, ", ");
+        h = h->next;
+    }
+
+    retval = qjs_string_create_chb(cx, &chain);
+
+    return retval;
+}
+
+
+static JSValue
+ngx_qjs_ext_fetch_headers_get(JSContext *cx, JSValueConst this_val,
+    int argc, JSValueConst *argv, int magic)
+{
+    njs_int_t  ret;
+    ngx_str_t  name;
+
+    ret = ngx_qjs_string(cx, argv[0], &name);
+    if (ret != NJS_OK) {
+        return JS_EXCEPTION;
+    }
+
+    return ngx_qjs_headers_get(cx, this_val, &name, magic);
+}
+
+
+static JSValue
+ngx_qjs_ext_fetch_headers_has(JSContext *cx, JSValueConst this_val,
+    int argc, JSValueConst *argv)
+{
+    JSValue    retval;
+    njs_int_t  ret;
+    ngx_str_t  name;
+
+    ret = ngx_qjs_string(cx, argv[0], &name);
+    if (ret != NJS_OK) {
+        return JS_EXCEPTION;
+    }
+
+    retval = ngx_qjs_headers_get(cx, this_val, &name, 0);
+    if (JS_IsException(retval)) {
+        return JS_EXCEPTION;
+    }
+
+    ret = !JS_IsNull(retval);
+    JS_FreeValue(cx, retval);
+
+    return JS_NewBool(cx, ret);
+}
+
+
+static JSValue
+ngx_qjs_ext_fetch_headers_set(JSContext *cx, JSValueConst this_val,
+    int argc, JSValueConst *argv)
+{
+    njs_int_t           ret;
+    ngx_str_t           name, value;
+    ngx_uint_t          i;
+    ngx_list_part_t    *part;
+    ngx_qjs_tb_elt_t   *h, **ph, **pp;
+    ngx_qjs_headers_t  *headers;
+
+    headers = JS_GetOpaque2(cx, this_val, NGX_QJS_CLASS_ID_FETCH_HEADERS);
+    if (headers == NULL) {
+        return JS_ThrowInternalError(cx,
+                                     "\"this\" is not fetch headers object");
+    }
+
+    ret = ngx_qjs_string(cx, argv[0], &name);
+    if (ret != NJS_OK) {
+        return JS_EXCEPTION;
+    }
+
+    ret = ngx_qjs_string(cx, argv[1], &value);
+    if (ret != NJS_OK) {
+        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
+            && (njs_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;
+            }
+
+            return JS_UNDEFINED;
+        }
+    }
+
+    ret = ngx_qjs_headers_append(cx, headers, name.data, name.len,
+                                 value.data, value.len);
+    if (ret != NJS_OK) {
+        return JS_EXCEPTION;
+    }
+
+    return JS_UNDEFINED;
+}
+
+
+static JSValue
+ngx_qjs_ext_fetch_headers_append(JSContext *cx, JSValueConst this_val,
+    int argc, JSValueConst *argv)
+{
+    njs_int_t           ret;
+    ngx_qjs_headers_t  *headers;
+
+    headers = JS_GetOpaque2(cx, this_val, NGX_QJS_CLASS_ID_FETCH_HEADERS);
+    if (headers == NULL) {
+        return JS_ThrowInternalError(cx,
+                                     "\"this\" is not fetch headers object");
+    }
+
+    ret = ngx_qjs_headers_fill_header_free(cx, headers,
+                                           JS_DupValue(cx, argv[0]),
+                                           JS_DupValue(cx, argv[1]));
+    if (ret != NJS_OK) {
+        return JS_EXCEPTION;
+    }
+
+    return JS_UNDEFINED;
+}
+
+
+static JSValue
+ngx_qjs_ext_fetch_headers_delete(JSContext *cx, JSValueConst this_val,
+    int argc, JSValueConst *argv)
+{
+    njs_int_t           ret;
+    ngx_str_t           name;
+    ngx_uint_t          i;
+    ngx_list_part_t    *part;
+    ngx_qjs_tb_elt_t   *h;
+    ngx_qjs_headers_t  *headers;
+
+    headers = JS_GetOpaque2(cx, this_val, NGX_QJS_CLASS_ID_FETCH_HEADERS);
+    if (headers == NULL) {
+        return JS_ThrowInternalError(cx,
+                                     "\"this\" is not fetch headers object");
+    }
+
+    ret = ngx_qjs_string(cx, argv[0], &name);
+    if (ret != NJS_OK) {
+        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
+            && (njs_strncasecmp(name.data, h[i].key.data, name.len) == 0))
+        {
+            h[i].hash = 0;
+        }
+    }
+
+    if (name.len == njs_strlen("Content-Type")
+        && ngx_strncasecmp(name.data, (u_char *) "Content-Type", name.len)
+           == 0)
+    {
+        headers->content_type = NULL;
+    }
+
+    return JS_UNDEFINED;
+}
+
+
+static JSValue
+ngx_qjs_headers_ext_keys(JSContext *cx, JSValue value)
+{
+    int                 ret;
+    uint32_t            length;
+    JSValue             keys, key, item, func, retval;
+    njs_str_t           hdr;
+    njs_bool_t          found;
+    ngx_uint_t          i, k, n;
+    ngx_list_part_t    *part;
+    ngx_qjs_tb_elt_t   *h;
+    ngx_qjs_headers_t  *headers;
+
+    headers = JS_GetOpaque2(cx, value, NGX_QJS_CLASS_ID_FETCH_HEADERS);
+    if (headers == NULL) {
+        return JS_NULL;
+    }
+
+    keys = JS_NewArray(cx);
+    if (JS_IsException(keys)) {
+        return JS_EXCEPTION;
+    }
+
+    n = 0;
+
+    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 (ngx_qjs_array_length(cx, &length, keys)) {
+            goto fail;
+        }
+
+        for (k = 0; k < length; k++) {
+            key = JS_GetPropertyUint32(cx, keys, k);
+            if (JS_IsException(key)) {
+                goto fail;
+            }
+
+            hdr.start = (u_char *) JS_ToCStringLen(cx, &hdr.length, key);
+            JS_FreeValue(cx, key);
+
+            found = h[i].key.len == hdr.length
+                    && njs_strncasecmp(h[i].key.data,
+                                       hdr.start, hdr.length) == 0;
+
+            JS_FreeCString(cx, (const char *) hdr.start);
+
+            if (found) {
+                break;
+            }
+        }
+
+        if (k == n) {
+            item = JS_NewStringLen(cx, (const char *) h[i].key.data,
+                                    h[i].key.len);
+            if (JS_IsException(value)) {
+                goto fail;
+            }
+
+            ret = JS_DefinePropertyValueUint32(cx, keys, n, item,
+                                               JS_PROP_C_W_E);
+            if (ret < 0) {
+                JS_FreeValue(cx, item);
+                goto fail;
+            }
+
+            n++;
+        }
+    }
+
+    func = JS_GetPropertyStr(cx, keys, "sort");
+
+    retval = JS_Call(cx, func, keys, 0, NULL);
+
+    JS_FreeValue(cx, func);
+    JS_FreeValue(cx, keys);
+
+    return retval;
+
+fail:
+
+    JS_FreeValue(cx, keys);
+
+    return JS_EXCEPTION;
+}
+
+
+static JSValue
+ngx_qjs_ext_fetch_headers_foreach(JSContext *cx, JSValueConst this_val,
+    int argc, JSValueConst *argv)
+{
+    int                 ret;
+    JSValue             callback, keys, key;
+    JSValue             header, retval, arguments[2];
+    uint32_t            length;;
+    ngx_str_t           name;
+    ngx_uint_t          i;
+    ngx_qjs_headers_t  *headers;
+
+    headers = JS_GetOpaque2(cx, this_val, NGX_QJS_CLASS_ID_FETCH_HEADERS);
+    if (headers == NULL) {
+        return JS_ThrowInternalError(cx,
+                                     "\"this\" is not fetch headers object");
+    }
+
+    callback = argv[0];
+
+    if (!JS_IsFunction(cx, callback)) {
+        return JS_ThrowInternalError(cx, "\"callback\" is not a function");
+    }
+
+    keys = ngx_qjs_headers_ext_keys(cx, this_val);
+    if (JS_IsException(keys)) {
+        return JS_EXCEPTION;
+    }
+
+    if (ngx_qjs_array_length(cx, &length, keys)) {
+        goto fail;
+    }
+
+    for (i = 0; i < length; i++) {
+        key = JS_GetPropertyUint32(cx, keys, i);
+        if (JS_IsException(key)) {
+            goto fail;
+        }
+
+        ret = ngx_qjs_string(cx, key, &name);
+        if (ret != NJS_OK) {
+            JS_FreeValue(cx, key);
+            goto fail;
+        }
+
+        header = ngx_qjs_headers_get(cx, this_val, &name, 0);
+        if (JS_IsException(header)) {
+            JS_FreeValue(cx, key);
+            goto fail;
+        }
+
+        arguments[0] = key;
+        arguments[1] = header;
+
+        retval = JS_Call(cx, callback, JS_UNDEFINED, 2, arguments);
+
+        JS_FreeValue(cx, key);
+        JS_FreeValue(cx, header);
+        JS_FreeValue(cx, retval);
+    }
+
+    JS_FreeValue(cx, keys);
+
+    return JS_UNDEFINED;
+
+fail:
+
+    JS_FreeValue(cx, keys);
+
+    return JS_EXCEPTION;
+}
+
+
+static JSValue
+ngx_qjs_ext_fetch_request_field(JSContext *cx, JSValueConst this_val, int magic)
+{
+    ngx_str_t          *field;
+    ngx_qjs_request_t  *request;
+
+    request = JS_GetOpaque2(cx, this_val, NGX_QJS_CLASS_ID_FETCH_REQUEST);
+    if (request == NULL) {
+        return JS_UNDEFINED;
+    }
+
+    field = (ngx_str_t *) ((u_char *) request + magic);
+
+    return qjs_string_create(cx, field->data, field->len);
+}
+
+
+static JSValue
+ngx_qjs_ext_fetch_request_headers(JSContext *cx, JSValueConst this_val)
+{
+    ngx_qjs_request_t  *request;
+
+    request = JS_GetOpaque2(cx, this_val, NGX_QJS_CLASS_ID_FETCH_REQUEST);
+    if (request == NULL) {
+        return JS_UNDEFINED;
+    }
+
+    if (JS_IsUndefined(request->header_value)) {
+        request->header_value = JS_NewObjectClass(cx,
+                                                NGX_QJS_CLASS_ID_FETCH_HEADERS);
+        if (JS_IsException(request->header_value)) {
+            return JS_ThrowInternalError(cx, "fetch header creation failed");
+        }
+
+        JS_SetOpaque(request->header_value, &request->headers);
+    }
+
+    return JS_DupValue(cx, request->header_value);
+}
+
+
+static JSValue
+ngx_qjs_ext_fetch_request_bodyused(JSContext *cx, JSValueConst this_val)
+{
+    ngx_qjs_request_t  *request;
+
+    request = JS_GetOpaque2(cx, this_val, NGX_QJS_CLASS_ID_FETCH_REQUEST);
+    if (request == NULL) {
+        return JS_UNDEFINED;
+    }
+
+    return JS_NewBool(cx, request->body_used);
+}
+
+
+static JSValue
+ngx_qjs_ext_fetch_request_cache(JSContext *cx, JSValueConst this_val)
+{
+    ngx_qjs_request_t  *request;
+
+    request = JS_GetOpaque2(cx, this_val, NGX_QJS_CLASS_ID_FETCH_REQUEST);
+    if (request == NULL) {
+        return JS_UNDEFINED;
+    }
+
+    return ngx_qjs_fetch_flag(cx, ngx_qjs_fetch_cache_modes,
+                              (njs_int_t) request->cache_mode);
+}
+
+
+static JSValue
+ngx_qjs_ext_fetch_request_credentials(JSContext *cx, JSValueConst this_val)
+{
+    ngx_qjs_request_t  *request;
+
+    request = JS_GetOpaque2(cx, this_val, NGX_QJS_CLASS_ID_FETCH_REQUEST);
+    if (request == NULL) {
+        return JS_UNDEFINED;
+    }
+
+    return ngx_qjs_fetch_flag(cx, ngx_qjs_fetch_credentials,
+                              (njs_int_t) request->credentials);
+}
+
+
+static JSValue
+ngx_qjs_ext_fetch_request_mode(JSContext *cx, JSValueConst this_val)
+{
+    ngx_qjs_request_t  *request;
+
+    request = JS_GetOpaque2(cx, this_val, NGX_QJS_CLASS_ID_FETCH_REQUEST);
+    if (request == NULL) {
+        return JS_UNDEFINED;
+    }
+
+    return ngx_qjs_fetch_flag(cx, ngx_qjs_fetch_modes,
+                              (njs_int_t) request->mode);
+}
+
+
+static JSValue
+ngx_qjs_ext_fetch_request_body(JSContext *cx, JSValueConst this_val,
+    int argc, JSValueConst *argv, int magic)
+{
+    JSValue             result;
+    ngx_qjs_request_t  *request;
+
+    request = JS_GetOpaque2(cx, this_val, NGX_QJS_CLASS_ID_FETCH_REQUEST);
+    if (request == NULL) {
+        return JS_UNDEFINED;
+    }
+
+    if (request->body_used) {
+        return JS_ThrowInternalError(cx, "body stream already read");
+    }
+
+    request->body_used = 1;
+
+    switch (magic) {
+
+    case NGX_QJS_BODY_TEXT:
+    default:
+        result = qjs_string_create(cx, request->body.data, request->body.len);
+        if (JS_IsException(result)) {
+            return JS_ThrowOutOfMemory(cx);
+        }        
+    }
+
+    return qjs_promise_result(cx, result);
+}
+
+
+static JSValue
+ngx_qjs_ext_fetch_response_field(JSContext *cx, JSValueConst this_val, int magic)
+{
+    ngx_str_t           *field;
+    ngx_qjs_response_t  *response;
+
+    response = JS_GetOpaque2(cx, this_val, NGX_QJS_CLASS_ID_FETCH_RESPONSE);
+    if (response == NULL) {
+        return JS_UNDEFINED;
+    }
+
+    field = (ngx_str_t *) ((u_char *) response + magic);
+
+    return qjs_string_create(cx, field->data, field->len);
+}
+
+
+static JSValue
+ngx_qjs_ext_fetch_response_body(JSContext *cx, JSValueConst this_val,
+    int argc, JSValueConst *argv, int magic)
+{
+    JSValue              result;
+    njs_int_t            ret;
+    njs_str_t            string;
+    ngx_qjs_response_t  *response;
+
+    response = JS_GetOpaque2(cx, this_val, NGX_QJS_CLASS_ID_FETCH_RESPONSE);
+    if (response == NULL) {
+        return JS_UNDEFINED;
+    }
+
+    if (response->body_used) {
+        return JS_ThrowInternalError(cx, "body stream already read");
+    }
+
+    response->body_used = 1;
+
+    ret = njs_chb_join(&response->chain, &string);
+    if (ret != NJS_OK) {
+        return JS_ThrowOutOfMemory(cx);
+    }
+
+    switch (magic) {
+    case NGX_QJS_BODY_ARRAY_BUFFER:
+        result = qjs_new_array_buffer(cx, string.start, string.length);
+        if (JS_IsException(result)) {
+            return JS_ThrowOutOfMemory(cx);
+        }
+
+        break;
+
+    case NGX_QJS_BODY_JSON:
+    case NGX_QJS_BODY_TEXT:
+    default:
+        result = qjs_string_create(cx, string.start, string.length);
+        if (JS_IsException(result)) {
+            response->chain.free(cx, string.start);
+            return JS_ThrowOutOfMemory(cx);
+        }
+
+        response->chain.free(cx, string.start);
+
+        if (magic == NGX_QJS_BODY_JSON) {
+            string.start = (u_char *) JS_ToCStringLen(cx, &string.length,
+                                                      result);
+
+            JS_FreeValue(cx, result);
+            result = JS_UNDEFINED;
+
+            if (string.start == NULL) {
+                JS_FreeCString(cx, (const char *) string.start);
+                ret = NJS_ERROR;
+                break;
+            }
+
+            result = JS_ParseJSON(cx, (const char *) string.start,
+                                  string.length, "<input>");
+            JS_FreeCString(cx, (const char *) string.start);
+            if (JS_IsException(result)) {
+                ret = NJS_ERROR;
+                break;
+            }
+        }
+    }
+
+    return qjs_promise_result(cx, result);
+}
+
+
+static JSValue
+ngx_qjs_ext_fetch_response_status(JSContext *cx, JSValueConst this_val)
+{
+    ngx_qjs_response_t  *response;
+
+    response = JS_GetOpaque2(cx, this_val, NGX_QJS_CLASS_ID_FETCH_RESPONSE);
+    if (response == NULL) {
+        return JS_UNDEFINED;
+    }
+
+    return JS_NewUint32(cx, response->code);
+}
+
+
+static JSValue
+ngx_qjs_ext_fetch_response_status_text(JSContext *cx, JSValueConst this_val)
+{
+    ngx_qjs_response_t  *response;
+
+    response = JS_GetOpaque2(cx, this_val, NGX_QJS_CLASS_ID_FETCH_RESPONSE);
+    if (response == NULL) {
+        return JS_UNDEFINED;
+    }
+
+    return qjs_string_create(cx, response->status_text.data,
+                             response->status_text.len);
+}
+
+
+static JSValue
+ngx_qjs_ext_fetch_response_headers(JSContext *cx, JSValueConst this_val)
+{
+    ngx_qjs_response_t  *response;
+
+    response = JS_GetOpaque2(cx, this_val, NGX_QJS_CLASS_ID_FETCH_RESPONSE);
+    if (response == NULL) {
+        return JS_UNDEFINED;
+    }
+
+    if (JS_IsUndefined(response->header_value)) {
+        response->header_value = JS_NewObjectClass(cx,
+                                                NGX_QJS_CLASS_ID_FETCH_HEADERS);
+        if (JS_IsException(response->header_value)) {
+            return JS_ThrowInternalError(cx, "fetch header creation failed");
+        }
+
+        JS_SetOpaque(response->header_value, &response->headers);
+    }
+
+    return JS_DupValue(cx, response->header_value);
+}
+
+
+static JSValue
+ngx_qjs_ext_fetch_response_bodyused(JSContext *cx, JSValueConst this_val)
+{
+    ngx_qjs_response_t  *response;
+
+    response = JS_GetOpaque2(cx, this_val, NGX_QJS_CLASS_ID_FETCH_RESPONSE);
+    if (response == NULL) {
+        return JS_UNDEFINED;
+    }
+
+    return JS_NewBool(cx, response->body_used);
+}
+
+
+static JSValue
+ngx_qjs_ext_fetch_response_ok(JSContext *cx, JSValueConst this_val)
+{
+    ngx_qjs_response_t  *response;
+
+    response = JS_GetOpaque2(cx, this_val, NGX_QJS_CLASS_ID_FETCH_RESPONSE);
+    if (response == NULL) {
+        return JS_UNDEFINED;
+    }
+
+    return JS_NewBool(cx, response->code >= 200 && response->code < 300);
+}
+
+
+static JSValue
+ngx_qjs_ext_fetch_response_type(JSContext *cx, JSValueConst this_val)
+{
+    ngx_qjs_response_t  *response;
+
+    response = JS_GetOpaque2(cx, this_val, NGX_QJS_CLASS_ID_FETCH_RESPONSE);
+    if (response == NULL) {
+        return JS_UNDEFINED;
+    }
+
+    return JS_NewString(cx, "basic");
+}
+
+
+static JSValue
+ngx_qjs_ext_fetch_response_redirected(JSContext *cx, JSValueConst this_val)
+{
+    ngx_qjs_response_t  *response;
+
+    response = JS_GetOpaque2(cx, this_val, NGX_QJS_CLASS_ID_FETCH_RESPONSE);
+    if (response == NULL) {
+        return JS_UNDEFINED;
+    }
+
+    return JS_NewBool(cx, 0);
+}
+
+
+static const JSCFunctionListEntry  ngx_qjs_ext_fetch_headers_funcs[] = {
+    JS_CFUNC_MAGIC_DEF("get", 1, ngx_qjs_ext_fetch_headers_get, 0),
+    JS_CFUNC_MAGIC_DEF("getAll", 1, ngx_qjs_ext_fetch_headers_get, 1),
+    JS_CFUNC_DEF("has", 1, ngx_qjs_ext_fetch_headers_has),
+    JS_CFUNC_DEF("set", 2, ngx_qjs_ext_fetch_headers_set),
+    JS_CFUNC_DEF("append", 2, ngx_qjs_ext_fetch_headers_append),
+    JS_CFUNC_DEF("delete", 1, ngx_qjs_ext_fetch_headers_delete),
+    JS_CFUNC_DEF("forEach", 1, ngx_qjs_ext_fetch_headers_foreach),
+};
+
+static const JSCFunctionListEntry  ngx_qjs_ext_fetch_request_funcs[] = {
+    JS_CGETSET_MAGIC_DEF("url", ngx_qjs_ext_fetch_request_field, NULL,
+                         offsetof(ngx_qjs_request_t, url) ),
+    JS_CGETSET_MAGIC_DEF("method", ngx_qjs_ext_fetch_request_field, NULL,
+                         offsetof(ngx_qjs_request_t, method) ),
+    JS_CGETSET_DEF("headers", ngx_qjs_ext_fetch_request_headers, NULL ),
+    JS_CGETSET_DEF("bodyUsed", ngx_qjs_ext_fetch_request_bodyused, NULL),
+    JS_CGETSET_DEF("cache", ngx_qjs_ext_fetch_request_cache, NULL),
+    JS_CGETSET_DEF("credentials", ngx_qjs_ext_fetch_request_credentials, NULL),
+    JS_CGETSET_DEF("mode", ngx_qjs_ext_fetch_request_mode, NULL),
+    JS_CFUNC_MAGIC_DEF("text", 1, ngx_qjs_ext_fetch_request_body,
+                       NGX_QJS_BODY_TEXT),
+};
+
+static const JSCFunctionListEntry  ngx_qjs_ext_fetch_response_funcs[] = {
+    JS_CGETSET_MAGIC_DEF("url", ngx_qjs_ext_fetch_response_field, NULL,
+                         offsetof(ngx_qjs_response_t, url) ),
+    JS_CFUNC_MAGIC_DEF("arrayBuffer", 1, ngx_qjs_ext_fetch_response_body,
+                       NGX_QJS_BODY_ARRAY_BUFFER),
+    JS_CFUNC_MAGIC_DEF("text", 1, ngx_qjs_ext_fetch_response_body,
+                       NGX_QJS_BODY_TEXT),
+    JS_CFUNC_MAGIC_DEF("json", 1, ngx_qjs_ext_fetch_response_body,
+                       NGX_QJS_BODY_JSON),
+    JS_CGETSET_DEF("status", ngx_qjs_ext_fetch_response_status, NULL),
+    JS_CGETSET_DEF("statusText", ngx_qjs_ext_fetch_response_status_text, NULL),
+    JS_CGETSET_DEF("headers", ngx_qjs_ext_fetch_response_headers, NULL ),
+    JS_CGETSET_DEF("bodyUsed", ngx_qjs_ext_fetch_response_bodyused, NULL),
+    JS_CGETSET_DEF("ok", ngx_qjs_ext_fetch_response_ok, NULL),
+    JS_CGETSET_DEF("type", ngx_qjs_ext_fetch_response_type, NULL),
+    JS_CGETSET_DEF("redirected", ngx_qjs_ext_fetch_response_redirected, NULL),
+};
+
+
+static const uint32_t  token_map[] = {
+    0x00000000,  /* 0000 0000 0000 0000  0000 0000 0000 0000 */
+
+                 /* ?>=< ;:98 7654 3210  /.-, +*)( '&%$ #"!  */
+    0x03ff6cfa,  /* 0000 0011 1111 1111  0110 1100 1111 1010 */
+
+                 /* _^]\ [ZYX WVUT SRQP  ONML KJIH GFED CBA@ */
+    0xc7fffffe,  /* 1100 0111 1111 1111  1111 1111 1111 1110 */
+
+                 /*  ~}| {zyx wvut srqp  onml kjih gfed cba` */
+    0x57ffffff,  /* 0101 0111 1111 1111  1111 1111 1111 1111 */
+
+    0x00000000,  /* 0000 0000 0000 0000  0000 0000 0000 0000 */
+    0x00000000,  /* 0000 0000 0000 0000  0000 0000 0000 0000 */
+    0x00000000,  /* 0000 0000 0000 0000  0000 0000 0000 0000 */
+    0x00000000,  /* 0000 0000 0000 0000  0000 0000 0000 0000 */
+};
+
+
+njs_inline njs_bool_t
+njs_is_token(uint32_t byte)
+{
+    return ((token_map[byte >> 5] & ((uint32_t) 1 << (byte & 0x1f))) != 0);
+}
+
+
+njs_inline njs_int_t
+ngx_qjs_http_whitespace(u_char c)
+{
+    switch (c) {
+    case 0x09:  /* <TAB>  */
+    case 0x0A:  /* <LF>   */
+    case 0x0D:  /* <CR>   */
+    case 0x20:  /* <SP>   */
+        return 1;
+
+    default:
+        return 0;
+    }
+}
+
+
+static void
+ngx_qjs_http_trim(u_char **value, size_t *len,
+    njs_bool_t trim_c0_control_or_space)
+{
+    u_char  *start, *end;
+
+    start = *value;
+    end = start + *len;
+
+    for ( ;; ) {
+        if (start == end) {
+            break;
+        }
+
+        if (ngx_qjs_http_whitespace(*start)
+            || (trim_c0_control_or_space && *start <= ' '))
+        {
+            start++;
+            continue;
+        }
+
+        break;
+    }
+
+    for ( ;; ) {
+        if (start == end) {
+            break;
+        }
+
+        end--;
+
+        if (ngx_qjs_http_whitespace(*end)
+            || (trim_c0_control_or_space && *end <= ' '))
+        {
+            continue;
+        }
+
+        end++;
+        break;
+    }
+
+    *value = start;
+    *len = end - start;
+}
+
+
+static njs_int_t
+ngx_qjs_headers_append(JSContext *cx, ngx_qjs_headers_t *headers,
+    u_char *name, size_t len, u_char *value, size_t vlen)
+{
+    u_char            *p, *end;
+    ngx_uint_t         i;
+    ngx_list_part_t   *part;
+    ngx_qjs_tb_elt_t  *h, **ph;
+
+    ngx_qjs_http_trim(&value, &vlen, 0);
+
+    p = name;
+    end = p + len;
+
+    while (p < end) {
+        if (!njs_is_token(*p)) {
+            JS_ThrowInternalError(cx, "invalid header name");
+            return NJS_ERROR;
+        }
+
+        p++;
+    }
+
+    p = value;
+    end = p + vlen;
+
+    while (p < end) {
+        if (*p == '\0') {
+            JS_ThrowInternalError(cx, "invalid header value");
+            return NJS_ERROR;
+        }
+
+        p++;
+    }
+
+    if (headers->guard == GUARD_IMMUTABLE) {
+        JS_ThrowInternalError(cx, "cannot append to immutable object");
+        return NJS_ERROR;
+    }
+
+    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 (len == h[i].key.len
+            && (njs_strncasecmp(name, h[i].key.data, len) == 0))
+        {
+            ph = &h[i].next;
+            while (*ph) { ph = &(*ph)->next; }
+            break;
+        }
+    }
+
+    h = ngx_list_push(&headers->header_list);
+    if (h == NULL) {
+        JS_ThrowOutOfMemory(cx);
+        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;
+
+    if (len == njs_strlen("Content-Type")
+        && ngx_strncasecmp(name, (u_char *) "Content-Type", len) == 0)
+    {
+        headers->content_type = h;
+    }
+
+    return NJS_OK;
+}
+
+
+static njs_int_t
+ngx_qjs_headers_fill_header_free(JSContext *cx, ngx_qjs_headers_t *headers,
+    JSValue prop_name, JSValue prop_value)
+{
+    int        ret;
+    ngx_str_t  name, value;
+
+    if (ngx_qjs_string(cx, prop_name, &name) != NGX_OK) {
+        JS_FreeValue(cx, prop_name);
+        JS_FreeValue(cx, prop_value);
+        return NJS_ERROR;
+    }
+
+    if (ngx_qjs_string(cx, prop_value, &value) != NGX_OK) {
+        JS_FreeValue(cx, prop_name);
+        JS_FreeValue(cx, prop_value);
+        return NJS_ERROR;
+    }
+
+    ret = ngx_qjs_headers_append(cx, headers, name.data, name.len,
+                                 value.data, value.len);
+
+    JS_FreeValue(cx, prop_name);
+    JS_FreeValue(cx, prop_value);
+
+    return ret;
+}
+
+
+static njs_int_t
+ngx_qjs_headers_inherit(JSContext *cx, ngx_qjs_headers_t *headers,
+    ngx_qjs_headers_t *orig)
+{
+    njs_int_t          ret;
+    ngx_uint_t         i;
+    ngx_list_part_t   *part;
+    ngx_qjs_tb_elt_t  *h;
+
+    part = &orig->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;
+        }
+
+        ret = ngx_qjs_headers_append(cx, headers, h[i].key.data, h[i].key.len,
+                                     h[i].value.data, h[i].value.len);
+        if (ret != NJS_OK) {
+            return NJS_ERROR;
+        }
+    }
+
+    return NJS_OK;
+}
+
+
+static njs_int_t
+ngx_qjs_headers_fill(JSContext *cx, ngx_qjs_headers_t *headers, JSValue init)
+{
+    int                 ret;
+    JSValue             header, prop_name, prop_value;
+    uint32_t            i, len, length;
+    JSPropertyEnum     *tab;
+    ngx_qjs_headers_t  *hh;
+
+    hh = JS_GetOpaque2(cx, init, NGX_QJS_CLASS_ID_FETCH_HEADERS);
+    if (hh != NULL) {
+        return ngx_qjs_headers_inherit(cx, headers, hh);
+    }
+
+    if (JS_GetOwnPropertyNames(cx, &tab, &len, init,
+                               JS_GPN_STRING_MASK | JS_GPN_ENUM_ONLY) < 0) {
+        return NJS_ERROR;
+    }
+
+    if (qjs_is_array(cx, init)) {
+
+        for (i = 0; i < len; i++) {
+            header = JS_GetPropertyUint32(cx, init, i);
+            if (JS_IsException(header)) {
+                goto fail;
+            }
+
+            if (ngx_qjs_array_length(cx, &length, header)) {
+                JS_FreeValue(cx, header);
+                goto fail;
+            }
+
+            if (length != 2) {
+                JS_FreeValue(cx, header);
+                JS_ThrowInternalError(cx,
+                                   "header does not contain exactly two items");
+                goto fail;
+            }
+
+            prop_name = JS_GetPropertyUint32(cx, header, 0);
+            prop_value = JS_GetPropertyUint32(cx, header, 1);
+
+            JS_FreeValue(cx, header);
+
+            ret = ngx_qjs_headers_fill_header_free(cx, headers, prop_name,
+                                                   prop_value);
+            if (ret != NJS_OK) {
+                goto fail;
+            }
+        }
+
+    } else {
+
+        for (i = 0; i < len; i++) {
+            prop_name = JS_AtomToString(cx, tab[i].atom);
+
+            prop_value = JS_GetProperty(cx, init, tab[i].atom);
+            if (JS_IsException(prop_value)) {
+                JS_FreeValue(cx, prop_name);
+                goto fail;
+            }
+
+            ret = ngx_qjs_headers_fill_header_free(cx, headers, prop_name,
+                                                   prop_value);
+            if (ret != NJS_OK) {
+                goto fail;
+            }
+        }
+    }
+
+    qjs_free_prop_enum(cx, tab, len);
+
+    return NJS_OK;
+
+fail:
+
+    qjs_free_prop_enum(cx, tab, len);
+
+    return NJS_ERROR;
+}
+
+
+static JSValue
+ngx_qjs_fetch_headers_ctor(JSContext *cx, JSValueConst new_target, int argc,
+    JSValueConst *argv)
+{
+    JSValue             init, proto, obj;
+    ngx_int_t           rc;
+    njs_int_t           ret;
+    ngx_pool_t         *pool;
+    ngx_qjs_headers_t  *headers;
+
+    pool = ngx_qjs_external_pool(cx, JS_GetContextOpaque(cx));
+
+    headers = ngx_pcalloc(pool, sizeof(ngx_qjs_headers_t));
+    if (headers == NULL) {
+        return JS_ThrowOutOfMemory(cx);
+    }
+
+    headers->guard = GUARD_NONE;
+
+    rc = ngx_list_init(&headers->header_list, pool, 4,
+                       sizeof(ngx_qjs_tb_elt_t));
+    if (rc != NGX_OK) {
+        return JS_ThrowOutOfMemory(cx);
+    }
+
+    init = argv[0];
+
+    if (JS_IsObject(init)) {
+        ret = ngx_qjs_headers_fill(cx, headers, init);
+        if (ret != NJS_OK) {
+            return JS_EXCEPTION;
+        }
+    }
+
+    proto = JS_GetPropertyStr(cx, new_target, "prototype");
+    if (JS_IsException(proto)) {
+        return JS_EXCEPTION;
+    }
+
+    obj = JS_NewObjectProtoClass(cx, proto, NGX_QJS_CLASS_ID_FETCH_HEADERS);
+    JS_FreeValue(cx, proto);
+
+    if (JS_IsException(obj)) {
+        return JS_EXCEPTION;
+    }
+
+    JS_SetOpaque(obj, headers);
+
+    return obj;
+}
+
+
+static njs_int_t
+ngx_qjs_method_process(JSContext *cx, ngx_qjs_request_t *request)
+{
+    u_char           *s, *p;
+    njs_str_t         method;
+    const njs_str_t  *m;
+
+    static const njs_str_t forbidden[] = {
+        njs_str("CONNECT"),
+        njs_str("TRACE"),
+        njs_str("TRACK"),
+        njs_null_str,
+    };
+
+    static const njs_str_t to_normalize[] = {
+        njs_str("DELETE"),
+        njs_str("GET"),
+        njs_str("HEAD"),
+        njs_str("OPTIONS"),
+        njs_str("POST"),
+        njs_str("PUT"),
+        njs_null_str,
+    };
+
+    method.start = request->method.data;
+    method.length = request->method.len;
+
+    for (m = &forbidden[0]; m->length != 0; m++) {
+        if (njs_strstr_case_eq(&method, m)) {
+            JS_ThrowInternalError(cx, "forbidden method: %.*s",
+                                  (int) m->length, m->start);
+            return NJS_ERROR;
+        }
+    }
+
+    for (m = &to_normalize[0]; m->length != 0; m++) {
+        if (njs_strstr_case_eq(&method, m)) {
+            s = &request->m[0];
+            p = m->start;
+
+            while (*p != '\0') {
+                *s++ = njs_upper_case(*p++);
+            }
+
+            request->method.data = &request->m[0];
+            request->method.len = m->length;
+            break;
+        }
+    }
+
+    return NJS_OK;
+}
+
+
+static njs_int_t
+ngx_qjs_fetch_flag_set(JSContext *cx, const ngx_qjs_entry_t *entries,
+     JSValue value, const char *type)
+{
+    njs_int_t               ret;
+    njs_str_t               str;
+    ngx_str_t               flag;
+    const ngx_qjs_entry_t  *e;
+
+    ret = ngx_qjs_string(cx, value, &flag);
+    if (ret != NJS_OK) {
+        return NJS_ERROR;
+    }
+
+    str.start = flag.data;
+    str.length = flag.len;
+
+    for (e = entries; e->name.length != 0; e++) {
+        if (njs_strstr_case_eq(&str, &e->name)) {
+            return e->value;
+        }
+    }
+
+    JS_ThrowInternalError(cx, "unknown %s type: %.*s",
+                          type, (int) flag.len, flag.data);
+
+    return NJS_ERROR;
+}
+
+
+static JSValue
+ngx_qjs_fetch_flag(JSContext *cx, const ngx_qjs_entry_t *entries,
+    njs_int_t value)
+{
+    const ngx_qjs_entry_t  *e;
+
+    for (e = entries; e->name.length != 0; e++) {
+        if (e->value == value) {
+            return qjs_string_create(cx, e->name.start, e->name.length);
+        }
+    }
+
+    return JS_EXCEPTION;
+}
+
+
+static njs_int_t
+ngx_qjs_request_constructor(JSContext *cx, ngx_qjs_request_t *request,
+    ngx_url_t *u, int argc, JSValueConst *argv)
+{
+    JSValue             input, init, value;
+    njs_int_t           ret;
+    ngx_uint_t          rc;
+    ngx_pool_t         *pool;
+    ngx_qjs_request_t  *orig;
+
+    input = argv[0];
+    if (JS_IsUndefined(input)) {
+        JS_ThrowInternalError(cx, "1st argument is required");
+        return NJS_ERROR;
+    }
+
+    /*
+     * set by ngx_memzero():
+     *
+     *  request->url.len = 0;
+     *  request->body.length = 0;
+     *  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_qjs_request_t));
+
+    ngx_str_set(&request->method, "GET");
+    ngx_str_null(&request->body);
+    request->headers.guard = GUARD_REQUEST;
+    request->header_value = JS_UNDEFINED;
+
+    pool = ngx_qjs_external_pool(cx, JS_GetContextOpaque(cx));
+
+    rc = ngx_list_init(&request->headers.header_list, pool, 4,
+                       sizeof(ngx_qjs_tb_elt_t));
+    if (rc != NGX_OK) {
+        JS_ThrowOutOfMemory(cx);
+        return NJS_ERROR;
+    }
+
+    if (JS_IsString(input)) {
+        ret = ngx_qjs_string(cx, input, &request->url);
+        if (ret != NJS_OK) {
+            JS_ThrowInternalError(cx, "failed to convert url arg");
+            return NJS_ERROR;
+        }
+
+    } else {
+        orig = JS_GetOpaque2(cx, input, NGX_QJS_CLASS_ID_FETCH_REQUEST);
+        if (orig == NULL) {
+            JS_ThrowInternalError(cx,
+                                  "input is not string or a Request object");
+            return NJS_ERROR;
+        }
+
+        request->url = orig->url;
+        request->method = orig->method;
+        request->body = orig->body;
+        request->body_used = orig->body_used;
+        request->cache_mode = orig->cache_mode;
+        request->credentials = orig->credentials;
+        request->mode = orig->mode;
+
+        ret = ngx_qjs_headers_inherit(cx, &request->headers, &orig->headers);
+        if (ret != NJS_OK) {
+            return NJS_ERROR;
+        }
+    }
+
+    ngx_qjs_http_trim(&request->url.data, &request->url.len, 1);
+
+    ngx_memzero(u, sizeof(ngx_url_t));
+
+    u->url.len = request->url.len;
+    u->url.data = request->url.data;
+    u->default_port = 80;
+    u->uri_part = 1;
+    u->no_resolve = 1;
+
+    if (u->url.len > 7
+        && njs_strncasecmp(u->url.data, (u_char *) "http://", 7) == 0)
+    {
+        u->url.len -= 7;
+        u->url.data += 7;
+
+#if (NGX_SSL)
+    } else if (u->url.len > 8
+        && njs_strncasecmp(u->url.data, (u_char *) "https://", 8) == 0)
+    {
+        u->url.len -= 8;
+        u->url.data += 8;
+        u->default_port = 443;
+#endif
+
+    } else {
+        JS_ThrowInternalError(cx, "unsupported URL schema (only http or https"
+                                  " are supported)");
+        return NJS_ERROR;
+    }
+
+    if (ngx_parse_url(pool, u) != NGX_OK) {
+        JS_ThrowInternalError(cx, "invalid url");
+        return NJS_ERROR;
+    }
+
+    if (JS_IsObject(argv[1])) {
+        init = argv[1];
+        value = JS_GetPropertyStr(cx, init, "method");
+        if (JS_IsException(value)) {
+            JS_ThrowInternalError(cx, "invalid Request method");
+            return NJS_ERROR;
+        }
+
+        if (!JS_IsUndefined(value)) {
+            ret = ngx_qjs_string(cx, value, &request->method);
+            JS_FreeValue(cx, value);
+
+            if (ret != NJS_OK) {
+                JS_ThrowInternalError(cx, "invalid Request method");
+                return NJS_ERROR;
+            }
+        }
+
+        ret = ngx_qjs_method_process(cx, request);
+        if (ret != NJS_OK) {
+            return NJS_ERROR;
+        }
+
+        value = JS_GetPropertyStr(cx, init, "cache");
+        if (JS_IsException(value)) {
+            JS_ThrowInternalError(cx, "invalid Request cache");
+            return NJS_ERROR;
+        }
+
+        if (!JS_IsUndefined(value)) {
+            ret = ngx_qjs_fetch_flag_set(cx, ngx_qjs_fetch_cache_modes, value,
+                                         "cache");
+            JS_FreeValue(cx, value);
+
+            if (ret == NJS_ERROR) {
+                return NJS_ERROR;
+            }
+
+            request->cache_mode = ret;
+        }
+
+        value = JS_GetPropertyStr(cx, init, "credentials");
+        if (JS_IsException(value)) {
+            JS_ThrowInternalError(cx, "invalid Request credentials");
+            return NJS_ERROR;
+        }
+
+        if (!JS_IsUndefined(value)) {
+            ret = ngx_qjs_fetch_flag_set(cx, ngx_qjs_fetch_credentials, value,
+                                         "credentials");
+            JS_FreeValue(cx, value);
+
+            if (ret == NJS_ERROR) {
+                return NJS_ERROR;
+            }
+
+            request->credentials = ret;
+        }
+
+        value = JS_GetPropertyStr(cx, init, "mode");
+        if (JS_IsException(value)) {
+            JS_ThrowInternalError(cx, "invalid Request mode");
+            return NJS_ERROR;
+        }
+
+        if (!JS_IsUndefined(value)) {
+            ret = ngx_qjs_fetch_flag_set(cx, ngx_qjs_fetch_modes, value,
+                                         "mode");
+            JS_FreeValue(cx, value);
+
+            if (ret == NJS_ERROR) {
+                return NJS_ERROR;
+            }
+
+            request->mode = ret;
+        }
+
+        value = JS_GetPropertyStr(cx, init, "headers");
+        if (JS_IsException(value)) {
+            JS_ThrowInternalError(cx, "invalid Request headers");
+            return NJS_ERROR;
+        }
+
+        if (!JS_IsUndefined(value)) {
+
+            if (!JS_IsObject(value)) {
+                JS_ThrowInternalError(cx, "Headers is not an object");
+                return NJS_ERROR;
+            }
+
+            /*
+             * There are no API to reset or destroy ngx_list,
+             * just allocating a new one.
+             */
+
+            ngx_memset(&request->headers, 0, sizeof(ngx_qjs_headers_t));
+            request->headers.guard = GUARD_REQUEST;
+
+            rc = ngx_list_init(&request->headers.header_list, pool, 4,
+                               sizeof(ngx_qjs_tb_elt_t));
+            if (rc != NGX_OK) {
+                JS_FreeValue(cx, value);
+                JS_ThrowOutOfMemory(cx);
+                return NJS_ERROR;
+            }
+
+            ret = ngx_qjs_headers_fill(cx, &request->headers, value);
+            JS_FreeValue(cx, value);
+
+            if (ret != NJS_OK) {
+                return NJS_ERROR;
+            }
+        }
+
+        value = JS_GetPropertyStr(cx, init, "body");
+        if (JS_IsException(value)) {
+            JS_ThrowInternalError(cx, "invalid Request body");
+            return NJS_ERROR;
+        }
+
+        if (!JS_IsUndefined(value)) {
+
+            if (ngx_qjs_string(cx, value, &request->body) != NGX_OK) {
+                JS_FreeValue(cx, value);
+                JS_ThrowInternalError(cx, "invalid Request body");
+                return NJS_ERROR;
+            }
+
+            if (request->headers.content_type == NULL && JS_IsString(value)) {
+                ret = ngx_qjs_headers_append(cx, &request->headers,
+                                        (u_char *) "Content-Type",
+                                        njs_length("Content-Type"),
+                                        (u_char *) "text/plain;charset=UTF-8",
+                                        njs_length("text/plain;charset=UTF-8"));
+                if (ret != NJS_OK) {
+                    JS_FreeValue(cx, value);
+                    return NJS_ERROR;
+                }
+            }
+
+            JS_FreeValue(cx, value);
+        }
+    }
+
+    return NJS_OK;    
+}
+
+
+static JSValue
+ngx_qjs_fetch_request_ctor(JSContext *cx, JSValueConst new_target, int argc,
+    JSValueConst *argv)
+{
+    JSValue             proto, obj;
+    njs_int_t           ret;
+    ngx_url_t           u;
+    ngx_pool_t         *pool;
+    ngx_qjs_request_t  *request;
+
+    pool = ngx_qjs_external_pool(cx, JS_GetContextOpaque(cx));
+
+    request = ngx_pcalloc(pool, sizeof(ngx_qjs_request_t));
+    if (request == NULL) {
+        return JS_ThrowOutOfMemory(cx);
+    }
+
+    ret = ngx_qjs_request_constructor(cx, request, &u, argc, argv);
+    if (ret != NJS_OK) {
+        return JS_EXCEPTION;
+    }
+
+    proto = JS_GetPropertyStr(cx, new_target, "prototype");
+    if (JS_IsException(proto)) {
+        return JS_EXCEPTION;
+    }
+
+    obj = JS_NewObjectProtoClass(cx, proto, NGX_QJS_CLASS_ID_FETCH_REQUEST);
+    JS_FreeValue(cx, proto);
+
+    if (JS_IsException(obj)) {
+        return JS_EXCEPTION;
+    }
+
+    JS_SetOpaque(obj, request);
+
+    return obj;
+}
+
+
+static JSValue
+ngx_qjs_fetch_response_ctor(JSContext *cx, JSValueConst new_target, int argc,
+    JSValueConst *argv)
+{
+    int                  ret;
+    u_char               *p, *end;
+    JSValue              init, value, body, proto, obj;
+    ngx_str_t            bd;
+    ngx_pool_t          *pool;
+    ngx_qjs_response_t  *response;
+
+    pool = ngx_qjs_external_pool(cx, JS_GetContextOpaque(cx));
+
+    response = ngx_pcalloc(pool, sizeof(ngx_qjs_response_t));
+    if (response == NULL) {
+        return JS_ThrowOutOfMemory(cx);
+    }
+
+    /*
+     * set by njs_mp_zalloc():
+     *
+     *  response->url.length = 0;
+     *  response->status_text.length = 0;
+     */
+
+    response->code = 200;
+    response->headers.guard = GUARD_RESPONSE;
+    response->header_value = JS_UNDEFINED;
+
+    pool = ngx_qjs_external_pool(cx, JS_GetContextOpaque(cx));
+
+    ret = ngx_list_init(&response->headers.header_list, pool, 4,
+                        sizeof(ngx_qjs_tb_elt_t));
+    if (ret != NGX_OK) {
+        JS_ThrowOutOfMemory(cx);
+    }
+
+    init = argv[1];
+
+    if (JS_IsObject(init)) {
+        value = JS_GetPropertyStr(cx, init, "status");
+        if (JS_IsException(value)) {
+            return JS_ThrowInternalError(cx, "invalid Response status");
+        }
+
+        if (!JS_IsUndefined(value)) {
+            ret = JS_ToInt64(cx, &response->code, value);
+            JS_FreeValue(cx, value);
+
+            if (ret < 0) {
+                return JS_EXCEPTION;
+            }
+
+            if (response->code < 200 || response->code > 599) {
+                return JS_ThrowInternalError(cx, "status provided (%d) is "
+                                                 "outside of [200, 599] range",
+                                             (int) response->code);
+            }
+        }
+
+        value = JS_GetPropertyStr(cx, init, "statusText");
+        if (JS_IsException(value)) {
+            return JS_ThrowInternalError(cx, "invalid Response statusText");
+        }
+
+        if (!JS_IsUndefined(value)) {
+            ret = ngx_qjs_string(cx, value, &response->status_text);
+            JS_FreeValue(cx, value);
+
+            if (ret < 0) {
+                return JS_EXCEPTION;
+            }
+
+            p = response->status_text.data;
+            end = p + response->status_text.len;
+
+            while (p < end) {
+                if (*p != '\t' && *p < ' ') {
+                    return JS_ThrowInternalError(cx, "invalid Response statusText");
+                }
+
+                p++;
+            }
+        }
+
+        value = JS_GetPropertyStr(cx, init, "headers");
+        if (JS_IsException(value)) {
+            return JS_ThrowInternalError(cx, "invalid Response headers");
+        }
+
+        if (!JS_IsUndefined(value)) {
+
+            if (!JS_IsObject(value)) {
+                JS_FreeValue(cx, value);
+                return JS_ThrowInternalError(cx, "Headers is not an object");
+            }
+
+            ret = ngx_qjs_headers_fill(cx, &response->headers, value);
+            JS_FreeValue(cx, value);
+
+            if (ret != NJS_OK) {
+                return JS_EXCEPTION;
+            }
+        }
+    }
+
+    NJS_CHB_CTX_INIT(&response->chain, cx);
+
+    body = argv[0];
+
+    if (!JS_IsNullOrUndefined(body)) {
+        if (ngx_qjs_string(cx, body, &bd) != NGX_OK) {
+            return JS_ThrowInternalError(cx, "invalid Response body");
+        }
+
+        njs_chb_append(&response->chain, bd.data, bd.len);
+
+        if (JS_IsString(body)) {
+            ret = ngx_qjs_headers_append(cx, &response->headers,
+                                    (u_char *) "Content-Type",
+                                    njs_length("Content-Type"),
+                                    (u_char *) "text/plain;charset=UTF-8",
+                                    njs_length("text/plain;charset=UTF-8"));
+            if (ret != NJS_OK) {
+                return JS_EXCEPTION;
+            }
+        }
+    }
+
+    proto = JS_GetPropertyStr(cx, new_target, "prototype");
+    if (JS_IsException(proto)) {
+        return JS_EXCEPTION;
+    }
+
+    obj = JS_NewObjectProtoClass(cx, proto, NGX_QJS_CLASS_ID_FETCH_RESPONSE);
+    JS_FreeValue(cx, proto);
+
+    if (JS_IsException(obj)) {
+        return JS_EXCEPTION;
+    }
+
+    JS_SetOpaque(obj, response);
+
+    return obj;
+}
+
+
+static const JSClassDef  *const ngx_qjs_fetch_class_ptr[3] = {
+    &ngx_qjs_fetch_headers_class,
+    &ngx_qjs_fetch_request_class,
+    &ngx_qjs_fetch_response_class,
+};
+
+static JSCFunction  *ngx_qjs_fetch_class_ctor_ptr[3] = {
+    ngx_qjs_fetch_headers_ctor,
+    ngx_qjs_fetch_request_ctor,
+    ngx_qjs_fetch_response_ctor,
+};
+
+static const JSCFunctionListEntry *const  ngx_qjs_fetch_proto_funcs_ptr[6] = {
+    ngx_qjs_ext_fetch_headers_funcs,
+    ngx_qjs_ext_fetch_request_funcs,
+    ngx_qjs_ext_fetch_response_funcs,
+};
+
+static const uint8_t  ngx_qjs_fetch_proto_funcs_count[3] = {
+    njs_nitems(ngx_qjs_ext_fetch_headers_funcs),
+    njs_nitems(ngx_qjs_ext_fetch_request_funcs),
+    njs_nitems(ngx_qjs_ext_fetch_response_funcs),
+};
+
+
+static JSModuleDef *
+ngx_qjs_fetch_init(JSContext *cx, const char *name)
+{
+    int      i, class_id;
+    JSValue  global_obj, proto, class;
+
+    global_obj = JS_GetGlobalObject(cx);
+
+    for (i = 0; i < 3; i++) {
+        class_id = NGX_QJS_CLASS_ID_FETCH_HEADERS + i;
+        JS_NewClass(JS_GetRuntime(cx), class_id, ngx_qjs_fetch_class_ptr[i]);
+
+        proto = JS_NewObject(cx);
+        if (JS_IsException(proto)) {
+            JS_FreeValue(cx, global_obj);
+            return NULL;
+        }
+
+        JS_SetPropertyFunctionList(cx, proto,
+                                   ngx_qjs_fetch_proto_funcs_ptr[i],
+                                   ngx_qjs_fetch_proto_funcs_count[i]);
+
+        class = JS_NewCFunction2(cx, ngx_qjs_fetch_class_ctor_ptr[i],
+                                 ngx_qjs_fetch_class_ptr[i]->class_name, 2,
+                                 JS_CFUNC_constructor, 0);
+
+        JS_SetConstructor(cx, class, proto);
+        JS_SetClassProto(cx, class_id, proto);
+
+        JS_SetPropertyStr(cx, global_obj,
+                          ngx_qjs_fetch_class_ptr[i]->class_name, class);
+    }
+
+    JS_FreeValue(cx, global_obj);
+
+    return JS_NewCModule(cx, name, NULL);
+}
+
+
+qjs_module_t  ngx_qjs_ngx_fetch_module = {
+    .name = "fetch",
+    .init = ngx_qjs_fetch_init,
+};
diff --git a/nginx/t/js_fetch.t b/nginx/t/js_fetch.t
index ae9d1f614..7ee1a6026 100644
--- a/nginx/t/js_fetch.t
+++ b/nginx/t/js_fetch.t
@@ -342,6 +342,8 @@ $t->write_file('test.js', <<EOF);
             }
         }
 
+        out.sort();
+
         r.return(200, JSON.stringify(out));
     }
 
@@ -411,8 +413,6 @@ EOF
 
 $t->try_run('no njs.fetch');
 
-plan(skip_all => 'not yet') if http_get('/engine') =~ /QuickJS$/m;
-
 $t->plan(37);
 
 $t->run_daemon(\&http_daemon, port(8082));
diff --git a/nginx/t/js_fetch_https.t b/nginx/t/js_fetch_https.t
index 9a44a3390..8ede10489 100644
--- a/nginx/t/js_fetch_https.t
+++ b/nginx/t/js_fetch_https.t
@@ -196,8 +196,6 @@ foreach my $name ('default.example.com', '1.example.com') {
 
 $t->try_run('no njs.fetch');
 
-plan(skip_all => 'not yet') if http_get('/engine') =~ /QuickJS$/m;
-
 $t->plan(7);
 
 $t->run_daemon(\&dns_daemon, port(8981), $t);
diff --git a/nginx/t/js_fetch_objects.t b/nginx/t/js_fetch_objects.t
index 67cabdfcb..82eb93fe8 100644
--- a/nginx/t/js_fetch_objects.t
+++ b/nginx/t/js_fetch_objects.t
@@ -271,7 +271,9 @@ $t->write_file('test.js', <<EOF);
                     })
 
                 } catch (e) {
-                    if (!e.message.startsWith('Cannot assign to read-only p')) {
+                    if (!e.message.startsWith('Cannot assign to read-only p')
+                        && !e.message.startsWith('no setter for property'))
+                    {
                         throw e;
                     }
                 }
@@ -515,8 +517,6 @@ EOF
 
 $t->try_run('no njs');
 
-plan(skip_all => 'not yet') if http_get('/engine') =~ /QuickJS$/m;
-
 $t->plan(5);
 
 ###############################################################################
diff --git a/nginx/t/js_fetch_resolver.t b/nginx/t/js_fetch_resolver.t
index 7cea33867..031ff43c9 100644
--- a/nginx/t/js_fetch_resolver.t
+++ b/nginx/t/js_fetch_resolver.t
@@ -146,8 +146,6 @@ EOF
 
 $t->try_run('no njs.fetch');
 
-plan(skip_all => 'not yet') if http_get('/engine') =~ /QuickJS$/m;
-
 $t->plan(5);
 
 $t->run_daemon(\&dns_daemon, port(8981), $t);
diff --git a/nginx/t/js_fetch_timeout.t b/nginx/t/js_fetch_timeout.t
index 5b207b90b..ab1ba24ad 100644
--- a/nginx/t/js_fetch_timeout.t
+++ b/nginx/t/js_fetch_timeout.t
@@ -116,8 +116,6 @@ EOF
 
 $t->try_run('no js_fetch_timeout');
 
-plan(skip_all => 'not yet') if http_get('/engine') =~ /QuickJS$/m;
-
 $t->plan(2);
 
 ###############################################################################
diff --git a/nginx/t/js_fetch_verify.t b/nginx/t/js_fetch_verify.t
index 4c97e04d7..f98b4d8c0 100644
--- a/nginx/t/js_fetch_verify.t
+++ b/nginx/t/js_fetch_verify.t
@@ -114,8 +114,6 @@ foreach my $name ('localhost') {
 
 $t->try_run('no js_fetch_verify');
 
-plan(skip_all => 'not yet') if http_get('/engine') =~ /QuickJS$/m;
-
 $t->plan(2);
 
 $t->run_daemon(\&dns_daemon, port(8981), $t);