
/*
 * Copyright (C) Dmitry Volyntsev
 * Copyright (C) hongzhidao
 * Copyright (C) Antoine Bonavita
 * Copyright (C) NGINX, Inc.
 */


#include "ngx_js.h"
#include "ngx_js_http.h"


typedef struct {
    njs_str_t       name;
    njs_int_t       value;
} ngx_js_entry_t;


typedef struct {
    ngx_js_http_t                  http;

    njs_vm_t                      *vm;
    ngx_js_event_t                *event;

    njs_opaque_value_t             response_value;

    njs_opaque_value_t             promise;
    njs_opaque_value_t             promise_callbacks[2];
} ngx_js_fetch_t;


static njs_int_t ngx_js_method_process(njs_vm_t *vm, ngx_js_request_t *r);
static njs_int_t ngx_js_headers_inherit(njs_vm_t *vm, ngx_js_headers_t *headers,
    ngx_js_headers_t *orig);
static njs_int_t ngx_js_headers_fill(njs_vm_t *vm, ngx_js_headers_t *headers,
    njs_value_t *init);
static ngx_js_fetch_t *ngx_js_fetch_alloc(njs_vm_t *vm, ngx_pool_t *pool,
    ngx_log_t *log, ngx_js_loc_conf_t *conf);
static void ngx_js_fetch_error(ngx_js_http_t *http, const char *err);
static void ngx_js_fetch_destructor(ngx_js_event_t *event);
static njs_int_t ngx_js_fetch_promissified_result(njs_vm_t *vm,
    njs_value_t *result, njs_int_t rc, njs_value_t *retval);
static void ngx_js_fetch_done(ngx_js_fetch_t *fetch, 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,
    njs_value_t *retval);

static njs_int_t ngx_js_request_constructor(njs_vm_t *vm,
    ngx_js_request_t *request, ngx_url_t *u, njs_external_ptr_t external,
    njs_value_t *args, njs_uint_t nargs);

static ngx_int_t ngx_js_fetch_append_headers(ngx_js_http_t *http,
    ngx_js_headers_t *headers, u_char *name, size_t len, u_char *value,
    size_t vlen);
static void ngx_js_fetch_process_done(ngx_js_http_t *http);
static njs_int_t ngx_js_headers_append(njs_vm_t *vm, ngx_js_headers_t *headers,
    u_char *name, size_t len, u_char *value, size_t vlen);

static njs_int_t ngx_headers_js_ext_append(njs_vm_t *vm, njs_value_t *args,
    njs_uint_t nargs, njs_index_t unused, njs_value_t *retval);
static njs_int_t ngx_headers_js_ext_delete(njs_vm_t *vm, njs_value_t *args,
    njs_uint_t nargs, njs_index_t unused, njs_value_t *retval);
static njs_int_t ngx_headers_js_ext_for_each(njs_vm_t *vm, njs_value_t *args,
    njs_uint_t nargs, njs_index_t as_array, njs_value_t *retval);
static njs_int_t ngx_headers_js_ext_get(njs_vm_t *vm, njs_value_t *args,
    njs_uint_t nargs, njs_index_t as_array, njs_value_t *retval);
static njs_int_t ngx_headers_js_ext_has(njs_vm_t *vm, njs_value_t *args,
    njs_uint_t nargs, njs_index_t unused, njs_value_t *retval);
static njs_int_t ngx_headers_js_ext_prop(njs_vm_t *vm,
    njs_object_prop_t *prop, uint32_t atom_id, njs_value_t *value,
    njs_value_t *setval, njs_value_t *retval);
static njs_int_t ngx_headers_js_ext_keys(njs_vm_t *vm, njs_value_t *value,
    njs_value_t *keys);
static njs_int_t ngx_headers_js_ext_set(njs_vm_t *vm, njs_value_t *args,
    njs_uint_t nargs, njs_index_t unused, njs_value_t *retval);
static njs_int_t ngx_request_js_ext_body(njs_vm_t *vm, njs_value_t *args,
     njs_uint_t nargs, njs_index_t unused, njs_value_t *retval);
static njs_int_t ngx_request_js_ext_body_used(njs_vm_t *vm,
    njs_object_prop_t *prop, uint32_t unused, njs_value_t *value,
    njs_value_t *setval, njs_value_t *retval);
static njs_int_t ngx_request_js_ext_cache(njs_vm_t *vm,
    njs_object_prop_t *prop, uint32_t unused, njs_value_t *value,
    njs_value_t *setval, njs_value_t *retval);
static njs_int_t ngx_request_js_ext_credentials(njs_vm_t *vm,
    njs_object_prop_t *prop, uint32_t unused, njs_value_t *value,
    njs_value_t *setval, njs_value_t *retval);
static njs_int_t ngx_request_js_ext_headers(njs_vm_t *vm,
    njs_object_prop_t *prop, uint32_t unused, njs_value_t *value,
    njs_value_t *setval, njs_value_t *retval);
static njs_int_t ngx_request_js_ext_mode(njs_vm_t *vm, njs_object_prop_t *prop,
    uint32_t unused, njs_value_t *value, njs_value_t *setval,
    njs_value_t *retval);
static njs_int_t ngx_response_js_ext_status(njs_vm_t *vm,
    njs_object_prop_t *prop, uint32_t unused, 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, uint32_t unused, 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, uint32_t unused, 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, uint32_t unused, njs_value_t *value,
    njs_value_t *setval, njs_value_t *retval);
static njs_int_t ngx_response_js_ext_headers(njs_vm_t *vm,
    njs_object_prop_t *prop, uint32_t unused, 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, uint32_t unused, 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, njs_value_t *retval);

static njs_int_t ngx_fetch_flag(njs_vm_t *vm, const ngx_js_entry_t *entries,
    njs_int_t value, njs_value_t *retval);
static njs_int_t ngx_fetch_flag_set(njs_vm_t *vm, const ngx_js_entry_t *entries,
     njs_value_t *value, const char *type);

static njs_int_t ngx_js_fetch_init(njs_vm_t *vm);


static const ngx_js_entry_t ngx_js_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_js_entry_t ngx_js_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_js_entry_t ngx_js_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 },
};


static njs_external_t  ngx_js_ext_http_headers[] = {

    {
        .flags = NJS_EXTERN_PROPERTY | NJS_EXTERN_SYMBOL,
        .name.symbol = NJS_SYMBOL_TO_STRING_TAG,
        .u.property = {
            .value = "Headers",
        }
    },

    {
        .flags = NJS_EXTERN_SELF,
        .u.object = {
            .enumerable = 1,
            .prop_handler = ngx_headers_js_ext_prop,
            .keys = ngx_headers_js_ext_keys,
        }
    },

    {
        .flags = NJS_EXTERN_METHOD,
        .name.string = njs_str("append"),
        .writable = 1,
        .configurable = 1,
        .enumerable = 1,
        .u.method = {
            .native = ngx_headers_js_ext_append,
        }
    },

    {
        .flags = NJS_EXTERN_METHOD,
        .name.string = njs_str("delete"),
        .writable = 1,
        .configurable = 1,
        .enumerable = 1,
        .u.method = {
            .native = ngx_headers_js_ext_delete,
        }
    },

    {
        .flags = NJS_EXTERN_METHOD,
        .name.string = njs_str("forEach"),
        .writable = 1,
        .configurable = 1,
        .enumerable = 1,
        .u.method = {
            .native = ngx_headers_js_ext_for_each,
        }
    },

    {
        .flags = NJS_EXTERN_METHOD,
        .name.string = njs_str("get"),
        .writable = 1,
        .configurable = 1,
        .enumerable = 1,
        .u.method = {
            .native = ngx_headers_js_ext_get,
        }
    },

    {
        .flags = NJS_EXTERN_METHOD,
        .name.string = njs_str("getAll"),
        .writable = 1,
        .configurable = 1,
        .enumerable = 1,
        .u.method = {
            .native = ngx_headers_js_ext_get,
            .magic8 = 1
        }
    },

    {
        .flags = NJS_EXTERN_METHOD,
        .name.string = njs_str("has"),
        .writable = 1,
        .configurable = 1,
        .enumerable = 1,
        .u.method = {
            .native = ngx_headers_js_ext_has,
        }
    },

    {
        .flags = NJS_EXTERN_METHOD,
        .name.string = njs_str("set"),
        .writable = 1,
        .configurable = 1,
        .enumerable = 1,
        .u.method = {
            .native = ngx_headers_js_ext_set,
        }
    },

};


static njs_external_t  ngx_js_ext_http_request[] = {

    {
        .flags = NJS_EXTERN_PROPERTY | NJS_EXTERN_SYMBOL,
        .name.symbol = NJS_SYMBOL_TO_STRING_TAG,
        .u.property = {
            .value = "Request",
        }
    },

    {
        .flags = NJS_EXTERN_METHOD,
        .name.string = njs_str("arrayBuffer"),
        .writable = 1,
        .configurable = 1,
        .enumerable = 1,
        .u.method = {
            .native = ngx_request_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_request_js_ext_body_used,
        }
    },

    {
        .flags = NJS_EXTERN_PROPERTY,
        .name.string = njs_str("cache"),
        .enumerable = 1,
        .u.property = {
            .handler = ngx_request_js_ext_cache,
        }
    },

    {
        .flags = NJS_EXTERN_PROPERTY,
        .name.string = njs_str("credentials"),
        .enumerable = 1,
        .u.property = {
            .handler = ngx_request_js_ext_credentials,
        }
    },

    {
        .flags = NJS_EXTERN_METHOD,
        .name.string = njs_str("json"),
        .writable = 1,
        .configurable = 1,
        .enumerable = 1,
        .u.method = {
            .native = ngx_request_js_ext_body,
            .magic8 = NGX_JS_BODY_JSON
        }
    },

    {
        .flags = NJS_EXTERN_PROPERTY,
        .name.string = njs_str("headers"),
        .enumerable = 1,
        .u.property = {
            .handler = ngx_request_js_ext_headers,
        }
    },

    {
        .flags = NJS_EXTERN_PROPERTY,
        .name.string = njs_str("method"),
        .enumerable = 1,
        .u.property = {
            .handler = ngx_js_ext_string,
            .magic32 = offsetof(ngx_js_request_t, method),
        }
    },

    {
        .flags = NJS_EXTERN_PROPERTY,
        .name.string = njs_str("mode"),
        .enumerable = 1,
        .u.property = {
            .handler = ngx_request_js_ext_mode,
        }
    },

    {
        .flags = NJS_EXTERN_METHOD,
        .name.string = njs_str("text"),
        .writable = 1,
        .configurable = 1,
        .enumerable = 1,
        .u.method = {
            .native = ngx_request_js_ext_body,
            .magic8 = NGX_JS_BODY_TEXT
        }
    },

    {
        .flags = NJS_EXTERN_PROPERTY,
        .name.string = njs_str("url"),
        .enumerable = 1,
        .u.property = {
            .handler = ngx_js_ext_string,
            .magic32 = offsetof(ngx_js_request_t, url),
        }
    },

};


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,
            .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_PROPERTY,
        .name.string = njs_str("headers"),
        .enumerable = 1,
        .u.property = {
            .handler = ngx_response_js_ext_headers,
        }
    },

    {
        .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_constant,
            .magic32 = 0,
            .magic16 = NGX_JS_BOOLEAN,
        }
    },

    {
        .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_response_t, url),
        }
    },
};


static njs_int_t    ngx_http_js_fetch_request_proto_id;
static njs_int_t    ngx_http_js_fetch_response_proto_id;
static njs_int_t    ngx_http_js_fetch_headers_proto_id;


njs_module_t  ngx_js_fetch_module = {
    .name = njs_str("fetch"),
    .preinit = NULL,
    .init = ngx_js_fetch_init,
};


njs_int_t
ngx_js_ext_fetch(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
    njs_index_t unused, njs_value_t *retval)
{
    njs_int_t            ret;
    ngx_url_t            u;
    ngx_str_t           *resolve_host;
    ngx_pool_t          *pool;
    njs_value_t         *init, *value;
    ngx_js_http_t       *http;
    ngx_js_fetch_t      *fetch;
    ngx_js_request_t     request;
    ngx_connection_t    *c;
    ngx_resolver_ctx_t  *ctx;
    njs_external_ptr_t   external;
    njs_opaque_value_t   lvalue;

    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");
#if (NGX_SSL)
    static const njs_str_t verify_key = njs_str("verify");
#endif

    external = njs_vm_external_ptr(vm);
    c = ngx_external_connection(vm, external);
    pool = ngx_external_pool(vm, external);

    fetch = ngx_js_fetch_alloc(vm, pool, c->log,
                               ngx_external_loc_conf(vm, external));
    if (fetch == NULL) {
        return NJS_ERROR;
    }

    http = &fetch->http;

    ret = ngx_js_request_constructor(vm, &request, &u, external, args, nargs);
    if (ret != NJS_OK) {
        goto fail;
    }

    if (u.host.len >= NGX_JS_HOST_MAX_LEN) {
        njs_vm_error(vm, "Host name too long");
        goto fail;
    }

    http->host = u.host;
    http->port = u.port;
    http->response.url = request.url;
    http->buffer_size = http->conf->buffer_size;
    http->max_response_body_size = http->conf->max_response_body_size;

#if (NGX_SSL)
    if (ngx_js_https(&u)) {
        http->ssl = http->conf->ssl;
        http->ssl_verify = http->conf->ssl_verify;
    }
#endif

    init = njs_arg(args, nargs, 2);

    if (njs_value_is_object(init)) {
        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;
        }

#if (NGX_SSL)
        value = njs_vm_object_prop(vm, init, &verify_key, &lvalue);
        if (value != NULL) {
            http->ssl_verify = njs_value_bool(value);
        }
#endif
    }

    if (request.method.len == 4
        && ngx_strncasecmp(request.method.data, (u_char *) "HEAD", 4) == 0)
    {
        http->header_only = 1;
    }

    NJS_CHB_MP_INIT(&http->chain, njs_vm_memory_pool(vm));
    NJS_CHB_MP_INIT(&http->response.chain, njs_vm_memory_pool(vm));

    resolve_host = NULL;
    http->connect_port = http->port;

    if (ngx_js_conf_proxy(http->conf)) {
        if (ngx_js_conf_dynamic_proxy(http->conf)) {
            if (http->conf->eval_proxy_url(http->pool, external, http->conf,
                                           &http->proxy.url, &http->proxy.auth)
                != NGX_OK)
            {
                njs_vm_error(vm, "failed to evaluate proxy URL");
                goto fail;
            }

        } else {
            http->proxy.url = http->conf->fetch_proxy_url;
            http->proxy.auth = http->conf->fetch_proxy_auth_header;
        }

        if (ngx_js_http_proxy(http) && http->proxy.url->addrs == NULL) {
            resolve_host = &http->proxy.url->host;
            http->connect_port = http->proxy.url->port;
        }
    }

    if (!ngx_js_http_proxy(http) && u.addrs == NULL) {
        resolve_host = &u.host;
    }

    ngx_js_fetch_build_request(http, &request, &u.uri, &u,
                               ngx_js_http_proxy(http) && !ngx_js_https(&u));

    if (resolve_host != NULL) {
        ngx_log_debug0(NGX_LOG_DEBUG_EVENT, http->log, 0,
                       "js http fetch: resolving");

        ctx = ngx_js_http_resolve(http, ngx_external_resolver(vm, external),
                                  resolve_host,
                                  ngx_external_resolver_timeout(vm, external));

        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;
        }

        njs_value_assign(retval, njs_value_arg(&fetch->promise));

        return NJS_OK;
    }

    http->naddrs = 1;
    http->addrs = &http->addr;

    if (ngx_js_http_proxy(http)) {
        ngx_memcpy(&http->addr, &http->proxy.url->addrs[0], sizeof(ngx_addr_t));

    } else {
        ngx_memcpy(&http->addr, &u.addrs[0], sizeof(ngx_addr_t));
    }

    ngx_js_http_connect(http);

    njs_value_assign(retval, njs_value_arg(&fetch->promise));

    return NJS_OK;

fail:

    ngx_log_debug2(NGX_LOG_DEBUG_EVENT, http->log, 0,
                   "js http done fetch:%p rc:%d", fetch, NJS_ERROR);

    ngx_js_del_event(ngx_external_ctx(vm,  njs_vm_external_ptr(vm)),
                     fetch->event);

    return ngx_js_fetch_promissified_result(vm, NULL, NJS_ERROR, retval);
}


static njs_int_t
ngx_js_ext_headers_constructor(njs_vm_t *vm, njs_value_t *args,
    njs_uint_t nargs, njs_index_t unused, njs_value_t *retval)
{
    ngx_int_t          rc;
    njs_int_t          ret;
    njs_value_t       *init;
    ngx_pool_t        *pool;
    ngx_js_headers_t  *headers;

    pool = ngx_external_pool(vm, njs_vm_external_ptr(vm));

    headers = ngx_palloc(pool, sizeof(ngx_js_headers_t));
    if (headers == NULL) {
        njs_vm_memory_error(vm);
        return NJS_ERROR;
    }

    headers->guard = GUARD_NONE;

    rc = ngx_list_init(&headers->header_list, pool, 4, sizeof(ngx_js_tb_elt_t));
    if (rc != NGX_OK) {
        njs_vm_memory_error(vm);
        return NJS_ERROR;
    }

    init = njs_arg(args, nargs, 1);

    if (njs_value_is_object(init)) {
        ret = ngx_js_headers_fill(vm, headers, init);
        if (ret != NJS_OK) {
            return NJS_ERROR;
        }
    }

    return njs_vm_external_create(vm, retval,
                                  ngx_http_js_fetch_headers_proto_id, headers,
                                  0);
}


static njs_int_t
ngx_js_ext_request_constructor(njs_vm_t *vm, njs_value_t *args,
    njs_uint_t nargs, njs_index_t unused, njs_value_t *retval)
{
    njs_int_t          ret;
    ngx_url_t          u;
    ngx_js_request_t  *request;

    request = njs_mp_alloc(njs_vm_memory_pool(vm), sizeof(ngx_js_request_t));
    if (request == NULL) {
        njs_vm_memory_error(vm);
        return NJS_ERROR;
    }

    ret = ngx_js_request_constructor(vm, request, &u, njs_vm_external_ptr(vm),
                                     args, nargs);
    if (ret != NJS_OK) {
        return NJS_ERROR;
    }

    return njs_vm_external_create(vm, retval,
                                  ngx_http_js_fetch_request_proto_id, request,
                                  0);
}


static njs_int_t
ngx_js_ext_response_constructor(njs_vm_t *vm, njs_value_t *args,
    njs_uint_t nargs, njs_index_t unused, njs_value_t *retval)
{
    u_char              *p, *end;
    ngx_int_t            rc;
    njs_int_t            ret;
    njs_str_t            bd;
    ngx_pool_t          *pool;
    njs_value_t         *body, *init, *value;
    ngx_js_response_t   *response;
    njs_opaque_value_t   lvalue;

    static const njs_str_t headers = njs_str("headers");
    static const njs_str_t status = njs_str("status");
    static const njs_str_t status_text = njs_str("statusText");

    response = njs_mp_zalloc(njs_vm_memory_pool(vm), sizeof(ngx_js_response_t));
    if (response == NULL) {
        njs_vm_memory_error(vm);
        return NJS_ERROR;
    }

    /*
     * set by njs_mp_zalloc():
     *
     *  request->url.length = 0;
     *  request->status_text.length = 0;
     */

    response->code = 200;
    response->headers.guard = GUARD_RESPONSE;

    pool = ngx_external_pool(vm, njs_vm_external_ptr(vm));

    rc = ngx_list_init(&response->headers.header_list, pool, 4,
                       sizeof(ngx_js_tb_elt_t));
    if (rc != NGX_OK) {
        njs_vm_memory_error(vm);
        return NJS_ERROR;
    }

    init = njs_arg(args, nargs, 2);

    if (njs_value_is_object(init)) {
        value = njs_vm_object_prop(vm, init, &status, &lvalue);
        if (value != NULL) {
            if (ngx_js_integer(vm, value, &response->code) != NGX_OK) {
                njs_vm_error(vm, "invalid Response status");
                return NJS_ERROR;
            }

            if (response->code < 200 || response->code > 599) {
                njs_vm_error(vm, "status provided (%i) is outside of "
                             "[200, 599] range", response->code);
                return NJS_ERROR;
            }
        }

        value = njs_vm_object_prop(vm, init, &status_text, &lvalue);
        if (value != NULL) {
            if (ngx_js_ngx_string(vm, value, &response->status_text) != NGX_OK) {
                njs_vm_error(vm, "invalid Response statusText");
                return NJS_ERROR;
            }

            p = response->status_text.data;
            end = p + response->status_text.len;

            while (p < end) {
                if (*p != '\t' && *p < ' ') {
                    njs_vm_error(vm, "invalid Response statusText");
                    return NJS_ERROR;
                }

                p++;
            }
        }

        value = njs_vm_object_prop(vm, init, &headers, &lvalue);
        if (value != NULL) {
            if (!njs_value_is_object(value)) {
                njs_vm_error(vm, "Headers is not an object");
                return NJS_ERROR;
            }

            ret = ngx_js_headers_fill(vm, &response->headers, value);
            if (ret != NJS_OK) {
                return NJS_ERROR;
            }
        }
    }

    NJS_CHB_MP_INIT(&response->chain, njs_vm_memory_pool(vm));

    body = njs_arg(args, nargs, 1);

    if (!njs_value_is_null_or_undefined(body)) {
        if (ngx_js_string(vm, body, &bd) != NGX_OK) {
            njs_vm_error(vm, "invalid Response body");
            return NJS_ERROR;
        }

        njs_chb_append(&response->chain, bd.start, bd.length);

        if (njs_value_is_string(body)) {
            ret = ngx_js_headers_append(vm, &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 NJS_ERROR;
            }
        }
    }

    return njs_vm_external_create(vm, retval,
                                  ngx_http_js_fetch_response_proto_id, response,
                                  0);
}


static njs_int_t
ngx_js_method_process(njs_vm_t *vm, ngx_js_request_t *request)
{
    u_char           *s, *p;
    njs_str_t        str;
    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,
    };

    str.start = request->method.data;
    str.length = request->method.len;

    for (m = &forbidden[0]; m->length != 0; m++) {
        if (njs_strstr_case_eq(&str, m)) {
            njs_vm_error(vm, "forbidden method: %V", m);
            return NJS_ERROR;
        }
    }

    for (m = &to_normalize[0]; m->length != 0; m++) {
        if (njs_strstr_case_eq(&str, 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_js_headers_inherit(njs_vm_t *vm, ngx_js_headers_t *headers,
    ngx_js_headers_t *orig)
{
    njs_int_t         ret;
    ngx_uint_t        i;
    ngx_list_part_t  *part;
    ngx_js_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_js_headers_append(vm, 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_js_headers_fill(njs_vm_t *vm, ngx_js_headers_t *headers, njs_value_t *init)
{
    int64_t              i, len, length;
    njs_int_t            ret;
    njs_str_t            name, header;
    njs_value_t         *keys, *value;
    ngx_js_headers_t    *hh;
    njs_opaque_value_t  *e, *start, lvalue;

    hh = njs_vm_external(vm, ngx_http_js_fetch_headers_proto_id, init);

    if (hh != NULL) {
        return ngx_js_headers_inherit(vm, headers, hh);
    }

    if (njs_value_is_array(init)) {
        start = (njs_opaque_value_t *) njs_vm_array_start(vm, init);
        if (start == NULL) {
            return NJS_ERROR;
        }

        (void) njs_vm_array_length(vm, init, &length);

        for (i = 0; i < length; i++) {
            e = (njs_opaque_value_t *) njs_vm_array_start(vm,
                                                          njs_value_arg(start));
            if (e == NULL) {
                return NJS_ERROR;
            }

            (void) njs_vm_array_length(vm, njs_value_arg(start), &len);

            start++;

            if (len != 2) {
                njs_vm_error(vm, "header does not contain exactly two items");
                return NJS_ERROR;
            }

            if (ngx_js_string(vm, njs_value_arg(&e[0]), &name) != NGX_OK) {
                return NJS_ERROR;
            }

            if (ngx_js_string(vm, njs_value_arg(&e[1]), &header) != NGX_OK) {
                return NJS_ERROR;
            }

            ret = ngx_js_headers_append(vm, headers, name.start, name.length,
                                        header.start, header.length);
            if (ret != NJS_OK) {
                return NJS_ERROR;
            }
        }

    } else {
        keys = njs_vm_object_keys(vm, init, njs_value_arg(&lvalue));
        if (keys == NULL) {
            return NJS_ERROR;
        }

        start = (njs_opaque_value_t *) njs_vm_array_start(vm, keys);
        if (start == NULL) {
            return NJS_ERROR;
        }

        (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) {
                return NJS_ERROR;
            }

            start++;

            value = njs_vm_object_prop(vm, init, &name, &lvalue);
            if (value == NULL) {
                return NJS_ERROR;
            }

            if (ngx_js_string(vm, value, &header) != NGX_OK) {
                return NJS_ERROR;
            }

            ret = ngx_js_headers_append(vm, headers, name.start, name.length,
                                        header.start, header.length);
            if (ret != NJS_OK) {
                return NJS_ERROR;
            }
        }
    }

    return NJS_OK;
}


static ngx_js_fetch_t *
ngx_js_fetch_alloc(njs_vm_t *vm, ngx_pool_t *pool, ngx_log_t *log,
    ngx_js_loc_conf_t *conf)
{
    njs_int_t        ret;
    ngx_js_ctx_t    *ctx;
    ngx_js_http_t   *http;
    ngx_js_fetch_t  *fetch;
    ngx_js_event_t  *event;
    njs_function_t  *callback;

    fetch = ngx_pcalloc(pool, sizeof(ngx_js_fetch_t));
    if (fetch == NULL) {
        goto failed;
    }

    /*
     * set by ngx_pcalloc():
     *
     * fetch->http.proxy.state = HTTP_STATE_DIRECT;
     */

    http = &fetch->http;

    http->pool = pool;
    http->log = log;
    http->conf = conf;

    http->content_length_n = -1;
    http->keepalive = (conf->fetch_keepalive > 0);

    http->append_headers = ngx_js_fetch_append_headers;
    http->ready_handler = ngx_js_fetch_process_done;
    http->error_handler = ngx_js_fetch_error;

    ret = njs_vm_promise_create(vm, njs_value_arg(&fetch->promise),
                                njs_value_arg(&fetch->promise_callbacks));
    if (ret != NJS_OK) {
        goto failed;
    }

    callback = njs_vm_function_alloc(vm, ngx_js_http_promise_trampoline, 0, 0);
    if (callback == NULL) {
        goto failed;
    }

    event = njs_mp_zalloc(njs_vm_memory_pool(vm), sizeof(ngx_js_event_t));
    if (njs_slow_path(event == NULL)) {
        goto failed;
    }

    ctx = ngx_external_ctx(vm, njs_vm_external_ptr(vm));

    event->ctx = vm;
    njs_value_function_set(njs_value_arg(&event->function), callback);
    event->destructor = ngx_js_fetch_destructor;
    event->fd = ctx->event_id++;
    event->data = fetch;

    ngx_js_add_event(ctx, event);

    fetch->vm = vm;
    fetch->event = event;

    ngx_log_debug1(NGX_LOG_DEBUG_EVENT, log, 0, "js http alloc:%p", fetch);

    return fetch;

failed:

    njs_vm_error(vm, "internal error");

    return NULL;
}


static void
ngx_js_fetch_error(ngx_js_http_t *http, const char *err)
{
    ngx_js_fetch_t  *fetch;

    fetch = (ngx_js_fetch_t *) http;

    njs_vm_error(fetch->vm, err);

    njs_vm_exception_get(fetch->vm, njs_value_arg(&fetch->response_value));

    ngx_js_fetch_done(fetch, &fetch->response_value, NJS_ERROR);
}


static void
ngx_js_fetch_destructor(ngx_js_event_t *event)
{
    ngx_js_http_t   *http;
    ngx_js_fetch_t  *fetch;

    fetch = event->data;
    http = &fetch->http;

    ngx_log_debug1(NGX_LOG_DEBUG_EVENT, http->log, 0, "js http destructor:%p",
                   fetch);

    ngx_js_http_resolve_done(http);
    ngx_js_http_close_peer(http);
}


static njs_int_t
ngx_js_fetch_promissified_result(njs_vm_t *vm, njs_value_t *result,
    njs_int_t rc, njs_value_t *retval)
{
    njs_int_t            ret;
    njs_function_t      *callback;
    njs_opaque_value_t   promise, arguments[2];

    ret = njs_vm_promise_create(vm, njs_value_arg(&promise),
                                njs_value_arg(&arguments));
    if (ret != NJS_OK) {
        goto error;
    }

    callback = njs_vm_function_alloc(vm, ngx_js_http_promise_trampoline, 0, 0);
    if (callback == NULL) {
        goto error;
    }

    njs_value_assign(&arguments[0], &arguments[(rc != NJS_OK)]);

    if (rc != NJS_OK) {
        njs_vm_exception_get(vm, njs_value_arg(&arguments[1]));

    } else {
        njs_value_assign(&arguments[1], result);
    }

    ret = njs_vm_enqueue_job(vm, callback, njs_value_arg(&arguments), 2);
    if (ret == NJS_ERROR) {
        goto error;
    }

    njs_value_assign(retval, &promise);

    return NJS_OK;

error:

    njs_vm_error(vm, "internal error");

    return NJS_ERROR;
}


static void
ngx_js_fetch_done(ngx_js_fetch_t *fetch, njs_opaque_value_t *retval,
    njs_int_t rc)
{
    njs_vm_t            *vm;
    ngx_js_ctx_t        *ctx;
    ngx_js_http_t       *http;
    ngx_js_event_t      *event;
    njs_opaque_value_t   arguments[2], *action;

    http = &fetch->http;

    ngx_log_debug2(NGX_LOG_DEBUG_EVENT, http->log, 0,
                   "js http done fetch:%p rc:%i", fetch, (ngx_int_t) rc);

    ngx_js_http_close_peer(http);

    if (fetch->event != NULL) {
        action = &fetch->promise_callbacks[(rc != NJS_OK)];
        njs_value_assign(&arguments[0], action);
        njs_value_assign(&arguments[1], retval);

        vm = fetch->vm;
        event = fetch->event;

        rc = ngx_js_call(vm, njs_value_function(njs_value_arg(&event->function)),
                         &arguments[0], 2);

        ctx = ngx_external_ctx(vm,  njs_vm_external_ptr(vm));
        ngx_js_del_event(ctx, event);

        ngx_external_event_finalize(vm)(njs_vm_external_ptr(vm), 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, njs_value_t *retval)
{
    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_request_constructor(njs_vm_t *vm, ngx_js_request_t *request,
    ngx_url_t *u, njs_external_ptr_t external, njs_value_t *args,
    njs_uint_t nargs)
{
    njs_int_t            ret;
    ngx_uint_t           rc;
    ngx_pool_t          *pool;
    njs_value_t         *input, *init, *value, *headers;
    ngx_js_request_t    *orig;
    njs_opaque_value_t   lvalue;

    static const njs_str_t body_key = njs_str("body");
    static const njs_str_t cache_key = njs_str("cache");
    static const njs_str_t cred_key = njs_str("credentials");
    static const njs_str_t headers_key = njs_str("headers");
    static const njs_str_t mode_key = njs_str("mode");
    static const njs_str_t method_key = njs_str("method");

    input = njs_arg(args, nargs, 1);
    if (njs_value_is_undefined(input)) {
        njs_vm_error(vm, "1st argument is required");
        return NJS_ERROR;
    }

    /*
     * set by ngx_memzero():
     *
     *  request->url.length = 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_js_request_t));

    request->method.data = (u_char *) "GET";
    request->method.len = 3;
    request->body.data = (u_char *) "";
    request->body.len = 0;
    request->headers.guard = GUARD_REQUEST;

    pool = ngx_external_pool(vm, external);

    rc = ngx_list_init(&request->headers.header_list, pool, 4,
                       sizeof(ngx_js_tb_elt_t));
    if (rc != NGX_OK) {
        njs_vm_memory_error(vm);
        return NJS_ERROR;
    }

    if (njs_value_is_string(input)) {
        ret = ngx_js_ngx_string(vm, input, &request->url);
        if (ret != NJS_OK) {
            njs_vm_error(vm, "failed to convert url arg");
            return NJS_ERROR;
        }

    } else {
        orig = njs_vm_external(vm, ngx_http_js_fetch_request_proto_id, input);
        if (orig == NULL) {
            njs_vm_error(vm, "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_js_headers_inherit(vm, &request->headers, &orig->headers);
        if (ret != NJS_OK) {
            return NJS_ERROR;
        }
    }

    ngx_js_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 = NGX_JS_HTTP_DEFAULT_PORT;
    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 = NGX_JS_HTTPS_DEFAULT_PORT;
#endif

    } else {
        njs_vm_error(vm, "unsupported URL schema (only http or https are"
                     " supported)");
        return NJS_ERROR;
    }

    if (ngx_parse_url(pool, u) != NGX_OK) {
        njs_vm_error(vm, "invalid url");
        return NJS_ERROR;
    }

    init = njs_arg(args, nargs, 2);

    if (njs_value_is_object(init)) {
        value = njs_vm_object_prop(vm, init, &method_key, &lvalue);
        if (value != NULL && ngx_js_ngx_string(vm, value, &request->method)
            != NGX_OK)
        {
            njs_vm_error(vm, "invalid Request method");
            return NJS_ERROR;
        }

        ret = ngx_js_method_process(vm, request);
        if (ret != NJS_OK) {
            return NJS_ERROR;
        }

        value = njs_vm_object_prop(vm, init, &cache_key, &lvalue);
        if (value != NULL) {
            ret = ngx_fetch_flag_set(vm, ngx_js_fetch_cache_modes, value,
                                     "cache");
            if (ret == NJS_ERROR) {
                return NJS_ERROR;
            }

            request->cache_mode = ret;
        }

        value = njs_vm_object_prop(vm, init, &cred_key, &lvalue);
        if (value != NULL) {
            ret = ngx_fetch_flag_set(vm, ngx_js_fetch_credentials, value,
                                     "credentials");
            if (ret == NJS_ERROR) {
                return NJS_ERROR;
            }

            request->credentials = ret;
        }

        value = njs_vm_object_prop(vm, init, &mode_key, &lvalue);
        if (value != NULL) {
            ret = ngx_fetch_flag_set(vm, ngx_js_fetch_modes, value, "mode");
            if (ret == NJS_ERROR) {
                return NJS_ERROR;
            }

            request->mode = ret;
        }

        headers = njs_vm_object_prop(vm, init, &headers_key, &lvalue);
        if (headers != NULL) {
            if (!njs_value_is_object(headers)) {
                njs_vm_error(vm, "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_js_headers_t));
            request->headers.guard = GUARD_REQUEST;

            rc = ngx_list_init(&request->headers.header_list, pool, 4,
                               sizeof(ngx_js_tb_elt_t));
            if (rc != NGX_OK) {
                njs_vm_memory_error(vm);
                return NJS_ERROR;
            }

            ret = ngx_js_headers_fill(vm, &request->headers, headers);
            if (ret != NJS_OK) {
                return NJS_ERROR;
            }
        }

        value = njs_vm_object_prop(vm, init, &body_key, &lvalue);
        if (value != NULL) {
            if (ngx_js_ngx_string(vm, value, &request->body) != NGX_OK) {
                njs_vm_error(vm, "invalid Request body");
                return NJS_ERROR;
            }

            if (request->headers.content_type == NULL
                && njs_value_is_string(value))
            {
                ret = ngx_js_headers_append(vm, &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) {
                    return NJS_ERROR;
                }
            }
        }
    }

    return NJS_OK;
}


static ngx_int_t
ngx_js_fetch_append_headers(ngx_js_http_t *http, ngx_js_headers_t *headers,
    u_char *name, size_t len, u_char *value, size_t vlen)
{
    ngx_js_fetch_t  *fetch;

    fetch = (ngx_js_fetch_t *) http;

    return ngx_js_headers_append(fetch->vm, headers, name, len, value, vlen);
}


static void
ngx_js_fetch_process_done(ngx_js_http_t *http)
{
    njs_int_t       ret;
    ngx_js_fetch_t  *fetch;

    fetch = (ngx_js_fetch_t *) http;

    ret = njs_vm_external_create(fetch->vm,
                                 njs_value_arg(&fetch->response_value),
                                 ngx_http_js_fetch_response_proto_id,
                                 &fetch->http.response, 0);
    if (ret != NJS_OK) {
        ngx_js_fetch_error(http, "fetch response creation failed");
        return;
    }

    ngx_js_fetch_done(fetch, &fetch->response_value, NJS_OK);
}


static njs_int_t
ngx_js_headers_append(njs_vm_t *vm, ngx_js_headers_t *headers,
    u_char *name, size_t len, u_char *value, size_t vlen)
{
    u_char           *p, *end;
    ngx_int_t         ret;
    ngx_uint_t        i;
    ngx_js_tb_elt_t  *h, **ph;
    ngx_list_part_t  *part;

    ngx_js_http_trim(&value, &vlen, 0);

    ret = ngx_js_check_header_name(name, len);
    if (ret != NGX_OK) {
        njs_vm_error(vm, "invalid header name");
        return NJS_ERROR;
    }

    p = value;
    end = p + vlen;

    while (p < end) {
        if (*p == '\0') {
            njs_vm_error(vm, "invalid header value");
            return NJS_ERROR;
        }

        p++;
    }

    if (headers->guard == GUARD_IMMUTABLE) {
        njs_vm_error(vm, "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) {
        njs_vm_memory_error(vm);
        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_headers_js_get(njs_vm_t *vm, njs_value_t *value, njs_str_t *name,
    njs_value_t *retval, njs_bool_t as_array)
{
    njs_int_t          rc;
    njs_chb_t          chain;
    ngx_uint_t         i;
    ngx_js_tb_elt_t   *h, *ph;
    ngx_list_part_t   *part;
    ngx_js_headers_t  *headers;

    headers = njs_vm_external(vm, ngx_http_js_fetch_headers_proto_id, value);
    if (headers == NULL) {
        njs_value_null_set(retval);
        return NJS_DECLINED;
    }

    if (as_array) {
        rc = njs_vm_array_alloc(vm, retval, 2);
        if (rc != NJS_OK) {
            return NJS_ERROR;
        }
    }

    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->length
            && njs_strncasecmp(h[i].key.data, name->start, name->length) == 0)
        {
            ph = &h[i];
            break;
        }
    }

    if (as_array) {
        while (ph != NULL) {
            value = njs_vm_array_push(vm, retval);
            if (value == NULL) {
                return NJS_ERROR;
            }

            rc = njs_vm_value_string_create(vm, value, ph->value.data,
                                            ph->value.len);
            if (rc != NJS_OK) {
                return NJS_ERROR;
            }

            ph = ph->next;
        }

        return NJS_OK;
    }

    if (ph == NULL) {
        njs_value_null_set(retval);
        return NJS_DECLINED;
    }

    NJS_CHB_MP_INIT(&chain, njs_vm_memory_pool(vm));

    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;
    }

    rc = njs_vm_value_string_create_chb(vm, retval, &chain);

    njs_chb_destroy(&chain);

    return rc;
}


static njs_int_t
ngx_headers_js_ext_append(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
    njs_index_t unused, njs_value_t *retval)
{
    njs_int_t          ret;
    njs_str_t          name, value;
    ngx_js_headers_t  *headers;

    headers = njs_vm_external(vm, ngx_http_js_fetch_headers_proto_id,
                              njs_argument(args, 0));
    if (headers == NULL) {
        njs_vm_error(vm, "\"this\" is not fetch headers object");
        return NJS_ERROR;
    }

    ret = ngx_js_string(vm, njs_arg(args, nargs, 1), &name);
    if (ret != NJS_OK) {
        return NJS_ERROR;
    }

    ret = ngx_js_string(vm, njs_arg(args, nargs, 2), &value);
    if (ret != NJS_OK) {
        return NJS_ERROR;
    }

    ret = ngx_js_headers_append(vm, headers, name.start, name.length,
                                value.start, value.length);
    if (ret != NJS_OK) {
        return NJS_ERROR;
    }

    njs_value_undefined_set(retval);

    return NJS_OK;
}


static njs_int_t
ngx_headers_js_ext_delete(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
    njs_index_t unused, njs_value_t *retval)
{
    njs_int_t          ret;
    njs_str_t          name;
    ngx_uint_t         i;
    ngx_list_part_t   *part;
    ngx_js_tb_elt_t   *h;
    ngx_js_headers_t  *headers;

    headers = njs_vm_external(vm, ngx_http_js_fetch_headers_proto_id,
                              njs_argument(args, 0));
    if (headers == NULL) {
        njs_vm_error(vm, "\"this\" is not fetch headers object");
        return NJS_ERROR;
    }

    ret = ngx_js_string(vm, njs_arg(args, nargs, 1), &name);
    if (ret != NJS_OK) {
        return NJS_ERROR;
    }

    part = &headers->header_list.part;
    h = part->elts;

    for (i = 0; /* void */; i++) {

        if (i >= part->nelts) {
            if (part->next == NULL) {
                break;
            }

            part = part->next;
            h = part->elts;
            i = 0;
        }

        if (h[i].hash == 0) {
            continue;
        }

        if (name.length == h[i].key.len
            && (njs_strncasecmp(name.start, h[i].key.data, name.length) == 0))
        {
            h[i].hash = 0;
        }
    }

    if (name.length == njs_strlen("Content-Type")
        && ngx_strncasecmp(name.start, (u_char *) "Content-Type", name.length)
           == 0)
    {
        headers->content_type = NULL;
    }

    njs_value_undefined_set(retval);

    return NJS_OK;
}


static njs_int_t
ngx_headers_js_ext_for_each(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
    njs_index_t unused, njs_value_t *retval)
{
    int64_t              length;
    njs_int_t            ret;
    njs_str_t            name;
    njs_value_t         *this, *callback;
    ngx_js_headers_t    *headers;
    njs_opaque_value_t  *k, *end, keys, arguments[2];

    this = njs_argument(args, 0);

    headers = njs_vm_external(vm, ngx_http_js_fetch_headers_proto_id, this);
    if (headers == NULL) {
        njs_vm_error(vm, "\"this\" is not fetch headers object");
        return NJS_ERROR;
    }

    callback = njs_arg(args, nargs, 1);

    if (!njs_value_is_function(callback)) {
        njs_vm_error(vm, "\"callback\" is not a function");
        return NJS_ERROR;
    }

    ret = ngx_headers_js_ext_keys(vm, this, njs_value_arg(&keys));
    if (ret != NJS_OK) {
        return NJS_ERROR;
    }

    (void) njs_vm_array_length(vm, njs_value_arg(&keys), &length);

    k = (njs_opaque_value_t *) njs_vm_array_start(vm, njs_value_arg(&keys));
    end = k + length;

    for (; k < end; k++) {
        ret = ngx_js_string(vm, njs_value_arg(k), &name);
        if (ret != NJS_OK) {
            return NJS_ERROR;
        }

        ret = ngx_headers_js_get(vm, this, &name, njs_value_arg(&arguments[1]),
                                 0);
        if (ret != NJS_OK) {
            return NJS_ERROR;
        }

        njs_value_assign(&arguments[0], k);

        ret = njs_vm_call(vm, njs_value_function(callback),
                          njs_value_arg(&arguments[0]), 2);
        if (ret != NJS_OK) {
            return NJS_ERROR;
        }
    }

    return NJS_OK;
}


static njs_int_t
ngx_headers_js_ext_get(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
    njs_index_t as_array, njs_value_t *retval)
{
    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_headers_js_get(vm, njs_argument(args, 0), &name,
                             retval, as_array);

    return (ret != NJS_ERROR) ? NJS_OK : NJS_ERROR;
}


static njs_int_t
ngx_headers_js_ext_has(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
    njs_index_t unused, njs_value_t *retval)
{
    njs_int_t  ret;
    njs_str_t  name;

    ret = ngx_js_string(vm, njs_arg(args, nargs, 1), &name);
    if (ret != NJS_OK) {
        return NJS_ERROR;
    }

    ret = ngx_headers_js_get(vm, njs_argument(args, 0), &name,
                             retval, 0);
    if (ret == NJS_ERROR) {
        return NJS_ERROR;
    }

    njs_value_boolean_set(retval, ret == NJS_OK);

    return NJS_OK;
}


static njs_int_t
ngx_headers_js_ext_prop(njs_vm_t *vm, njs_object_prop_t *prop, uint32_t atom_id,
    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, atom_id, &name);
    if (ret != NJS_OK) {
        return NJS_ERROR;
    }

    return ngx_headers_js_get(vm, value, &name, retval, 0);
}


static ngx_int_t
ngx_string_compare(const void *s1, const void *s2, void *ctx)
{
    return njs_vm_string_compare(ctx, s1, s2);
}


static void
ngx_js_sort(void *base, size_t n, size_t size,
    ngx_int_t (*cmp)(const void *, const void *, void *), void *ctx)
{
    u_char  *p1, *p2, *p;

    p = ngx_alloc(size, ngx_cycle->log);
    if (p == NULL) {
        return;
    }

    for (p1 = (u_char *) base + size;
         p1 < (u_char *) base + n * size;
         p1 += size)
    {
        ngx_memcpy(p, p1, size);

        for (p2 = p1;
             p2 > (u_char *) base && cmp(p2 - size, p, ctx) > 0;
             p2 -= size)
        {
            ngx_memcpy(p2, p2 - size, size);
        }

        ngx_memcpy(p2, p, size);
    }

    ngx_free(p);
}


static njs_int_t
ngx_headers_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_list_part_t   *part;
    ngx_js_tb_elt_t   *h;
    ngx_js_headers_t  *headers;

    headers = njs_vm_external(vm, ngx_http_js_fetch_headers_proto_id, value);
    if (headers == NULL) {
        njs_value_null_set(keys);
        return NJS_DECLINED;
    }

    rc = njs_vm_array_alloc(vm, keys, 8);
    if (rc != NJS_OK) {
        return NJS_ERROR;
    }

    length = 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;
        }

        start = njs_vm_array_start(vm, keys);

        for (k = 0; k < length; k++) {
            njs_value_string_get(vm, njs_argument(start, k), &hdr);

            if (h[i].key.len == hdr.length
                && njs_strncasecmp(h[i].key.data, hdr.start, hdr.length) == 0)
            {
                break;
            }
        }

        if (k == length) {
            value = njs_vm_array_push(vm, keys);
            if (value == NULL) {
                return NJS_ERROR;
            }

            rc = njs_vm_value_string_create(vm, value, h[i].key.data,
                                            h[i].key.len);
            if (rc != NJS_OK) {
                return NJS_ERROR;
            }

            length++;
        }
    }

    start = njs_vm_array_start(vm, keys);

    ngx_js_sort(start, (size_t) length, sizeof(njs_opaque_value_t),
                ngx_string_compare, vm);

    return NJS_OK;
}


static njs_int_t
ngx_headers_js_ext_set(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
    njs_index_t unused, njs_value_t *retval)
{
    njs_int_t          ret;
    njs_str_t          name, value;
    ngx_uint_t         i;
    ngx_list_part_t   *part;
    ngx_js_tb_elt_t   *h, **ph, **pp;
    ngx_js_headers_t  *headers;

    headers = njs_vm_external(vm, ngx_http_js_fetch_headers_proto_id,
                              njs_argument(args, 0));
    if (headers == NULL) {
        njs_vm_error(vm, "\"this\" is not fetch headers object");
        return NJS_ERROR;
    }

    ret = ngx_js_string(vm, njs_arg(args, nargs, 1), &name);
    if (ret != NJS_OK) {
        return NJS_ERROR;
    }

    ret = ngx_js_string(vm, njs_arg(args, nargs, 2), &value);
    if (ret != NJS_OK) {
        return NJS_ERROR;
    }

    part = &headers->header_list.part;
    h = part->elts;

    for (i = 0; /* void */; i++) {

        if (i >= part->nelts) {
            if (part->next == NULL) {
                break;
            }

            part = part->next;
            h = part->elts;
            i = 0;
        }

        if (h[i].hash == 0) {
            continue;
        }

        if (name.length == h[i].key.len
            && (njs_strncasecmp(name.start, h[i].key.data, name.length) == 0))
        {
            h[i].value.len = value.length;
            h[i].value.data = value.start;

            ph = &h[i].next;

            while (*ph) {
                pp = ph;
                ph = &(*ph)->next;
                *pp = NULL;
            }

            goto done;
        }
    }

    ret = ngx_js_headers_append(vm, headers, name.start, name.length,
                                value.start, value.length);
    if (ret != NJS_OK) {
        return NJS_ERROR;
    }

done:

    njs_value_undefined_set(retval);

    return NJS_OK;
}


static njs_int_t
ngx_request_js_ext_body(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
    njs_index_t type, njs_value_t *retval)
{
    njs_int_t            ret;
    ngx_js_request_t    *request;
    njs_opaque_value_t   result;

    request = njs_vm_external(vm, ngx_http_js_fetch_request_proto_id,
                              njs_argument(args, 0));
    if (request == NULL) {
        njs_value_undefined_set(retval);
        return NJS_DECLINED;
    }

    if (request->body_used) {
        njs_vm_error(vm, "body stream already read");
        return NJS_ERROR;
    }

    request->body_used = 1;

    switch (type) {
    case NGX_JS_BODY_ARRAY_BUFFER:
        ret = njs_vm_value_array_buffer_set(vm, njs_value_arg(&result),
                                            request->body.data,
                                            request->body.len);
        if (ret != NJS_OK) {
            njs_vm_memory_error(vm);
            return NJS_ERROR;
        }

        break;

    case NGX_JS_BODY_JSON:
    case NGX_JS_BODY_TEXT:
    default:
        ret = njs_vm_value_string_create(vm, njs_value_arg(&result),
                                         request->body.data,
                                         request->body.len);
        if (ret != NJS_OK) {
            njs_vm_memory_error(vm);
            return NJS_ERROR;
        }

        if (type == NGX_JS_BODY_JSON) {
            ret = njs_vm_json_parse(vm, njs_value_arg(&result), 1,
                                    njs_value_arg(&result));
        }
    }

    return ngx_js_fetch_promissified_result(vm, njs_value_arg(&result), ret,
                                            retval);
}


static njs_int_t
ngx_request_js_ext_body_used(njs_vm_t *vm, njs_object_prop_t *prop,
    uint32_t unused, njs_value_t *value, njs_value_t *setval,
    njs_value_t *retval)
{
    ngx_js_request_t  *request;

    request = njs_vm_external(vm, ngx_http_js_fetch_request_proto_id, value);
    if (request == NULL) {
        njs_value_undefined_set(retval);
        return NJS_DECLINED;
    }

    njs_value_boolean_set(retval, request->body_used);

    return NJS_OK;
}


static njs_int_t
ngx_request_js_ext_cache(njs_vm_t *vm, njs_object_prop_t *prop, uint32_t unused,
    njs_value_t *value, njs_value_t *setval, njs_value_t *retval)
{
    ngx_js_request_t  *request;

    request = njs_vm_external(vm, ngx_http_js_fetch_request_proto_id, value);
    if (request == NULL) {
        njs_value_undefined_set(retval);
        return NJS_DECLINED;
    }

    return ngx_fetch_flag(vm, ngx_js_fetch_cache_modes,
                          (njs_int_t) request->cache_mode, retval);
}


static njs_int_t
ngx_request_js_ext_credentials(njs_vm_t *vm, njs_object_prop_t *prop,
    uint32_t unused, njs_value_t *value, njs_value_t *setval,
    njs_value_t *retval)
{
    ngx_js_request_t  *request;

    request = njs_vm_external(vm, ngx_http_js_fetch_request_proto_id, value);
    if (request == NULL) {
        njs_value_undefined_set(retval);
        return NJS_DECLINED;
    }

    return ngx_fetch_flag(vm, ngx_js_fetch_credentials,
                          (njs_int_t) request->credentials, retval);
}


static njs_int_t
ngx_request_js_ext_headers(njs_vm_t *vm, njs_object_prop_t *prop,
    uint32_t unused, njs_value_t *value, njs_value_t *setval,
    njs_value_t *retval)
{
    njs_int_t          ret;
    ngx_js_request_t  *request;

    request = njs_vm_external(vm, ngx_http_js_fetch_request_proto_id, value);
    if (request == NULL) {
        njs_value_undefined_set(retval);
        return NJS_DECLINED;
    }

    if (njs_value_is_null(njs_value_arg(&request->header_value))) {
        ret = njs_vm_external_create(vm, njs_value_arg(&request->header_value),
                                     ngx_http_js_fetch_headers_proto_id,
                                     &request->headers, 0);
        if (ret != NJS_OK) {
            njs_vm_error(vm, "fetch header creation failed");
            return NJS_ERROR;
        }
    }

    njs_value_assign(retval, &request->header_value);

    return NJS_OK;
}


static njs_int_t
ngx_request_js_ext_mode(njs_vm_t *vm, njs_object_prop_t *prop, uint32_t unused,
    njs_value_t *value, njs_value_t *setval, njs_value_t *retval)
{
    ngx_js_request_t  *request;

    request = njs_vm_external(vm, ngx_http_js_fetch_request_proto_id, value);
    if (request == NULL) {
        njs_value_undefined_set(retval);
        return NJS_DECLINED;
    }

    return ngx_fetch_flag(vm, ngx_js_fetch_modes, (njs_int_t) request->mode,
                          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 type, njs_value_t *retval)
{
    njs_int_t            ret;
    njs_str_t            string;
    ngx_js_response_t   *response;
    njs_opaque_value_t   result;

    response = njs_vm_external(vm, ngx_http_js_fetch_response_proto_id,
                               njs_argument(args, 0));
    if (response == NULL) {
        njs_value_undefined_set(retval);
        return NJS_DECLINED;
    }

    if (response->body_used) {
        njs_vm_error(vm, "body stream already read");
        return NJS_ERROR;
    }

    response->body_used = 1;

    ret = njs_chb_join(&response->chain, &string);
    if (ret != NJS_OK) {
        njs_vm_memory_error(vm);
        return NJS_ERROR;
    }

    switch (type) {
    case NGX_JS_BODY_ARRAY_BUFFER:
        ret = njs_vm_value_array_buffer_set(vm, njs_value_arg(&result),
                                            string.start, string.length);
        if (ret != NJS_OK) {
            njs_vm_memory_error(vm);
            return NJS_ERROR;
        }

        break;

    case NGX_JS_BODY_JSON:
    case NGX_JS_BODY_TEXT:
    default:
        ret = njs_vm_value_string_create(vm, njs_value_arg(&result),
                                         string.start, string.length);
        if (ret != NJS_OK) {
            njs_vm_memory_error(vm);
            return NJS_ERROR;
        }

        if (type == NGX_JS_BODY_JSON) {
            ret = njs_vm_json_parse(vm, njs_value_arg(&result), 1, retval);
            njs_value_assign(&result, retval);
        }
    }

    return ngx_js_fetch_promissified_result(vm, njs_value_arg(&result), ret,
                                            retval);
}


static njs_int_t
ngx_response_js_ext_body_used(njs_vm_t *vm, njs_object_prop_t *prop,
    uint32_t unused, njs_value_t *value, njs_value_t *setval,
    njs_value_t *retval)
{
    ngx_js_response_t  *response;

    response = njs_vm_external(vm, ngx_http_js_fetch_response_proto_id, value);
    if (response == NULL) {
        njs_value_undefined_set(retval);
        return NJS_DECLINED;
    }

    njs_value_boolean_set(retval, response->body_used);

    return NJS_OK;
}


static njs_int_t
ngx_response_js_ext_headers(njs_vm_t *vm, njs_object_prop_t *prop,
    uint32_t unused, njs_value_t *value, njs_value_t *setval,
    njs_value_t *retval)
{
    njs_int_t           ret;
    ngx_js_response_t  *response;

    response = njs_vm_external(vm, ngx_http_js_fetch_response_proto_id, value);
    if (response == NULL) {
        njs_value_undefined_set(retval);
        return NJS_DECLINED;
    }

    if (njs_value_is_null(njs_value_arg(&response->header_value))) {
        ret = njs_vm_external_create(vm, njs_value_arg(&response->header_value),
                                     ngx_http_js_fetch_headers_proto_id,
                                     &response->headers, 0);
        if (ret != NJS_OK) {
            njs_vm_error(vm, "fetch header creation failed");
            return NJS_ERROR;
        }
    }

    njs_value_assign(retval, &response->header_value);

    return NJS_OK;
}


static njs_int_t
ngx_response_js_ext_ok(njs_vm_t *vm, njs_object_prop_t *prop, uint32_t unused,
    njs_value_t *value, njs_value_t *setval, njs_value_t *retval)
{
    ngx_uint_t          code;
    ngx_js_response_t  *response;

    response = njs_vm_external(vm, ngx_http_js_fetch_response_proto_id, value);
    if (response == NULL) {
        njs_value_undefined_set(retval);
        return NJS_DECLINED;
    }

    code = response->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,
    uint32_t unused, njs_value_t *value, njs_value_t *setval,
    njs_value_t *retval)
{
    ngx_js_response_t  *response;

    response = njs_vm_external(vm, ngx_http_js_fetch_response_proto_id, value);
    if (response == NULL) {
        njs_value_undefined_set(retval);
        return NJS_DECLINED;
    }

    njs_value_number_set(retval, response->code);

    return NJS_OK;
}


static njs_int_t
ngx_response_js_ext_status_text(njs_vm_t *vm, njs_object_prop_t *prop,
    uint32_t unused, njs_value_t *value, njs_value_t *setval,
    njs_value_t *retval)
{
    ngx_js_response_t  *response;

    response = njs_vm_external(vm, ngx_http_js_fetch_response_proto_id, value);
    if (response == NULL) {
        njs_value_undefined_set(retval);
        return NJS_DECLINED;
    }

    njs_vm_value_string_create(vm, retval, response->status_text.data,
                               response->status_text.len);

    return NJS_OK;
}


static njs_int_t
ngx_response_js_ext_type(njs_vm_t *vm, njs_object_prop_t *prop, uint32_t unused,
    njs_value_t *value, njs_value_t *setval, njs_value_t *retval)
{
    ngx_js_response_t  *response;

    response = njs_vm_external(vm, ngx_http_js_fetch_response_proto_id, value);
    if (response == NULL) {
        njs_value_undefined_set(retval);
        return NJS_DECLINED;
    }

    return njs_vm_value_string_create(vm, retval, (u_char *) "basic",
                                      njs_length("basic"));
}


static njs_int_t
ngx_fetch_flag(njs_vm_t *vm, const ngx_js_entry_t *entries, njs_int_t value,
    njs_value_t *retval)
{
    const ngx_js_entry_t  *e;

    for (e = entries; e->name.length != 0; e++) {
        if (e->value == value) {
            return njs_vm_value_string_create(vm, retval, e->name.start,
                                              e->name.length);
        }
    }

    return NJS_ERROR;
}


static njs_int_t
ngx_fetch_flag_set(njs_vm_t *vm, const ngx_js_entry_t *entries,
     njs_value_t *value, const char *type)
{
    njs_int_t              ret;
    njs_str_t              flag;
    const ngx_js_entry_t  *e;

    ret = ngx_js_string(vm, value, &flag);
    if (ret != NJS_OK) {
        return NJS_ERROR;
    }

    for (e = entries; e->name.length != 0; e++) {
        if (njs_strstr_case_eq(&flag, &e->name)) {
            return e->value;
        }
    }

    njs_vm_error(vm, "unknown %s type: %V", type, &flag);

    return NJS_ERROR;
}


static njs_int_t
ngx_js_fetch_function_bind(njs_vm_t *vm, const njs_str_t *name,
    njs_function_native_t native, njs_bool_t ctor)
{
    njs_function_t      *f;
    njs_opaque_value_t   value;

    f = njs_vm_function_alloc(vm, native, 1, ctor);
    if (f == NULL) {
        return NJS_ERROR;
    }

    njs_value_function_set(njs_value_arg(&value), f);

    return njs_vm_bind(vm, name, njs_value_arg(&value), 1);
}


static njs_int_t
ngx_js_fetch_init(njs_vm_t *vm)
{
    njs_int_t  ret;

    static const njs_str_t  headers = njs_str("Headers");
    static const njs_str_t  request = njs_str("Request");
    static const njs_str_t  response = njs_str("Response");

    ngx_http_js_fetch_headers_proto_id = njs_vm_external_prototype(vm,
                                          ngx_js_ext_http_headers,
                                          njs_nitems(ngx_js_ext_http_headers));
    if (ngx_http_js_fetch_headers_proto_id < 0) {
        return NJS_ERROR;
    }

    ngx_http_js_fetch_request_proto_id = njs_vm_external_prototype(vm,
                                          ngx_js_ext_http_request,
                                          njs_nitems(ngx_js_ext_http_request));
    if (ngx_http_js_fetch_request_proto_id < 0) {
        return NJS_ERROR;
    }

    ngx_http_js_fetch_response_proto_id = njs_vm_external_prototype(vm,
                                          ngx_js_ext_http_response,
                                          njs_nitems(ngx_js_ext_http_response));
    if (ngx_http_js_fetch_response_proto_id < 0) {
        return NJS_ERROR;
    }

    ret = ngx_js_fetch_function_bind(vm, &headers,
                                     ngx_js_ext_headers_constructor, 1);
    if (ret != NJS_OK) {
        return NJS_ERROR;
    }

    ret = ngx_js_fetch_function_bind(vm, &request,
                                     ngx_js_ext_request_constructor, 1);
    if (ret != NJS_OK) {
        return NJS_ERROR;
    }

    ret = ngx_js_fetch_function_bind(vm, &response,
                                     ngx_js_ext_response_constructor, 1);
    if (ret != NJS_OK) {
        return NJS_ERROR;
    }

    return NJS_OK;
}
