aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorZhidao HONG <z.hong@f5.com>2025-04-22 10:56:27 +0800
committerhongzhidao <hongzhidao@gmail.com>2025-05-09 01:14:21 +0800
commitb7f76b71f4a1511cd9d954e6da3b0da4c9de40fb (patch)
tree7e87027c23f8d0e6424fee3900b1432433f59377
parenta1e3f15f0a580f7b9d465a5b2ed2287a1705dc84 (diff)
downloadnjs-b7f76b71f4a1511cd9d954e6da3b0da4c9de40fb.tar.gz
njs-b7f76b71f4a1511cd9d954e6da3b0da4c9de40fb.zip
Fetch: QuickJS support.
-rw-r--r--nginx/config1
-rw-r--r--nginx/ngx_http_js_module.c3
-rw-r--r--nginx/ngx_js.c1
-rw-r--r--nginx/ngx_js.h7
-rw-r--r--nginx/ngx_js_fetch.c6
-rw-r--r--nginx/ngx_qjs_fetch.c2520
-rw-r--r--nginx/ngx_stream_js_module.c1
-rw-r--r--nginx/t/js_fetch.t4
-rw-r--r--nginx/t/js_fetch_https.t2
-rw-r--r--nginx/t/js_fetch_objects.t23
-rw-r--r--nginx/t/js_fetch_resolver.t2
-rw-r--r--nginx/t/js_fetch_timeout.t2
-rw-r--r--nginx/t/js_fetch_verify.t2
-rw-r--r--nginx/t/js_periodic_fetch.t2
-rw-r--r--nginx/t/stream_js_fetch.t2
-rw-r--r--nginx/t/stream_js_fetch_https.t2
-rw-r--r--nginx/t/stream_js_fetch_init.t2
-rw-r--r--nginx/t/stream_js_periodic_fetch.t1
-rw-r--r--src/qjs.c25
-rw-r--r--src/qjs.h2
20 files changed, 2585 insertions, 25 deletions
diff --git a/nginx/config b/nginx/config
index b994f97f..1c303d9c 100644
--- a/nginx/config
+++ b/nginx/config
@@ -156,6 +156,7 @@ NJS_ENGINE_LIB="$ngx_addon_dir/../build/libnjs.a"
if [ "$NJS_HAVE_QUICKJS" = "YES" ]; then
NJS_ENGINE_DEP="$ngx_addon_dir/../build/libqjs.a"
NJS_ENGINE_LIB="$ngx_addon_dir/../build/libnjs.a $ngx_addon_dir/../build/libqjs.a"
+ QJS_SRCS="$QJS_SRCS $ngx_addon_dir/ngx_qjs_fetch.c"
fi
if [ $HTTP != NO ]; then
diff --git a/nginx/ngx_http_js_module.c b/nginx/ngx_http_js_module.c
index 3ac95478..40bb83a5 100644
--- a/nginx/ngx_http_js_module.c
+++ b/nginx/ngx_http_js_module.c
@@ -1134,6 +1134,9 @@ static JSClassDef ngx_http_qjs_headers_out_class = {
qjs_module_t *njs_http_qjs_addon_modules[] = {
&ngx_qjs_ngx_module,
&ngx_qjs_ngx_shared_dict_module,
+#ifdef NJS_HAVE_QUICKJS
+ &ngx_qjs_ngx_fetch_module,
+#endif
/*
* Shared addons should be in the same order and the same positions
* in all nginx modules.
diff --git a/nginx/ngx_js.c b/nginx/ngx_js.c
index 34221d28..e4bae32a 100644
--- a/nginx/ngx_js.c
+++ b/nginx/ngx_js.c
@@ -441,6 +441,7 @@ static const JSCFunctionListEntry ngx_qjs_ext_ngx[] = {
JS_CGETSET_MAGIC_DEF("ERR", ngx_qjs_ext_constant_integer, NULL,
NGX_LOG_ERR),
JS_CGETSET_DEF("error_log_path", ngx_qjs_ext_error_log_path, NULL),
+ JS_CFUNC_DEF("fetch", 2, ngx_qjs_ext_fetch),
JS_CGETSET_MAGIC_DEF("INFO", ngx_qjs_ext_constant_integer, NULL,
NGX_LOG_INFO),
JS_CFUNC_MAGIC_DEF("log", 1, ngx_qjs_ext_log, 0),
diff --git a/nginx/ngx_js.h b/nginx/ngx_js.h
index e13efc41..bb7c1d26 100644
--- a/nginx/ngx_js.h
+++ b/nginx/ngx_js.h
@@ -63,6 +63,9 @@
#define NGX_QJS_CLASS_ID_SHARED (NGX_QJS_CLASS_ID_OFFSET + 11)
#define NGX_QJS_CLASS_ID_SHARED_DICT (NGX_QJS_CLASS_ID_OFFSET + 12)
#define NGX_QJS_CLASS_ID_SHARED_DICT_ERROR (NGX_QJS_CLASS_ID_OFFSET + 13)
+#define NGX_QJS_CLASS_ID_FETCH_HEADERS (NGX_QJS_CLASS_ID_OFFSET + 14)
+#define NGX_QJS_CLASS_ID_FETCH_REQUEST (NGX_QJS_CLASS_ID_OFFSET + 15)
+#define NGX_QJS_CLASS_ID_FETCH_RESPONSE (NGX_QJS_CLASS_ID_OFFSET + 16)
typedef struct ngx_js_loc_conf_s ngx_js_loc_conf_t;
@@ -346,6 +349,9 @@ ngx_int_t ngx_qjs_exception(ngx_engine_t *e, ngx_str_t *s);
ngx_int_t ngx_qjs_integer(JSContext *cx, JSValueConst val, ngx_int_t *n);
ngx_int_t ngx_qjs_string(JSContext *cx, JSValueConst val, ngx_str_t *str);
+JSValue ngx_qjs_ext_fetch(JSContext *cx, JSValueConst this_val, int argc,
+ JSValueConst *argv);
+
#define ngx_qjs_prop(cx, type, start, len) \
((type == NGX_JS_STRING) ? qjs_string_create(cx, start, len) \
: qjs_buffer_create(cx, (u_char *) start, len))
@@ -382,6 +388,7 @@ extern qjs_module_t qjs_xml_module;
extern qjs_module_t qjs_zlib_module;
extern qjs_module_t ngx_qjs_ngx_module;
extern qjs_module_t ngx_qjs_ngx_shared_dict_module;
+extern qjs_module_t ngx_qjs_ngx_fetch_module;
#endif
diff --git a/nginx/ngx_js_fetch.c b/nginx/ngx_js_fetch.c
index 05d220b9..45f2dc10 100644
--- a/nginx/ngx_js_fetch.c
+++ b/nginx/ngx_js_fetch.c
@@ -1171,7 +1171,7 @@ ngx_js_fetch_alloc(njs_vm_t *vm, ngx_pool_t *pool, ngx_log_t *log)
fetch->vm = vm;
fetch->event = event;
- ngx_log_debug1(NGX_LOG_DEBUG_EVENT, log, 0, "js fetch alloc:%p", fetch);
+ ngx_log_debug1(NGX_LOG_DEBUG_EVENT, log, 0, "js http alloc:%p", fetch);
return fetch;
@@ -1207,7 +1207,7 @@ ngx_js_fetch_destructor(ngx_js_event_t *event)
fetch = event->data;
http = &fetch->http;
- ngx_log_debug1(NGX_LOG_DEBUG_EVENT, http->log, 0, "js fetch destructor:%p",
+ ngx_log_debug1(NGX_LOG_DEBUG_EVENT, http->log, 0, "js http destructor:%p",
fetch);
ngx_js_http_resolve_done(http);
@@ -1273,7 +1273,7 @@ ngx_js_fetch_done(ngx_js_fetch_t *fetch, njs_opaque_value_t *retval,
http = &fetch->http;
ngx_log_debug2(NGX_LOG_DEBUG_EVENT, http->log, 0,
- "js fetch done fetch:%p rc:%i", fetch, (ngx_int_t) rc);
+ "js http done fetch:%p rc:%i", fetch, (ngx_int_t) rc);
ngx_js_http_close_peer(http);
diff --git a/nginx/ngx_qjs_fetch.c b/nginx/ngx_qjs_fetch.c
new file mode 100644
index 00000000..084162ba
--- /dev/null
+++ b/nginx/ngx_qjs_fetch.c
@@ -0,0 +1,2520 @@
+
+/*
+ * Copyright (C) hongzhidao
+ * Copyright (C) F5, Inc.
+ */
+
+
+#include <ngx_config.h>
+#include <ngx_core.h>
+#include <ngx_event.h>
+#include <ngx_event_connect.h>
+#include "ngx_js.h"
+#include "ngx_js_http.h"
+
+
+typedef struct {
+ ngx_str_t name;
+ ngx_int_t value;
+} ngx_qjs_entry_t;
+
+
+typedef struct {
+ ngx_js_http_t http;
+
+ JSContext *cx;
+ ngx_qjs_event_t *event;
+
+ JSValue response_value;
+
+ JSValue promise;
+ JSValue promise_callbacks[2];
+} ngx_qjs_fetch_t;
+
+
+static ngx_int_t ngx_qjs_method_process(JSContext *cx,
+ ngx_js_request_t *request);
+static ngx_int_t ngx_qjs_headers_inherit(JSContext *cx,
+ ngx_js_headers_t *headers, ngx_js_headers_t *orig);
+static ngx_int_t ngx_qjs_headers_fill(JSContext *cx, ngx_js_headers_t *headers,
+ JSValue init);
+static ngx_qjs_fetch_t *ngx_qjs_fetch_alloc(JSContext *cx, ngx_pool_t *pool,
+ ngx_log_t *log);
+static void ngx_qjs_fetch_error(ngx_js_http_t *http, const char *err);
+static void ngx_qjs_fetch_destructor(ngx_qjs_event_t *event);
+static void ngx_qjs_fetch_done(ngx_qjs_fetch_t *fetch, JSValue retval,
+ ngx_int_t rc);
+
+static ngx_int_t ngx_qjs_request_ctor(JSContext *cx, ngx_js_request_t *request,
+ ngx_url_t *u, int argc, JSValueConst *argv);
+
+static ngx_int_t ngx_qjs_fetch_append_headers(ngx_js_http_t *http,
+ ngx_js_headers_t *headers, u_char *name, size_t len, u_char *value,
+ size_t vlen);
+static void ngx_qjs_fetch_process_done(ngx_js_http_t *http);
+static ngx_int_t ngx_qjs_headers_append(JSContext *cx,
+ ngx_js_headers_t *headers, u_char *name, size_t len, u_char *value,
+ size_t vlen);
+
+static JSValue ngx_qjs_fetch_headers_ctor(JSContext *cx,
+ JSValueConst new_target, int argc, JSValueConst *argv);
+static int ngx_qjs_fetch_headers_own_property(JSContext *cx,
+ JSPropertyDescriptor *desc, JSValueConst obj, JSAtom prop);
+static int ngx_qjs_fetch_headers_own_property_names(JSContext *cx,
+ JSPropertyEnum **ptab, uint32_t *plen, JSValueConst obj);
+static JSValue ngx_qjs_ext_fetch_headers_append(JSContext *cx,
+ JSValueConst this_val, int argc, JSValueConst *argv);
+static JSValue ngx_qjs_ext_fetch_headers_delete(JSContext *cx,
+ JSValueConst this_val, int argc, JSValueConst *argv);
+static JSValue ngx_qjs_ext_fetch_headers_foreach(JSContext *cx,
+ JSValueConst this_val, int argc, JSValueConst *argv);
+static JSValue ngx_qjs_ext_fetch_headers_get(JSContext *cx,
+ JSValueConst this_val, int argc, JSValueConst *argv, int magic);
+static JSValue ngx_qjs_ext_fetch_headers_has(JSContext *cx,
+ JSValueConst this_val, int argc, JSValueConst *argv);
+static JSValue ngx_qjs_ext_fetch_headers_set(JSContext *cx,
+ JSValueConst this_val, int argc, JSValueConst *argv);
+
+static JSValue ngx_qjs_fetch_request_ctor(JSContext *cx,
+ JSValueConst new_target, int argc, JSValueConst *argv);
+static JSValue ngx_qjs_ext_fetch_request_body(JSContext *cx,
+ JSValueConst this_val, int argc, JSValueConst *argv, int magic);
+static JSValue ngx_qjs_ext_fetch_request_body_used(JSContext *cx,
+ JSValueConst this_val);
+static JSValue ngx_qjs_ext_fetch_request_cache(JSContext *cx,
+ JSValueConst this_val);
+static JSValue ngx_qjs_ext_fetch_request_credentials(JSContext *cx,
+ JSValueConst this_val);
+static JSValue ngx_qjs_ext_fetch_request_headers(JSContext *cx,
+ JSValueConst this_val);
+static JSValue ngx_qjs_ext_fetch_request_field(JSContext *cx,
+ JSValueConst this_val, int magic);
+static JSValue ngx_qjs_ext_fetch_request_mode(JSContext *cx,
+ JSValueConst this_val);
+static void ngx_qjs_fetch_request_finalizer(JSRuntime *rt, JSValue val);
+
+static JSValue ngx_qjs_fetch_response_ctor(JSContext *cx,
+ JSValueConst new_target, int argc, JSValueConst *argv);
+static JSValue ngx_qjs_ext_fetch_response_status(JSContext *cx,
+ JSValueConst this_val);
+static JSValue ngx_qjs_ext_fetch_response_status_text(JSContext *cx,
+ JSValueConst this_val);
+static JSValue ngx_qjs_ext_fetch_response_ok(JSContext *cx,
+ JSValueConst this_val);
+static JSValue ngx_qjs_ext_fetch_response_body_used(JSContext *cx,
+ JSValueConst this_val);
+static JSValue ngx_qjs_ext_fetch_response_headers(JSContext *cx,
+ JSValueConst this_val);
+static JSValue ngx_qjs_ext_fetch_response_type(JSContext *cx,
+ JSValueConst this_val);
+static JSValue ngx_qjs_ext_fetch_response_body(JSContext *cx,
+ JSValueConst this_val, int argc, JSValueConst *argv, int magic);
+static JSValue ngx_qjs_ext_fetch_response_redirected(JSContext *cx,
+ JSValueConst this_val);
+static JSValue ngx_qjs_ext_fetch_response_field(JSContext *cx,
+ JSValueConst this_val, int magic);
+static void ngx_qjs_fetch_response_finalizer(JSRuntime *rt, JSValue val);
+
+static JSValue ngx_qjs_fetch_flag(JSContext *cx, const ngx_qjs_entry_t *entries,
+ ngx_int_t value);
+static ngx_int_t ngx_qjs_fetch_flag_set(JSContext *cx,
+ const ngx_qjs_entry_t *entries, JSValue object, const char *prop);
+
+static JSModuleDef *ngx_qjs_fetch_init(JSContext *cx, const char *name);
+
+
+static const JSCFunctionListEntry ngx_qjs_ext_fetch_headers_proto[] = {
+ JS_CFUNC_DEF("append", 2, ngx_qjs_ext_fetch_headers_append),
+ JS_CFUNC_DEF("delete", 1, ngx_qjs_ext_fetch_headers_delete),
+ JS_CFUNC_DEF("forEach", 1, ngx_qjs_ext_fetch_headers_foreach),
+ JS_CFUNC_MAGIC_DEF("get", 1, ngx_qjs_ext_fetch_headers_get, 0),
+ JS_CFUNC_MAGIC_DEF("getAll", 1, ngx_qjs_ext_fetch_headers_get, 1),
+ JS_CFUNC_DEF("has", 1, ngx_qjs_ext_fetch_headers_has),
+ JS_CFUNC_DEF("set", 2, ngx_qjs_ext_fetch_headers_set),
+};
+
+
+static const JSCFunctionListEntry ngx_qjs_ext_fetch_request_proto[] = {
+#define NGX_QJS_BODY_ARRAY_BUFFER 0
+#define NGX_QJS_BODY_JSON 1
+#define NGX_QJS_BODY_TEXT 2
+ JS_CFUNC_MAGIC_DEF("arrayBuffer", 0, ngx_qjs_ext_fetch_request_body,
+ NGX_QJS_BODY_ARRAY_BUFFER),
+ JS_CGETSET_DEF("bodyUsed", ngx_qjs_ext_fetch_request_body_used, NULL),
+ JS_CGETSET_DEF("cache", ngx_qjs_ext_fetch_request_cache, NULL),
+ JS_CGETSET_DEF("credentials", ngx_qjs_ext_fetch_request_credentials, NULL),
+ JS_CFUNC_MAGIC_DEF("json", 0, ngx_qjs_ext_fetch_request_body,
+ NGX_QJS_BODY_JSON),
+ JS_CGETSET_DEF("headers", ngx_qjs_ext_fetch_request_headers, NULL ),
+ JS_CGETSET_MAGIC_DEF("method", ngx_qjs_ext_fetch_request_field, NULL,
+ offsetof(ngx_js_request_t, method) ),
+ JS_CGETSET_DEF("mode", ngx_qjs_ext_fetch_request_mode, NULL),
+ JS_CFUNC_MAGIC_DEF("text", 0, ngx_qjs_ext_fetch_request_body,
+ NGX_QJS_BODY_TEXT),
+ JS_CGETSET_MAGIC_DEF("url", ngx_qjs_ext_fetch_request_field, NULL,
+ offsetof(ngx_js_request_t, url) ),
+};
+
+
+static const JSCFunctionListEntry ngx_qjs_ext_fetch_response_proto[] = {
+ JS_CFUNC_MAGIC_DEF("arrayBuffer", 0, ngx_qjs_ext_fetch_response_body,
+ NGX_QJS_BODY_ARRAY_BUFFER),
+ JS_CGETSET_DEF("bodyUsed", ngx_qjs_ext_fetch_response_body_used, NULL),
+ JS_CGETSET_DEF("headers", ngx_qjs_ext_fetch_response_headers, NULL ),
+ JS_CFUNC_MAGIC_DEF("json", 0, ngx_qjs_ext_fetch_response_body,
+ NGX_QJS_BODY_JSON),
+ JS_CGETSET_DEF("ok", ngx_qjs_ext_fetch_response_ok, NULL),
+ JS_CGETSET_DEF("redirected", ngx_qjs_ext_fetch_response_redirected, NULL),
+ JS_CGETSET_DEF("status", ngx_qjs_ext_fetch_response_status, NULL),
+ JS_CGETSET_DEF("statusText", ngx_qjs_ext_fetch_response_status_text, NULL),
+ JS_CFUNC_MAGIC_DEF("text", 0, ngx_qjs_ext_fetch_response_body,
+ NGX_QJS_BODY_TEXT),
+ JS_CGETSET_DEF("type", ngx_qjs_ext_fetch_response_type, NULL),
+ JS_CGETSET_MAGIC_DEF("url", ngx_qjs_ext_fetch_response_field, NULL,
+ offsetof(ngx_js_response_t, url) ),
+};
+
+
+static const JSClassDef ngx_qjs_fetch_headers_class = {
+ "Headers",
+ .finalizer = NULL,
+ .exotic = & (JSClassExoticMethods) {
+ .get_own_property = ngx_qjs_fetch_headers_own_property,
+ .get_own_property_names = ngx_qjs_fetch_headers_own_property_names,
+ },
+};
+
+
+static const JSClassDef ngx_qjs_fetch_request_class = {
+ "Request",
+ .finalizer = ngx_qjs_fetch_request_finalizer,
+};
+
+
+static const JSClassDef ngx_qjs_fetch_response_class = {
+ "Response",
+ .finalizer = ngx_qjs_fetch_response_finalizer,
+};
+
+
+static const ngx_qjs_entry_t ngx_qjs_fetch_cache_modes[] = {
+ { ngx_string("default"), CACHE_MODE_DEFAULT },
+ { ngx_string("no-store"), CACHE_MODE_NO_STORE },
+ { ngx_string("reload"), CACHE_MODE_RELOAD },
+ { ngx_string("no-cache"), CACHE_MODE_NO_CACHE },
+ { ngx_string("force-cache"), CACHE_MODE_FORCE_CACHE },
+ { ngx_string("only-if-cached"), CACHE_MODE_ONLY_IF_CACHED },
+ { ngx_null_string, 0 },
+};
+
+
+static const ngx_qjs_entry_t ngx_qjs_fetch_credentials[] = {
+ { ngx_string("same-origin"), CREDENTIALS_SAME_ORIGIN },
+ { ngx_string("omit"), CREDENTIALS_OMIT },
+ { ngx_string("include"), CREDENTIALS_INCLUDE },
+ { ngx_null_string, 0 },
+};
+
+
+static const ngx_qjs_entry_t ngx_qjs_fetch_modes[] = {
+ { ngx_string("no-cors"), MODE_NO_CORS },
+ { ngx_string("cors"), MODE_CORS },
+ { ngx_string("same-origin"), MODE_SAME_ORIGIN },
+ { ngx_string("navigate"), MODE_NAVIGATE },
+ { ngx_string("websocket"), MODE_WEBSOCKET },
+ { ngx_null_string, 0 },
+};
+
+
+qjs_module_t ngx_qjs_ngx_fetch_module = {
+ .name = "fetch",
+ .init = ngx_qjs_fetch_init,
+};
+
+
+JSValue
+ngx_qjs_ext_fetch(JSContext *cx, JSValueConst this_val, int argc,
+ JSValueConst *argv)
+{
+ int has_host;
+ void *external;
+ JSValue init, value, promise;
+ ngx_int_t rc;
+ ngx_url_t u;
+ ngx_uint_t i;
+ ngx_pool_t *pool;
+ ngx_js_ctx_t *ctx;
+ ngx_js_http_t *http;
+ ngx_qjs_fetch_t *fetch;
+ ngx_list_part_t *part;
+ ngx_js_tb_elt_t *h;
+ ngx_connection_t *c;
+ ngx_js_request_t request;
+ ngx_resolver_ctx_t *rs;
+
+ external = JS_GetContextOpaque(cx);
+ c = ngx_qjs_external_connection(cx, external);
+ pool = ngx_qjs_external_pool(cx, external);
+
+ fetch = ngx_qjs_fetch_alloc(cx, pool, c->log);
+ if (fetch == NULL) {
+ return JS_ThrowOutOfMemory(cx);
+ }
+
+ promise = JS_DupValue(cx, fetch->promise);
+
+ rc = ngx_qjs_request_ctor(cx, &request, &u, argc, argv);
+ if (rc != NGX_OK) {
+ goto fail;
+ }
+
+ http = &fetch->http;
+ http->response.url = request.url;
+ http->timeout = ngx_qjs_external_fetch_timeout(cx, external);
+ http->buffer_size = ngx_qjs_external_buffer_size(cx, external);
+ http->max_response_body_size =
+ ngx_qjs_external_max_response_buffer_size(cx, external);
+
+#if (NGX_SSL)
+ if (u.default_port == 443) {
+ http->ssl = ngx_qjs_external_ssl(cx, external);
+ http->ssl_verify = ngx_qjs_external_ssl_verify(cx, external);
+ }
+#endif
+
+ if (JS_IsObject(argv[1])) {
+ init = argv[1];
+ value = JS_GetPropertyStr(cx, init, "buffer_size");
+ if (JS_IsException(value)) {
+ goto fail;
+ }
+
+ if (!JS_IsUndefined(value)) {
+ if (JS_ToInt64(cx, (int64_t *) &http->buffer_size, value) < 0) {
+ JS_FreeValue(cx, value);
+ goto fail;
+ }
+ }
+
+ value = JS_GetPropertyStr(cx, init, "max_response_body_size");
+ if (JS_IsException(value)) {
+ goto fail;
+ }
+
+ if (!JS_IsUndefined(value)) {
+ if (JS_ToInt64(cx, (int64_t *) &http->max_response_body_size,
+ value) < 0)
+ {
+ JS_FreeValue(cx, value);
+ goto fail;
+ }
+ }
+
+#if (NGX_SSL)
+ value = JS_GetPropertyStr(cx, init, "verify");
+ if (JS_IsException(value)) {
+ goto fail;
+ }
+
+ if (!JS_IsUndefined(value)) {
+ http->ssl_verify = JS_ToBool(cx, value);
+ }
+#endif
+ }
+
+ if (request.method.len == 4
+ && ngx_strncasecmp(request.method.data, (u_char *) "HEAD", 4) == 0)
+ {
+ http->header_only = 1;
+ }
+
+ ctx = ngx_qjs_external_ctx(cx, JS_GetContextOpaque(cx));
+
+ NJS_CHB_MP_INIT(&http->chain, ctx->engine->pool);
+ NJS_CHB_MP_INIT(&http->response.chain, ctx->engine->pool);
+
+ njs_chb_append(&http->chain, request.method.data, request.method.len);
+ njs_chb_append_literal(&http->chain, " ");
+
+ if (u.uri.len == 0 || u.uri.data[0] != '/') {
+ njs_chb_append_literal(&http->chain, "/");
+ }
+
+ njs_chb_append(&http->chain, u.uri.data, u.uri.len);
+ njs_chb_append_literal(&http->chain, " HTTP/1.1" CRLF);
+
+ has_host = 0;
+ part = &request.headers.header_list.part;
+ h = part->elts;
+
+ for (i = 0; /* void */; i++) {
+
+ if (i >= part->nelts) {
+ if (part->next == NULL) {
+ break;
+ }
+
+ part = part->next;
+ h = part->elts;
+ i = 0;
+ }
+
+ if (h[i].hash == 0) {
+ continue;
+ }
+
+ if (h[i].key.len == 4
+ && ngx_strncasecmp(h[i].key.data, (u_char *) "Host", 4) == 0)
+ {
+ has_host = 1;
+ njs_chb_append_literal(&http->chain, "Host: ");
+ njs_chb_append(&http->chain, h[i].value.data, h[i].value.len);
+ njs_chb_append_literal(&http->chain, CRLF);
+ break;
+ }
+ }
+
+ if (!has_host) {
+ njs_chb_append_literal(&http->chain, "Host: ");
+ njs_chb_append(&http->chain, u.host.data, u.host.len);
+
+ if (!u.no_port) {
+ njs_chb_sprintf(&http->chain, 32, ":%d", u.port);
+ }
+
+ njs_chb_append_literal(&http->chain, CRLF);
+ }
+
+ part = &request.headers.header_list.part;
+ h = part->elts;
+
+ for (i = 0; /* void */; i++) {
+
+ if (i >= part->nelts) {
+ if (part->next == NULL) {
+ break;
+ }
+
+ part = part->next;
+ h = part->elts;
+ i = 0;
+ }
+
+ if (h[i].hash == 0) {
+ continue;
+ }
+
+ if (h[i].key.len == 4
+ && ngx_strncasecmp(h[i].key.data, (u_char *) "Host", 4) == 0)
+ {
+ continue;
+ }
+
+ njs_chb_append(&http->chain, h[i].key.data, h[i].key.len);
+ njs_chb_append_literal(&http->chain, ": ");
+ njs_chb_append(&http->chain, h[i].value.data, h[i].value.len);
+ njs_chb_append_literal(&http->chain, CRLF);
+ }
+
+ njs_chb_append_literal(&http->chain, "Connection: close" CRLF);
+
+#if (NGX_SSL)
+ http->tls_name.data = u.host.data;
+ http->tls_name.len = u.host.len;
+#endif
+
+ if (request.body.len != 0) {
+ njs_chb_sprintf(&http->chain, 32, "Content-Length: %uz" CRLF CRLF,
+ request.body.len);
+ njs_chb_append(&http->chain, request.body.data, request.body.len);
+
+ } else {
+ njs_chb_append_literal(&http->chain, CRLF);
+ }
+
+ if (u.addrs == NULL) {
+ rs = ngx_js_http_resolve(http, ngx_qjs_external_resolver(cx, external),
+ &u.host, u.port,
+ ngx_qjs_external_resolver_timeout(cx, external));
+ if (rs == NULL) {
+ JS_FreeValue(cx, promise);
+ return JS_ThrowOutOfMemory(cx);
+ }
+
+ if (rs == NGX_NO_RESOLVER) {
+ JS_ThrowInternalError(cx, "no resolver defined");
+ goto fail;
+ }
+
+ return promise;
+ }
+
+ http->naddrs = 1;
+ ngx_memcpy(&http->addr, &u.addrs[0], sizeof(ngx_addr_t));
+ http->addrs = &http->addr;
+
+ ngx_js_http_connect(http);
+
+ return promise;
+
+fail:
+
+ fetch->response_value = JS_GetException(cx);
+
+ ngx_qjs_fetch_done(fetch, fetch->response_value, NGX_ERROR);
+
+ return promise;
+}
+
+
+static JSValue
+ngx_qjs_fetch_headers_ctor(JSContext *cx, JSValueConst new_target, int argc,
+ JSValueConst *argv)
+{
+ JSValue init, proto, obj;
+ ngx_int_t rc;
+ ngx_pool_t *pool;
+ ngx_js_headers_t *headers;
+
+ pool = ngx_qjs_external_pool(cx, JS_GetContextOpaque(cx));
+
+ headers = ngx_pcalloc(pool, sizeof(ngx_js_headers_t));
+ if (headers == NULL) {
+ return JS_ThrowOutOfMemory(cx);
+ }
+
+ headers->guard = GUARD_NONE;
+
+ rc = ngx_list_init(&headers->header_list, pool, 4,
+ sizeof(ngx_js_tb_elt_t));
+ if (rc != NGX_OK) {
+ return JS_ThrowOutOfMemory(cx);
+ }
+
+ init = argv[0];
+
+ if (JS_IsObject(init)) {
+ rc = ngx_qjs_headers_fill(cx, headers, init);
+ if (rc != NGX_OK) {
+ return JS_EXCEPTION;
+ }
+ }
+
+ proto = JS_GetPropertyStr(cx, new_target, "prototype");
+ if (JS_IsException(proto)) {
+ return JS_EXCEPTION;
+ }
+
+ obj = JS_NewObjectProtoClass(cx, proto, NGX_QJS_CLASS_ID_FETCH_HEADERS);
+ JS_FreeValue(cx, proto);
+
+ if (JS_IsException(obj)) {
+ return JS_EXCEPTION;
+ }
+
+ JS_SetOpaque(obj, headers);
+
+ return obj;
+}
+
+
+static JSValue
+ngx_qjs_fetch_request_ctor(JSContext *cx, JSValueConst new_target, int argc,
+ JSValueConst *argv)
+{
+ JSValue proto, obj;
+ ngx_int_t rc;
+ ngx_url_t u;
+ ngx_pool_t *pool;
+ ngx_js_request_t *request;
+
+ pool = ngx_qjs_external_pool(cx, JS_GetContextOpaque(cx));
+
+ request = ngx_pcalloc(pool, sizeof(ngx_js_request_t));
+ if (request == NULL) {
+ return JS_ThrowOutOfMemory(cx);
+ }
+
+ rc = ngx_qjs_request_ctor(cx, request, &u, argc, argv);
+ if (rc != NGX_OK) {
+ return JS_EXCEPTION;
+ }
+
+ proto = JS_GetPropertyStr(cx, new_target, "prototype");
+ if (JS_IsException(proto)) {
+ return JS_EXCEPTION;
+ }
+
+ obj = JS_NewObjectProtoClass(cx, proto, NGX_QJS_CLASS_ID_FETCH_REQUEST);
+ JS_FreeValue(cx, proto);
+
+ if (JS_IsException(obj)) {
+ return JS_EXCEPTION;
+ }
+
+ JS_SetOpaque(obj, request);
+
+ return obj;
+}
+
+
+static ngx_int_t
+ngx_qjs_request_ctor(JSContext *cx, ngx_js_request_t *request,
+ ngx_url_t *u, int argc, JSValueConst *argv)
+{
+ JSValue input, init, value;
+ ngx_int_t rc;
+ ngx_pool_t *pool;
+ ngx_js_request_t *orig;
+
+ input = argv[0];
+ if (JS_IsUndefined(input)) {
+ JS_ThrowInternalError(cx, "1st argument is required");
+ return NGX_ERROR;
+ }
+
+ /*
+ * set by ngx_memzero():
+ *
+ * request->url.len = 0;
+ * request->body.length = 0;
+ * request->cache_mode = CACHE_MODE_DEFAULT;
+ * request->credentials = CREDENTIALS_SAME_ORIGIN;
+ * request->mode = MODE_NO_CORS;
+ * request->headers.content_type = NULL;
+ */
+
+ ngx_memzero(request, sizeof(ngx_js_request_t));
+
+ request->method.data = (u_char *) "GET";
+ request->method.len = 3;
+ request->body.data = NULL;
+ request->body.len = 0;
+ request->headers.guard = GUARD_REQUEST;
+ ngx_qjs_arg(request->header_value) = JS_UNDEFINED;
+
+ pool = ngx_qjs_external_pool(cx, JS_GetContextOpaque(cx));
+
+ rc = ngx_list_init(&request->headers.header_list, pool, 4,
+ sizeof(ngx_js_tb_elt_t));
+ if (rc != NGX_OK) {
+ JS_ThrowOutOfMemory(cx);
+ return NGX_ERROR;
+ }
+
+ if (JS_IsString(input)) {
+ rc = ngx_qjs_string(cx, input, &request->url);
+ if (rc != NGX_OK) {
+ JS_ThrowInternalError(cx, "failed to convert url arg");
+ return NGX_ERROR;
+ }
+
+ } else {
+ orig = JS_GetOpaque2(cx, input, NGX_QJS_CLASS_ID_FETCH_REQUEST);
+ if (orig == NULL) {
+ JS_ThrowInternalError(cx,
+ "input is not string or a Request object");
+ return NGX_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;
+
+ rc = ngx_qjs_headers_inherit(cx, &request->headers, &orig->headers);
+ if (rc != NGX_OK) {
+ return NGX_ERROR;
+ }
+ }
+
+ ngx_js_http_trim(&request->url.data, &request->url.len, 1);
+
+ ngx_memzero(u, sizeof(ngx_url_t));
+
+ u->url = request->url;
+ 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;
+
+#if (NGX_SSL)
+ } else if (u->url.len > 8
+ && ngx_strncasecmp(u->url.data, (u_char *) "https://", 8) == 0)
+ {
+ u->url.len -= 8;
+ u->url.data += 8;
+ u->default_port = 443;
+#endif
+
+ } else {
+ JS_ThrowInternalError(cx, "unsupported URL schema (only http or https"
+ " are supported)");
+ return NGX_ERROR;
+ }
+
+ if (ngx_parse_url(pool, u) != NGX_OK) {
+ JS_ThrowInternalError(cx, "invalid url");
+ return NGX_ERROR;
+ }
+
+ if (JS_IsObject(argv[1])) {
+ init = argv[1];
+ value = JS_GetPropertyStr(cx, init, "method");
+ if (JS_IsException(value)) {
+ JS_ThrowInternalError(cx, "invalid Request method");
+ return NGX_ERROR;
+ }
+
+ if (!JS_IsUndefined(value)) {
+ rc = ngx_qjs_string(cx, value, &request->method);
+ JS_FreeValue(cx, value);
+
+ if (rc != NGX_OK) {
+ JS_ThrowInternalError(cx, "invalid Request method");
+ return NGX_ERROR;
+ }
+ }
+
+ rc = ngx_qjs_method_process(cx, request);
+ if (rc != NGX_OK) {
+ return NGX_ERROR;
+ }
+
+ rc = ngx_qjs_fetch_flag_set(cx, ngx_qjs_fetch_cache_modes, init,
+ "cache");
+ if (rc == NGX_ERROR) {
+ return NGX_ERROR;
+ }
+
+ request->cache_mode = rc;
+
+ rc = ngx_qjs_fetch_flag_set(cx, ngx_qjs_fetch_credentials, init,
+ "credentials");
+ if (rc == NGX_ERROR) {
+ return NGX_ERROR;
+ }
+
+ request->credentials = rc;
+
+ rc = ngx_qjs_fetch_flag_set(cx, ngx_qjs_fetch_modes, init, "mode");
+ if (rc == NGX_ERROR) {
+ return NGX_ERROR;
+ }
+
+ request->mode = rc;
+
+ value = JS_GetPropertyStr(cx, init, "headers");
+ if (JS_IsException(value)) {
+ JS_ThrowInternalError(cx, "invalid Request headers");
+ return NGX_ERROR;
+ }
+
+ if (!JS_IsUndefined(value)) {
+ if (!JS_IsObject(value)) {
+ JS_ThrowInternalError(cx, "Headers is not an object");
+ return NGX_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) {
+ JS_FreeValue(cx, value);
+ JS_ThrowOutOfMemory(cx);
+ return NGX_ERROR;
+ }
+
+ rc = ngx_qjs_headers_fill(cx, &request->headers, value);
+ JS_FreeValue(cx, value);
+
+ if (rc != NGX_OK) {
+ return NGX_ERROR;
+ }
+ }
+
+ value = JS_GetPropertyStr(cx, init, "body");
+ if (JS_IsException(value)) {
+ JS_ThrowInternalError(cx, "invalid Request body");
+ return NGX_ERROR;
+ }
+
+ if (!JS_IsUndefined(value)) {
+ if (ngx_qjs_string(cx, value, &request->body) != NGX_OK) {
+ JS_FreeValue(cx, value);
+ JS_ThrowInternalError(cx, "invalid Request body");
+ return NGX_ERROR;
+ }
+
+ if (request->headers.content_type == NULL && JS_IsString(value)) {
+ rc = ngx_qjs_headers_append(cx, &request->headers,
+ (u_char *) "Content-Type",
+ sizeof("Content-Type") - 1,
+ (u_char *) "text/plain;charset=UTF-8",
+ sizeof("text/plain;charset=UTF-8") - 1);
+ if (rc != NGX_OK) {
+ JS_FreeValue(cx, value);
+ return NGX_ERROR;
+ }
+ }
+
+ JS_FreeValue(cx, value);
+ }
+ }
+
+ return NGX_OK;
+}
+
+
+static JSValue
+ngx_qjs_fetch_response_ctor(JSContext *cx, JSValueConst new_target, int argc,
+ JSValueConst *argv)
+{
+ int ret;
+ u_char *p, *end;
+ JSValue init, value, body, proto, obj;
+ ngx_str_t bd;
+ ngx_int_t rc;
+ ngx_pool_t *pool;
+ ngx_js_ctx_t *ctx;
+ ngx_js_response_t *response;
+
+ pool = ngx_qjs_external_pool(cx, JS_GetContextOpaque(cx));
+
+ response = ngx_pcalloc(pool, sizeof(ngx_js_response_t));
+ if (response == NULL) {
+ return JS_ThrowOutOfMemory(cx);
+ }
+
+ /*
+ * set by ngx_pcalloc():
+ *
+ * response->url.length = 0;
+ * response->status_text.length = 0;
+ */
+
+ response->code = 200;
+ response->headers.guard = GUARD_RESPONSE;
+ ngx_qjs_arg(response->header_value) = JS_UNDEFINED;
+
+ ret = ngx_list_init(&response->headers.header_list, pool, 4,
+ sizeof(ngx_js_tb_elt_t));
+ if (ret != NGX_OK) {
+ JS_ThrowOutOfMemory(cx);
+ }
+
+ init = argv[1];
+
+ if (JS_IsObject(init)) {
+ value = JS_GetPropertyStr(cx, init, "status");
+ if (JS_IsException(value)) {
+ return JS_ThrowInternalError(cx, "invalid Response status");
+ }
+
+ if (!JS_IsUndefined(value)) {
+ ret = JS_ToInt64(cx, (int64_t *) &response->code, value);
+ JS_FreeValue(cx, value);
+
+ if (ret < 0) {
+ return JS_EXCEPTION;
+ }
+
+ if (response->code < 200 || response->code > 599) {
+ return JS_ThrowInternalError(cx, "status provided (%d) is "
+ "outside of [200, 599] range",
+ (int) response->code);
+ }
+ }
+
+ value = JS_GetPropertyStr(cx, init, "statusText");
+ if (JS_IsException(value)) {
+ return JS_ThrowInternalError(cx, "invalid Response statusText");
+ }
+
+ if (!JS_IsUndefined(value)) {
+ ret = ngx_qjs_string(cx, value, &response->status_text);
+ JS_FreeValue(cx, value);
+
+ if (ret < 0) {
+ return JS_EXCEPTION;
+ }
+
+ p = response->status_text.data;
+ end = p + response->status_text.len;
+
+ while (p < end) {
+ if (*p != '\t' && *p < ' ') {
+ return JS_ThrowInternalError(cx,
+ "invalid Response statusText");
+ }
+
+ p++;
+ }
+ }
+
+ value = JS_GetPropertyStr(cx, init, "headers");
+ if (JS_IsException(value)) {
+ return JS_ThrowInternalError(cx, "invalid Response headers");
+ }
+
+ if (!JS_IsUndefined(value)) {
+ if (!JS_IsObject(value)) {
+ JS_FreeValue(cx, value);
+ return JS_ThrowInternalError(cx, "Headers is not an object");
+ }
+
+ rc = ngx_qjs_headers_fill(cx, &response->headers, value);
+ JS_FreeValue(cx, value);
+
+ if (ret != NGX_OK) {
+ return JS_EXCEPTION;
+ }
+ }
+ }
+
+ ctx = ngx_qjs_external_ctx(cx, JS_GetContextOpaque(cx));
+
+ NJS_CHB_MP_INIT(&response->chain, ctx->engine->pool);
+
+ body = argv[0];
+
+ if (!JS_IsNullOrUndefined(body)) {
+ if (ngx_qjs_string(cx, body, &bd) != NGX_OK) {
+ return JS_ThrowInternalError(cx, "invalid Response body");
+ }
+
+ njs_chb_append(&response->chain, bd.data, bd.len);
+
+ if (JS_IsString(body)) {
+ rc = ngx_qjs_headers_append(cx, &response->headers,
+ (u_char *) "Content-Type",
+ sizeof("Content-Type") - 1,
+ (u_char *) "text/plain;charset=UTF-8",
+ sizeof("text/plain;charset=UTF-8") - 1);
+ if (rc != NGX_OK) {
+ return JS_EXCEPTION;
+ }
+ }
+ }
+
+ proto = JS_GetPropertyStr(cx, new_target, "prototype");
+ if (JS_IsException(proto)) {
+ return JS_EXCEPTION;
+ }
+
+ obj = JS_NewObjectProtoClass(cx, proto, NGX_QJS_CLASS_ID_FETCH_RESPONSE);
+ JS_FreeValue(cx, proto);
+
+ if (JS_IsException(obj)) {
+ return JS_EXCEPTION;
+ }
+
+ JS_SetOpaque(obj, response);
+
+ return obj;
+}
+
+
+static u_char
+ngx_js_upper_case(u_char c)
+{
+ return (u_char) ((c >= 'a' && c <= 'z') ? c & 0xDF : c);
+}
+
+
+static ngx_int_t
+ngx_qjs_method_process(JSContext *cx, ngx_js_request_t *request)
+{
+ u_char *s;
+ const u_char *p;
+ const ngx_str_t *m;
+
+ static const ngx_str_t forbidden[] = {
+ ngx_string("CONNECT"),
+ ngx_string("TRACE"),
+ ngx_string("TRACK"),
+ ngx_null_string,
+ };
+
+ static const ngx_str_t to_normalize[] = {
+ ngx_string("DELETE"),
+ ngx_string("GET"),
+ ngx_string("HEAD"),
+ ngx_string("OPTIONS"),
+ ngx_string("POST"),
+ ngx_string("PUT"),
+ ngx_null_string,
+ };
+
+ for (m = &forbidden[0]; m->len != 0; m++) {
+ if (request->method.len == m->len
+ && ngx_strncasecmp(request->method.data, m->data, m->len) == 0)
+ {
+ JS_ThrowInternalError(cx, "forbidden method: %.*s",
+ (int) m->len, m->data);
+ return NGX_ERROR;
+ }
+ }
+
+ for (m = &to_normalize[0]; m->len != 0; m++) {
+ if (request->method.len == m->len
+ && ngx_strncasecmp(request->method.data, m->data, m->len) == 0)
+ {
+ s = &request->m[0];
+ p = m->data;
+
+ while (*p != '\0') {
+ *s++ = ngx_js_upper_case(*p++);
+ }
+
+ request->method.data = &request->m[0];
+ request->method.len = m->len;
+ break;
+ }
+ }
+
+ return NGX_OK;
+}
+
+
+static ngx_int_t
+ngx_qjs_headers_inherit(JSContext *cx, ngx_js_headers_t *headers,
+ ngx_js_headers_t *orig)
+{
+ ngx_int_t rc;
+ 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;
+ }
+
+ rc = ngx_qjs_headers_append(cx, headers, h[i].key.data, h[i].key.len,
+ h[i].value.data, h[i].value.len);
+ if (rc != NGX_OK) {
+ return NGX_ERROR;
+ }
+ }
+
+ return NGX_OK;
+}
+
+
+static ngx_int_t
+ngx_qjs_headers_fill_header_free(JSContext *cx, ngx_js_headers_t *headers,
+ JSValue prop_name, JSValue prop_value)
+{
+ ngx_int_t rc;
+ ngx_str_t name, value;
+
+ if (ngx_qjs_string(cx, prop_name, &name) != NGX_OK) {
+ JS_FreeValue(cx, prop_name);
+ JS_FreeValue(cx, prop_value);
+ return NGX_ERROR;
+ }
+
+ if (ngx_qjs_string(cx, prop_value, &value) != NGX_OK) {
+ JS_FreeValue(cx, prop_name);
+ JS_FreeValue(cx, prop_value);
+ return NGX_ERROR;
+ }
+
+ rc = ngx_qjs_headers_append(cx, headers, name.data, name.len,
+ value.data, value.len);
+
+ JS_FreeValue(cx, prop_name);
+ JS_FreeValue(cx, prop_value);
+
+ return rc;
+}
+
+
+static ngx_int_t
+ngx_qjs_headers_fill(JSContext *cx, ngx_js_headers_t *headers, JSValue init)
+{
+ JSValue header, prop_name, prop_value;
+ uint32_t i, len, length;
+ ngx_int_t rc;
+ JSPropertyEnum *tab;
+ ngx_js_headers_t *hh;
+
+ hh = JS_GetOpaque2(cx, init, NGX_QJS_CLASS_ID_FETCH_HEADERS);
+ if (hh != NULL) {
+ return ngx_qjs_headers_inherit(cx, headers, hh);
+ }
+
+ if (JS_GetOwnPropertyNames(cx, &tab, &len, init,
+ JS_GPN_STRING_MASK | JS_GPN_ENUM_ONLY) < 0) {
+ return NGX_ERROR;
+ }
+
+ if (qjs_is_array(cx, init)) {
+ for (i = 0; i < len; i++) {
+ header = JS_GetPropertyUint32(cx, init, i);
+ if (JS_IsException(header)) {
+ goto fail;
+ }
+
+ if (qjs_array_length(cx, header, &length)) {
+ JS_FreeValue(cx, header);
+ goto fail;
+ }
+
+ if (length != 2) {
+ JS_FreeValue(cx, header);
+ JS_ThrowInternalError(cx,
+ "header does not contain exactly two items");
+ goto fail;
+ }
+
+ prop_name = JS_GetPropertyUint32(cx, header, 0);
+ prop_value = JS_GetPropertyUint32(cx, header, 1);
+
+ JS_FreeValue(cx, header);
+
+ rc = ngx_qjs_headers_fill_header_free(cx, headers, prop_name,
+ prop_value);
+ if (rc != NGX_OK) {
+ goto fail;
+ }
+ }
+
+ } else {
+
+ for (i = 0; i < len; i++) {
+ prop_name = JS_AtomToString(cx, tab[i].atom);
+
+ prop_value = JS_GetProperty(cx, init, tab[i].atom);
+ if (JS_IsException(prop_value)) {
+ JS_FreeValue(cx, prop_name);
+ goto fail;
+ }
+
+ rc = ngx_qjs_headers_fill_header_free(cx, headers, prop_name,
+ prop_value);
+ if (rc != NGX_OK) {
+ goto fail;
+ }
+ }
+ }
+
+ qjs_free_prop_enum(cx, tab, len);
+
+ return NGX_OK;
+
+fail:
+
+ qjs_free_prop_enum(cx, tab, len);
+
+ return NGX_ERROR;
+}
+
+
+static ngx_qjs_fetch_t *
+ngx_qjs_fetch_alloc(JSContext *cx, ngx_pool_t *pool, ngx_log_t *log)
+{
+ ngx_js_ctx_t *ctx;
+ ngx_js_http_t *http;
+ ngx_qjs_fetch_t *fetch;
+ ngx_qjs_event_t *event;
+
+ fetch = ngx_pcalloc(pool, sizeof(ngx_qjs_fetch_t));
+ if (fetch == NULL) {
+ return NULL;
+ }
+
+ http = &fetch->http;
+
+ http->pool = pool;
+ http->log = log;
+
+ http->timeout = 10000;
+
+ http->http_parse.content_length_n = -1;
+
+ ngx_qjs_arg(http->response.header_value) = JS_UNDEFINED;
+
+ http->append_headers = ngx_qjs_fetch_append_headers;
+ http->ready_handler = ngx_qjs_fetch_process_done;
+ http->error_handler = ngx_qjs_fetch_error;
+
+ fetch->promise = JS_NewPromiseCapability(cx, fetch->promise_callbacks);
+ if (JS_IsException(fetch->promise)) {
+ return NULL;
+ }
+
+ event = ngx_palloc(pool, sizeof(ngx_qjs_event_t));
+ if (event == NULL) {
+ goto fail;
+ }
+
+ ctx = ngx_qjs_external_ctx(cx, JS_GetContextOpaque(cx));
+
+ event->ctx = cx;
+ event->destructor = ngx_qjs_fetch_destructor;
+ event->fd = ctx->event_id++;
+ event->data = fetch;
+
+ ngx_js_add_event(ctx, event);
+
+ fetch->cx = cx;
+ fetch->event = event;
+
+ ngx_log_debug1(NGX_LOG_DEBUG_EVENT, log, 0, "js http alloc:%p", fetch);
+
+ return fetch;
+
+fail:
+
+ JS_FreeValue(cx, fetch->promise);
+ JS_FreeValue(cx, fetch->promise_callbacks[0]);
+ JS_FreeValue(cx, fetch->promise_callbacks[1]);
+
+ JS_ThrowInternalError(cx, "internal error");
+
+ return NULL;
+}
+
+
+static void
+ngx_qjs_fetch_error(ngx_js_http_t *http, const char *err)
+{
+ ngx_qjs_fetch_t *fetch;
+
+ fetch = (ngx_qjs_fetch_t *) http;
+
+ JS_ThrowInternalError(fetch->cx, "%s", err);
+
+ fetch->response_value = JS_GetException(fetch->cx);
+
+ ngx_qjs_fetch_done(fetch, fetch->response_value, NGX_ERROR);
+}
+
+
+static void
+ngx_qjs_fetch_destructor(ngx_qjs_event_t *event)
+{
+ JSContext *cx;
+ ngx_js_http_t *http;
+ ngx_qjs_fetch_t *fetch;
+
+ cx = event->ctx;
+ 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);
+
+ JS_FreeValue(cx, fetch->promise_callbacks[0]);
+ JS_FreeValue(cx, fetch->promise_callbacks[1]);
+ JS_FreeValue(cx, fetch->promise);
+ JS_FreeValue(cx, fetch->response_value);
+}
+
+
+static void
+ngx_qjs_fetch_done(ngx_qjs_fetch_t *fetch, JSValue retval, ngx_int_t rc)
+{
+ void *external;
+ JSValue action;
+ JSContext *cx;
+ ngx_js_ctx_t *ctx;
+ ngx_js_http_t *http;
+ ngx_qjs_event_t *event;
+
+ http = &fetch->http;
+
+ ngx_log_debug2(NGX_LOG_DEBUG_EVENT, http->log, 0,
+ "js http done fetch:%p rc:%i", fetch, rc);
+
+ ngx_js_http_close_peer(http);
+
+ if (fetch->event != NULL) {
+ action = fetch->promise_callbacks[(rc != NGX_OK)];
+
+ cx = fetch->cx;
+ event = fetch->event;
+
+ rc = ngx_qjs_call(cx, action, &retval, 1);
+
+ external = JS_GetContextOpaque(cx);
+ ctx = ngx_qjs_external_ctx(cx, external);
+ ngx_js_del_event(ctx, event);
+
+ ngx_qjs_external_event_finalize(cx)(external, rc);
+ }
+}
+
+
+static ngx_int_t
+ngx_qjs_fetch_append_headers(ngx_js_http_t *http, ngx_js_headers_t *headers,
+ u_char *name, size_t len, u_char *value, size_t vlen)
+{
+ ngx_qjs_fetch_t *fetch;
+
+ fetch = (ngx_qjs_fetch_t *) http;
+
+ return ngx_qjs_headers_append(fetch->cx, &http->response.headers,
+ name, len, value, vlen);
+}
+
+
+static void
+ngx_qjs_fetch_process_done(ngx_js_http_t *http)
+{
+ ngx_qjs_fetch_t *fetch;
+
+ fetch = (ngx_qjs_fetch_t *) http;
+
+ fetch->response_value = JS_NewObjectClass(fetch->cx,
+ NGX_QJS_CLASS_ID_FETCH_RESPONSE);
+ if (JS_IsException(fetch->response_value)) {
+ ngx_qjs_fetch_error(http, "fetch response creation failed");
+ return;
+ }
+
+ JS_SetOpaque(fetch->response_value, &http->response);
+
+ ngx_qjs_fetch_done(fetch, fetch->response_value, NGX_OK);
+}
+
+
+static ngx_int_t
+ngx_qjs_headers_append(JSContext *cx, ngx_js_headers_t *headers,
+ u_char *name, size_t len, u_char *value, size_t vlen)
+{
+ u_char *p, *end;
+ ngx_int_t ret;
+ ngx_uint_t i;
+ ngx_list_part_t *part;
+ ngx_js_tb_elt_t *h, **ph;
+
+ ngx_js_http_trim(&value, &vlen, 0);
+
+ ret = ngx_js_check_header_name(name, len);
+ if (ret != NGX_OK) {
+ JS_ThrowInternalError(cx, "invalid header name");
+ return NGX_ERROR;
+ }
+
+ p = value;
+ end = p + vlen;
+
+ while (p < end) {
+ if (*p == '\0') {
+ JS_ThrowInternalError(cx, "invalid header value");
+ return NGX_ERROR;
+ }
+
+ p++;
+ }
+
+ if (headers->guard == GUARD_IMMUTABLE) {
+ JS_ThrowInternalError(cx, "cannot append to immutable object");
+ return NGX_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
+ && (ngx_strncasecmp(name, h[i].key.data, len) == 0))
+ {
+ ph = &h[i].next;
+ while (*ph) { ph = &(*ph)->next; }
+ break;
+ }
+ }
+
+ h = ngx_list_push(&headers->header_list);
+ if (h == NULL) {
+ JS_ThrowOutOfMemory(cx);
+ return NGX_ERROR;
+ }
+
+ if (ph != NULL) {
+ *ph = h;
+ }
+
+ h->hash = 1;
+ h->key.data = name;
+ h->key.len = len;
+ h->value.data = value;
+ h->value.len = vlen;
+ h->next = NULL;
+
+ if (len == (sizeof("Content-Type") - 1)
+ && ngx_strncasecmp(name, (u_char *) "Content-Type", len) == 0)
+ {
+ headers->content_type = h;
+ }
+
+ return NGX_OK;
+}
+
+
+static JSValue
+ngx_qjs_headers_ext_keys(JSContext *cx, JSValue value)
+{
+ int ret, found;
+ JSValue keys, key, item, func, retval;
+ uint32_t length;
+ ngx_str_t hdr;
+ ngx_uint_t i, k, n;
+ ngx_list_part_t *part;
+ ngx_js_tb_elt_t *h;
+ ngx_js_headers_t *headers;
+
+ headers = JS_GetOpaque2(cx, value, NGX_QJS_CLASS_ID_FETCH_HEADERS);
+ if (headers == NULL) {
+ return JS_NULL;
+ }
+
+ keys = JS_NewArray(cx);
+ if (JS_IsException(keys)) {
+ return JS_EXCEPTION;
+ }
+
+ n = 0;
+
+ part = &headers->header_list.part;
+ h = part->elts;
+
+ for (i = 0; /* void */; i++) {
+
+ if (i >= part->nelts) {
+ if (part->next == NULL) {
+ break;
+ }
+
+ part = part->next;
+ h = part->elts;
+ i = 0;
+ }
+
+ if (h[i].hash == 0) {
+ continue;
+ }
+
+ if (qjs_array_length(cx, keys, &length)) {
+ goto fail;
+ }
+
+ for (k = 0; k < length; k++) {
+ key = JS_GetPropertyUint32(cx, keys, k);
+ if (JS_IsException(key)) {
+ goto fail;
+ }
+
+ hdr.data = (u_char *) JS_ToCStringLen(cx, &hdr.len, key);
+ JS_FreeValue(cx, key);
+
+ found = h[i].key.len == hdr.len
+ && ngx_strncasecmp(h[i].key.data,
+ hdr.data, hdr.len) == 0;
+
+ JS_FreeCString(cx, (const char *) hdr.data);
+
+ if (found) {
+ break;
+ }
+ }
+
+ if (k == n) {
+ item = JS_NewStringLen(cx, (const char *) h[i].key.data,
+ h[i].key.len);
+ if (JS_IsException(value)) {
+ goto fail;
+ }
+
+ ret = JS_DefinePropertyValueUint32(cx, keys, n, item,
+ JS_PROP_C_W_E);
+ if (ret < 0) {
+ JS_FreeValue(cx, item);
+ goto fail;
+ }
+
+ n++;
+ }
+ }
+
+ func = JS_GetPropertyStr(cx, keys, "sort");
+ if (JS_IsException(func)) {
+ JS_ThrowInternalError(cx, "sort function not found");
+ goto fail;
+ }
+
+ retval = JS_Call(cx, func, keys, 0, NULL);
+
+ JS_FreeValue(cx, func);
+ JS_FreeValue(cx, keys);
+
+ return retval;
+
+fail:
+
+ JS_FreeValue(cx, keys);
+
+ return JS_EXCEPTION;
+}
+
+
+static JSValue
+ngx_qjs_headers_get(JSContext *cx, JSValue this_val, ngx_str_t *name,
+ int as_array)
+{
+ int ret;
+ JSValue retval, value;
+ njs_chb_t chain;
+ ngx_uint_t i;
+ ngx_list_part_t *part;
+ ngx_js_tb_elt_t *h, *ph;
+ ngx_js_headers_t *headers;
+
+ headers = JS_GetOpaque2(cx, this_val, NGX_QJS_CLASS_ID_FETCH_HEADERS);
+ if (headers == NULL) {
+ return JS_NULL;
+ }
+
+ part = &headers->header_list.part;
+ h = part->elts;
+ ph = NULL;
+
+ for (i = 0; /* void */; i++) {
+
+ if (i >= part->nelts) {
+ if (part->next == NULL) {
+ break;
+ }
+
+ part = part->next;
+ h = part->elts;
+ i = 0;
+ }
+
+ if (h[i].hash == 0) {
+ continue;
+ }
+
+ if (h[i].key.len == name->len
+ && ngx_strncasecmp(h[i].key.data, name->data, name->len) == 0)
+ {
+ ph = &h[i];
+ break;
+ }
+ }
+
+ if (as_array) {
+ retval = JS_NewArray(cx);
+ if (JS_IsException(retval)) {
+ return JS_EXCEPTION;
+ }
+
+ i = 0;
+ while (ph != NULL) {
+ value = JS_NewStringLen(cx, (const char *) ph->value.data,
+ ph->value.len);
+ if (JS_IsException(value)) {
+ JS_FreeValue(cx, retval);
+ return JS_EXCEPTION;
+ }
+
+ ret = JS_DefinePropertyValueUint32(cx, retval, i, value,
+ JS_PROP_C_W_E);
+ if (ret < 0) {
+ JS_FreeValue(cx, retval);
+ JS_FreeValue(cx, value);
+ return JS_EXCEPTION;
+ }
+
+ i++;
+ ph = ph->next;
+ }
+
+ return retval;
+ }
+
+ if (ph == NULL) {
+ return JS_NULL;
+ }
+
+ NJS_CHB_CTX_INIT(&chain, cx);
+
+ h = ph;
+
+ for ( ;; ) {
+ njs_chb_append(&chain, h->value.data, h->value.len);
+
+ if (h->next == NULL) {
+ break;
+ }
+
+ njs_chb_append_literal(&chain, ", ");
+ h = h->next;
+ }
+
+ retval = qjs_string_create_chb(cx, &chain);
+
+ return retval;
+}
+
+
+static int
+ngx_qjs_fetch_headers_own_property(JSContext *cx, JSPropertyDescriptor *desc,
+ JSValueConst obj, JSAtom prop)
+{
+ JSValue value;
+ ngx_str_t name;
+
+ name.data = (u_char *) JS_AtomToCString(cx, prop);
+ if (name.data == NULL) {
+ return -1;
+ }
+
+ name.len = ngx_strlen(name.data);
+
+ value = ngx_qjs_headers_get(cx, obj, &name, 0);
+ JS_FreeCString(cx, (char *) name.data);
+
+ if (JS_IsException(value)) {
+ return -1;
+ }
+
+ if (JS_IsNull(value)) {
+ return 0;
+ }
+
+ if (desc == NULL) {
+ JS_FreeValue(cx, value);
+
+ } else {
+ desc->flags = JS_PROP_ENUMERABLE;
+ desc->getter = JS_UNDEFINED;
+ desc->setter = JS_UNDEFINED;
+ desc->value = value;
+ }
+
+ return 1;
+}
+
+
+static int
+ngx_qjs_fetch_headers_own_property_names(JSContext *cx, JSPropertyEnum **ptab,
+ uint32_t *plen, JSValueConst obj)
+{
+ int ret;
+ JSAtom key;
+ JSValue keys;
+ ngx_uint_t i;
+ ngx_list_part_t *part;
+ ngx_js_tb_elt_t *h;
+ ngx_js_headers_t *headers;
+
+ headers = JS_GetOpaque2(cx, obj, NGX_QJS_CLASS_ID_FETCH_HEADERS);
+ if (headers == NULL) {
+ (void) JS_ThrowInternalError(cx, "\"this\" is not a Headers object");
+ return -1;
+ }
+
+ keys = JS_NewObject(cx);
+ if (JS_IsException(keys)) {
+ return -1;
+ }
+
+ part = &headers->header_list.part;
+ h = part->elts;
+
+ for (i = 0; /* void */; i++) {
+
+ if (i >= part->nelts) {
+ if (part->next == NULL) {
+ break;
+ }
+
+ part = part->next;
+ h = part->elts;
+ i = 0;
+ }
+
+ if (h[i].hash == 0) {
+ continue;
+ }
+
+ key = JS_NewAtomLen(cx, (const char *) h[i].key.data, h[i].key.len);
+ if (key == JS_ATOM_NULL) {
+ goto fail;
+ }
+
+ if (JS_DefinePropertyValue(cx, keys, key, JS_UNDEFINED,
+ JS_PROP_ENUMERABLE) < 0)
+ {
+ JS_FreeAtom(cx, key);
+ goto fail;
+ }
+
+ JS_FreeAtom(cx, key);
+ }
+
+ ret = JS_GetOwnPropertyNames(cx, ptab, plen, keys,
+ JS_GPN_STRING_MASK | JS_GPN_ENUM_ONLY);
+
+ JS_FreeValue(cx, keys);
+
+ return ret;
+
+fail:
+
+ JS_FreeValue(cx, keys);
+
+ return -1;
+}
+
+
+static JSValue
+ngx_qjs_ext_fetch_headers_append(JSContext *cx, JSValueConst this_val,
+ int argc, JSValueConst *argv)
+{
+ ngx_int_t rc;
+ ngx_js_headers_t *headers;
+
+ headers = JS_GetOpaque2(cx, this_val, NGX_QJS_CLASS_ID_FETCH_HEADERS);
+ if (headers == NULL) {
+ return JS_ThrowInternalError(cx,
+ "\"this\" is not fetch headers object");
+ }
+
+ rc = ngx_qjs_headers_fill_header_free(cx, headers,
+ JS_DupValue(cx, argv[0]),
+ JS_DupValue(cx, argv[1]));
+ if (rc != NGX_OK) {
+ return JS_EXCEPTION;
+ }
+
+ return JS_UNDEFINED;
+}
+
+
+static JSValue
+ngx_qjs_ext_fetch_headers_delete(JSContext *cx, JSValueConst this_val,
+ int argc, JSValueConst *argv)
+{
+ ngx_int_t rc;
+ ngx_str_t name;
+ ngx_uint_t i;
+ ngx_list_part_t *part;
+ ngx_js_tb_elt_t *h;
+ ngx_js_headers_t *headers;
+
+ headers = JS_GetOpaque2(cx, this_val, NGX_QJS_CLASS_ID_FETCH_HEADERS);
+ if (headers == NULL) {
+ return JS_ThrowInternalError(cx,
+ "\"this\" is not fetch headers object");
+ }
+
+ rc = ngx_qjs_string(cx, argv[0], &name);
+ if (rc != NGX_OK) {
+ return JS_EXCEPTION;
+ }
+
+ part = &headers->header_list.part;
+ h = part->elts;
+
+ for (i = 0; /* void */; i++) {
+
+ if (i >= part->nelts) {
+ if (part->next == NULL) {
+ break;
+ }
+
+ part = part->next;
+ h = part->elts;
+ i = 0;
+ }
+
+ if (h[i].hash == 0) {
+ continue;
+ }
+
+ if (name.len == h[i].key.len
+ && (ngx_strncasecmp(name.data, h[i].key.data, name.len) == 0))
+ {
+ h[i].hash = 0;
+ }
+ }
+
+ if (name.len == (sizeof("Content-Type") - 1)
+ && ngx_strncasecmp(name.data, (u_char *) "Content-Type", name.len)
+ == 0)
+ {
+ headers->content_type = NULL;
+ }
+
+ return JS_UNDEFINED;
+}
+
+
+static JSValue
+ngx_qjs_ext_fetch_headers_foreach(JSContext *cx, JSValueConst this_val,
+ int argc, JSValueConst *argv)
+{
+ int ret;
+ JSValue callback, keys, key;
+ JSValue header, retval, arguments[2];
+ uint32_t length;;
+ ngx_str_t name;
+ ngx_uint_t i;
+ ngx_js_headers_t *headers;
+
+ headers = JS_GetOpaque2(cx, this_val, NGX_QJS_CLASS_ID_FETCH_HEADERS);
+ if (headers == NULL) {
+ return JS_ThrowInternalError(cx,
+ "\"this\" is not fetch headers object");
+ }
+
+ callback = argv[0];
+
+ if (!JS_IsFunction(cx, callback)) {
+ return JS_ThrowInternalError(cx, "\"callback\" is not a function");
+ }
+
+ keys = ngx_qjs_headers_ext_keys(cx, this_val);
+ if (JS_IsException(keys)) {
+ return JS_EXCEPTION;
+ }
+
+ if (qjs_array_length(cx, keys, &length)) {
+ goto fail;
+ }
+
+ for (i = 0; i < length; i++) {
+ key = JS_GetPropertyUint32(cx, keys, i);
+ if (JS_IsException(key)) {
+ goto fail;
+ }
+
+ ret = ngx_qjs_string(cx, key, &name);
+ if (ret != NGX_OK) {
+ JS_FreeValue(cx, key);
+ goto fail;
+ }
+
+ header = ngx_qjs_headers_get(cx, this_val, &name, 0);
+ if (JS_IsException(header)) {
+ JS_FreeValue(cx, key);
+ goto fail;
+ }
+
+ arguments[0] = key;
+ arguments[1] = header;
+
+ retval = JS_Call(cx, callback, JS_UNDEFINED, 2, arguments);
+
+ JS_FreeValue(cx, key);
+ JS_FreeValue(cx, header);
+ JS_FreeValue(cx, retval);
+ }
+
+ JS_FreeValue(cx, keys);
+
+ return JS_UNDEFINED;
+
+fail:
+
+ JS_FreeValue(cx, keys);
+
+ return JS_EXCEPTION;
+}
+
+
+static JSValue
+ngx_qjs_ext_fetch_headers_get(JSContext *cx, JSValueConst this_val,
+ int argc, JSValueConst *argv, int magic)
+{
+ ngx_int_t rc;
+ ngx_str_t name;
+
+ rc = ngx_qjs_string(cx, argv[0], &name);
+ if (rc != NGX_OK) {
+ return JS_EXCEPTION;
+ }
+
+ return ngx_qjs_headers_get(cx, this_val, &name, magic);
+}
+
+
+static JSValue
+ngx_qjs_ext_fetch_headers_has(JSContext *cx, JSValueConst this_val,
+ int argc, JSValueConst *argv)
+{
+ JSValue retval;
+ ngx_int_t rc;
+ ngx_str_t name;
+
+ rc = ngx_qjs_string(cx, argv[0], &name);
+ if (rc != NGX_OK) {
+ return JS_EXCEPTION;
+ }
+
+ retval = ngx_qjs_headers_get(cx, this_val, &name, 0);
+ if (JS_IsException(retval)) {
+ return JS_EXCEPTION;
+ }
+
+ rc = !JS_IsNull(retval);
+ JS_FreeValue(cx, retval);
+
+ return JS_NewBool(cx, rc);
+}
+
+
+static JSValue
+ngx_qjs_ext_fetch_headers_set(JSContext *cx, JSValueConst this_val,
+ int argc, JSValueConst *argv)
+{
+ ngx_int_t rc;
+ ngx_str_t name, value;
+ ngx_uint_t i;
+ ngx_list_part_t *part;
+ ngx_js_tb_elt_t *h, **ph, **pp;
+ ngx_js_headers_t *headers;
+
+ headers = JS_GetOpaque2(cx, this_val, NGX_QJS_CLASS_ID_FETCH_HEADERS);
+ if (headers == NULL) {
+ return JS_ThrowInternalError(cx,
+ "\"this\" is not fetch headers object");
+ }
+
+ rc = ngx_qjs_string(cx, argv[0], &name);
+ if (rc != NGX_OK) {
+ return JS_EXCEPTION;
+ }
+
+ rc = ngx_qjs_string(cx, argv[1], &value);
+ if (rc != NGX_OK) {
+ return JS_EXCEPTION;
+ }
+
+ part = &headers->header_list.part;
+ h = part->elts;
+
+ for (i = 0; /* void */; i++) {
+
+ if (i >= part->nelts) {
+ if (part->next == NULL) {
+ break;
+ }
+
+ part = part->next;
+ h = part->elts;
+ i = 0;
+ }
+
+ if (h[i].hash == 0) {
+ continue;
+ }
+
+ if (name.len == h[i].key.len
+ && (ngx_strncasecmp(name.data, h[i].key.data, name.len) == 0))
+ {
+ h[i].value.len = value.len;
+ h[i].value.data = value.data;
+
+ ph = &h[i].next;
+
+ while (*ph) {
+ pp = ph;
+ ph = &(*ph)->next;
+ *pp = NULL;
+ }
+
+ return JS_UNDEFINED;
+ }
+ }
+
+ rc = ngx_qjs_headers_append(cx, headers, name.data, name.len,
+ value.data, value.len);
+ if (rc != NGX_OK) {
+ return JS_EXCEPTION;
+ }
+
+ return JS_UNDEFINED;
+}
+
+
+static JSValue
+ngx_qjs_ext_fetch_request_body(JSContext *cx, JSValueConst this_val,
+ int argc, JSValueConst *argv, int magic)
+{
+ char * string;
+ JSValue result;
+ ngx_js_request_t *request;
+
+ request = JS_GetOpaque2(cx, this_val, NGX_QJS_CLASS_ID_FETCH_REQUEST);
+ if (request == NULL) {
+ return JS_UNDEFINED;
+ }
+
+ if (request->body_used) {
+ return JS_ThrowInternalError(cx, "body stream already read");
+ }
+
+ request->body_used = 1;
+
+ switch (magic) {
+ case NGX_QJS_BODY_ARRAY_BUFFER:
+ /*
+ * no free_func for JS_NewArrayBuffer()
+ * because request->body is allocated from e->pool
+ * and will be freed when context is freed.
+ */
+ result = JS_NewArrayBuffer(cx, request->body.data, request->body.len,
+ NULL, NULL, 0);
+ if (JS_IsException(result)) {
+ return JS_ThrowOutOfMemory(cx);
+ }
+
+ break;
+
+ case NGX_QJS_BODY_JSON:
+ case NGX_QJS_BODY_TEXT:
+ default:
+ result = qjs_string_create(cx, request->body.data, request->body.len);
+ if (JS_IsException(result)) {
+ return JS_ThrowOutOfMemory(cx);
+ }
+
+ if (magic == NGX_QJS_BODY_JSON) {
+ string = js_malloc(cx, request->body.len + 1);
+
+ JS_FreeValue(cx, result);
+ result = JS_UNDEFINED;
+
+ if (string == NULL) {
+ return JS_ThrowOutOfMemory(cx);
+ }
+
+ ngx_memcpy(string, request->body.data, request->body.len);
+ string[request->body.len] = '\0';
+
+ /* 'string' must be zero terminated. */
+ result = JS_ParseJSON(cx, string, request->body.len, "<input>");
+ js_free(cx, string);
+ if (JS_IsException(result)) {
+ break;
+ }
+ }
+ }
+
+ return qjs_promise_result(cx, result);
+}
+
+
+static JSValue
+ngx_qjs_ext_fetch_request_body_used(JSContext *cx, JSValueConst this_val)
+{
+ ngx_js_request_t *request;
+
+ request = JS_GetOpaque2(cx, this_val, NGX_QJS_CLASS_ID_FETCH_REQUEST);
+ if (request == NULL) {
+ return JS_UNDEFINED;
+ }
+
+ return JS_NewBool(cx, request->body_used);
+}
+
+
+static JSValue
+ngx_qjs_ext_fetch_request_cache(JSContext *cx, JSValueConst this_val)
+{
+ ngx_js_request_t *request;
+
+ request = JS_GetOpaque2(cx, this_val, NGX_QJS_CLASS_ID_FETCH_REQUEST);
+ if (request == NULL) {
+ return JS_UNDEFINED;
+ }
+
+ return ngx_qjs_fetch_flag(cx, ngx_qjs_fetch_cache_modes,
+ request->cache_mode);
+}
+
+
+static JSValue
+ngx_qjs_ext_fetch_request_credentials(JSContext *cx, JSValueConst this_val)
+{
+ ngx_js_request_t *request;
+
+ request = JS_GetOpaque2(cx, this_val, NGX_QJS_CLASS_ID_FETCH_REQUEST);
+ if (request == NULL) {
+ return JS_UNDEFINED;
+ }
+
+ return ngx_qjs_fetch_flag(cx, ngx_qjs_fetch_credentials,
+ request->credentials);
+}
+
+
+static JSValue
+ngx_qjs_ext_fetch_request_headers(JSContext *cx, JSValueConst this_val)
+{
+ JSValue header;
+ ngx_js_request_t *request;
+
+ request = JS_GetOpaque2(cx, this_val, NGX_QJS_CLASS_ID_FETCH_REQUEST);
+ if (request == NULL) {
+ return JS_UNDEFINED;
+ }
+
+ header = ngx_qjs_arg(request->header_value);
+
+ if (JS_IsUndefined(header)) {
+ header = JS_NewObjectClass(cx, NGX_QJS_CLASS_ID_FETCH_HEADERS);
+ if (JS_IsException(header)) {
+ return JS_ThrowInternalError(cx, "fetch header creation failed");
+ }
+
+ JS_SetOpaque(header, &request->headers);
+
+ ngx_qjs_arg(request->header_value) = header;
+ }
+
+ return JS_DupValue(cx, header);
+}
+
+
+static JSValue
+ngx_qjs_ext_fetch_request_field(JSContext *cx, JSValueConst this_val, int magic)
+{
+ ngx_str_t *field;
+ ngx_js_request_t *request;
+
+ request = JS_GetOpaque2(cx, this_val, NGX_QJS_CLASS_ID_FETCH_REQUEST);
+ if (request == NULL) {
+ return JS_UNDEFINED;
+ }
+
+ field = (ngx_str_t *) ((u_char *) request + magic);
+
+ return qjs_string_create(cx, field->data, field->len);
+}
+
+
+static JSValue
+ngx_qjs_ext_fetch_request_mode(JSContext *cx, JSValueConst this_val)
+{
+ ngx_js_request_t *request;
+
+ request = JS_GetOpaque2(cx, this_val, NGX_QJS_CLASS_ID_FETCH_REQUEST);
+ if (request == NULL) {
+ return JS_UNDEFINED;
+ }
+
+ return ngx_qjs_fetch_flag(cx, ngx_qjs_fetch_modes, request->mode);
+}
+
+
+static void
+ngx_qjs_fetch_request_finalizer(JSRuntime *rt, JSValue val)
+{
+ ngx_js_request_t *request;
+
+ request = JS_GetOpaque(val, NGX_QJS_CLASS_ID_FETCH_REQUEST);
+
+ JS_FreeValueRT(rt, ngx_qjs_arg(request->header_value));
+}
+
+
+static JSValue
+ngx_qjs_ext_fetch_response_status(JSContext *cx, JSValueConst this_val)
+{
+ ngx_js_response_t *response;
+
+ response = JS_GetOpaque2(cx, this_val, NGX_QJS_CLASS_ID_FETCH_RESPONSE);
+ if (response == NULL) {
+ return JS_UNDEFINED;
+ }
+
+ return JS_NewUint32(cx, response->code);
+}
+
+
+static JSValue
+ngx_qjs_ext_fetch_response_status_text(JSContext *cx, JSValueConst this_val)
+{
+ ngx_js_response_t *response;
+
+ response = JS_GetOpaque2(cx, this_val, NGX_QJS_CLASS_ID_FETCH_RESPONSE);
+ if (response == NULL) {
+ return JS_UNDEFINED;
+ }
+
+ return qjs_string_create(cx, response->status_text.data,
+ response->status_text.len);
+}
+
+
+static JSValue
+ngx_qjs_ext_fetch_response_ok(JSContext *cx, JSValueConst this_val)
+{
+ ngx_js_response_t *response;
+
+ response = JS_GetOpaque2(cx, this_val, NGX_QJS_CLASS_ID_FETCH_RESPONSE);
+ if (response == NULL) {
+ return JS_UNDEFINED;
+ }
+
+ return JS_NewBool(cx, response->code >= 200 && response->code < 300);
+}
+
+
+static JSValue
+ngx_qjs_ext_fetch_response_body_used(JSContext *cx, JSValueConst this_val)
+{
+ ngx_js_response_t *response;
+
+ response = JS_GetOpaque2(cx, this_val, NGX_QJS_CLASS_ID_FETCH_RESPONSE);
+ if (response == NULL) {
+ return JS_UNDEFINED;
+ }
+
+ return JS_NewBool(cx, response->body_used);
+}
+
+
+static JSValue
+ngx_qjs_ext_fetch_response_headers(JSContext *cx, JSValueConst this_val)
+{
+ JSValue header;
+ ngx_js_response_t *response;
+
+ response = JS_GetOpaque2(cx, this_val, NGX_QJS_CLASS_ID_FETCH_RESPONSE);
+ if (response == NULL) {
+ return JS_UNDEFINED;
+ }
+
+ header = ngx_qjs_arg(response->header_value);
+
+ if (JS_IsUndefined(header)) {
+ header = JS_NewObjectClass(cx, NGX_QJS_CLASS_ID_FETCH_HEADERS);
+ if (JS_IsException(header)) {
+ return JS_ThrowInternalError(cx, "fetch header creation failed");
+ }
+
+ JS_SetOpaque(header, &response->headers);
+
+ ngx_qjs_arg(response->header_value) = header;
+ }
+
+ return JS_DupValue(cx, header);
+}
+
+
+static JSValue
+ngx_qjs_ext_fetch_response_type(JSContext *cx, JSValueConst this_val)
+{
+ ngx_js_response_t *response;
+
+ response = JS_GetOpaque2(cx, this_val, NGX_QJS_CLASS_ID_FETCH_RESPONSE);
+ if (response == NULL) {
+ return JS_UNDEFINED;
+ }
+
+ return JS_NewString(cx, "basic");
+}
+
+
+static JSValue
+ngx_qjs_ext_fetch_response_body(JSContext *cx, JSValueConst this_val,
+ int argc, JSValueConst *argv, int magic)
+{
+ JSValue result;
+ njs_int_t ret;
+ njs_str_t string;
+ ngx_js_response_t *response;
+
+ response = JS_GetOpaque2(cx, this_val, NGX_QJS_CLASS_ID_FETCH_RESPONSE);
+ if (response == NULL) {
+ return JS_UNDEFINED;
+ }
+
+ if (response->body_used) {
+ return JS_ThrowInternalError(cx, "body stream already read");
+ }
+
+ response->body_used = 1;
+
+ switch (magic) {
+ case NGX_QJS_BODY_ARRAY_BUFFER:
+ case NGX_QJS_BODY_TEXT:
+ ret = njs_chb_join(&response->chain, &string);
+ if (ret != NJS_OK) {
+ return JS_ThrowOutOfMemory(cx);
+ }
+
+ if (magic == NGX_QJS_BODY_TEXT) {
+ result = qjs_string_create(cx, string.start, string.length);
+ if (JS_IsException(result)) {
+ return JS_ThrowOutOfMemory(cx);
+ }
+
+ break;
+ }
+
+ /*
+ * no free_func for JS_NewArrayBuffer()
+ * because string.start is allocated from e->pool
+ * and will be freed when context is freed.
+ */
+ result = JS_NewArrayBuffer(cx, string.start, string.length, NULL, NULL,
+ 0);
+ if (JS_IsException(result)) {
+ return JS_ThrowOutOfMemory(cx);
+ }
+
+ break;
+
+ case NGX_QJS_BODY_JSON:
+ default:
+ /* 'string.start' must be zero terminated. */
+ njs_chb_append_literal(&response->chain, "\0");
+ ret = njs_chb_join(&response->chain, &string);
+ if (ret != NJS_OK) {
+ return JS_ThrowOutOfMemory(cx);
+ }
+
+ result = JS_ParseJSON(cx, (char *) string.start, string.length - 1,
+ "<input>");
+ if (JS_IsException(result)) {
+ break;
+ }
+ }
+
+ return qjs_promise_result(cx, result);
+}
+
+
+static JSValue
+ngx_qjs_ext_fetch_response_redirected(JSContext *cx, JSValueConst this_val)
+{
+ ngx_js_response_t *response;
+
+ response = JS_GetOpaque2(cx, this_val, NGX_QJS_CLASS_ID_FETCH_RESPONSE);
+ if (response == NULL) {
+ return JS_UNDEFINED;
+ }
+
+ return JS_NewBool(cx, 0);
+}
+
+
+static JSValue
+ngx_qjs_ext_fetch_response_field(JSContext *cx, JSValueConst this_val, int magic)
+{
+ ngx_str_t *field;
+ ngx_js_response_t *response;
+
+ response = JS_GetOpaque2(cx, this_val, NGX_QJS_CLASS_ID_FETCH_RESPONSE);
+ if (response == NULL) {
+ return JS_UNDEFINED;
+ }
+
+ field = (ngx_str_t *) ((u_char *) response + magic);
+
+ return qjs_string_create(cx, field->data, field->len);
+}
+
+
+static void
+ngx_qjs_fetch_response_finalizer(JSRuntime *rt, JSValue val)
+{
+ ngx_js_response_t *response;
+
+ response = JS_GetOpaque(val, NGX_QJS_CLASS_ID_FETCH_RESPONSE);
+
+ JS_FreeValueRT(rt, ngx_qjs_arg(response->header_value));
+ njs_chb_destroy(&response->chain);
+}
+
+
+static JSValue
+ngx_qjs_fetch_flag(JSContext *cx, const ngx_qjs_entry_t *entries,
+ ngx_int_t value)
+{
+ const ngx_qjs_entry_t *e;
+
+ for (e = entries; e->name.len != 0; e++) {
+ if (e->value == value) {
+ return qjs_string_create(cx, e->name.data, e->name.len);
+ }
+ }
+
+ return JS_ThrowInternalError(cx, "unknown fetch flag: %i", (int) value);
+}
+
+
+static ngx_int_t
+ngx_qjs_fetch_flag_set(JSContext *cx, const ngx_qjs_entry_t *entries,
+ JSValue object, const char *prop)
+{
+ JSValue value;
+ ngx_int_t rc;
+ ngx_str_t flag;
+ const ngx_qjs_entry_t *e;
+
+ value = JS_GetPropertyStr(cx, object, prop);
+ if (JS_IsException(value)) {
+ JS_ThrowInternalError(cx, "failed to get %s property", prop);
+ return NGX_ERROR;
+ }
+
+ if (JS_IsUndefined(value)) {
+ return entries[0].value;
+ }
+
+ rc = ngx_qjs_string(cx, value, &flag);
+ JS_FreeValue(cx, value);
+ if (rc != NGX_OK) {
+ return NGX_ERROR;
+ }
+
+ for (e = entries; e->name.len != 0; e++) {
+ if (flag.len == e->name.len
+ && ngx_strncasecmp(e->name.data, flag.data, flag.len) == 0)
+ {
+ return e->value;
+ }
+ }
+
+ JS_ThrowInternalError(cx, "unknown %s type: %.*s", prop,
+ (int) flag.len, flag.data);
+
+ return NGX_ERROR;
+}
+
+
+static JSModuleDef *
+ngx_qjs_fetch_init(JSContext *cx, const char *name)
+{
+ int i, class_id;
+ JSValue global_obj, proto, class;
+
+ static const JSClassDef *const classes[] = {
+ &ngx_qjs_fetch_headers_class,
+ &ngx_qjs_fetch_request_class,
+ &ngx_qjs_fetch_response_class,
+ NULL
+ };
+
+ static JSCFunction *ctors[] = {
+ ngx_qjs_fetch_headers_ctor,
+ ngx_qjs_fetch_request_ctor,
+ ngx_qjs_fetch_response_ctor,
+ NULL
+ };
+
+ static const JSCFunctionListEntry *const protos[] = {
+ ngx_qjs_ext_fetch_headers_proto,
+ ngx_qjs_ext_fetch_request_proto,
+ ngx_qjs_ext_fetch_response_proto,
+ NULL
+ };
+
+ static const uint8_t proto_sizes[] = {
+ njs_nitems(ngx_qjs_ext_fetch_headers_proto),
+ njs_nitems(ngx_qjs_ext_fetch_request_proto),
+ njs_nitems(ngx_qjs_ext_fetch_response_proto),
+ 0
+ };
+
+ global_obj = JS_GetGlobalObject(cx);
+
+ for (i = 0; classes[i] != NULL; i++) {
+ class_id = NGX_QJS_CLASS_ID_FETCH_HEADERS + i;
+
+ if (JS_NewClass(JS_GetRuntime(cx), class_id, classes[i]) < 0) {
+ return NULL;
+ }
+
+ proto = JS_NewObject(cx);
+ if (JS_IsException(proto)) {
+ JS_FreeValue(cx, global_obj);
+ return NULL;
+ }
+
+ JS_SetPropertyFunctionList(cx, proto, protos[i], proto_sizes[i]);
+
+ class = JS_NewCFunction2(cx, ctors[i], classes[i]->class_name, 2,
+ JS_CFUNC_constructor, 0);
+ if (JS_IsException(class)) {
+ JS_FreeValue(cx, proto);
+ JS_FreeValue(cx, global_obj);
+ return NULL;
+ }
+
+ JS_SetConstructor(cx, class, proto);
+ JS_SetClassProto(cx, class_id, proto);
+
+ if (JS_SetPropertyStr(cx, global_obj, classes[i]->class_name, class)
+ < 0)
+ {
+ JS_FreeValue(cx, class);
+ JS_FreeValue(cx, proto);
+ JS_FreeValue(cx, global_obj);
+ return NULL;
+ }
+ }
+
+ JS_FreeValue(cx, global_obj);
+
+ return JS_NewCModule(cx, name, NULL);
+}
diff --git a/nginx/ngx_stream_js_module.c b/nginx/ngx_stream_js_module.c
index 5c837494..0e022eb0 100644
--- a/nginx/ngx_stream_js_module.c
+++ b/nginx/ngx_stream_js_module.c
@@ -834,6 +834,7 @@ static JSClassDef ngx_stream_qjs_variables_class = {
qjs_module_t *njs_stream_qjs_addon_modules[] = {
&ngx_qjs_ngx_module,
&ngx_qjs_ngx_shared_dict_module,
+ &ngx_qjs_ngx_fetch_module,
/*
* Shared addons should be in the same order and the same positions
* in all nginx modules.
diff --git a/nginx/t/js_fetch.t b/nginx/t/js_fetch.t
index ae9d1f61..7ee1a602 100644
--- a/nginx/t/js_fetch.t
+++ b/nginx/t/js_fetch.t
@@ -342,6 +342,8 @@ $t->write_file('test.js', <<EOF);
}
}
+ out.sort();
+
r.return(200, JSON.stringify(out));
}
@@ -411,8 +413,6 @@ EOF
$t->try_run('no njs.fetch');
-plan(skip_all => 'not yet') if http_get('/engine') =~ /QuickJS$/m;
-
$t->plan(37);
$t->run_daemon(\&http_daemon, port(8082));
diff --git a/nginx/t/js_fetch_https.t b/nginx/t/js_fetch_https.t
index 9a44a339..8ede1048 100644
--- a/nginx/t/js_fetch_https.t
+++ b/nginx/t/js_fetch_https.t
@@ -196,8 +196,6 @@ foreach my $name ('default.example.com', '1.example.com') {
$t->try_run('no njs.fetch');
-plan(skip_all => 'not yet') if http_get('/engine') =~ /QuickJS$/m;
-
$t->plan(7);
$t->run_daemon(\&dns_daemon, port(8981), $t);
diff --git a/nginx/t/js_fetch_objects.t b/nginx/t/js_fetch_objects.t
index 67cabdfc..bc5cc7ed 100644
--- a/nginx/t/js_fetch_objects.t
+++ b/nginx/t/js_fetch_objects.t
@@ -271,7 +271,9 @@ $t->write_file('test.js', <<EOF);
})
} catch (e) {
- if (!e.message.startsWith('Cannot assign to read-only p')) {
+ if (!e.message.startsWith('Cannot assign to read-only p')
+ && !e.message.startsWith('no setter for property'))
+ {
throw e;
}
}
@@ -386,6 +388,17 @@ $t->write_file('test.js', <<EOF);
var body = await r.text();
return `\${body}: \${r.headers.get('Content-Type')}`;
}, 'ABC: text/plain;charset=UTF-8'],
+ ['arrayBuffer()', async () => {
+ var r = new Request("http://nginx.org", {body: 'ABC'});
+ var body = await r.arrayBuffer();
+ body = new Uint8Array(body);
+ return new TextDecoder().decode(body);
+ }, 'ABC'],
+ ['json()', async () => {
+ var r = new Request("http://nginx.org", {body: '{"a": 42}'});
+ var body = await r.json();
+ return body.a;
+ }, 42],
['user content type', async () => {
var r = new Request("http://nginx.org",
{body: 'ABC',
@@ -443,6 +456,12 @@ $t->write_file('test.js', <<EOF);
var body = await r.text();
return `\${r.url}: \${body} \${r.headers.get('b')}`;
}, ': Y'],
+ ['arrayBuffer', async () => {
+ var r = new Response('foo');
+ var body = await r.arrayBuffer();
+ body = new Uint8Array(body);
+ return new TextDecoder().decode(body);
+ }, 'foo'],
['json', async () => {
var r = new Response('{"a": {"b": 42}}');
var json = await r.json();
@@ -515,8 +534,6 @@ EOF
$t->try_run('no njs');
-plan(skip_all => 'not yet') if http_get('/engine') =~ /QuickJS$/m;
-
$t->plan(5);
###############################################################################
diff --git a/nginx/t/js_fetch_resolver.t b/nginx/t/js_fetch_resolver.t
index 7cea3386..031ff43c 100644
--- a/nginx/t/js_fetch_resolver.t
+++ b/nginx/t/js_fetch_resolver.t
@@ -146,8 +146,6 @@ EOF
$t->try_run('no njs.fetch');
-plan(skip_all => 'not yet') if http_get('/engine') =~ /QuickJS$/m;
-
$t->plan(5);
$t->run_daemon(\&dns_daemon, port(8981), $t);
diff --git a/nginx/t/js_fetch_timeout.t b/nginx/t/js_fetch_timeout.t
index 5b207b90..ab1ba24a 100644
--- a/nginx/t/js_fetch_timeout.t
+++ b/nginx/t/js_fetch_timeout.t
@@ -116,8 +116,6 @@ EOF
$t->try_run('no js_fetch_timeout');
-plan(skip_all => 'not yet') if http_get('/engine') =~ /QuickJS$/m;
-
$t->plan(2);
###############################################################################
diff --git a/nginx/t/js_fetch_verify.t b/nginx/t/js_fetch_verify.t
index 4c97e04d..f98b4d8c 100644
--- a/nginx/t/js_fetch_verify.t
+++ b/nginx/t/js_fetch_verify.t
@@ -114,8 +114,6 @@ foreach my $name ('localhost') {
$t->try_run('no js_fetch_verify');
-plan(skip_all => 'not yet') if http_get('/engine') =~ /QuickJS$/m;
-
$t->plan(2);
$t->run_daemon(\&dns_daemon, port(8981), $t);
diff --git a/nginx/t/js_periodic_fetch.t b/nginx/t/js_periodic_fetch.t
index d7bcfb76..0231b662 100644
--- a/nginx/t/js_periodic_fetch.t
+++ b/nginx/t/js_periodic_fetch.t
@@ -121,8 +121,6 @@ EOF
$t->try_run('no js_periodic with fetch');
-plan(skip_all => 'not yet') if http_get('/engine') =~ /QuickJS$/m;
-
$t->plan(3);
###############################################################################
diff --git a/nginx/t/stream_js_fetch.t b/nginx/t/stream_js_fetch.t
index c57128a8..9a42ae29 100644
--- a/nginx/t/stream_js_fetch.t
+++ b/nginx/t/stream_js_fetch.t
@@ -171,8 +171,6 @@ EOF
$t->try_run('no stream njs available');
-plan(skip_all => 'not yet') if http_get('/engine') =~ /QuickJS$/m;
-
$t->plan(9);
$t->run_daemon(\&stream_daemon, port(8090), port(8091));
diff --git a/nginx/t/stream_js_fetch_https.t b/nginx/t/stream_js_fetch_https.t
index 5d7c5c20..987a896a 100644
--- a/nginx/t/stream_js_fetch_https.t
+++ b/nginx/t/stream_js_fetch_https.t
@@ -273,8 +273,6 @@ foreach my $name ('default.example.com', '1.example.com') {
$t->try_run('no njs.fetch');
-plan(skip_all => 'not yet') if http_get('/engine') =~ /QuickJS$/m;
-
$t->plan(6);
$t->run_daemon(\&dns_daemon, port(8981), $t);
diff --git a/nginx/t/stream_js_fetch_init.t b/nginx/t/stream_js_fetch_init.t
index 3f6d7262..f48b9d5e 100644
--- a/nginx/t/stream_js_fetch_init.t
+++ b/nginx/t/stream_js_fetch_init.t
@@ -92,8 +92,6 @@ EOF
$t->try_run('no stream njs available');
-plan(skip_all => 'not yet') if http_get('/engine') =~ /QuickJS$/m;
-
$t->plan(1);
$t->run_daemon(\&stream_daemon, port(8090));
diff --git a/nginx/t/stream_js_periodic_fetch.t b/nginx/t/stream_js_periodic_fetch.t
index e88d69d5..60599423 100644
--- a/nginx/t/stream_js_periodic_fetch.t
+++ b/nginx/t/stream_js_periodic_fetch.t
@@ -147,7 +147,6 @@ EOF
$t->run_daemon(\&stream_daemon, port(8090));
$t->try_run('no js_periodic with fetch');
-plan(skip_all => 'not yet') if http_get('/engine') =~ /QuickJS$/m;
$t->plan(3);
$t->waitforsocket('127.0.0.1:' . port(8090));
diff --git a/src/qjs.c b/src/qjs.c
index a941ba71..7993de1b 100644
--- a/src/qjs.c
+++ b/src/qjs.c
@@ -1147,6 +1147,31 @@ qjs_string_base64url(JSContext *cx, const njs_str_t *src)
}
+int
+qjs_array_length(JSContext *cx, JSValueConst arr, uint32_t *plen)
+{
+ int ret;
+ JSValue value;
+ uint32_t len;
+
+ value = JS_GetPropertyStr(cx, arr, "length");
+ if (JS_IsException(value)) {
+ return -1;
+ }
+
+ ret = JS_ToUint32(cx, &len, value);
+ JS_FreeValue(cx, value);
+
+ if (ret) {
+ return -1;
+ }
+
+ *plen = len;
+
+ return 0;
+}
+
+
static JSValue
qjs_promise_fill_trampoline(JSContext *cx, int argc, JSValueConst *argv)
{
diff --git a/src/qjs.h b/src/qjs.h
index d3bbc0e8..e920453e 100644
--- a/src/qjs.h
+++ b/src/qjs.h
@@ -136,6 +136,8 @@ JSValue qjs_string_create_chb(JSContext *cx, njs_chb_t *chain);
void qjs_free_prop_enum(JSContext *ctx, JSPropertyEnum *tab, uint32_t len);
+int qjs_array_length(JSContext *cx, JSValueConst arr, uint32_t *plen);
+
JSValue qjs_promise_result(JSContext *cx, JSValue result);
JSValue qjs_string_hex(JSContext *cx, const njs_str_t *src);