Modules: added ngx.fetch().

This is an initial implementation of Fetch API.

The following init options are supported:
body, headers, buffer_size (nginx specific), max_response_body_size
(nginx specific), method.

The following properties and methods of Response object are implemented:
arrayBuffer(), bodyUsed, json(), headers, ok, redirect, status, statusText,
text(), type, url.

The following properties and methods of Header object are implemented:
get(), getAll(), has().

Notable limitations: only http:// scheme is supported, redirects
are not handled.

In collaboration with 洪志道 (Hong Zhi Dao).
diff --git a/nginx/config b/nginx/config
index b927c9d..5b9012b 100644
--- a/nginx/config
+++ b/nginx/config
@@ -1,7 +1,8 @@
 ngx_addon_name="ngx_js_module"
 
 NJS_DEPS="$ngx_addon_dir/ngx_js.h"
-NJS_SRCS="$ngx_addon_dir/ngx_js.c"
+NJS_SRCS="$ngx_addon_dir/ngx_js.c \
+    $ngx_addon_dir/ngx_js_fetch.c"
 
 if [ $HTTP != NO ]; then
     ngx_module_type=HTTP
diff --git a/nginx/ngx_http_js_module.c b/nginx/ngx_http_js_module.c
index 5bf28d3..5600178 100644
--- a/nginx/ngx_http_js_module.c
+++ b/nginx/ngx_http_js_module.c
@@ -179,6 +179,13 @@
 static void ngx_http_js_clear_timer(njs_external_ptr_t external,
     njs_host_event_t event);
 static void ngx_http_js_timer_handler(ngx_event_t *ev);
+static ngx_pool_t *ngx_http_js_pool(njs_vm_t *vm, ngx_http_request_t *r);
+static ngx_resolver_t *ngx_http_js_resolver(njs_vm_t *vm,
+    ngx_http_request_t *r);
+static ngx_msec_t ngx_http_js_resolver_timeout(njs_vm_t *vm,
+    ngx_http_request_t *r);
+static void ngx_http_js_handle_vm_event(ngx_http_request_t *r,
+    njs_vm_event_t vm_event, njs_value_t *args, njs_uint_t nargs);
 static void ngx_http_js_handle_event(ngx_http_request_t *r,
     njs_vm_event_t vm_event, njs_value_t *args, njs_uint_t nargs);
 
@@ -576,11 +583,15 @@
 
 static uintptr_t ngx_http_js_uptr[] = {
     offsetof(ngx_http_request_t, connection),
+    (uintptr_t) ngx_http_js_pool,
+    (uintptr_t) ngx_http_js_resolver,
+    (uintptr_t) ngx_http_js_resolver_timeout,
+    (uintptr_t) ngx_http_js_handle_event,
 };
 
 
 static njs_vm_meta_t ngx_http_js_metas = {
-    .size = 1,
+    .size = 5,
     .values = ngx_http_js_uptr
 };
 
@@ -2754,7 +2765,7 @@
         return NGX_ERROR;
     }
 
-    ngx_http_js_handle_event(r->parent, vm_event, njs_value_arg(&reply), 1);
+    ngx_http_js_handle_vm_event(r->parent, vm_event, njs_value_arg(&reply), 1);
 
     return NGX_OK;
 }
@@ -2895,7 +2906,6 @@
 static void
 ngx_http_js_timer_handler(ngx_event_t *ev)
 {
-    ngx_connection_t     *c;
     ngx_http_request_t   *r;
     ngx_http_js_event_t  *js_event;
 
@@ -2903,16 +2913,41 @@
 
     r = js_event->request;
 
-    c = r->connection;
-
     ngx_http_js_handle_event(r, js_event->vm_event, NULL, 0);
+}
 
-    ngx_http_run_posted_requests(c);
+
+static ngx_pool_t *
+ngx_http_js_pool(njs_vm_t *vm, ngx_http_request_t *r)
+{
+    return r->pool;
+}
+
+
+static ngx_resolver_t *
+ngx_http_js_resolver(njs_vm_t *vm, ngx_http_request_t *r)
+{
+    ngx_http_core_loc_conf_t  *clcf;
+
+    clcf = ngx_http_get_module_loc_conf(r, ngx_http_core_module);
+
+    return clcf->resolver;
+}
+
+
+static ngx_msec_t
+ngx_http_js_resolver_timeout(njs_vm_t *vm, ngx_http_request_t *r)
+{
+    ngx_http_core_loc_conf_t  *clcf;
+
+    clcf = ngx_http_get_module_loc_conf(r, ngx_http_core_module);
+
+    return clcf->resolver_timeout;
 }
 
 
 static void
-ngx_http_js_handle_event(ngx_http_request_t *r, njs_vm_event_t vm_event,
+ngx_http_js_handle_vm_event(ngx_http_request_t *r, njs_vm_event_t vm_event,
     njs_value_t *args, njs_uint_t nargs)
 {
     njs_int_t           rc;
@@ -2925,6 +2960,10 @@
 
     rc = njs_vm_run(ctx->vm);
 
+    ngx_log_debug2(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
+                   "http js post event handler rc: %i event: %p",
+                   (ngx_int_t) rc, vm_event);
+
     if (rc == NJS_ERROR) {
         njs_vm_retval_string(ctx->vm, &exception);
 
@@ -2940,6 +2979,16 @@
 }
 
 
+static void
+ngx_http_js_handle_event(ngx_http_request_t *r, njs_vm_event_t vm_event,
+    njs_value_t *args, njs_uint_t nargs)
+{
+    ngx_http_js_handle_vm_event(r, vm_event, args, nargs);
+
+    ngx_http_run_posted_requests(r->connection);
+}
+
+
 static char *
 ngx_http_js_init_main_conf(ngx_conf_t *cf, void *conf)
 {
diff --git a/nginx/ngx_js.c b/nginx/ngx_js.c
index f47b916..e2c083a 100644
--- a/nginx/ngx_js.c
+++ b/nginx/ngx_js.c
@@ -9,6 +9,7 @@
 #include <ngx_config.h>
 #include <ngx_core.h>
 #include "ngx_js.h"
+#include "ngx_js_fetch.h"
 
 
 static njs_external_t  ngx_js_ext_core[] = {
@@ -50,6 +51,17 @@
             .magic32 = NGX_LOG_ERR,
         }
     },
+
+    {
+        .flags = NJS_EXTERN_METHOD,
+        .name.string = njs_str("fetch"),
+        .writable = 1,
+        .configurable = 1,
+        .enumerable = 1,
+        .u.method = {
+            .native = ngx_js_ext_fetch,
+        }
+    },
 };
 
 
@@ -117,10 +129,16 @@
 ngx_int_t
 ngx_js_core_init(njs_vm_t *vm, ngx_log_t *log)
 {
+    ngx_int_t           rc;
     njs_int_t           ret, proto_id;
     njs_str_t           name;
     njs_opaque_value_t  value;
 
+    rc = ngx_js_fetch_init(vm, log);
+    if (rc != NGX_OK) {
+        return NGX_ERROR;
+    }
+
     proto_id = njs_vm_external_prototype(vm, ngx_js_ext_core,
                                          njs_nitems(ngx_js_ext_core));
     if (proto_id < 0) {
@@ -178,6 +196,16 @@
 
 
 njs_int_t
+ngx_js_ext_boolean(njs_vm_t *vm, njs_object_prop_t *prop,
+    njs_value_t *value, njs_value_t *setval, njs_value_t *retval)
+{
+    njs_value_boolean_set(retval, njs_vm_prop_magic32(prop));
+
+    return NJS_OK;
+}
+
+
+njs_int_t
 ngx_js_ext_log(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
     njs_index_t level)
 {
diff --git a/nginx/ngx_js.h b/nginx/ngx_js.h
index fe31124..b2165e2 100644
--- a/nginx/ngx_js.h
+++ b/nginx/ngx_js.h
@@ -20,10 +20,28 @@
 #define NGX_JS_BUFFER  2
 
 #define NGX_JS_PROTO_MAIN      0
+#define NGX_JS_PROTO_RESPONSE  1
 
 
-#define ngx_external_connection(vm, ext)                                    \
-    (*((ngx_connection_t **) ((u_char *) ext + njs_vm_meta(vm, 0))))
+typedef ngx_pool_t *(*ngx_external_pool_pt)(njs_vm_t *vm, njs_external_ptr_t e);
+typedef void (*ngx_js_event_handler_pt)(njs_external_ptr_t e,
+	njs_vm_event_t vm_event, njs_value_t *args, njs_uint_t nargs);
+typedef ngx_resolver_t *(*ngx_external_resolver_pt)(njs_vm_t *vm,
+    njs_external_ptr_t e);
+typedef ngx_msec_t (*ngx_external_resolver_timeout_pt)(njs_vm_t *vm,
+    njs_external_ptr_t e);
+
+
+#define ngx_external_connection(vm, e)                                        \
+    (*((ngx_connection_t **) ((u_char *) (e) + njs_vm_meta(vm, 0))))
+#define ngx_external_pool(vm, e)                                              \
+	((ngx_external_pool_pt) njs_vm_meta(vm, 1))(vm, e)
+#define ngx_external_resolver(vm, e)                                          \
+	((ngx_external_resolver_pt) njs_vm_meta(vm, 2))(vm, e)
+#define ngx_external_resolver_timeout(vm, e)                                  \
+	((ngx_external_resolver_timeout_pt) njs_vm_meta(vm, 3))(vm, e)
+#define ngx_external_event_handler(vm, e)                                     \
+    ((ngx_js_event_handler_pt) njs_vm_meta(vm, 4))
 
 
 #define ngx_js_prop(vm, type, value, start, len)                              \
@@ -41,6 +59,8 @@
     njs_value_t *value, njs_value_t *setval, njs_value_t *retval);
 njs_int_t ngx_js_ext_constant(njs_vm_t *vm, njs_object_prop_t *prop,
     njs_value_t *value, njs_value_t *setval, njs_value_t *retval);
+njs_int_t ngx_js_ext_boolean(njs_vm_t *vm, njs_object_prop_t *prop,
+    njs_value_t *value, njs_value_t *setval, njs_value_t *retval);
 
 ngx_int_t ngx_js_core_init(njs_vm_t *vm, ngx_log_t *log);
 
diff --git a/nginx/ngx_js_fetch.c b/nginx/ngx_js_fetch.c
new file mode 100644
index 0000000..bcaf856
--- /dev/null
+++ b/nginx/ngx_js_fetch.c
@@ -0,0 +1,2212 @@
+
+/*
+ * Copyright (C) Dmitry Volyntsev
+ * Copyright (C) hongzhidao
+ * 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_js_http_s  ngx_js_http_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_js_http_parse_t;
+
+
+typedef struct {
+    u_char                        *pos;
+    uint64_t                       chunk_size;
+    uint8_t                        state;
+    uint8_t                        last;
+} ngx_js_http_chunk_parse_t;
+
+
+struct ngx_js_http_s {
+    ngx_log_t                     *log;
+    ngx_pool_t                    *pool;
+
+    njs_vm_t                      *vm;
+    njs_external_ptr_t             external;
+    njs_vm_event_t                 vm_event;
+    ngx_js_event_handler_pt        event_handler;
+
+    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;
+
+    njs_str_t                      url;
+    ngx_array_t                    headers;
+
+    ngx_buf_t                     *buffer;
+    ngx_buf_t                     *chunk;
+    njs_chb_t                      chain;
+
+    njs_opaque_value_t             reply;
+    njs_opaque_value_t             promise;
+    njs_opaque_value_t             promise_callbacks[2];
+
+    uint8_t                        done;
+    uint8_t                        body_used;
+    ngx_js_http_parse_t            http_parse;
+    ngx_js_http_chunk_parse_t      http_chunk_parse;
+    ngx_int_t                    (*process)(ngx_js_http_t *http);
+};
+
+
+#define ngx_js_http_error(http, err, fmt, ...)                                \
+    do {                                                                      \
+        njs_vm_value_error_set((http)->vm, njs_value_arg(&(http)->reply),     \
+                               fmt, ##__VA_ARGS__);                           \
+        ngx_js_http_fetch_done(http, &(http)->reply, NJS_ERROR);              \
+    } while (0)
+
+
+static ngx_js_http_t *ngx_js_http_alloc(njs_vm_t *vm, ngx_pool_t *pool,
+    ngx_log_t *log);
+static void ngx_js_resolve_handler(ngx_resolver_ctx_t *ctx);
+static njs_int_t ngx_js_fetch_result(njs_vm_t *vm, ngx_js_http_t *http,
+    njs_value_t *result, njs_int_t rc);
+static njs_int_t ngx_js_fetch_promissified_result(njs_vm_t *vm,
+    njs_value_t *result, njs_int_t rc);
+static void ngx_js_http_fetch_done(ngx_js_http_t *http,
+    njs_opaque_value_t *retval, njs_int_t rc);
+static njs_int_t ngx_js_http_promise_trampoline(njs_vm_t *vm,
+    njs_value_t *args, njs_uint_t nargs, njs_index_t unused);
+static njs_int_t ngx_js_http_connect(ngx_js_http_t *http);
+static njs_int_t ngx_js_http_next(ngx_js_http_t *http);
+static void ngx_js_http_write_handler(ngx_event_t *wev);
+static void ngx_js_http_read_handler(ngx_event_t *rev);
+static ngx_int_t ngx_js_http_process_status_line(ngx_js_http_t *http);
+static ngx_int_t ngx_js_http_process_headers(ngx_js_http_t *http);
+static ngx_int_t ngx_js_http_process_body(ngx_js_http_t *http);
+static ngx_int_t ngx_js_http_parse_status_line(ngx_js_http_parse_t *hp,
+    ngx_buf_t *b);
+static ngx_int_t ngx_js_http_parse_header_line(ngx_js_http_parse_t *hp,
+    ngx_buf_t *b);
+static ngx_int_t ngx_js_http_parse_chunked(ngx_js_http_chunk_parse_t *hcp,
+    ngx_buf_t *b, njs_chb_t *chain);
+static void ngx_js_http_dummy_handler(ngx_event_t *ev);
+
+static njs_int_t ngx_response_js_ext_headers_get(njs_vm_t *vm,
+    njs_value_t *args,  njs_uint_t nargs, njs_index_t as_array);
+static njs_int_t ngx_response_js_ext_headers_has(njs_vm_t *vm,
+    njs_value_t *args,  njs_uint_t nargs, njs_index_t unused);
+static njs_int_t ngx_response_js_ext_header(njs_vm_t *vm,
+    njs_object_prop_t *prop, njs_value_t *value, njs_value_t *setval,
+    njs_value_t *retval);
+static njs_int_t ngx_response_js_ext_keys(njs_vm_t *vm, njs_value_t *value,
+    njs_value_t *keys);
+static njs_int_t ngx_response_js_ext_status(njs_vm_t *vm,
+    njs_object_prop_t *prop, njs_value_t *value, njs_value_t *setval,
+    njs_value_t *retval);
+static njs_int_t ngx_response_js_ext_status_text(njs_vm_t *vm,
+    njs_object_prop_t *prop, njs_value_t *value, njs_value_t *setval,
+    njs_value_t *retval);
+static njs_int_t ngx_response_js_ext_ok(njs_vm_t *vm,
+    njs_object_prop_t *prop, njs_value_t *value, njs_value_t *setval,
+    njs_value_t *retval);
+static njs_int_t ngx_response_js_ext_body_used(njs_vm_t *vm,
+    njs_object_prop_t *prop, njs_value_t *value, njs_value_t *setval,
+    njs_value_t *retval);
+static njs_int_t ngx_response_js_ext_type(njs_vm_t *vm,
+    njs_object_prop_t *prop, njs_value_t *value, njs_value_t *setval,
+    njs_value_t *retval);
+static njs_int_t ngx_response_js_ext_body(njs_vm_t *vm, njs_value_t *args,
+     njs_uint_t nargs, njs_index_t unused);
+
+
+static njs_external_t  ngx_js_ext_http_response_headers[] = {
+
+    {
+        .flags = NJS_EXTERN_PROPERTY | NJS_EXTERN_SYMBOL,
+        .name.symbol = NJS_SYMBOL_TO_STRING_TAG,
+        .u.property = {
+            .value = "Headers",
+        }
+    },
+
+    {
+        .flags = NJS_EXTERN_METHOD,
+        .name.string = njs_str("get"),
+        .writable = 1,
+        .configurable = 1,
+        .enumerable = 1,
+        .u.method = {
+            .native = ngx_response_js_ext_headers_get,
+        }
+    },
+
+    {
+        .flags = NJS_EXTERN_METHOD,
+        .name.string = njs_str("getAll"),
+        .writable = 1,
+        .configurable = 1,
+        .enumerable = 1,
+        .u.method = {
+            .native = ngx_response_js_ext_headers_get,
+            .magic8 = 1
+        }
+    },
+
+    {
+        .flags = NJS_EXTERN_METHOD,
+        .name.string = njs_str("has"),
+        .writable = 1,
+        .configurable = 1,
+        .enumerable = 1,
+        .u.method = {
+            .native = ngx_response_js_ext_headers_has,
+        }
+    },
+
+};
+
+
+static njs_external_t  ngx_js_ext_http_response[] = {
+
+    {
+        .flags = NJS_EXTERN_PROPERTY | NJS_EXTERN_SYMBOL,
+        .name.symbol = NJS_SYMBOL_TO_STRING_TAG,
+        .u.property = {
+            .value = "Response",
+        }
+    },
+
+    {
+        .flags = NJS_EXTERN_METHOD,
+        .name.string = njs_str("arrayBuffer"),
+        .writable = 1,
+        .configurable = 1,
+        .enumerable = 1,
+        .u.method = {
+            .native = ngx_response_js_ext_body,
+#define NGX_JS_BODY_ARRAY_BUFFER   0
+#define NGX_JS_BODY_JSON           1
+#define NGX_JS_BODY_TEXT           2
+            .magic8 = NGX_JS_BODY_ARRAY_BUFFER
+        }
+    },
+
+    {
+        .flags = NJS_EXTERN_PROPERTY,
+        .name.string = njs_str("bodyUsed"),
+        .enumerable = 1,
+        .u.property = {
+            .handler = ngx_response_js_ext_body_used,
+        }
+    },
+
+    {
+        .flags = NJS_EXTERN_OBJECT,
+        .name.string = njs_str("headers"),
+        .enumerable = 1,
+        .u.object = {
+            .enumerable = 1,
+            .properties = ngx_js_ext_http_response_headers,
+            .nproperties = njs_nitems(ngx_js_ext_http_response_headers),
+            .prop_handler = ngx_response_js_ext_header,
+            .keys = ngx_response_js_ext_keys,
+        }
+    },
+
+    {
+        .flags = NJS_EXTERN_METHOD,
+        .name.string = njs_str("json"),
+        .writable = 1,
+        .configurable = 1,
+        .enumerable = 1,
+        .u.method = {
+            .native = ngx_response_js_ext_body,
+            .magic8 = NGX_JS_BODY_JSON
+        }
+    },
+
+    {
+        .flags = NJS_EXTERN_PROPERTY,
+        .name.string = njs_str("ok"),
+        .enumerable = 1,
+        .u.property = {
+            .handler = ngx_response_js_ext_ok,
+        }
+    },
+
+    {
+        .flags = NJS_EXTERN_PROPERTY,
+        .name.string = njs_str("redirected"),
+        .enumerable = 1,
+        .u.property = {
+            .handler = ngx_js_ext_boolean,
+            .magic32 = 0,
+        }
+    },
+
+    {
+        .flags = NJS_EXTERN_PROPERTY,
+        .name.string = njs_str("status"),
+        .enumerable = 1,
+        .u.property = {
+            .handler = ngx_response_js_ext_status,
+        }
+    },
+
+    {
+        .flags = NJS_EXTERN_PROPERTY,
+        .name.string = njs_str("statusText"),
+        .enumerable = 1,
+        .u.property = {
+            .handler = ngx_response_js_ext_status_text,
+        }
+    },
+
+    {
+        .flags = NJS_EXTERN_METHOD,
+        .name.string = njs_str("text"),
+        .writable = 1,
+        .configurable = 1,
+        .enumerable = 1,
+        .u.method = {
+            .native = ngx_response_js_ext_body,
+            .magic8 = NGX_JS_BODY_TEXT
+        }
+    },
+
+    {
+        .flags = NJS_EXTERN_PROPERTY,
+        .name.string = njs_str("type"),
+        .enumerable = 1,
+        .u.property = {
+            .handler = ngx_response_js_ext_type,
+        }
+    },
+
+    {
+        .flags = NJS_EXTERN_PROPERTY,
+        .name.string = njs_str("url"),
+        .enumerable = 1,
+        .u.property = {
+            .handler = ngx_js_ext_string,
+            .magic32 = offsetof(ngx_js_http_t, url),
+        }
+    },
+};
+
+
+njs_int_t
+ngx_js_ext_fetch(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
+    njs_index_t unused)
+{
+    int64_t              i, length;
+    njs_int_t            ret;
+    njs_str_t            method, body, name, header;
+    ngx_url_t            u;
+    njs_bool_t           has_host;
+    ngx_pool_t          *pool;
+    njs_value_t         *init, *value, *headers, *keys;
+    ngx_js_http_t       *http;
+    ngx_connection_t    *c;
+    ngx_resolver_ctx_t  *ctx;
+    njs_external_ptr_t   external;
+    njs_opaque_value_t  *start, lvalue, headers_value;
+
+    static const njs_str_t body_key = njs_str("body");
+    static const njs_str_t headers_key = njs_str("headers");
+    static const njs_str_t buffer_size_key = njs_str("buffer_size");
+    static const njs_str_t body_size_key = njs_str("max_response_body_size");
+    static const njs_str_t method_key = njs_str("method");
+
+    external = njs_vm_external(vm, njs_argument(args, 0));
+    if (external == NULL) {
+        njs_vm_error(vm, "\"this\" is not an external");
+        return NJS_ERROR;
+    }
+
+    c = ngx_external_connection(vm, external);
+    pool = ngx_external_pool(vm, external);
+
+    http = ngx_js_http_alloc(vm, pool, c->log);
+    if (http == NULL) {
+        return NJS_ERROR;
+    }
+
+    http->external = external;
+    http->event_handler = ngx_external_event_handler(vm, external);
+    http->buffer_size = 4096;
+    http->max_response_body_size = 32 * 1024;
+
+    ret = ngx_js_string(vm, njs_arg(args, nargs, 1), &http->url);
+    if (ret != NJS_OK) {
+        njs_vm_error(vm, "failed to convert url arg");
+        goto fail;
+    }
+
+    ngx_memzero(&u, sizeof(ngx_url_t));
+
+    u.url.len = http->url.length;
+    u.url.data = http->url.start;
+    u.default_port = 80;
+    u.uri_part = 1;
+    u.no_resolve = 1;
+
+    if (u.url.len > 7
+        && ngx_strncasecmp(u.url.data, (u_char *) "http://", 7) == 0)
+    {
+        u.url.len -= 7;
+        u.url.data += 7;
+
+    } else {
+        njs_vm_error(vm, "unsupported URL prefix");
+        goto fail;
+    }
+
+    if (ngx_parse_url(pool, &u) != NGX_OK) {
+        njs_vm_error(vm, "invalid url");
+        goto fail;
+    }
+
+    init = njs_arg(args, nargs, 2);
+
+    method = njs_str_value("GET");
+    body = njs_str_value("");
+    headers = NULL;
+
+    if (njs_value_is_object(init)) {
+        value = njs_vm_object_prop(vm, init, &method_key, &lvalue);
+        if (value != NULL && ngx_js_string(vm, value, &method) != NGX_OK) {
+            goto fail;
+        }
+
+        headers = njs_vm_object_prop(vm, init, &headers_key, &headers_value);
+        if (headers != NULL && !njs_value_is_object(headers)) {
+            njs_vm_error(vm, "headers is not an object");
+            goto fail;
+        }
+
+        value = njs_vm_object_prop(vm, init, &body_key, &lvalue);
+        if (value != NULL && ngx_js_string(vm, value, &body) != NGX_OK) {
+            goto fail;
+        }
+
+        value = njs_vm_object_prop(vm, init, &buffer_size_key, &lvalue);
+        if (value != NULL
+            && ngx_js_integer(vm, value, &http->buffer_size)
+               != NGX_OK)
+        {
+            goto fail;
+        }
+
+        value = njs_vm_object_prop(vm, init, &body_size_key, &lvalue);
+        if (value != NULL
+            && ngx_js_integer(vm, value, &http->max_response_body_size)
+               != NGX_OK)
+        {
+            goto fail;
+        }
+    }
+
+    njs_chb_init(&http->chain, njs_vm_memory_pool(vm));
+
+    njs_chb_append(&http->chain, method.start, method.length);
+    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);
+    njs_chb_append_literal(&http->chain, "Connection: close" CRLF);
+
+    has_host = 0;
+
+    if (headers != NULL) {
+        keys = njs_vm_object_keys(vm, headers, njs_value_arg(&lvalue));
+        if (keys == NULL) {
+            goto fail;
+        }
+
+        start = (njs_opaque_value_t *) njs_vm_array_start(vm, keys);
+        if (start == NULL) {
+            goto fail;
+        }
+
+        (void) njs_vm_array_length(vm, keys, &length);
+
+        for (i = 0; i < length; i++) {
+            if (ngx_js_string(vm, njs_value_arg(start), &name) != NGX_OK) {
+                goto fail;
+            }
+
+            start++;
+
+            value = njs_vm_object_prop(vm, headers, &name, &lvalue);
+            if (ret != NJS_OK) {
+                goto fail;
+            }
+
+            if (njs_value_is_null_or_undefined(value)) {
+                continue;
+            }
+
+            if (ngx_js_string(vm, value, &header) != NGX_OK) {
+                goto fail;
+            }
+
+            if (name.length == 4
+                && ngx_strncasecmp(name.start, (u_char *) "Host", 4) == 0)
+            {
+                has_host = 1;
+            }
+
+            njs_chb_append(&http->chain, name.start, name.length);
+            njs_chb_append_literal(&http->chain, ": ");
+            njs_chb_append(&http->chain, header.start, header.length);
+            njs_chb_append_literal(&http->chain, CRLF);
+        }
+    }
+
+    if (!has_host) {
+        njs_chb_append_literal(&http->chain, "Host: ");
+        njs_chb_append(&http->chain, u.host.data, u.host.len);
+        njs_chb_append_literal(&http->chain, CRLF);
+    }
+
+    if (body.length != 0) {
+        njs_chb_sprintf(&http->chain, 32, "Content-Length: %uz" CRLF CRLF,
+                        body.length);
+        njs_chb_append(&http->chain, body.start, body.length);
+
+    } else {
+        njs_chb_append_literal(&http->chain, CRLF);
+    }
+
+    if (u.addrs == NULL) {
+        ctx = ngx_resolve_start(ngx_external_resolver(vm, external), NULL);
+        if (ctx == NULL) {
+            njs_vm_memory_error(vm);
+            return NJS_ERROR;
+        }
+
+        if (ctx == NGX_NO_RESOLVER) {
+            njs_vm_error(vm, "no resolver defined");
+            goto fail;
+        }
+
+        http->ctx = ctx;
+        http->port = u.port;
+
+        ctx->name = u.host;
+        ctx->handler = ngx_js_resolve_handler;
+        ctx->data = http;
+        ctx->timeout = ngx_external_resolver_timeout(vm, external);
+
+        ret = ngx_resolve_name(http->ctx);
+        if (ret != NGX_OK) {
+            http->ctx = NULL;
+            njs_vm_memory_error(vm);
+            return NJS_ERROR;
+        }
+
+    } else {
+        http->naddrs = 1;
+        ngx_memcpy(&http->addr, &u.addrs[0], sizeof(ngx_addr_t));
+        http->addrs = &http->addr;
+
+        ret = ngx_js_http_connect(http);
+    }
+
+    return ngx_js_fetch_result(vm, http, njs_value_arg(&http->reply), ret);
+
+fail:
+
+    return ngx_js_fetch_result(vm, http, njs_vm_retval(vm), NJS_ERROR);
+}
+
+
+static ngx_js_http_t *
+ngx_js_http_alloc(njs_vm_t *vm, ngx_pool_t *pool, ngx_log_t *log)
+{
+    ngx_js_http_t  *http;
+
+    http = ngx_pcalloc(pool, sizeof(ngx_js_http_t));
+    if (http == NULL) {
+        goto failed;
+    }
+
+    http->pool = pool;
+    http->log = log;
+    http->vm = vm;
+
+    http->timeout = 10000;
+
+    ngx_log_debug1(NGX_LOG_DEBUG_EVENT, log, 0, "js http alloc:%p", http);
+
+    return http;
+
+failed:
+
+    njs_vm_error(vm, "internal error");
+
+    return NULL;
+}
+
+
+static void
+ngx_js_resolve_handler(ngx_resolver_ctx_t *ctx)
+{
+    u_char           *p;
+    size_t            len;
+    socklen_t         socklen;
+    ngx_uint_t        i;
+    ngx_js_http_t    *http;
+    struct sockaddr  *sockaddr;
+
+    http = ctx->data;
+
+    if (ctx->state) {
+        ngx_js_http_error(http, 0, "\"%V\" could not be resolved (%i: %s)",
+                          &ctx->name, 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;
+
+    (void) ngx_js_http_connect(http);
+
+    return;
+
+failed:
+
+    ngx_js_http_error(http, 0, "memory error");
+}
+
+
+static void
+njs_js_http_destructor(njs_external_ptr_t external, njs_host_event_t host)
+{
+    ngx_js_http_t  *http;
+
+    http = host;
+
+    ngx_log_debug1(NGX_LOG_DEBUG_EVENT, http->log, 0, "js http destructor:%p",
+                   http);
+
+    if (http->ctx != NULL) {
+        ngx_resolve_name_done(http->ctx);
+        http->ctx = NULL;
+    }
+
+    if (http->peer.connection != NULL) {
+        ngx_close_connection(http->peer.connection);
+        http->peer.connection = NULL;
+    }
+}
+
+
+static njs_int_t
+ngx_js_fetch_result(njs_vm_t *vm, ngx_js_http_t *http, njs_value_t *result,
+    njs_int_t rc)
+{
+    njs_int_t            ret;
+    njs_function_t      *callback;
+    njs_vm_event_t       vm_event;
+    njs_opaque_value_t   arguments[2];
+
+    ret = njs_vm_promise_create(vm, njs_value_arg(&http->promise),
+                                njs_value_arg(&http->promise_callbacks));
+    if (ret != NJS_OK) {
+        goto error;
+    }
+
+    callback = njs_vm_function_alloc(vm, ngx_js_http_promise_trampoline);
+    if (callback == NULL) {
+        goto error;
+    }
+
+    vm_event = njs_vm_add_event(vm, callback, 1, http, njs_js_http_destructor);
+    if (vm_event == NULL) {
+        goto error;
+    }
+
+    http->vm_event = vm_event;
+
+    if (rc == NJS_ERROR) {
+        njs_value_assign(&arguments[0], &http->promise_callbacks[1]);
+        njs_value_assign(&arguments[1], result);
+
+        ret = njs_vm_post_event(vm, vm_event, njs_value_arg(&arguments), 2);
+        if (ret == NJS_ERROR) {
+            goto error;
+        }
+    }
+
+    njs_vm_retval_set(vm, njs_value_arg(&http->promise));
+
+    return NJS_OK;
+
+error:
+
+    njs_vm_error(vm, "internal error");
+
+    return NJS_ERROR;
+}
+
+
+static njs_int_t
+ngx_js_fetch_promissified_result(njs_vm_t *vm, njs_value_t *result,
+    njs_int_t rc)
+{
+    njs_int_t            ret;
+    njs_function_t      *callback;
+    njs_vm_event_t       vm_event;
+    njs_opaque_value_t   retval, arguments[2];
+
+    ret = njs_vm_promise_create(vm, njs_value_arg(&retval),
+                                njs_value_arg(&arguments));
+    if (ret != NJS_OK) {
+        goto error;
+    }
+
+    callback = njs_vm_function_alloc(vm, ngx_js_http_promise_trampoline);
+    if (callback == NULL) {
+        goto error;
+    }
+
+    vm_event = njs_vm_add_event(vm, callback, 1, NULL, NULL);
+    if (vm_event == NULL) {
+        goto error;
+    }
+
+    njs_value_assign(&arguments[0], &arguments[(rc != NJS_OK)]);
+    njs_value_assign(&arguments[1], result);
+
+    ret = njs_vm_post_event(vm, vm_event, njs_value_arg(&arguments), 2);
+    if (ret == NJS_ERROR) {
+        goto error;
+    }
+
+    njs_vm_retval_set(vm, njs_value_arg(&retval));
+
+    return NJS_OK;
+
+error:
+
+    njs_vm_error(vm, "internal error");
+
+    return NJS_ERROR;
+}
+
+
+static void
+ngx_js_http_fetch_done(ngx_js_http_t *http, njs_opaque_value_t *retval,
+    njs_int_t rc)
+{
+    njs_opaque_value_t  arguments[2], *action;
+
+    ngx_log_debug2(NGX_LOG_DEBUG_EVENT, http->log, 0,
+                   "js fetch done http:%p rc:%i", http, (ngx_int_t) rc);
+
+    if (http->peer.connection != NULL) {
+        ngx_close_connection(http->peer.connection);
+        http->peer.connection = NULL;
+    }
+
+    if (http->vm_event != NULL) {
+        action = &http->promise_callbacks[(rc != NJS_OK)];
+        njs_value_assign(&arguments[0], action);
+        njs_value_assign(&arguments[1], retval);
+        http->event_handler(http->external, http->vm_event,
+                            njs_value_arg(&arguments), 2);
+    }
+}
+
+
+static njs_int_t
+ngx_js_http_promise_trampoline(njs_vm_t *vm, njs_value_t *args,
+    njs_uint_t nargs, njs_index_t unused)
+{
+    njs_function_t  *callback;
+
+    callback = njs_value_function(njs_argument(args, 1));
+
+    if (callback != NULL) {
+        return njs_vm_call(vm, callback, njs_argument(args, 2), 1);
+    }
+
+    return NJS_OK;
+}
+
+
+static njs_int_t
+ngx_js_http_connect(ngx_js_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,
+                   "js http 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_js_http_error(http, 0, "connect failed");
+        return NJS_ERROR;
+    }
+
+    if (rc == NGX_BUSY || rc == NGX_DECLINED) {
+        return ngx_js_http_next(http);
+    }
+
+    http->peer.connection->data = http;
+    http->peer.connection->pool = http->pool;
+
+    http->peer.connection->write->handler = ngx_js_http_write_handler;
+    http->peer.connection->read->handler = ngx_js_http_read_handler;
+
+    http->process = ngx_js_http_process_status_line;
+
+    if (http->timeout) {
+        ngx_add_timer(http->peer.connection->read, http->timeout);
+        ngx_add_timer(http->peer.connection->write, http->timeout);
+    }
+
+    if (rc == NGX_OK) {
+        ngx_js_http_write_handler(http->peer.connection->write);
+    }
+
+    return NJS_OK;
+}
+
+
+static njs_int_t
+ngx_js_http_next(ngx_js_http_t *http)
+{
+    ngx_log_debug0(NGX_LOG_DEBUG_EVENT, http->log, 0, "js http next");
+
+    if (++http->naddr >= http->naddrs) {
+        ngx_js_http_error(http, 0, "connect failed");
+        return NJS_ERROR;
+    }
+
+    if (http->peer.connection != NULL) {
+        ngx_close_connection(http->peer.connection);
+        http->peer.connection = NULL;
+    }
+
+    http->buffer = NULL;
+
+    return ngx_js_http_connect(http);
+}
+
+
+static void
+ngx_js_http_write_handler(ngx_event_t *wev)
+{
+    ssize_t            n, size;
+    ngx_buf_t         *b;
+    ngx_js_http_t     *http;
+    ngx_connection_t  *c;
+
+    c = wev->data;
+    http = c->data;
+
+    ngx_log_debug0(NGX_LOG_DEBUG_EVENT, wev->log, 0, "js http write handler");
+
+    if (wev->timedout) {
+        ngx_js_http_error(http, NGX_ETIMEDOUT, "write timed out");
+        return;
+    }
+
+    b = http->buffer;
+
+    if (b == NULL) {
+        size = njs_chb_size(&http->chain);
+        if (size < 0) {
+            ngx_js_http_error(http, 0, "memory error");
+            return;
+        }
+
+        b = ngx_create_temp_buf(http->pool, size);
+        if (b == NULL) {
+            ngx_js_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 = ngx_send(c, b->pos, size);
+
+    if (n == NGX_ERROR) {
+        (void) ngx_js_http_next(http);
+        return;
+    }
+
+    if (n > 0) {
+        b->pos += n;
+
+        if (n == size) {
+            wev->handler = ngx_js_http_dummy_handler;
+
+            http->buffer = NULL;
+
+            if (wev->timer_set) {
+                ngx_del_timer(wev);
+            }
+
+            if (ngx_handle_write_event(wev, 0) != NGX_OK) {
+                ngx_js_http_error(http, 0, "write failed");
+            }
+
+            return;
+        }
+    }
+
+    if (!wev->timer_set && http->timeout) {
+        ngx_add_timer(wev, http->timeout);
+    }
+}
+
+
+static void
+ngx_js_http_read_handler(ngx_event_t *rev)
+{
+    ssize_t            n, size;
+    ngx_int_t          rc;
+    ngx_buf_t         *b;
+    ngx_js_http_t     *http;
+    ngx_connection_t  *c;
+
+    c = rev->data;
+    http = c->data;
+
+    ngx_log_debug0(NGX_LOG_DEBUG_EVENT, rev->log, 0, "js http read handler");
+
+    if (rev->timedout) {
+        ngx_js_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_js_http_error(http, 0, "memory error");
+            return;
+        }
+
+        http->buffer = b;
+    }
+
+    for ( ;; ) {
+        b = http->buffer;
+        size = b->end - b->last;
+
+        n = ngx_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_js_http_error(http, 0, "read failed");
+            }
+
+            return;
+        }
+
+        if (n == NGX_ERROR) {
+            (void) ngx_js_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_js_http_error(http, 0, "prematurely closed connection");
+    }
+}
+
+
+static ngx_int_t
+ngx_js_http_process_status_line(ngx_js_http_t *http)
+{
+    ngx_int_t             rc;
+    ngx_js_http_parse_t  *hp;
+
+    hp = &http->http_parse;
+
+    rc = ngx_js_http_parse_status_line(hp, http->buffer);
+
+    if (rc == NGX_OK) {
+        ngx_log_debug1(NGX_LOG_DEBUG_EVENT, http->log, 0, "js http status %ui",
+                       hp->code);
+
+        http->process = ngx_js_http_process_headers;
+
+        return http->process(http);
+    }
+
+    if (rc == NGX_AGAIN) {
+        return NGX_AGAIN;
+    }
+
+    /* rc == NGX_ERROR */
+
+    ngx_js_http_error(http, 0, "invalid fetch status line");
+
+    return NGX_ERROR;
+}
+
+
+static ngx_int_t
+ngx_js_http_process_headers(ngx_js_http_t *http)
+{
+    ngx_int_t             rc;
+    ngx_table_elt_t      *h;
+    ngx_js_http_parse_t  *hp;
+
+    ngx_log_debug0(NGX_LOG_DEBUG_EVENT, http->log, 0,
+                   "js http process headers");
+
+    hp = &http->http_parse;
+
+    if (http->headers.size == 0) {
+        rc = ngx_array_init(&http->headers, http->pool, 4,
+                            sizeof(ngx_table_elt_t));
+        if (rc != NGX_OK) {
+            return NGX_ERROR;
+        }
+    }
+
+    for ( ;; ) {
+        rc = ngx_js_http_parse_header_line(hp, http->buffer);
+
+        if (rc == NGX_OK) {
+            h = ngx_array_push(&http->headers);
+            if (h == NULL) {
+                return NGX_ERROR;
+            }
+
+            h->hash = 1;
+            h->key.data = hp->header_name_start;
+            h->key.len = hp->header_name_end - hp->header_name_start;
+
+            h->value.data = hp->header_start;
+            h->value.len = hp->header_end - hp->header_start;
+
+            ngx_log_debug4(NGX_LOG_DEBUG_EVENT, http->log, 0,
+                           "js http header \"%*s: %*s\"",
+                           h->key.len, h->key.data, h->value.len,
+                           h->value.data);
+
+            if (h->key.len == njs_strlen("Transfer-Encoding")
+                && h->value.len == njs_strlen("chunked")
+                && ngx_strncasecmp(h->key.data, (u_char *) "Transfer-Encoding",
+                                   h->key.len) == 0
+                && ngx_strncasecmp(h->value.data, (u_char *) "chunked",
+                                   h->value.len) == 0)
+            {
+                hp->chunked = 1;
+            }
+
+            if (h->key.len == njs_strlen("Content-Length")
+                && ngx_strncasecmp(h->key.data, (u_char *) "Content-Length",
+                                   h->key.len) == 0)
+            {
+                hp->content_length_n = ngx_atoof(h->value.data, h->value.len);
+                if (hp->content_length_n == NGX_ERROR) {
+                    ngx_js_http_error(http, 0, "invalid fetch content length");
+                    return NGX_ERROR;
+                }
+
+                if (hp->content_length_n
+                    > (off_t) http->max_response_body_size)
+                {
+                    ngx_js_http_error(http, 0,
+                                      "fetch content length is too large");
+                    return NGX_ERROR;
+                }
+            }
+
+            continue;
+        }
+
+        if (rc == NGX_DONE) {
+            break;
+        }
+
+        if (rc == NGX_AGAIN) {
+            return NGX_AGAIN;
+        }
+
+        /* rc == NGX_ERROR */
+
+        ngx_js_http_error(http, 0, "invalid fetch header");
+
+        return NGX_ERROR;
+    }
+
+    njs_chb_destroy(&http->chain);
+    njs_chb_init(&http->chain, njs_vm_memory_pool(http->vm));
+
+    http->process = ngx_js_http_process_body;
+
+    return http->process(http);
+}
+
+
+static ngx_int_t
+ngx_js_http_process_body(ngx_js_http_t *http)
+{
+    ssize_t     size, need;
+    ngx_int_t   rc;
+    njs_int_t   ret;
+    ngx_buf_t  *b;
+
+    ngx_log_debug1(NGX_LOG_DEBUG_EVENT, http->log, 0,
+                   "js http process body done:%ui", (ngx_uint_t) http->done);
+
+    if (http->done) {
+        size = njs_chb_size(&http->chain);
+        if (size < 0) {
+            ngx_js_http_error(http, 0, "memory error");
+            return NGX_ERROR;
+        }
+
+        if (size == http->http_parse.content_length_n) {
+            ret = njs_vm_external_create(http->vm, njs_value_arg(&http->reply),
+                                         NGX_JS_PROTO_RESPONSE, http, 0);
+            if (ret != NJS_OK) {
+                ngx_js_http_error(http, 0, "fetch object creation failed");
+                return NGX_ERROR;
+            }
+
+            ngx_js_http_fetch_done(http, &http->reply, NJS_OK);
+            return NGX_DONE;
+        }
+
+        if (http->http_parse.chunked
+            && http->http_parse.content_length_n == 0)
+        {
+            ngx_js_http_error(http, 0, "invalid fetch chunked response");
+            return NGX_ERROR;
+        }
+
+        if (size < http->http_parse.content_length_n) {
+            return NGX_AGAIN;
+        }
+
+        ngx_js_http_error(http, 0, "fetch trailing data");
+        return NGX_ERROR;
+    }
+
+    b = http->buffer;
+
+    if (http->http_parse.chunked) {
+        rc = ngx_js_http_parse_chunked(&http->http_chunk_parse, b,
+                                       &http->chain);
+        if (rc == NGX_ERROR) {
+            ngx_js_http_error(http, 0, "invalid fetch chunked response");
+            return NGX_ERROR;
+        }
+
+        size = njs_chb_size(&http->chain);
+
+        if (rc == NGX_OK) {
+            http->http_parse.content_length_n = size;
+        }
+
+        if (size > http->max_response_body_size * 10) {
+            ngx_js_http_error(http, 0, "very large fetch chunked response");
+            return NGX_ERROR;
+        }
+
+        b->pos = http->http_chunk_parse.pos;
+
+    } else {
+        need = http->http_parse.content_length_n - njs_chb_size(&http->chain);
+        size = ngx_min(need, b->last - b->pos);
+
+        if (size > 0) {
+            njs_chb_append(&http->chain, b->pos, size);
+            b->pos += size;
+            rc = NGX_AGAIN;
+
+        } else {
+            rc = 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_js_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_js_http_parse_status_line(ngx_js_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_js_http_parse_header_line(ngx_js_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 == '-') {
+                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_JS_HTTP_CHUNK_MIDDLE     0
+#define NGX_JS_HTTP_CHUNK_ON_BORDER  1
+#define NGX_JS_HTTP_CHUNK_END        2
+
+
+static ngx_int_t
+ngx_js_http_chunk_buffer(ngx_js_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_JS_HTTP_CHUNK_END;
+    }
+
+    njs_chb_append(chain, hcp->pos, size);
+    hcp->pos += size;
+
+    hcp->chunk_size -= size;
+
+    if (hcp->chunk_size == 0) {
+        return NGX_JS_HTTP_CHUNK_ON_BORDER;
+    }
+
+    return NGX_JS_HTTP_CHUNK_MIDDLE;
+}
+
+
+static ngx_int_t
+ngx_js_http_parse_chunked(ngx_js_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_js_http_chunk_buffer(hcp, b, chain);
+            if (rc == NGX_ERROR) {
+                return rc;
+            }
+
+            if (rc == NGX_JS_HTTP_CHUNK_MIDDLE) {
+                break;
+            }
+
+            state = sw_chunk_end_newline;
+
+            if (rc == NGX_JS_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 void
+ngx_js_http_dummy_handler(ngx_event_t *ev)
+{
+    ngx_log_debug0(NGX_LOG_DEBUG_EVENT, ev->log, 0, "js http dummy handler");
+}
+
+
+static njs_int_t
+ngx_response_js_ext_header_get(njs_vm_t *vm, njs_value_t *value,
+    njs_str_t *name, njs_value_t *retval, njs_bool_t as_array)
+{
+    u_char           *data, *p, *start, *end;
+    size_t            len;
+    njs_int_t         rc;
+    ngx_uint_t        i;
+    ngx_js_http_t    *http;
+    ngx_table_elt_t  *header, *h;
+
+    http = njs_vm_external(vm, value);
+    if (http == NULL) {
+        njs_value_null_set(retval);
+        return NJS_DECLINED;
+    }
+
+    p = NULL;
+    start = NULL;
+    end = NULL;
+
+    if (as_array) {
+        rc = njs_vm_array_alloc(vm, retval, 2);
+        if (rc != NJS_OK) {
+            return NJS_ERROR;
+        }
+    }
+
+    header = http->headers.elts;
+
+    for (i = 0; i < http->headers.nelts; i++) {
+        h = &header[i];
+
+        if (h->hash == 0
+            || h->key.len != name->length
+            || ngx_strncasecmp(h->key.data, name->start, name->length) != 0)
+        {
+            continue;
+        }
+
+        if (as_array) {
+            value = njs_vm_array_push(vm, retval);
+            if (value == NULL) {
+                return NJS_ERROR;
+            }
+
+            rc = njs_vm_value_string_set(vm, value, h->value.data,
+                                         h->value.len);
+            if (rc != NJS_OK) {
+                return NJS_ERROR;
+            }
+
+            continue;
+        }
+
+        if (p == NULL) {
+            start = h->value.data;
+            end = h->value.data + h->value.len;
+            p = end;
+            continue;
+        }
+
+        if (p + h->value.len + 1 > end) {
+            len = njs_max(p + h->value.len + 1 - start, 2 * (end - start));
+
+            data = ngx_pnalloc(http->pool, len);
+            if (data == NULL) {
+                njs_vm_memory_error(vm);
+                return NJS_ERROR;
+            }
+
+            p = ngx_cpymem(data, start, p - start);
+            start = data;
+            end = data + len;
+        }
+
+        *p++ = ',';
+        p = ngx_cpymem(p, h->value.data, h->value.len);
+    }
+
+    if (as_array) {
+        return NJS_OK;
+    }
+
+    if (p == NULL) {
+        njs_value_null_set(retval);
+        return NJS_DECLINED;
+    }
+
+    return njs_vm_value_string_set(vm, retval, start, p - start);
+}
+
+
+static njs_int_t
+ngx_response_js_ext_headers_get(njs_vm_t *vm, njs_value_t *args,
+     njs_uint_t nargs, njs_index_t as_array)
+{
+    njs_int_t  ret;
+    njs_str_t  name;
+
+    ret = ngx_js_string(vm, njs_arg(args, nargs, 1), &name);
+    if (ret != NJS_OK) {
+        return NJS_ERROR;
+    }
+
+    ret = ngx_response_js_ext_header_get(vm, njs_argument(args, 0),
+                                         &name, njs_vm_retval(vm), as_array);
+
+    return (ret != NJS_ERROR) ? NJS_OK : NJS_ERROR;
+}
+
+
+static njs_int_t
+ngx_response_js_ext_headers_has(njs_vm_t *vm, njs_value_t *args,
+     njs_uint_t nargs, njs_index_t unused)
+{
+    njs_int_t  ret;
+    njs_str_t  name;
+
+    ret = ngx_js_string(vm, njs_arg(args, nargs, 1), &name);
+    if (ret != NJS_OK) {
+        return NJS_ERROR;
+    }
+
+    ret = ngx_response_js_ext_header_get(vm, njs_argument(args, 0),
+                                         &name, njs_vm_retval(vm), 0);
+    if (ret == NJS_ERROR) {
+        return NJS_ERROR;
+    }
+
+    njs_value_boolean_set(njs_vm_retval(vm), ret == NJS_OK);
+
+    return NJS_OK;
+}
+
+
+static njs_int_t
+ngx_response_js_ext_header(njs_vm_t *vm, njs_object_prop_t *prop,
+    njs_value_t *value, njs_value_t *setval, njs_value_t *retval)
+{
+    njs_int_t  ret;
+    njs_str_t  name;
+
+    ret = njs_vm_prop_name(vm, prop, &name);
+    if (ret != NJS_OK) {
+        return NJS_ERROR;
+    }
+
+    return ngx_response_js_ext_header_get(vm, value, &name, njs_vm_retval(vm),
+                                          0);
+}
+
+
+static njs_int_t
+ngx_response_js_ext_keys(njs_vm_t *vm, njs_value_t *value, njs_value_t *keys)
+{
+    njs_int_t         rc;
+    njs_str_t         hdr;
+    ngx_uint_t        i, k, length;
+    njs_value_t      *start;
+    ngx_js_http_t    *http;
+    ngx_table_elt_t  *h, *headers;
+
+    http = njs_vm_external(vm, value);
+    if (http == NULL) {
+        njs_value_undefined_set(keys);
+        return NJS_DECLINED;
+    }
+
+    rc = njs_vm_array_alloc(vm, keys, 8);
+    if (rc != NJS_OK) {
+        return NJS_ERROR;
+    }
+
+    length = 0;
+    headers = http->headers.elts;
+    start = njs_vm_array_start(vm, keys);
+
+    for (i = 0; i < http->headers.nelts; i++) {
+        h = &headers[i];
+
+        for (k = 0; k < length; k++) {
+            njs_value_string_get(njs_argument(start, k), &hdr);
+
+            if (h->key.len == hdr.length
+                && ngx_strncasecmp(h->key.data, hdr.start, hdr.length) == 0)
+            {
+                break;
+            }
+        }
+    }
+
+    return NJS_OK;
+}
+
+
+static njs_int_t
+ngx_response_js_ext_body(njs_vm_t *vm, njs_value_t *args,
+     njs_uint_t nargs, njs_index_t type)
+{
+    njs_int_t            ret;
+    njs_str_t            string;
+    ngx_js_http_t       *http;
+    njs_opaque_value_t   retval;
+
+    http = njs_vm_external(vm, njs_argument(args, 0));
+    if (http == NULL) {
+        njs_value_undefined_set(njs_vm_retval(vm));
+        return NJS_DECLINED;
+    }
+
+    if (http->body_used) {
+        njs_vm_error(vm, "body stream already read");
+        return NJS_ERROR;
+    }
+
+    http->body_used = 1;
+
+    ret = njs_chb_join(&http->chain, &string);
+    if (ret != NJS_OK) {
+        njs_vm_memory_error(http->vm);
+        return NJS_ERROR;
+    }
+
+    switch (type) {
+    case NGX_JS_BODY_ARRAY_BUFFER:
+        ret = njs_vm_value_array_buffer_set(http->vm, njs_value_arg(&retval),
+                                            string.start, string.length);
+        if (ret != NJS_OK) {
+            njs_vm_memory_error(http->vm);
+            return NJS_ERROR;
+        }
+
+        break;
+
+    case NGX_JS_BODY_JSON:
+    case NGX_JS_BODY_TEXT:
+    default:
+        ret = njs_vm_value_string_set(http->vm, njs_value_arg(&retval),
+                                      string.start, string.length);
+        if (ret != NJS_OK) {
+            njs_vm_memory_error(http->vm);
+            return NJS_ERROR;
+        }
+
+        if (type == NGX_JS_BODY_JSON) {
+            ret = njs_vm_json_parse(vm, njs_value_arg(&retval), 1);
+            njs_value_assign(&retval, njs_vm_retval(vm));
+        }
+    }
+
+    return ngx_js_fetch_promissified_result(http->vm, njs_value_arg(&retval),
+                                            ret);
+}
+
+
+static njs_int_t
+ngx_response_js_ext_body_used(njs_vm_t *vm, njs_object_prop_t *prop,
+    njs_value_t *value, njs_value_t *setval, njs_value_t *retval)
+{
+    ngx_js_http_t  *http;
+
+    http = njs_vm_external(vm, value);
+    if (http == NULL) {
+        njs_value_undefined_set(retval);
+        return NJS_DECLINED;
+    }
+
+    njs_value_boolean_set(retval, http->body_used);
+
+    return NJS_OK;
+}
+
+
+static njs_int_t
+ngx_response_js_ext_ok(njs_vm_t *vm, njs_object_prop_t *prop,
+    njs_value_t *value, njs_value_t *setval, njs_value_t *retval)
+{
+    ngx_uint_t      code;
+    ngx_js_http_t  *http;
+
+    http = njs_vm_external(vm, value);
+    if (http == NULL) {
+        njs_value_undefined_set(retval);
+        return NJS_DECLINED;
+    }
+
+    code = http->http_parse.code;
+
+    njs_value_boolean_set(retval, code >= 200 && code < 300);
+
+    return NJS_OK;
+}
+
+
+static njs_int_t
+ngx_response_js_ext_status(njs_vm_t *vm, njs_object_prop_t *prop,
+    njs_value_t *value, njs_value_t *setval, njs_value_t *retval)
+{
+    ngx_js_http_t  *http;
+
+    http = njs_vm_external(vm, value);
+    if (http == NULL) {
+        njs_value_undefined_set(retval);
+        return NJS_DECLINED;
+    }
+
+    njs_value_number_set(retval, http->http_parse.code);
+
+    return NJS_OK;
+}
+
+
+static njs_int_t
+ngx_response_js_ext_status_text(njs_vm_t *vm, njs_object_prop_t *prop,
+    njs_value_t *value, njs_value_t *setval, njs_value_t *retval)
+{
+    ngx_js_http_t  *http;
+
+    http = njs_vm_external(vm, value);
+    if (http == NULL) {
+        njs_value_undefined_set(retval);
+        return NJS_DECLINED;
+    }
+
+    njs_vm_value_string_set(vm, retval, http->http_parse.status_text,
+                            http->http_parse.status_text_end
+                            - http->http_parse.status_text);
+
+    return NJS_OK;
+}
+
+
+static njs_int_t
+ngx_response_js_ext_type(njs_vm_t *vm, njs_object_prop_t *prop,
+    njs_value_t *value, njs_value_t *setval, njs_value_t *retval)
+{
+    ngx_js_http_t  *http;
+
+    http = njs_vm_external(vm, value);
+    if (http == NULL) {
+        njs_value_undefined_set(retval);
+        return NJS_DECLINED;
+    }
+
+    return njs_vm_value_string_set(vm, retval, (u_char *) "basic",
+                                   njs_length("basic"));
+}
+
+
+ngx_int_t
+ngx_js_fetch_init(njs_vm_t *vm, ngx_log_t *log)
+{
+    njs_int_t  proto_id;
+
+    proto_id = njs_vm_external_prototype(vm, ngx_js_ext_http_response,
+                                         njs_nitems(ngx_js_ext_http_response));
+    if (proto_id != NGX_JS_PROTO_RESPONSE) {
+        ngx_log_error(NGX_LOG_EMERG, log, 0,
+                      "failed to add js http.response proto");
+        return NGX_ERROR;
+    }
+
+    return NGX_OK;
+}
diff --git a/nginx/ngx_js_fetch.h b/nginx/ngx_js_fetch.h
new file mode 100644
index 0000000..7f107c4
--- /dev/null
+++ b/nginx/ngx_js_fetch.h
@@ -0,0 +1,18 @@
+
+/*
+ * Copyright (C) Dmitry Volyntsev
+ * Copyright (C) NGINX, Inc.
+ */
+
+
+#ifndef _NGX_JS_FETCH_H_INCLUDED_
+#define _NGX_JS_FETCH_H_INCLUDED_
+
+
+njs_int_t ngx_js_ext_fetch(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
+    njs_index_t level);
+
+ngx_int_t ngx_js_fetch_init(njs_vm_t *vm, ngx_log_t *log);
+
+
+#endif /* _NGX_JS_FETCH_H_INCLUDED_ */
diff --git a/nginx/ngx_stream_js_module.c b/nginx/ngx_stream_js_module.c
index 91852ba..b4e3388 100644
--- a/nginx/ngx_stream_js_module.c
+++ b/nginx/ngx_stream_js_module.c
@@ -111,6 +111,11 @@
 static void ngx_stream_js_clear_timer(njs_external_ptr_t external,
     njs_host_event_t event);
 static void ngx_stream_js_timer_handler(ngx_event_t *ev);
+static ngx_pool_t *ngx_stream_js_pool(njs_vm_t *vm, ngx_stream_session_t *s);
+static ngx_resolver_t *ngx_stream_js_resolver(njs_vm_t *vm,
+    ngx_stream_session_t *s);
+static ngx_msec_t ngx_stream_js_resolver_timeout(njs_vm_t *vm,
+    ngx_stream_session_t *s);
 static void ngx_stream_js_handle_event(ngx_stream_session_t *s,
     njs_vm_event_t vm_event, njs_value_t *args, njs_uint_t nargs);
 
@@ -379,11 +384,15 @@
 
 static uintptr_t ngx_stream_js_uptr[] = {
     offsetof(ngx_stream_session_t, connection),
+    (uintptr_t) ngx_stream_js_pool,
+    (uintptr_t) ngx_stream_js_resolver,
+    (uintptr_t) ngx_stream_js_resolver_timeout,
+    (uintptr_t) ngx_stream_js_handle_event,
 };
 
 
 static njs_vm_meta_t ngx_stream_js_metas = {
-    .size = 1,
+    .size = 5,
     .values = ngx_stream_js_uptr
 };
 
@@ -1266,6 +1275,35 @@
 }
 
 
+static ngx_pool_t *
+ngx_stream_js_pool(njs_vm_t *vm, ngx_stream_session_t *s)
+{
+    return s->connection->pool;
+}
+
+
+static ngx_resolver_t *
+ngx_stream_js_resolver(njs_vm_t *vm, ngx_stream_session_t *s)
+{
+    ngx_stream_core_srv_conf_t  *cscf;
+
+    cscf = ngx_stream_get_module_srv_conf(s, ngx_stream_core_module);
+
+    return cscf->resolver;
+}
+
+
+static ngx_msec_t
+ngx_stream_js_resolver_timeout(njs_vm_t *vm, ngx_stream_session_t *s)
+{
+    ngx_stream_core_srv_conf_t  *cscf;
+
+    cscf = ngx_stream_get_module_srv_conf(s, ngx_stream_core_module);
+
+    return cscf->resolver_timeout;
+}
+
+
 static void
 ngx_stream_js_handle_event(ngx_stream_session_t *s, njs_vm_event_t vm_event,
     njs_value_t *args, njs_uint_t nargs)