[njs] Modules: introduced QuickJS engine.

noreply at nginx.com noreply at nginx.com
Wed Sep 18 01:06:02 UTC 2024


details:   https://github.com/nginx/njs/commit/201b127679e27fe63eff5c1b4356ec4ed9ec4611
branches:  master
commit:    201b127679e27fe63eff5c1b4356ec4ed9ec4611
user:      Dmitry Volyntsev <xeioex at nginx.com>
date:      Fri, 14 Jun 2024 20:54:28 -0700
description:
Modules: introduced QuickJS engine.

"js_engine" directive is introduced which sets JavaScript engine.
When the module is built with QuickJS library "js_engine qjs;" sets
QuickJS engine for the current block. By default njs engine is used.

For example,

nginx.conf:

    location /a {
        js_engine qjs;
        # will be handled by QuickJS
        js_content main.handler;
    }

    location /b {
        # will be handled by njs
        js_content main.handler;
    }

QuickJS engine implements drop-in replacement for nginx/njs objects
with the following exceptions:

    * nginx module API to be added later: ngx.fetch(), ngx.shared.dict.
    * Built-in modules to be added later: fs, crypto, WebCrypto, xml.
    * NJS specific API: njs.dump(), njs.on(), console.dump().
    * js_preload_object directive.

---
 .github/workflows/check-pr.yml     |   65 +
 nginx/config                       |  100 +-
 nginx/config.make                  |   10 +-
 nginx/ngx_http_js_module.c         | 3121 +++++++++++++++++++++++++++++++++++-
 nginx/ngx_js.c                     | 1838 +++++++++++++++++++--
 nginx/ngx_js.h                     |  131 ++
 nginx/ngx_stream_js_module.c       | 1137 ++++++++++++-
 nginx/t/js_console.t               |   20 +-
 nginx/t/js_dump.t                  |   22 +-
 nginx/t/js_engine.t                |  140 ++
 nginx/t/js_fetch.t                 |   16 +-
 nginx/t/js_fetch_https.t           |   16 +-
 nginx/t/js_fetch_objects.t         |   18 +-
 nginx/t/js_fetch_resolver.t        |   16 +-
 nginx/t/js_fetch_timeout.t         |   17 +-
 nginx/t/js_fetch_verify.t          |   16 +-
 nginx/t/js_object.t                |   18 +-
 nginx/t/js_periodic.t              |   16 +-
 nginx/t/js_preload_object.t        |   16 +-
 nginx/t/js_shared_dict.t           |   18 +-
 nginx/t/stream_js_console.t        |   38 +-
 nginx/t/stream_js_exit.t           |   16 +-
 nginx/t/stream_js_fetch.t          |   16 +-
 nginx/t/stream_js_fetch_https.t    |   16 +-
 nginx/t/stream_js_fetch_init.t     |   16 +-
 nginx/t/stream_js_object.t         |   85 +-
 nginx/t/stream_js_preload_object.t |   27 +-
 nginx/t/stream_js_shared_dict.t    |   16 +-
 src/qjs.h                          |    6 +
 src/qjs_buffer.c                   |   23 +-
 30 files changed, 6751 insertions(+), 259 deletions(-)

diff --git a/.github/workflows/check-pr.yml b/.github/workflows/check-pr.yml
index e26b1e29..7dd2bd57 100644
--- a/.github/workflows/check-pr.yml
+++ b/.github/workflows/check-pr.yml
@@ -84,3 +84,68 @@ jobs:
           TEST_NGINX_BINARY: "${{ github.workspace }}/nginx-source/objs/nginx"
           TEST_NGINX_GLOBALS: "load_module ${{ github.workspace }}/nginx-source/objs/ngx_http_js_module.so; load_module ${{ github.workspace }}/nginx-source/objs/ngx_stream_js_module.so;"
           TEST_NGINX_VERBOSE: 1
+
+      - name: Create LSAN suppression file
+        run: |
+          cat << EOF > lsan_suppressions.txt
+          leak:ngx_event_process_init
+          EOF
+
+      - name: Configure and build nginx and njs modules with quickjs, static modules
+        run: |
+          cd nginx-source
+          $NGINX_CONFIGURE_CMD --with-cc-opt="$CC_OPT -I${{ github.workspace }}/quickjs -fsanitize=address" --with-ld-opt="$LD_OPT -L${{ github.workspace }}/quickjs -fsanitize=address" --add-module=../nginx || cat objs/autoconf.err
+          $MAKE_UTILITY -j$(nproc)
+
+      - name: Test njs modules, static modules
+        run: |
+          ulimit -c unlimited
+          prove -v -j$(nproc) -Inginx-tests/lib --state=save nginx/t . || prove -v -Inginx-tests/lib --state=failed
+        env:
+          TEST_NGINX_BINARY: "${{ github.workspace }}/nginx-source/objs/nginx"
+          TEST_NGINX_VERBOSE: 1
+          ASAN_OPTIONS: "detect_odr_violation=0:report_globals=0"
+          LSAN_OPTIONS: "suppressions=${{ github.workspace }}/lsan_suppressions.txt"
+
+      - name: Test njs modules (js_engine qjs), static modules
+        run: |
+          ulimit -c unlimited
+          prove -v -j$(nproc) -Inginx-tests/lib --state=save nginx/t . || prove -v -Inginx-tests/lib --state=failed
+        env:
+          TEST_NGINX_BINARY: "${{ github.workspace }}/nginx-source/objs/nginx"
+          TEST_NGINX_GLOBALS_HTTP: "js_engine qjs;"
+          TEST_NGINX_GLOBALS_STREAM: "js_engine qjs;"
+          TEST_NGINX_VERBOSE: 1
+          ASAN_OPTIONS: "detect_odr_violation=0:report_globals=0"
+          LSAN_OPTIONS: "suppressions=${{ github.workspace }}/lsan_suppressions.txt"
+
+      - name: Configure and build nginx and njs modules with quickjs, dynamic modules
+        run: |
+          cd nginx-source
+          $NGINX_CONFIGURE_CMD --with-debug --with-cc-opt="$CC_OPT -I${{ github.workspace }}/quickjs -fsanitize=address" --with-ld-opt="$LD_OPT -L${{ github.workspace }}/quickjs -fsanitize=address" --add-dynamic-module=../nginx || cat objs/autoconf.err
+          $MAKE_UTILITY -j$(nproc) modules
+          $MAKE_UTILITY -j$(nproc)
+
+      - name: Test njs modules, dynamic modules
+        run: |
+          ulimit -c unlimited
+          prove -v -j$(nproc) -Inginx-tests/lib --state=save nginx/t . || prove -v -Inginx-tests/lib --state=failed
+        env:
+          TEST_NGINX_BINARY: "${{ github.workspace }}/nginx-source/objs/nginx"
+          TEST_NGINX_GLOBALS: "load_module ${{ github.workspace }}/nginx-source/objs/ngx_http_js_module.so; load_module ${{ github.workspace }}/nginx-source/objs/ngx_stream_js_module.so;"
+          TEST_NGINX_VERBOSE: 1
+          ASAN_OPTIONS: "detect_odr_violation=0:report_globals=0:fast_unwind_on_malloc=0"
+          LSAN_OPTIONS: "suppressions=${{ github.workspace }}/lsan_suppressions.txt"
+
+      - name: Test njs modules (js_engine qjs), dynamic modules
+        run: |
+          ulimit -c unlimited
+          prove -v -j$(nproc) -Inginx-tests/lib --state=save nginx/t . || prove -v -Inginx-tests/lib --state=failed
+        env:
+          TEST_NGINX_BINARY: "${{ github.workspace }}/nginx-source/objs/nginx"
+          TEST_NGINX_GLOBALS: "load_module ${{ github.workspace }}/nginx-source/objs/ngx_stream_js_module.so; load_module ${{ github.workspace }}/nginx-source/objs/ngx_http_js_module.so;"
+          TEST_NGINX_GLOBALS_HTTP: "js_engine qjs;"
+          TEST_NGINX_GLOBALS_STREAM: "js_engine qjs;"
+          TEST_NGINX_VERBOSE: 1
+          ASAN_OPTIONS: "detect_odr_violation=0:report_globals=0:fast_unwind_on_malloc=0"
+          LSAN_OPTIONS: "suppressions=${{ github.workspace }}/lsan_suppressions.txt"
diff --git a/nginx/config b/nginx/config
index 700ae4ab..436f06cb 100644
--- a/nginx/config
+++ b/nginx/config
@@ -3,6 +3,7 @@ ngx_addon_name="ngx_js_module"
 NJS_OPENSSL=${NJS_OPENSSL:-YES}
 NJS_LIBXSLT=${NJS_LIBXSLT:-YES}
 NJS_ZLIB=${NJS_ZLIB:-YES}
+NJS_QUICKJS=${NJS_QUICKJS:-YES}
 
 NJS_DEPS="$ngx_addon_dir/ngx_js.h \
     $ngx_addon_dir/ngx_js_fetch.h \
@@ -12,9 +13,78 @@ NJS_SRCS="$ngx_addon_dir/ngx_js.c \
     $ngx_addon_dir/ngx_js_regex.c \
     $ngx_addon_dir/ngx_js_shared_dict.c"
 
+QJS_DEPS=""
+QJS_SRCS=""
+
 NJS_OPENSSL_LIB=
 NJS_XSLT_LIB=
 NJS_ZLIB_LIB=
+NJS_QUICKJS_LIB=
+NJS_QUICKJS_INC=
+NJS_HAVE_QUICKJS=
+
+if [ $NJS_QUICKJS != NO ]; then
+
+    ngx_feature="QuickJS library -lquickjs.lto"
+    ngx_feature_name=NJS_HAVE_QUICKJS
+    ngx_feature_run=yes
+    ngx_feature_incs="#if defined(__GNUC__) && (__GNUC__ >= 8)
+                      #pragma GCC diagnostic push
+                      #pragma GCC diagnostic ignored \"-Wcast-function-type\"
+                      #endif
+
+                      #include <quickjs.h>"
+    ngx_feature_path=""
+    ngx_feature_libs="-lquickjs.lto -lm -ldl -lpthread"
+    ngx_feature_test="JSRuntime *rt;
+
+                      rt = JS_NewRuntime();
+                      (void) JS_GetClassID;
+                      JS_FreeRuntime(rt);
+                      return 0;"
+    . auto/feature
+
+    if [ $ngx_found = no ]; then
+        ngx_feature="QuickJS library -lquickjs"
+        ngx_feature_libs="-lquickjs -lm -ldl -lpthread"
+
+        . auto/feature
+    fi
+
+    if [ $ngx_found = no ]; then
+        ngx_feature="QuickJS library -I/usr/include/quickjs/ -L/usr/lib/quickjs/ -lquickjs.lto"
+        ngx_feature_path="/usr/include/quickjs/"
+        ngx_feature_libs="-L/usr/lib/quickjs/ -lquickjs.lto -lm -ldl -lpthread"
+
+        . auto/feature
+    fi
+
+    if [ $ngx_found = no ]; then
+        ngx_feature="QuickJS library -I/usr/include/quickjs/ -L/usr/lib/quickjs/ -lquickjs"
+        ngx_feature_libs="-L/usr/lib/quickjs/ -lquickjs -lm -ldl -lpthread"
+
+        . auto/feature
+    fi
+
+    if [ $ngx_found = yes ]; then
+
+        ngx_feature="QuickJS JS_NewTypedArray()"
+        ngx_feature_test="(void) JS_NewTypedArray;
+                          return 0;"
+
+        . auto/feature
+
+        if [ $ngx_found = yes ]; then
+            have=NJS_HAVE_QUICKJS_NEW_TYPED_ARRAY . auto/have
+        fi
+
+        NJS_HAVE_QUICKJS=YES
+        NJS_QUICKJS_LIB="$ngx_feature_libs"
+        NJS_QUICKJS_INC="$ngx_feature_path"
+
+        echo " enabled QuickJS engine"
+    fi
+fi
 
 if [ $NJS_OPENSSL != NO ]; then
     NJS_OPENSSL_LIB=OPENSSL
@@ -37,17 +107,30 @@ if [ $NJS_ZLIB != NO ]; then
     have=NJS_HAVE_ZLIB . auto/have
     NJS_SRCS="$NJS_SRCS $ngx_addon_dir/../external/njs_zlib_module.c"
 
+    if [ "$NJS_HAVE_QUICKJS" = "YES" ];  then
+        NJS_SRCS="$NJS_SRCS $ngx_addon_dir/../external/qjs_zlib_module.c"
+    fi
+
     echo " enabled zlib module"
 fi
 
+
+NJS_ENGINE_DEP="$ngx_addon_dir/../build/libnjs.a"
+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"
+fi
+
 if [ $HTTP != NO ]; then
     ngx_module_type=HTTP_AUX_FILTER
     ngx_module_name=ngx_http_js_module
-    ngx_module_incs="$ngx_addon_dir/../src $ngx_addon_dir/../build"
-    ngx_module_deps="$ngx_addon_dir/../build/libnjs.a $NJS_DEPS"
-    ngx_module_srcs="$ngx_addon_dir/ngx_http_js_module.c $NJS_SRCS"
+    ngx_module_incs="$ngx_addon_dir/../src $ngx_addon_dir/../build \
+                     $NJS_QUICKJS_INC"
+    ngx_module_deps="$NJS_ENGINE_DEP $NJS_DEPS $QJS_DEPS"
+    ngx_module_srcs="$ngx_addon_dir/ngx_http_js_module.c $NJS_SRCS $QJS_SRCS"
     ngx_module_libs="PCRE $NJS_OPENSSL_LIB $NJS_XSLT_LIB $NJS_ZLIB_LIB \
-                     $ngx_addon_dir/../build/libnjs.a -lm"
+                     $NJS_QUICKJS_LIB $NJS_ENGINE_LIB -lm"
 
     . auto/module
 
@@ -59,11 +142,12 @@ fi
 if [ $STREAM != NO ]; then
     ngx_module_type=STREAM
     ngx_module_name=ngx_stream_js_module
-    ngx_module_incs="$ngx_addon_dir/../src $ngx_addon_dir/../build"
-    ngx_module_deps="$ngx_addon_dir/../build/libnjs.a $NJS_DEPS"
-    ngx_module_srcs="$ngx_addon_dir/ngx_stream_js_module.c $NJS_SRCS"
+    ngx_module_incs="$ngx_addon_dir/../src $ngx_addon_dir/../build \
+                     $NJS_QUICKJS_INC"
+    ngx_module_deps="$NJS_ENGINE_DEP $NJS_DEPS $QJS_DEPS"
+    ngx_module_srcs="$ngx_addon_dir/ngx_stream_js_module.c $NJS_SRCS $QJS_SRCS"
     ngx_module_libs="PCRE $NJS_OPENSSL_LIB $NJS_XSLT_LIB $NJS_ZLIB_LIB \
-                     $ngx_addon_dir/../build/libnjs.a -lm"
+                     $NJS_QUICKJS_LIB $NJS_ENGINE_LIB -lm"
 
     . auto/module
 fi
diff --git a/nginx/config.make b/nginx/config.make
index cf7859e9..2fa40063 100644
--- a/nginx/config.make
+++ b/nginx/config.make
@@ -3,7 +3,15 @@ cat << END                                            >> $NGX_MAKEFILE
 $ngx_addon_dir/../build/libnjs.a: $NGX_MAKEFILE
 	cd $ngx_addon_dir/.. \\
 	&& if [ -f build/Makefile ]; then \$(MAKE) clean; fi \\
-	&& CFLAGS="\$(CFLAGS)" CC="\$(CC)" ./configure --no-openssl --no-libxml2 --no-zlib --no-pcre \\
+	&& CFLAGS="\$(CFLAGS)" CC="\$(CC)" ./configure --no-openssl \\
+		--no-libxml2 --no-zlib --no-pcre --no-quickjs \\
 	&& \$(MAKE) libnjs
 
+$ngx_addon_dir/../build/libqjs.a: $NGX_MAKEFILE
+	cd $ngx_addon_dir/.. \\
+	&& if [ -f build/Makefile ]; then \$(MAKE) clean; fi \\
+	&& CFLAGS="\$(CFLAGS)" CC="\$(CC)" ./configure --no-openssl \\
+		--no-libxml2 --no-zlib --no-pcre \\
+	&& \$(MAKE) libnjs libqjs
+
 END
diff --git a/nginx/ngx_http_js_module.c b/nginx/ngx_http_js_module.c
index 35f988d0..4a50a949 100644
--- a/nginx/ngx_http_js_module.c
+++ b/nginx/ngx_http_js_module.c
@@ -46,6 +46,7 @@ typedef struct {
 #define NJS_HEADER_SEMICOLON   0x1
 #define NJS_HEADER_SINGLE      0x2
 #define NJS_HEADER_ARRAY       0x4
+#define NJS_HEADER_GET         0x8
 
 
 typedef struct ngx_http_js_ctx_s  ngx_http_js_ctx_t;
@@ -86,6 +87,20 @@ typedef njs_int_t (*njs_http_js_header_handler_t)(njs_vm_t *vm,
 typedef njs_int_t (*njs_http_js_header_handler122_t)(njs_vm_t *vm,
     ngx_http_request_t *r, ngx_list_t *headers, njs_str_t *name,
     njs_value_t *setval, njs_value_t *retval);
+#if (NJS_HAVE_QUICKJS)
+typedef int (*njs_http_qjs_header_handler_t)(JSContext *cx,
+    ngx_http_request_t *r, ngx_str_t *name, JSPropertyDescriptor *pdesc,
+    JSValue *value, unsigned flags);
+
+
+typedef struct {
+    ngx_http_request_t  *request;
+    JSValue              args;
+    JSValue              request_body;
+    JSValue              response_body;
+} ngx_http_qjs_request_t;
+
+#endif
 
 
 typedef struct {
@@ -260,6 +275,88 @@ static njs_int_t ngx_http_js_server(njs_vm_t *vm, ngx_http_request_t *r,
     unsigned flags, njs_str_t *name, njs_value_t *setval,
     njs_value_t *retval);
 
+#if (NJS_HAVE_QUICKJS)
+static JSValue ngx_http_qjs_ext_to_string_tag(JSContext *cx,
+    JSValueConst this_val);
+static JSValue ngx_http_qjs_ext_args(JSContext *cx, JSValueConst this_val);
+static JSValue ngx_http_qjs_ext_done(JSContext *cx, JSValueConst this_val,
+    int argc, JSValueConst *argv);
+static JSValue ngx_http_qjs_ext_finish(JSContext *cx, JSValueConst this_val,
+    int argc, JSValueConst *argv);
+static JSValue ngx_http_qjs_ext_headers_in(JSContext *cx,
+    JSValueConst this_val);
+static JSValue ngx_http_qjs_ext_headers_out(JSContext *cx,
+    JSValueConst this_val);
+static JSValue ngx_http_qjs_ext_http_version(JSContext *cx,
+    JSValueConst this_val);
+static JSValue ngx_http_qjs_ext_internal(JSContext *cx, JSValueConst this_val);
+static JSValue ngx_http_qjs_ext_internal_redirect(JSContext *cx,
+    JSValueConst this_val, int argc, JSValueConst *argv);
+static JSValue ngx_http_qjs_ext_log(JSContext *cx, JSValueConst this_val,
+    int argc, JSValueConst *argv, int level);
+static JSValue ngx_http_qjs_ext_periodic_to_string_tag(JSContext *cx,
+    JSValueConst this_val);
+static JSValue ngx_http_qjs_ext_periodic_variables(JSContext *cx,
+    JSValueConst this_val, int type);
+static JSValue ngx_http_qjs_ext_parent(JSContext *cx, JSValueConst this_val);
+static JSValue ngx_http_qjs_ext_remote_address(JSContext *cx,
+    JSValueConst this_val);
+static JSValue ngx_http_qjs_ext_request_body(JSContext *cx,
+    JSValueConst this_val, int type);
+static JSValue ngx_http_qjs_ext_response_body(JSContext *cx,
+    JSValueConst this_val, int type);
+static JSValue ngx_http_qjs_ext_return(JSContext *cx, JSValueConst this_val,
+    int argc, JSValueConst *argv);
+static JSValue ngx_http_qjs_ext_send(JSContext *cx, JSValueConst this_val,
+    int argc, JSValueConst *argv);
+static JSValue ngx_http_qjs_ext_send_buffer(JSContext *cx,
+    JSValueConst this_val, int argc, JSValueConst *argv);
+static JSValue ngx_http_qjs_ext_send_header(JSContext *cx,
+    JSValueConst this_val, int argc, JSValueConst *argv);
+static JSValue ngx_http_qjs_ext_set_return_value(JSContext *cx,
+    JSValueConst this_val, int argc, JSValueConst *argv);
+static JSValue ngx_http_qjs_ext_status_get(JSContext *cx,
+    JSValueConst this_val);
+static JSValue ngx_http_qjs_ext_status_set(JSContext *cx, JSValueConst this_val,
+    JSValueConst value);
+static JSValue ngx_http_qjs_ext_string(JSContext *cx, JSValueConst this_val,
+    int offset);
+static JSValue ngx_http_qjs_ext_subrequest(JSContext *cx, JSValueConst this_val,
+    int argc, JSValueConst *argv);
+static JSValue ngx_http_qjs_ext_raw_headers(JSContext *cx,
+    JSValueConst this_val, int out);
+static JSValue ngx_http_qjs_ext_variables(JSContext *cx,
+    JSValueConst this_val, int type);
+
+static int ngx_http_qjs_variables_own_property(JSContext *cx,
+    JSPropertyDescriptor *pdesc, JSValueConst obj, JSAtom prop);
+static int ngx_http_qjs_variables_set_property(JSContext *cx, JSValueConst obj,
+    JSAtom atom, JSValueConst value, JSValueConst receiver, int flags);
+
+static int ngx_http_qjs_headers_in_own_property(JSContext *cx,
+    JSPropertyDescriptor *pdesc, JSValueConst obj, JSAtom prop);
+static int ngx_http_qjs_headers_in_own_property_names(JSContext *ctx,
+    JSPropertyEnum **ptab, uint32_t *plen, JSValueConst obj);
+
+static int ngx_http_qjs_headers_out_own_property(JSContext *cx,
+    JSPropertyDescriptor *pdesc, JSValueConst obj, JSAtom prop);
+static int ngx_http_qjs_headers_out_own_property_names(JSContext *cx,
+    JSPropertyEnum **ptab, uint32_t *plen, JSValueConst obj);
+static int ngx_http_qjs_headers_out_set_property(JSContext *cx,
+    JSValueConst obj, JSAtom atom, JSValueConst value, JSValueConst receiver,
+    int flags);
+static int ngx_http_qjs_headers_out_define_own_property(JSContext *cx,
+    JSValueConst this_obj, JSAtom prop, JSValueConst val, JSValueConst getter,
+    JSValueConst setter, int flags);
+static int ngx_http_qjs_headers_out_delete_property(JSContext *cx,
+    JSValueConst obj, JSAtom prop);
+
+static ngx_http_request_t *ngx_http_qjs_request(JSValueConst val);
+static JSValue ngx_http_qjs_request_make(JSContext *cx, ngx_int_t proto_id,
+    ngx_http_request_t *r);
+static void ngx_http_qjs_request_finalizer(JSRuntime *rt, JSValue val);
+#endif
+
 static ngx_pool_t *ngx_http_js_pool(ngx_http_request_t *r);
 static ngx_resolver_t *ngx_http_js_resolver(ngx_http_request_t *r);
 static ngx_msec_t ngx_http_js_resolver_timeout(ngx_http_request_t *r);
@@ -304,6 +401,9 @@ static ngx_int_t ngx_http_js_parse_unsafe_uri(ngx_http_request_t *r,
 
 static ngx_conf_bitmask_t  ngx_http_js_engines[] = {
     { ngx_string("njs"), NGX_ENGINE_NJS },
+#if (NJS_HAVE_QUICKJS)
+    { ngx_string("qjs"), NGX_ENGINE_QJS },
+#endif
     { ngx_null_string, 0 }
 };
 
@@ -328,6 +428,13 @@ static ngx_command_t  ngx_http_js_commands[] = {
       offsetof(ngx_http_js_loc_conf_t, type),
       &ngx_http_js_engines },
 
+    { ngx_string("js_context_reuse"),
+      NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1,
+      ngx_conf_set_size_slot,
+      NGX_HTTP_LOC_CONF_OFFSET,
+      offsetof(ngx_http_js_loc_conf_t, reuse),
+      NULL },
+
     { ngx_string("js_import"),
       NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE13,
       ngx_js_import,
@@ -497,8 +604,8 @@ static ngx_http_output_header_filter_pt  ngx_http_next_header_filter;
 static ngx_http_output_body_filter_pt    ngx_http_next_body_filter;
 
 
-static njs_int_t    ngx_http_js_request_proto_id;
-static njs_int_t    ngx_http_js_periodic_session_proto_id;
+static njs_int_t    ngx_http_js_request_proto_id = 1;
+static njs_int_t    ngx_http_js_periodic_session_proto_id = 2;
 
 
 static njs_external_t  ngx_http_js_ext_request[] = {
@@ -924,6 +1031,125 @@ static ngx_http_js_entry_t ngx_http_methods[] = {
 };
 
 
+#if (NJS_HAVE_QUICKJS)
+
+static const JSCFunctionListEntry ngx_http_qjs_ext_request[] = {
+    JS_CGETSET_DEF("[Symbol.toStringTag]", ngx_http_qjs_ext_to_string_tag,
+                   NULL),
+    JS_CGETSET_DEF("args", ngx_http_qjs_ext_args, NULL),
+    JS_CFUNC_DEF("done", 0, ngx_http_qjs_ext_done),
+    JS_CFUNC_MAGIC_DEF("error", 1, ngx_http_qjs_ext_log, NGX_LOG_ERR),
+    JS_CFUNC_DEF("finish", 0, ngx_http_qjs_ext_finish),
+    JS_CGETSET_DEF("headersIn", ngx_http_qjs_ext_headers_in, NULL),
+    JS_CGETSET_DEF("headersOut", ngx_http_qjs_ext_headers_out, NULL),
+    JS_CGETSET_DEF("httpVersion", ngx_http_qjs_ext_http_version, NULL),
+    JS_CGETSET_DEF("internal", ngx_http_qjs_ext_internal, NULL),
+    JS_CFUNC_DEF("internalRedirect", 1, ngx_http_qjs_ext_internal_redirect),
+    JS_CFUNC_MAGIC_DEF("log", 1, ngx_http_qjs_ext_log, NGX_LOG_INFO),
+    JS_CGETSET_MAGIC_DEF("method", ngx_http_qjs_ext_string, NULL,
+                         offsetof(ngx_http_request_t, method_name)),
+    JS_CGETSET_DEF("parent", ngx_http_qjs_ext_parent, NULL),
+    JS_CGETSET_MAGIC_DEF("rawHeadersIn", ngx_http_qjs_ext_raw_headers, NULL, 0),
+    JS_CGETSET_MAGIC_DEF("rawHeadersOut", ngx_http_qjs_ext_raw_headers, NULL,
+                         1),
+    JS_CGETSET_MAGIC_DEF("rawVariables", ngx_http_qjs_ext_variables,
+                   NULL, NGX_JS_BUFFER),
+    JS_CGETSET_DEF("remoteAddress", ngx_http_qjs_ext_remote_address, NULL),
+    JS_CGETSET_MAGIC_DEF("requestBuffer", ngx_http_qjs_ext_request_body, NULL,
+                         NGX_JS_BUFFER),
+    JS_CGETSET_MAGIC_DEF("requestText", ngx_http_qjs_ext_request_body, NULL,
+                         NGX_JS_STRING),
+    JS_CGETSET_MAGIC_DEF("responseBuffer", ngx_http_qjs_ext_response_body, NULL,
+                         NGX_JS_BUFFER),
+    JS_CGETSET_MAGIC_DEF("responseText", ngx_http_qjs_ext_response_body, NULL,
+                         NGX_JS_STRING),
+    JS_CFUNC_DEF("return", 2, ngx_http_qjs_ext_return),
+    JS_CFUNC_DEF("send", 1, ngx_http_qjs_ext_send),
+    JS_CFUNC_DEF("sendBuffer", 2, ngx_http_qjs_ext_send_buffer),
+    JS_CFUNC_DEF("sendHeader", 0, ngx_http_qjs_ext_send_header),
+    JS_CFUNC_DEF("setReturnValue", 1, ngx_http_qjs_ext_set_return_value),
+    JS_CGETSET_DEF("status", ngx_http_qjs_ext_status_get,
+                   ngx_http_qjs_ext_status_set),
+    JS_CFUNC_DEF("subrequest", 3, ngx_http_qjs_ext_subrequest),
+    JS_CGETSET_MAGIC_DEF("uri", ngx_http_qjs_ext_string, NULL,
+                         offsetof(ngx_http_request_t, uri)),
+    JS_CGETSET_MAGIC_DEF("variables", ngx_http_qjs_ext_variables,
+                         NULL, NGX_JS_STRING),
+    JS_CFUNC_MAGIC_DEF("warn", 1, ngx_http_qjs_ext_log, NGX_LOG_WARN),
+};
+
+
+static const JSCFunctionListEntry ngx_http_qjs_ext_periodic[] = {
+    JS_CGETSET_DEF("[Symbol.toStringTag]",
+                   ngx_http_qjs_ext_periodic_to_string_tag, NULL),
+    JS_CGETSET_MAGIC_DEF("rawVariables", ngx_http_qjs_ext_periodic_variables,
+                   NULL, NGX_JS_BUFFER),
+    JS_CGETSET_MAGIC_DEF("variables", ngx_http_qjs_ext_periodic_variables,
+                         NULL, NGX_JS_STRING),
+};
+
+
+static JSClassDef ngx_http_qjs_request_class = {
+    "Request",
+    .finalizer = ngx_http_qjs_request_finalizer,
+};
+
+
+static JSClassDef ngx_http_qjs_periodic_class = {
+    "PeriodicSession",
+    .finalizer = NULL,
+};
+
+
+static JSClassDef ngx_http_qjs_variables_class = {
+    "Variables",
+    .finalizer = NULL,
+    .exotic = & (JSClassExoticMethods) {
+        .get_own_property = ngx_http_qjs_variables_own_property,
+        .set_property = ngx_http_qjs_variables_set_property,
+    },
+};
+
+
+static JSClassDef ngx_http_qjs_headers_in_class = {
+    "headersIn",
+    .finalizer = NULL,
+    .exotic = & (JSClassExoticMethods) {
+        .get_own_property = ngx_http_qjs_headers_in_own_property,
+        .get_own_property_names = ngx_http_qjs_headers_in_own_property_names,
+    },
+};
+
+
+static JSClassDef ngx_http_qjs_headers_out_class = {
+    "headersOut",
+    .finalizer = NULL,
+    .exotic = & (JSClassExoticMethods) {
+        .get_own_property = ngx_http_qjs_headers_out_own_property,
+        .get_own_property_names = ngx_http_qjs_headers_out_own_property_names,
+        .set_property = ngx_http_qjs_headers_out_set_property,
+        .define_own_property = ngx_http_qjs_headers_out_define_own_property,
+        .delete_property = ngx_http_qjs_headers_out_delete_property,
+    },
+};
+
+
+qjs_module_t *njs_http_qjs_addon_modules[] = {
+    &ngx_qjs_ngx_module,
+    /*
+     * Shared addons should be in the same order and the same positions
+     * in all nginx modules.
+     */
+#ifdef NJS_HAVE_ZLIB
+    &qjs_zlib_module,
+#endif
+    NULL,
+};
+
+
+#endif
+
+
 static ngx_int_t
 ngx_http_js_content_handler(ngx_http_request_t *r)
 {
@@ -1426,6 +1652,14 @@ ngx_http_js_cleanup_ctx(void *data)
                    ctx->engine);
 
     r = ngx_js_ctx_external(ctx);
+
+    /*
+     * Restoring the original module context, because it can be reset
+     * by internalRedirect() method. Proper ctx is required for
+     * ngx_http_qjs_request_finalizer() to work correctly.
+     */
+    ngx_http_set_ctx(r, ctx, ngx_http_js_module);
+
     jlcf = ngx_http_get_module_loc_conf(r, ngx_http_js_module);
 
     ngx_js_ctx_destroy((ngx_js_ctx_t *) ctx, (ngx_js_loc_conf_t *) jlcf);
@@ -4447,25 +4681,2886 @@ ngx_engine_njs_clone(ngx_js_ctx_t *ctx, ngx_js_loc_conf_t *cf,
 }
 
 
+#if (NJS_HAVE_QUICKJS)
+
 static ngx_int_t
-ngx_http_js_init_conf_vm(ngx_conf_t *cf, ngx_js_loc_conf_t *conf)
+ngx_http_qjs_query_string_decode(njs_chb_t *chain, const u_char *start,
+    size_t size)
 {
-    ngx_engine_opts_t    options;
-    ngx_js_main_conf_t  *jmcf;
+    u_char                *dst;
+    uint32_t               cp;
+    const u_char          *p, *end;
+    njs_unicode_decode_t   ctx;
 
-    memset(&options, 0, sizeof(ngx_engine_opts_t));
+    static const int8_t  hex[256]
+        njs_aligned(32) =
+    {
+        -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+        -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+        -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+         0,  1,  2,  3,  4,  5,  6,  7,  8,  9, -1, -1, -1, -1, -1, -1,
+        -1, 10, 11, 12, 13, 14, 15, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+        -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+        -1, 10, 11, 12, 13, 14, 15, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+        -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+        -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+        -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+        -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+        -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+        -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+        -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+        -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+        -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+    };
 
-    options.engine = conf->type;
+    njs_utf8_decode_init(&ctx);
 
-    if (conf->type == NGX_ENGINE_NJS) {
-        jmcf = ngx_http_conf_get_module_main_conf(cf, ngx_http_js_module);
-        ngx_http_js_uptr[NGX_JS_MAIN_CONF_INDEX] = (uintptr_t) jmcf;
+    cp = 0;
 
-        options.u.njs.metas = &ngx_http_js_metas;
-        options.u.njs.addons = njs_http_js_addon_modules;
-        options.clone = ngx_engine_njs_clone;
+    p = start;
+    end = p + size;
+
+    while (p < end) {
+        if (*p == '%' && end - p > 2 && hex[p[1]] >= 0 && hex[p[2]] >= 0) {
+            cp = njs_utf8_consume(&ctx, (hex[p[1]] << 4) | hex[p[2]]);
+            p += 3;
+
+        } else {
+            if (*p == '+') {
+                cp = ' ';
+                p++;
+
+            } else {
+                cp = njs_utf8_decode(&ctx, &p, end);
+            }
+        }
+
+        if (cp > NJS_UNICODE_MAX_CODEPOINT) {
+            if (cp == NJS_UNICODE_CONTINUE) {
+                continue;
+            }
+
+            cp = NJS_UNICODE_REPLACEMENT;
+        }
+
+        dst = njs_chb_reserve(chain, 4);
+        if (dst == NULL) {
+            return NGX_ERROR;
+        }
+
+        njs_chb_written(chain, njs_utf8_encode(dst, cp) - dst);
+    }
+
+    if (cp == NJS_UNICODE_CONTINUE) {
+        dst = njs_chb_reserve(chain, 3);
+        if (dst == NULL) {
+            return NGX_ERROR;
+        }
+
+        njs_chb_written(chain,
+                        njs_utf8_encode(dst, NJS_UNICODE_REPLACEMENT) - dst);
+    }
+
+    return NGX_OK;
+}
+
+
+static JSValue
+ngx_http_qjs_ext_to_string_tag(JSContext *cx,
+    JSValueConst this_val)
+{
+    return JS_NewString(cx, "Request");
+}
+
+
+static JSValue
+ngx_http_qjs_ext_args(JSContext *cx, JSValueConst this_val)
+{
+    u_char                  *start, *end, *p, *v;
+    uint32_t                 len;
+    JSAtom                   key;
+    JSValue                  args, val, prev, length, arr;
+    njs_str_t                decoded;
+    njs_int_t                ret;
+    ngx_int_t                rc;
+    njs_chb_t                chain;
+    ngx_http_request_t      *r;
+    ngx_http_qjs_request_t  *req;
+
+    req = JS_GetOpaque(this_val, NGX_QJS_CLASS_ID_HTTP_REQUEST);
+    if (req == NULL) {
+        return JS_ThrowInternalError(cx, "\"this\" is not a request object");
+    }
+
+    if (!JS_IsUndefined(req->args)) {
+        return JS_DupValue(cx, req->args);
+    }
+
+    args = JS_NewObject(cx);
+    if (JS_IsException(args)) {
+        return JS_EXCEPTION;
+    }
+
+    NJS_CHB_CTX_INIT(&chain, cx);
+
+    r = req->request;
+
+    rc = ngx_http_qjs_query_string_decode(&chain, r->args.data, r->args.len);
+    if (rc != NGX_OK) {
+        njs_chb_destroy(&chain);
+        return JS_ThrowOutOfMemory(cx);
+    }
+
+    ret = njs_chb_join(&chain, &decoded);
+    njs_chb_destroy(&chain);
+
+    if (ret != NJS_OK) {
+        return JS_ThrowOutOfMemory(cx);
+    }
+
+    start = decoded.start;
+    end = start + decoded.length;
+
+    while (start < end) {
+        p = ngx_strlchr(start, end, '&');
+        if (p == NULL) {
+            p = end;
+        }
+
+        v = ngx_strlchr(start, p, '=');
+        if (v == NULL) {
+            v = p;
+        }
+
+        if (v == start) {
+            start = p + 1;
+            continue;
+        }
+
+        key = JS_NewAtomLen(cx, (const char *) start, v - start);
+        if (key == JS_ATOM_NULL) {
+            chain.free(cx, decoded.start);
+            return JS_EXCEPTION;
+        }
+
+        val = qjs_string_create(cx, v + 1, p - v - 1);
+        if (JS_IsException(val)) {
+            chain.free(cx, decoded.start);
+            JS_FreeAtom(cx, key);
+            return JS_EXCEPTION;
+        }
+
+        prev = JS_GetProperty(cx, args, key);
+        if (JS_IsException(prev)) {
+            chain.free(cx, decoded.start);
+            JS_FreeAtom(cx, key);
+            JS_FreeValue(cx, val);
+            return JS_EXCEPTION;
+        }
+
+        if (JS_IsUndefined(prev)) {
+            if (JS_SetProperty(cx, args, key, val) < 0) {
+                goto exception;
+            }
+
+        } else if (JS_IsArray(cx, prev)) {
+            length = JS_GetPropertyStr(cx, prev, "length");
+
+            if (JS_ToUint32(cx, &len, length)) {
+                goto exception;
+            }
+
+            JS_FreeValue(cx, length);
+
+            if (JS_SetPropertyUint32(cx, prev, len, val) < 0) {
+                goto exception;
+            }
+
+            JS_FreeValue(cx, prev);
+
+        } else {
+
+            arr = JS_NewArray(cx);
+            if (JS_IsException(arr)) {
+                goto exception;
+            }
+
+            if (JS_SetPropertyUint32(cx, arr, 0, prev) < 0) {
+                goto exception;
+            }
+
+            if (JS_SetPropertyUint32(cx, arr, 1, val) < 0) {
+                goto exception;
+            }
+
+            if (JS_SetProperty(cx, args, key, arr) < 0) {
+                goto exception;
+            }
+        }
+
+        JS_FreeAtom(cx, key);
+        start = p + 1;
+    }
+
+    chain.free(cx, decoded.start);
+    req->args = args;
+
+    return JS_DupValue(cx, args);
+
+exception:
+
+    chain.free(cx, decoded.start);
+    JS_FreeAtom(cx, key);
+    JS_FreeValue(cx, val);
+    JS_FreeValue(cx, prev);
+
+    return JS_EXCEPTION;
+}
+
+
+static JSValue
+ngx_http_qjs_ext_done(JSContext *cx, JSValueConst this_val,
+    int argc, JSValueConst *argv)
+{
+    ngx_http_js_ctx_t   *ctx;
+    ngx_http_request_t  *r;
+
+    r = ngx_http_qjs_request(this_val);
+    if (r == NULL) {
+        return JS_ThrowInternalError(cx, "\"this\" is not a request object");
+    }
+
+    ctx = ngx_http_get_module_ctx(r, ngx_http_js_module);
+
+    if (!ctx->filter) {
+        return JS_ThrowTypeError(cx, "cannot set done while not filtering");
+    }
+
+    ctx->done = 1;
+
+    return JS_UNDEFINED;
+}
+
+
+static JSValue
+ngx_http_qjs_ext_finish(JSContext *cx, JSValueConst this_val,
+    int argc, JSValueConst *argv)
+{
+    ngx_http_js_ctx_t   *ctx;
+    ngx_http_request_t  *r;
+
+    r = ngx_http_qjs_request(this_val);
+    if (r == NULL) {
+        return JS_ThrowInternalError(cx, "\"this\" is not a request object");
+    }
+
+    if (ngx_http_send_special(r, NGX_HTTP_LAST) == NGX_ERROR) {
+        return JS_ThrowInternalError(cx, "failed to send response");
+    }
+
+    ctx = ngx_http_get_module_ctx(r, ngx_http_js_module);
+
+    ctx->status = NGX_OK;
+
+    return JS_UNDEFINED;
+}
+
+
+static JSValue
+ngx_http_qjs_ext_headers_in(JSContext *cx, JSValueConst this_val)
+{
+    JSValue              obj;
+    ngx_http_request_t  *r;
+
+    r = ngx_http_qjs_request(this_val);
+    if (r == NULL) {
+        return JS_ThrowInternalError(cx, "\"this\" is not a request object");
+    }
+
+    obj = JS_NewObjectProtoClass(cx, JS_NULL, NGX_QJS_CLASS_ID_HTTP_HEADERS_IN);
+
+    JS_SetOpaque(obj, r);
+
+    return obj;
+}
+
+
+static JSValue
+ngx_http_qjs_ext_headers_out(JSContext *cx, JSValueConst this_val)
+{
+    JSValue              obj;
+    ngx_http_request_t  *r;
+
+    r = ngx_http_qjs_request(this_val);
+    if (r == NULL) {
+        return JS_ThrowInternalError(cx, "\"this\" is not a request object");
+    }
+
+    obj = JS_NewObjectProtoClass(cx, JS_NULL,
+                                 NGX_QJS_CLASS_ID_HTTP_HEADERS_OUT);
+
+    JS_SetOpaque(obj, r);
+
+    return obj;
+}
+
+
+static JSValue
+ngx_http_qjs_ext_http_version(JSContext *cx, JSValueConst this_val)
+{
+    ngx_str_t            v;
+    ngx_http_request_t  *r;
+
+    r = ngx_http_qjs_request(this_val);
+    if (r == NULL) {
+        return JS_ThrowInternalError(cx, "\"this\" is not a request object");
+    }
+
+    switch (r->http_version) {
+    case NGX_HTTP_VERSION_9:
+        ngx_str_set(&v, "0.9");
+        break;
+
+    case NGX_HTTP_VERSION_10:
+        ngx_str_set(&v, "1.0");
+        break;
+
+    case NGX_HTTP_VERSION_11:
+        ngx_str_set(&v, "1.1");
+        break;
+
+    case NGX_HTTP_VERSION_20:
+        ngx_str_set(&v, "2.0");
+        break;
+
+#if (NGX_HTTP_VERSION_30)
+    case NGX_HTTP_VERSION_30:
+        ngx_str_set(&v, "3.0");
+        break;
+#endif
+
+    default:
+        ngx_str_set(&v, "");
+        break;
     }
 
+    return qjs_string_create(cx, v.data, v.len);
+}
+
+
+static JSValue
+ngx_http_qjs_ext_internal(JSContext *cx, JSValueConst this_val)
+{
+    ngx_http_request_t  *r;
+
+    r = ngx_http_qjs_request(this_val);
+    if (r == NULL) {
+        return JS_ThrowInternalError(cx, "\"this\" is not a request object");
+    }
+
+    return JS_NewBool(cx, r->internal);
+}
+
+
+static JSValue
+ngx_http_qjs_ext_internal_redirect(JSContext *cx, JSValueConst this_val,
+    int argc, JSValueConst *argv)
+{
+    ngx_http_js_ctx_t   *ctx;
+    ngx_http_request_t  *r;
+
+    r = ngx_http_qjs_request(this_val);
+    if (r == NULL) {
+        return JS_ThrowInternalError(cx, "\"this\" is not a request object");
+    }
+
+    if (r->parent != NULL) {
+        return JS_ThrowTypeError(cx,
+                         "internalRedirect cannot be called from a subrequest");
+    }
+
+    ctx = ngx_http_get_module_ctx(r, ngx_http_js_module);
+
+    if (ctx->filter) {
+        return JS_ThrowTypeError(cx,
+                         "internalRedirect cannot be called while filtering");
+    }
+
+    if (ngx_qjs_string(ctx->engine, argv[0], &ctx->redirect_uri) != NGX_OK) {
+        return JS_EXCEPTION;
+    }
+
+    ctx->status = NGX_DONE;
+
+    return JS_UNDEFINED;
+}
+
+
+static JSValue
+ngx_http_qjs_ext_log(JSContext *cx, JSValueConst this_val, int argc,
+    JSValueConst *argv, int level)
+{
+    int                  n;
+    const char          *msg;
+    ngx_http_request_t  *r;
+
+    r = ngx_http_qjs_request(this_val);
+    if (r == NULL) {
+        return JS_ThrowInternalError(cx, "\"this\" is not a request object");
+    }
+
+    for (n = 0; n < argc; n++) {
+        msg = JS_ToCString(cx, argv[n]);
+
+        ngx_js_logger(r->connection, level, (u_char *) msg, ngx_strlen(msg));
+
+        JS_FreeCString(cx, msg);
+    }
+
+    return JS_UNDEFINED;
+}
+
+
+static JSValue
+ngx_http_qjs_ext_periodic_to_string_tag(JSContext *cx,
+    JSValueConst this_val)
+{
+    return JS_NewString(cx, "PeriodicSession");
+}
+
+
+static JSValue
+ngx_http_qjs_ext_periodic_variables(JSContext *cx,
+    JSValueConst this_val, int type)
+{
+    JSValue                  obj;
+    ngx_http_qjs_request_t  *req;
+
+    req = JS_GetOpaque(this_val, NGX_QJS_CLASS_ID_HTTP_PERIODIC);
+    if (req == NULL) {
+        return JS_ThrowInternalError(cx, "\"this\" is not a periodic object");
+    }
+
+    obj = JS_NewObjectProtoClass(cx, JS_NULL, NGX_QJS_CLASS_ID_HTTP_VARS);
+
+    /*
+     * Using lowest bit of the pointer to store the buffer type.
+     */
+    type = (type == NGX_JS_BUFFER) ? 1 : 0;
+    JS_SetOpaque(obj, (void *) ((uintptr_t) req->request | (uintptr_t) type));
+
+    return obj;
+}
+
+
+static JSValue
+ngx_http_qjs_ext_parent(JSContext *cx, JSValueConst this_val)
+{
+    ngx_http_js_ctx_t   *ctx;
+    ngx_http_request_t  *r;
+
+    r = ngx_http_qjs_request(this_val);
+    if (r == NULL) {
+        return JS_ThrowInternalError(cx, "\"this\" is not a request object");
+    }
+
+    ctx = r->parent ? ngx_http_get_module_ctx(r->parent, ngx_http_js_module)
+                    : NULL;
+
+    if (ctx == NULL) {
+        return JS_UNDEFINED;
+    }
+
+    return JS_DupValue(cx, ngx_qjs_arg(ctx->args[0]));
+}
+
+
+static JSValue
+ngx_http_qjs_ext_remote_address(JSContext *cx, JSValueConst this_val)
+{
+    ngx_connection_t    *c;
+    ngx_http_request_t  *r;
+
+    r = ngx_http_qjs_request(this_val);
+    if (r == NULL) {
+        return JS_ThrowInternalError(cx, "\"this\" is not a request object");
+    }
+
+    c = r->connection;
+
+    return qjs_string_create(cx, c->addr_text.data, c->addr_text.len);
+}
+
+
+static JSValue
+ngx_http_qjs_ext_response_body(JSContext *cx, JSValueConst this_val, int type)
+{
+    u_char                  *p;
+    size_t                   len;
+    uint32_t                 buffer_type;
+    ngx_buf_t               *b;
+    JSValue                  body;
+    ngx_http_request_t      *r;
+    ngx_http_qjs_request_t  *req;
+
+    req = JS_GetOpaque(this_val, NGX_QJS_CLASS_ID_HTTP_REQUEST);
+    if (req == NULL) {
+        return JS_ThrowInternalError(cx, "\"this\" is not a request object");
+    }
+
+    buffer_type = ngx_js_buffer_type(type);
+
+    if (!JS_IsUndefined(req->response_body)) {
+        if ((buffer_type == NGX_JS_STRING) == JS_IsString(req->response_body)) {
+            return JS_DupValue(cx, req->response_body);
+        }
+    }
+
+    r = req->request;
+
+    b = r->out ? r->out->buf : NULL;
+
+    if (b == NULL) {
+        return JS_UNDEFINED;
+    }
+
+    len = b->last - b->pos;
+
+    p = ngx_pnalloc(r->pool, len);
+    if (p == NULL) {
+        return JS_ThrowOutOfMemory(cx);
+    }
+
+    if (len) {
+        ngx_memcpy(p, b->pos, len);
+    }
+
+    body = ngx_qjs_prop(cx, buffer_type, p, len);
+    if (JS_IsException(body)) {
+        return JS_EXCEPTION;
+    }
+
+    req->response_body = body;
+
+    return JS_DupValue(cx, req->response_body);
+}
+
+
+static JSValue
+ngx_http_qjs_ext_request_body(JSContext *cx, JSValueConst this_val, int type)
+{
+    u_char                  *p, *data;
+    size_t                   len;
+    JSValue                  body;
+    uint32_t                 buffer_type;
+    ngx_buf_t               *buf;
+    ngx_chain_t             *cl;
+    ngx_http_request_t      *r;
+    ngx_http_qjs_request_t  *req;
+
+    req = JS_GetOpaque(this_val, NGX_QJS_CLASS_ID_HTTP_REQUEST);
+    if (req == NULL) {
+        return JS_ThrowInternalError(cx, "\"this\" is not a request object");
+    }
+
+    buffer_type = ngx_js_buffer_type(type);
+
+    if (!JS_IsUndefined(req->request_body)) {
+        if ((buffer_type == NGX_JS_STRING) == JS_IsString(req->request_body)) {
+            return JS_DupValue(cx, req->request_body);
+        }
+
+        JS_FreeValue(cx, req->request_body);
+    }
+
+    r = req->request;
+
+    if (r->request_body == NULL || r->request_body->bufs == NULL) {
+        return JS_UNDEFINED;
+    }
+
+    if (r->request_body->temp_file) {
+        return JS_ThrowTypeError(cx, "request body is in a file");
+    }
+
+    cl = r->request_body->bufs;
+    buf = cl->buf;
+
+    if (cl->next == NULL) {
+        len = buf->last - buf->pos;
+        data = buf->pos;
+
+        goto done;
+    }
+
+    len = buf->last - buf->pos;
+    cl = cl->next;
+
+    for ( /* void */ ; cl; cl = cl->next) {
+        buf = cl->buf;
+        len += buf->last - buf->pos;
+    }
+
+    p = ngx_pnalloc(r->pool, len);
+    if (p == NULL) {
+        return JS_ThrowOutOfMemory(cx);
+    }
+
+    data = p;
+    cl = r->request_body->bufs;
+
+    for ( /* void */ ; cl; cl = cl->next) {
+        buf = cl->buf;
+        p = ngx_cpymem(p, buf->pos, buf->last - buf->pos);
+    }
+
+done:
+
+    body = ngx_qjs_prop(cx, buffer_type, data, len);
+    if (JS_IsException(body)) {
+        return JS_EXCEPTION;
+    }
+
+    req->request_body = body;
+
+    return JS_DupValue(cx, req->request_body);
+}
+
+
+static JSValue
+ngx_http_qjs_ext_return(JSContext *cx, JSValueConst this_val,
+    int argc, JSValueConst *argv)
+{
+    ngx_str_t                  body;
+    ngx_int_t                  status;
+    ngx_http_js_ctx_t         *ctx;
+    ngx_http_request_t        *r;
+    ngx_http_complex_value_t   cv;
+
+    r = ngx_http_qjs_request(this_val);
+    if (r == NULL) {
+        return JS_ThrowInternalError(cx, "\"this\" is not a request object");
+    }
+
+    if (ngx_qjs_integer(cx, argv[0], &status) != NGX_OK) {
+        return JS_EXCEPTION;
+    }
+
+    if (status < 0 || status > 999) {
+        return JS_ThrowRangeError(cx, "code is out of range");
+    }
+
+    ctx = ngx_http_get_module_ctx(r, ngx_http_js_module);
+
+    if (ngx_qjs_string(ctx->engine, argv[1], &body) != NGX_OK) {
+        return JS_ThrowOutOfMemory(cx);
+    }
+
+    if (status < NGX_HTTP_BAD_REQUEST || body.len) {
+        ngx_memzero(&cv, sizeof(ngx_http_complex_value_t));
+
+        cv.value.data = body.data;
+        cv.value.len = body.len;
+
+        ctx->status = ngx_http_send_response(r, status, NULL, &cv);
+
+        if (ctx->status == NGX_ERROR) {
+            return JS_ThrowTypeError(cx, "failed to send response");
+        }
+
+    } else {
+        ctx->status = status;
+    }
+
+    return JS_UNDEFINED;
+}
+
+
+static JSValue
+ngx_http_qjs_ext_status_get(JSContext *cx, JSValueConst this_val)
+{
+    ngx_http_request_t  *r;
+
+    r = ngx_http_qjs_request(this_val);
+    if (r == NULL) {
+        return JS_ThrowInternalError(cx, "\"this\" is not a request object");
+    }
+
+    return JS_NewInt32(cx, r->headers_out.status);
+}
+
+
+static JSValue
+ngx_http_qjs_ext_status_set(JSContext *cx, JSValueConst this_val,
+    JSValueConst value)
+{
+    ngx_int_t            n;
+    ngx_http_request_t  *r;
+
+    r = ngx_http_qjs_request(this_val);
+    if (r == NULL) {
+        return JS_ThrowInternalError(cx, "\"this\" is not a request object");
+    }
+
+    if (ngx_qjs_integer(cx, value, &n) != NGX_OK) {
+        return JS_EXCEPTION;
+    }
+
+    r->headers_out.status = n;
+    r->headers_out.status_line.len = 0;
+
+    return JS_UNDEFINED;
+}
+
+
+static JSValue
+ngx_http_qjs_ext_string(JSContext *cx, JSValueConst this_val, int offset)
+{
+    ngx_str_t           *field;
+    ngx_http_request_t  *r;
+
+    r = ngx_http_qjs_request(this_val);
+    if (r == NULL) {
+        return JS_ThrowInternalError(cx, "\"this\" is not a request object");
+    }
+
+    field = (ngx_str_t *) ((u_char *) r + offset);
+
+    return qjs_string_create(cx, field->data, field->len);
+}
+
+
+static JSValue
+ngx_http_qjs_ext_send(JSContext *cx, JSValueConst this_val,
+    int argc, JSValueConst *argv)
+{
+    ngx_str_t            s;
+    ngx_buf_t           *b;
+    ngx_uint_t           n;
+    ngx_chain_t         *out, *cl, **ll;
+    ngx_http_js_ctx_t   *ctx;
+    ngx_http_request_t  *r;
+
+    r = ngx_http_qjs_request(this_val);
+    if (r == NULL) {
+        return JS_ThrowInternalError(cx, "\"this\" is not a request object");
+    }
+
+    ctx = ngx_http_get_module_ctx(r, ngx_http_js_module);
+
+    if (ctx->filter) {
+        return JS_ThrowTypeError(cx, "cannot send while in body filter");
+    }
+
+    out = NULL;
+    ll = &out;
+
+    for (n = 0; n < (ngx_uint_t) argc; n++) {
+        if (ngx_qjs_string(ctx->engine, argv[n], &s) != NGX_OK) {
+            return JS_ThrowTypeError(cx, "failed to convert arg");
+        }
+
+        if (s.len == 0) {
+            continue;
+        }
+
+        b = ngx_calloc_buf(r->pool);
+        if (b == NULL) {
+            return JS_ThrowInternalError(cx, "failed to allocate buffer");
+        }
+
+        b->start = s.data;
+        b->pos = b->start;
+        b->end = s.data + s.len;
+        b->last = b->end;
+        b->memory = 1;
+
+        cl = ngx_alloc_chain_link(r->pool);
+        if (cl == NULL) {
+            return JS_ThrowInternalError(cx, "failed to allocate chain link");
+        }
+
+        cl->buf = b;
+
+        *ll = cl;
+        ll = &cl->next;
+    }
+
+    *ll = NULL;
+
+    if (ngx_http_output_filter(r, out) == NGX_ERROR) {
+        return JS_ThrowInternalError(cx, "failed to send response");
+    }
+
+    return JS_UNDEFINED;
+}
+
+
+static JSValue
+ngx_http_qjs_ext_send_buffer(JSContext *cx, JSValueConst this_val,
+    int argc, JSValueConst *argv)
+{
+    unsigned             last_buf, flush;
+    JSValue              flags, value;
+    ngx_str_t            buffer;
+    ngx_buf_t           *b;
+    ngx_chain_t         *cl;
+    ngx_http_js_ctx_t   *ctx;
+    ngx_http_request_t  *r;
+
+    r = ngx_http_qjs_request(this_val);
+    if (r == NULL) {
+        return JS_ThrowInternalError(cx, "\"this\" is not a request object");
+    }
+
+    ctx = ngx_http_get_module_ctx(r, ngx_http_js_module);
+
+    if (!ctx->filter) {
+        return JS_ThrowTypeError(cx, "cannot send buffer while not filtering");
+    }
+
+    if (ngx_qjs_string(ctx->engine, argv[0], &buffer) != NGX_OK) {
+        return JS_ThrowTypeError(cx, "failed get buffer arg");
+    }
+
+    flush = ctx->buf->flush;
+    last_buf = ctx->buf->last_buf;
+
+    flags = argv[1];
+
+    if (JS_IsObject(flags)) {
+        value = JS_GetPropertyStr(cx, flags, "flush");
+        if (JS_IsException(value)) {
+            return JS_EXCEPTION;
+        }
+
+        flush = JS_ToBool(cx, value);
+        JS_FreeValue(cx, value);
+
+        value = JS_GetPropertyStr(cx, flags, "last");
+        if (JS_IsException(value)) {
+            return JS_EXCEPTION;
+        }
+
+        last_buf = JS_ToBool(cx, value);
+        JS_FreeValue(cx, value);
+    }
+
+    cl = ngx_chain_get_free_buf(r->pool, &ctx->free);
+    if (cl == NULL) {
+        return JS_ThrowOutOfMemory(cx);
+    }
+
+    b = cl->buf;
+
+    b->flush = flush;
+    b->last_buf = last_buf;
+
+    b->memory = (buffer.len ? 1 : 0);
+    b->sync = (buffer.len ? 0 : 1);
+    b->tag = (ngx_buf_tag_t) &ngx_http_js_module;
+
+    b->start = buffer.data;
+    b->end = buffer.data + buffer.len;
+    b->pos = b->start;
+    b->last = b->end;
+
+    *ctx->last_out = cl;
+    ctx->last_out = &cl->next;
+
+    return JS_UNDEFINED;
+}
+
+
+static JSValue
+ngx_http_qjs_ext_send_header(JSContext *cx, JSValueConst this_val,
+    int argc, JSValueConst *argv)
+{
+    ngx_http_request_t  *r;
+
+    r = ngx_http_qjs_request(this_val);
+    if (r == NULL) {
+        return JS_ThrowInternalError(cx, "\"this\" is not a request object");
+    }
+
+    if (ngx_http_set_content_type(r) != NGX_OK) {
+        return JS_ThrowInternalError(cx, "failed to set content type");
+    }
+
+    if (ngx_http_send_header(r) == NGX_ERROR) {
+        return JS_ThrowInternalError(cx, "failed to send header");
+    }
+
+    return JS_UNDEFINED;
+}
+
+
+static JSValue
+ngx_http_qjs_ext_set_return_value(JSContext *cx, JSValueConst this_val,
+    int argc, JSValueConst *argv)
+{
+    ngx_js_ctx_t        *ctx;
+    ngx_http_request_t  *r;
+
+    r = ngx_http_qjs_request(this_val);
+    if (r == NULL) {
+        return JS_ThrowInternalError(cx, "\"this\" is not a request object");
+    }
+
+    ctx = ngx_http_get_module_ctx(r, ngx_http_js_module);
+
+    JS_FreeValue(cx, ngx_qjs_arg(ctx->retval));
+    ngx_qjs_arg(ctx->retval) = JS_DupValue(cx, argv[0]);
+
+    return JS_UNDEFINED;
+}
+
+
+static ngx_int_t
+ngx_http_qjs_subrequest_done(ngx_http_request_t *r, void *data, ngx_int_t rc)
+{
+    ngx_qjs_event_t  *event = data;
+
+    JSValue              reply;
+    JSContext           *cx;
+    ngx_http_js_ctx_t   *ctx, *sctx;
+
+    if (rc != NGX_OK || r->connection->error || r->buffered) {
+        return rc;
+    }
+
+    sctx = ngx_http_get_module_ctx(r, ngx_http_js_module);
+
+    if (sctx && sctx->done) {
+        return NGX_OK;
+    }
+
+    if (sctx == NULL) {
+        sctx = ngx_pcalloc(r->pool, sizeof(ngx_http_js_ctx_t));
+        if (sctx == NULL) {
+            return NGX_ERROR;
+        }
+
+        ngx_http_set_ctx(r, sctx, ngx_http_js_module);
+
+        ngx_qjs_arg(sctx->response_body) = JS_UNDEFINED;
+    }
+
+    sctx->done = 1;
+
+    ctx = ngx_http_get_module_ctx(r->parent, ngx_http_js_module);
+
+    ngx_log_debug2(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
+                   "js subrequest done s: %ui parent ctx: %p",
+                   r->headers_out.status, ctx);
+
+    if (ctx == NULL) {
+        ngx_log_error(NGX_LOG_ERR, r->connection->log, 0,
+                      "js subrequest: failed to get the parent context");
+
+        return NGX_ERROR;
+    }
+
+    cx = ctx->engine->u.qjs.ctx;
+
+    if (!JS_IsObject(ngx_qjs_arg(sctx->args[0]))) {
+        reply = ngx_http_qjs_request_make(cx, NGX_QJS_CLASS_ID_HTTP_REQUEST, r);
+        if (JS_IsException(reply)) {
+            ngx_log_error(NGX_LOG_ERR, r->connection->log, 0,
+                          "js subrequest reply creation failed");
+            return NGX_ERROR;
+        }
+
+
+    } else {
+        reply = JS_DupValue(cx, ngx_qjs_arg(sctx->args[0]));
+    }
+
+    rc = ngx_qjs_call((ngx_js_ctx_t *) ctx, event->function, &reply, 1);
+
+    JS_FreeValue(cx, reply);
+    ngx_js_del_event(ctx, event);
+
+    ngx_http_js_event_finalize(r->parent, rc);
+
+    return NGX_OK;
+}
+
+
+static void
+ngx_http_js_subrequest_event_destructor(ngx_qjs_event_t *event)
+{
+    JSContext  *cx;
+
+    cx = event->ctx;
+
+    JS_FreeValue(cx, event->function);
+    JS_FreeValue(cx, event->args[0]);
+    JS_FreeValue(cx, event->args[1]);
+}
+
+
+static JSValue
+ngx_http_qjs_ext_subrequest(JSContext *cx, JSValueConst this_val,
+    int argc, JSValueConst *argv)
+{
+    JSValue                      arg, options, callback, value, retval;
+    ngx_int_t                    rc;
+    ngx_str_t                    uri, args, method_name, body_arg;
+    ngx_uint_t                   method, methods_max, has_body, detached, flags,
+                                 promise;
+    ngx_qjs_event_t             *event;
+    ngx_http_js_ctx_t           *ctx;
+    ngx_http_request_t          *r, *sr;
+    ngx_http_request_body_t     *rb;
+    ngx_http_post_subrequest_t  *ps;
+
+    r = ngx_http_qjs_request(this_val);
+    if (r == NULL) {
+        return JS_ThrowInternalError(cx, "\"this\" is not a request object");
+    }
+
+    ctx = ngx_http_get_module_ctx(r, ngx_http_js_module);
+
+    if (r->main != r) {
+        return JS_ThrowTypeError(cx, "subrequest can only be created for "
+                                     "the primary request");
+    }
+
+    if (ngx_qjs_string(ctx->engine, argv[0], &uri) != NGX_OK) {
+        return JS_ThrowTypeError(cx, "failed to convert uri arg");
+    }
+
+    if (uri.len == 0) {
+        return JS_ThrowTypeError(cx, "uri is empty");
+    }
+
+    options = JS_UNDEFINED;
+    callback = JS_UNDEFINED;
+
+    method = 0;
+    methods_max = sizeof(ngx_http_methods) / sizeof(ngx_http_methods[0]);
+
+    args.len = 0;
+    args.data = NULL;
+
+    method_name.len = 0;
+    method_name.data = NULL;
+
+    has_body = 0;
+    detached = 0;
+
+    arg = argv[1];
+
+    if (JS_IsString(arg)) {
+        if (ngx_qjs_string(ctx->engine, arg, &args) != NGX_OK) {
+            return JS_ThrowTypeError(cx, "failed to convert args");
+        }
+
+    } else if (JS_IsFunction(cx, arg)) {
+        callback = arg;
+
+    } else if (JS_IsObject(arg)) {
+        options = arg;
+
+    } else if (!JS_IsNullOrUndefined(arg)) {
+        return JS_ThrowTypeError(cx, "failed to convert args");
+    }
+
+    if (!JS_IsUndefined(options)) {
+        value = JS_GetPropertyStr(cx, options, "args");
+        if (JS_IsException(value)) {
+            return JS_EXCEPTION;
+        }
+
+        if (!JS_IsUndefined(value)) {
+            rc = ngx_qjs_string(ctx->engine, value, &args);
+            JS_FreeValue(cx, value);
+
+            if (rc != NGX_OK) {
+                return JS_ThrowTypeError(cx, "failed to convert options.args");
+            }
+        }
+
+        value = JS_GetPropertyStr(cx, options, "detached");
+        if (JS_IsException(value)) {
+            return JS_EXCEPTION;
+        }
+
+        if (!JS_IsUndefined(value)) {
+            detached = JS_ToBool(cx, value);
+            JS_FreeValue(cx, value);
+        }
+
+        value = JS_GetPropertyStr(cx, options, "method");
+        if (JS_IsException(value)) {
+            return JS_EXCEPTION;
+        }
+
+        if (!JS_IsUndefined(value)) {
+            rc = ngx_qjs_string(ctx->engine, value, &method_name);
+            JS_FreeValue(cx, value);
+
+            if (rc != NGX_OK) {
+                return JS_ThrowTypeError(cx, "failed to convert option.method");
+            }
+
+            while (method < methods_max) {
+                if (method_name.len == ngx_http_methods[method].name.len
+                    && ngx_memcmp(method_name.data,
+                                  ngx_http_methods[method].name.data,
+                                  method_name.len)
+                       == 0)
+                {
+                    break;
+                }
+
+                method++;
+            }
+        }
+
+        value = JS_GetPropertyStr(cx, options, "body");
+        if (JS_IsException(value)) {
+            return JS_EXCEPTION;
+        }
+
+        if (!JS_IsUndefined(value)) {
+            rc = ngx_qjs_string(ctx->engine, value, &body_arg);
+            JS_FreeValue(cx, value);
+
+            if (rc != NGX_OK) {
+                return JS_ThrowTypeError(cx, "failed to convert option.body");
+            }
+
+            has_body = 1;
+        }
+    }
+
+    flags = NGX_HTTP_LOG_UNSAFE;
+
+    if (ngx_http_parse_unsafe_uri(r, &uri, &args, &flags) != NGX_OK) {
+        return JS_ThrowTypeError(cx, "unsafe uri");
+    }
+
+    arg = argv[2];
+
+    if (JS_IsUndefined(callback) && !JS_IsNullOrUndefined(arg)) {
+        if (!JS_IsFunction(cx, arg)) {
+            return JS_ThrowTypeError(cx, "callback is not a function");
+        }
+
+        callback = arg;
+    }
+
+    if (detached && !JS_IsUndefined(callback)) {
+        return JS_ThrowTypeError(cx, "detached flag and callback are mutually "
+                                     "exclusive");
+    }
+
+    promise = 0;
+    retval = JS_UNDEFINED;
+    flags = NGX_HTTP_SUBREQUEST_BACKGROUND;
+
+    if (!detached) {
+        ps = ngx_palloc(r->pool, sizeof(ngx_http_post_subrequest_t));
+        if (ps == NULL) {
+            return JS_ThrowOutOfMemory(cx);
+        }
+
+        promise = !!JS_IsUndefined(callback);
+
+        event = ngx_pcalloc(r->pool, sizeof(ngx_qjs_event_t)
+                                     + sizeof(JSValue) * 2);
+        if (event == NULL) {
+            return JS_ThrowOutOfMemory(cx);
+        }
+
+        event->ctx = cx;
+        event->fd = ctx->event_id++;
+        event->args = (JSValue *) &event[1];
+        event->destructor = ngx_http_js_subrequest_event_destructor;
+
+        if (promise) {
+            retval = JS_NewPromiseCapability(cx, &event->args[0]);
+            if (JS_IsException(retval)) {
+                return JS_EXCEPTION;
+            }
+
+            callback = event->args[0];
+
+        } else {
+            event->args[0] = JS_UNDEFINED;
+            event->args[1] = JS_UNDEFINED;
+        }
+
+        event->function = JS_DupValue(cx, callback);
+
+        ps->handler = ngx_http_qjs_subrequest_done;
+        ps->data = event;
+
+        flags |= NGX_HTTP_SUBREQUEST_IN_MEMORY;
+
+    } else {
+        ps = NULL;
+        event = NULL;
+    }
+
+    if (ngx_http_subrequest(r, &uri, args.len ? &args : NULL, &sr, ps, flags)
+        != NGX_OK)
+    {
+        return JS_ThrowInternalError(cx, "subrequest creation failed");
+    }
+
+    if (event != NULL) {
+        ngx_js_add_event(ctx, event);
+    }
+
+    if (method != methods_max) {
+        sr->method = ngx_http_methods[method].value;
+        sr->method_name = ngx_http_methods[method].name;
+
+    } else {
+        sr->method = NGX_HTTP_UNKNOWN;
+        sr->method_name = method_name;
+    }
+
+    sr->header_only = (sr->method == NGX_HTTP_HEAD) || JS_IsUndefined(callback);
+
+    if (has_body) {
+        rb = ngx_pcalloc(r->pool, sizeof(ngx_http_request_body_t));
+        if (rb == NULL) {
+            goto memory_error;
+        }
+
+        if (body_arg.len != 0) {
+            rb->bufs = ngx_alloc_chain_link(r->pool);
+            if (rb->bufs == NULL) {
+                goto memory_error;
+            }
+
+            rb->bufs->next = NULL;
+
+            rb->bufs->buf = ngx_calloc_buf(r->pool);
+            if (rb->bufs->buf == NULL) {
+                goto memory_error;
+            }
+
+            rb->bufs->buf->memory = 1;
+            rb->bufs->buf->last_buf = 1;
+
+            rb->bufs->buf->pos = body_arg.data;
+            rb->bufs->buf->last = body_arg.data + body_arg.len;
+        }
+
+        sr->request_body = rb;
+        sr->headers_in.content_length_n = body_arg.len;
+        sr->headers_in.chunked = 0;
+    }
+
+    return retval;
+
+memory_error:
+
+    return JS_ThrowOutOfMemory(cx);
+}
+
+
+static JSValue
+ngx_http_qjs_ext_raw_headers(JSContext *cx, JSValueConst this_val, int out)
+{
+    JSValue              array, elem, key, val;
+    uint32_t             idx;
+    ngx_uint_t           i;
+    ngx_list_t          *headers;
+    ngx_list_part_t     *part;
+    ngx_table_elt_t     *header, *h;
+    ngx_http_request_t  *r;
+
+    r = ngx_http_qjs_request(this_val);
+    if (r == NULL) {
+        return JS_ThrowInternalError(cx, "\"this\" is not a request object");
+    }
+
+    headers = (out) ? &r->headers_out.headers : &r->headers_in.headers;
+
+    array = JS_NewArray(cx);
+    if (JS_IsException(array)) {
+        return JS_EXCEPTION;
+    }
+
+    idx = 0;
+    part = &headers->part;
+    header = part->elts;
+
+    for (i = 0; /* void */ ; i++) {
+
+        if (i >= part->nelts) {
+            if (part->next == NULL) {
+                break;
+            }
+
+            part = part->next;
+            header = part->elts;
+            i = 0;
+        }
+
+        h = &header[i];
+
+        if (h->hash == 0) {
+            continue;
+        }
+
+        elem = JS_NewArray(cx);
+        if (JS_IsException(elem)) {
+            JS_FreeValue(cx, array);
+            return JS_EXCEPTION;
+        }
+
+        if (JS_DefinePropertyValueUint32(cx, array, idx++, elem,
+                                         JS_PROP_C_W_E) < 0)
+        {
+            JS_FreeValue(cx, elem);
+            JS_FreeValue(cx, array);
+            return JS_EXCEPTION;
+        }
+
+        key = qjs_string_create(cx, h->key.data, h->key.len);
+        if (JS_IsException(key)) {
+            JS_FreeValue(cx, array);
+            return JS_EXCEPTION;
+        }
+
+        if (JS_DefinePropertyValueUint32(cx, elem, 0, key, JS_PROP_C_W_E) < 0) {
+            JS_FreeValue(cx, key);
+            JS_FreeValue(cx, array);
+            return JS_EXCEPTION;
+        }
+
+        val = qjs_string_create(cx, h->value.data, h->value.len);
+        if (JS_IsException(val)) {
+            JS_FreeValue(cx, array);
+            return JS_EXCEPTION;
+        }
+
+        if (JS_DefinePropertyValueUint32(cx, elem, 1, val, JS_PROP_C_W_E) < 0) {
+            JS_FreeValue(cx, val);
+            JS_FreeValue(cx, array);
+            return JS_EXCEPTION;
+        }
+    }
+
+    return array;
+}
+
+
+static JSValue
+ngx_http_qjs_ext_variables(JSContext *cx, JSValueConst this_val, int type)
+{
+    JSValue              obj;
+    ngx_http_request_t  *r;
+
+    r = ngx_http_qjs_request(this_val);
+    if (r == NULL) {
+        return JS_ThrowInternalError(cx, "\"this\" is not a request object");
+    }
+
+    obj = JS_NewObjectProtoClass(cx, JS_NULL, NGX_QJS_CLASS_ID_HTTP_VARS);
+
+    /*
+     * Using lowest bit of the pointer to store the buffer type.
+     */
+    type = (type == NGX_JS_BUFFER) ? 1 : 0;
+    JS_SetOpaque(obj, (void *) ((uintptr_t) r | (uintptr_t) type));
+
+    return obj;
+}
+
+
+static int
+ngx_http_qjs_variables_own_property(JSContext *cx, JSPropertyDescriptor *pdesc,
+    JSValueConst obj, JSAtom prop)
+{
+    uint32_t                    buffer_type;
+    ngx_str_t                   name;
+    ngx_uint_t                  i, key, start, length, is_capture;
+    ngx_http_request_t         *r;
+    ngx_http_variable_value_t  *vv;
+
+    r = JS_GetOpaque(obj, NGX_QJS_CLASS_ID_HTTP_VARS);
+
+    buffer_type = ((uintptr_t) r & 1) ? NGX_JS_BUFFER : NGX_JS_STRING;
+    r = (ngx_http_request_t *) ((uintptr_t) r & ~(uintptr_t) 1);
+
+    if (r == NULL) {
+        (void) JS_ThrowInternalError(cx, "\"this\" is not a request object");
+        return -1;
+    }
+
+    name.data = (u_char *) JS_AtomToCString(cx, prop);
+    if (name.data == NULL) {
+        return -1;
+    }
+
+    name.len = ngx_strlen(name.data);
+
+    is_capture = 1;
+    for (i = 0; i < name.len; i++) {
+        if (name.data[i] < '0' || name.data[i] > '9') {
+            is_capture = 0;
+            break;
+        }
+    }
+
+    if (is_capture) {
+        key = ngx_atoi(name.data, name.len) * 2;
+        JS_FreeCString(cx, (char *) name.data);
+        if (r->captures == NULL || r->captures_data == NULL
+            || r->ncaptures <= key)
+        {
+            return 0;
+        }
+
+
+        if (pdesc != NULL) {
+            pdesc->flags = JS_PROP_WRITABLE | JS_PROP_CONFIGURABLE;
+            pdesc->getter = JS_UNDEFINED;
+            pdesc->setter = JS_UNDEFINED;
+
+            start = r->captures[key];
+            length = r->captures[key + 1] - start;
+            pdesc->value = ngx_qjs_prop(cx, buffer_type,
+                                        &r->captures_data[start], length);
+        }
+
+        return 1;
+    }
+
+    key = ngx_hash_strlow(name.data, name.data, name.len);
+
+    vv = ngx_http_get_variable(r, &name, key);
+    JS_FreeCString(cx, (char *) name.data);
+    if (vv == NULL || vv->not_found) {
+        return 0;
+    }
+
+    if (pdesc != NULL) {
+        pdesc->flags = JS_PROP_WRITABLE | JS_PROP_CONFIGURABLE;
+        pdesc->getter = JS_UNDEFINED;
+        pdesc->setter = JS_UNDEFINED;
+        pdesc->value = ngx_qjs_prop(cx, buffer_type, vv->data, vv->len);
+    }
+
+    return 1;
+}
+
+
+static int
+ngx_http_qjs_variables_set_property(JSContext *cx, JSValueConst obj,
+    JSAtom prop, JSValueConst value, JSValueConst receiver, int flags)
+{
+    ngx_str_t                   name, s;
+    ngx_uint_t                  key;
+    ngx_http_js_ctx_t          *ctx;
+    ngx_http_request_t         *r;
+    ngx_http_variable_t        *v;
+    ngx_http_variable_value_t  *vv;
+    ngx_http_core_main_conf_t  *cmcf;
+
+    r = JS_GetOpaque(obj, NGX_QJS_CLASS_ID_HTTP_VARS);
+
+    r = (ngx_http_request_t *) ((uintptr_t) r & ~(uintptr_t) 1);
+
+    if (r == NULL) {
+        (void) JS_ThrowInternalError(cx, "\"this\" is not a request object");
+        return -1;
+    }
+
+    name.data = (u_char *) JS_AtomToCString(cx, prop);
+    if (name.data == NULL) {
+        return -1;
+    }
+
+    name.len = ngx_strlen(name.data);
+
+    key = ngx_hash_strlow(name.data, name.data, name.len);
+
+    cmcf = ngx_http_get_module_main_conf(r, ngx_http_core_module);
+
+    v = ngx_hash_find(&cmcf->variables_hash, key, name.data, name.len);
+    JS_FreeCString(cx, (char *) name.data);
+
+    if (v == NULL) {
+        (void) JS_ThrowInternalError(cx, "variable not found");
+        return -1;
+    }
+
+    ctx = ngx_http_get_module_ctx(r, ngx_http_js_module);
+
+    if (ngx_qjs_string(ctx->engine, value, &s) != NGX_OK) {
+        return -1;
+    }
+
+    if (v->set_handler != NULL) {
+        vv = ngx_pcalloc(r->pool, sizeof(ngx_http_variable_value_t));
+        if (vv == NULL) {
+            (void) JS_ThrowOutOfMemory(cx);
+            return -1;
+        }
+
+        vv->valid = 1;
+        vv->not_found = 0;
+        vv->data = s.data;
+        vv->len = s.len;
+
+        v->set_handler(r, vv, v->data);
+
+        return 1;
+    }
+
+    if (!(v->flags & NGX_HTTP_VAR_INDEXED)) {
+        (void) JS_ThrowTypeError(cx, "variable is not writable");
+        return -1;
+    }
+
+    vv = &r->variables[v->index];
+
+    vv->valid = 1;
+    vv->not_found = 0;
+
+    vv->data = ngx_pnalloc(r->pool, s.len);
+    if (vv->data == NULL) {
+        vv->valid = 0;
+        (void) JS_ThrowOutOfMemory(cx);
+        return -1;
+    }
+
+    vv->len = s.len;
+    ngx_memcpy(vv->data, s.data, vv->len);
+
+    return 1;
+}
+
+
+static int
+ngx_http_qjs_ext_keys_header(JSContext *cx, ngx_list_t *headers, JSValue keys,
+    JSPropertyEnum **ptab, uint32_t *plen)
+{
+    JSAtom            key;
+    ngx_uint_t        item;
+    ngx_list_part_t  *part;
+    ngx_table_elt_t  *header, *h;
+
+    part = &headers->part;
+    item = 0;
+
+    while (part) {
+        if (item >= part->nelts) {
+            part = part->next;
+            item = 0;
+            continue;
+        }
+
+        header = part->elts;
+        h = &header[item++];
+
+        if (h->hash == 0) {
+            continue;
+        }
+
+        key = JS_NewAtomLen(cx, (const char *) h->key.data, h->key.len);
+        if (key == JS_ATOM_NULL) {
+            return -1;
+        }
+
+        if (JS_DefinePropertyValue(cx, keys, key, JS_UNDEFINED,
+                                   JS_PROP_ENUMERABLE) < 0)
+        {
+            JS_FreeAtom(cx, key);
+            return -1;
+        }
+
+        JS_FreeAtom(cx, key);
+    }
+
+    return JS_GetOwnPropertyNames(cx, ptab, plen, keys, JS_GPN_STRING_MASK);
+}
+
+
+static int
+ngx_http_qjs_headers_in_own_property_names(JSContext *cx,
+    JSPropertyEnum **ptab, uint32_t *plen, JSValueConst obj)
+{
+    int                  ret;
+    JSValue              keys;
+    ngx_http_request_t  *r;
+
+    r = JS_GetOpaque(obj, NGX_QJS_CLASS_ID_HTTP_HEADERS_IN);
+    if (r == NULL) {
+        (void) JS_ThrowInternalError(cx, "\"this\" is not a headers_in object");
+        return -1;
+    }
+
+    keys = JS_NewObject(cx);
+    if (JS_IsException(keys)) {
+        return -1;
+    }
+
+    ret = ngx_http_qjs_ext_keys_header(cx, &r->headers_in.headers, keys, ptab,
+                                       plen);
+    JS_FreeValue(cx, keys);
+
+    return ret;
+}
+
+
+static njs_int_t
+ngx_http_qjs_header_generic(JSContext *cx, ngx_http_request_t *r,
+    ngx_list_t *headers, ngx_table_elt_t **ph, ngx_str_t *name,
+    JSPropertyDescriptor *pdesc, unsigned flags)
+{
+    int               ret;
+    u_char            sep;
+    njs_chb_t         chain;
+    JSValue           val;
+    ngx_uint_t        i;
+    ngx_list_part_t  *part;
+    ngx_table_elt_t  *header, *h;
+
+    if (ph == NULL) {
+        /* iterate over all headers */
+
+        ph = &header;
+        part = &headers->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
+                || name->len != h[i].key.len
+                || ngx_strncasecmp(name->data, h[i].key.data, name->len)
+                   != 0)
+            {
+                continue;
+            }
+
+            *ph = &h[i];
+            ph = &h[i].next;
+        }
+
+        *ph = NULL;
+        ph = &header;
+    }
+
+    if (*ph == NULL) {
+        return 0;
+    }
+
+    if (flags & NJS_HEADER_ARRAY) {
+        if (pdesc == NULL) {
+            return 1;
+        }
+
+        pdesc->flags = JS_PROP_ENUMERABLE;
+        pdesc->getter = JS_UNDEFINED;
+        pdesc->setter = JS_UNDEFINED;
+        pdesc->value = JS_NewArray(cx);
+        if (JS_IsException(pdesc->value)) {
+            return -1;
+        }
+
+        for (h = *ph, i = 0; h; h = h->next, i++) {
+            val = qjs_string_create(cx, h->value.data, h->value.len);
+            if (JS_IsException(val)) {
+                JS_FreeValue(cx, pdesc->value);
+                return -1;
+            }
+
+            if (JS_DefinePropertyValueUint32(cx, pdesc->value, i, val,
+                                             JS_PROP_ENUMERABLE) < 0)
+            {
+                JS_FreeValue(cx, pdesc->value);
+                return -1;
+            }
+        }
+
+        return 1;
+    }
+
+    if ((*ph)->next == NULL || flags & NJS_HEADER_SINGLE) {
+        if (pdesc != NULL) {
+            pdesc->flags = JS_PROP_ENUMERABLE;
+            pdesc->getter = JS_UNDEFINED;
+            pdesc->setter = JS_UNDEFINED;
+            pdesc->value = qjs_string_create(cx, (*ph)->value.data,
+                                             (*ph)->value.len);
+            if (JS_IsException(pdesc->value)) {
+                return -1;
+            }
+        }
+
+        return 1;
+    }
+
+    NJS_CHB_CTX_INIT(&chain, cx);
+
+    sep = flags & NJS_HEADER_SEMICOLON ? ';' : ',';
+
+    for (h = *ph; h; h = h->next) {
+        njs_chb_append(&chain, h->value.data, h->value.len);
+        njs_chb_append(&chain, &sep, 1);
+        njs_chb_append_literal(&chain, " ");
+    }
+
+    ret = 1;
+
+    if (pdesc != NULL) {
+        pdesc->flags = JS_PROP_ENUMERABLE;
+        pdesc->getter = JS_UNDEFINED;
+        pdesc->setter = JS_UNDEFINED;
+        pdesc->value = qjs_string_create_chb(cx, &chain);
+        if (JS_IsException(pdesc->value)) {
+            ret = -1;
+            goto done;
+        }
+    }
+
+done:
+
+    njs_chb_destroy(&chain);
+
+    return ret;
+}
+
+
+static int
+ngx_http_qjs_header_in(JSContext *cx, ngx_http_request_t *r, unsigned flags,
+    ngx_str_t *name, JSPropertyDescriptor *pdesc)
+{
+    u_char                      *lowcase_key;
+    ngx_uint_t                   hash;
+    ngx_table_elt_t            **ph;
+    ngx_http_header_t           *hh;
+    ngx_http_core_main_conf_t   *cmcf;
+
+    /* look up hashed headers */
+
+    lowcase_key = ngx_pnalloc(r->pool, name->len);
+    if (lowcase_key == NULL) {
+        (void) JS_ThrowOutOfMemory(cx);
+        return -1;
+    }
+
+    hash = ngx_hash_strlow(lowcase_key, name->data, name->len);
+
+    cmcf = ngx_http_get_module_main_conf(r, ngx_http_core_module);
+
+    hh = ngx_hash_find(&cmcf->headers_in_hash, hash, lowcase_key,
+                       name->len);
+
+    ph = NULL;
+
+    if (hh) {
+        if (hh->offset == offsetof(ngx_http_headers_in_t, cookie)) {
+            flags |= NJS_HEADER_SEMICOLON;
+        }
+
+        ph = (ngx_table_elt_t **) ((char *) &r->headers_in + hh->offset);
+    }
+
+    return ngx_http_qjs_header_generic(cx, r, &r->headers_in.headers, ph, name,
+                                       pdesc, flags);
+}
+
+
+static int
+ngx_http_qjs_headers_in_own_property(JSContext *cx, JSPropertyDescriptor *pdesc,
+    JSValueConst obj, JSAtom prop)
+{
+    int                  ret;
+    unsigned             flags;
+    ngx_str_t            name, *h;
+    ngx_http_request_t  *r;
+
+    static ngx_str_t single_headers_in[] = {
+        ngx_string("Content-Type"),
+        ngx_string("ETag"),
+        ngx_string("From"),
+        ngx_string("Max-Forwards"),
+        ngx_string("Referer"),
+        ngx_string("Proxy-Authorization"),
+        ngx_string("User-Agent"),
+        ngx_string(""),
+    };
+
+    r = JS_GetOpaque(obj, NGX_QJS_CLASS_ID_HTTP_HEADERS_IN);
+    if (r == NULL) {
+        (void) JS_ThrowInternalError(cx, "\"this\" is not a headers_in object");
+        return -1;
+    }
+
+    name.data = (u_char *) JS_AtomToCString(cx, prop);
+    if (name.data == NULL) {
+        return -1;
+    }
+
+    name.len = ngx_strlen(name.data);
+
+    flags = 0;
+
+    for (h = single_headers_in; h->len > 0; h++) {
+        if (h->len == name.len
+            && ngx_strncasecmp(h->data, name.data, name.len) == 0)
+        {
+            flags |= NJS_HEADER_SINGLE;
+            break;
+        }
+    }
+
+    ret = ngx_http_qjs_header_in(cx, r, flags, &name, pdesc);
+    JS_FreeCString(cx, (char *) name.data);
+
+    return ret;
+}
+
+
+static int
+ngx_http_qjs_headers_out_own_property_names(JSContext *cx,
+    JSPropertyEnum **ptab, uint32_t *plen, JSValueConst obj)
+{
+    int                  ret;
+    JSAtom               key;
+    JSValue              keys;
+    ngx_http_request_t  *r;
+
+    r = JS_GetOpaque(obj, NGX_QJS_CLASS_ID_HTTP_HEADERS_OUT);
+    if (r == NULL) {
+        (void) JS_ThrowInternalError(cx, "\"this\" is not a headers_out"
+                                     " object");
+        return -1;
+    }
+
+    keys = JS_NewObject(cx);
+    if (JS_IsException(keys)) {
+        return -1;
+    }
+
+    if (r->headers_out.content_type.len) {
+        key = JS_NewAtomLen(cx, "Content-Type", njs_length("Content-Type"));
+        if (key == JS_ATOM_NULL) {
+            return -1;
+        }
+
+        if (JS_DefinePropertyValue(cx, keys, key, JS_UNDEFINED,
+                                   JS_PROP_ENUMERABLE) < 0)
+        {
+            JS_FreeAtom(cx, key);
+            return -1;
+        }
+
+        JS_FreeAtom(cx, key);
+    }
+
+    if (r->headers_out.content_length == NULL
+        && r->headers_out.content_length_n >= 0)
+    {
+        key = JS_NewAtomLen(cx, "Content-Length", njs_length("Content-Length"));
+        if (key == JS_ATOM_NULL) {
+            return -1;
+        }
+
+        if (JS_DefinePropertyValue(cx, keys, key, JS_UNDEFINED,
+                                   JS_PROP_ENUMERABLE) < 0)
+        {
+            JS_FreeAtom(cx, key);
+            return -1;
+        }
+
+        JS_FreeAtom(cx, key);
+    }
+
+    ret = ngx_http_qjs_ext_keys_header(cx, &r->headers_out.headers, keys, ptab,
+                                       plen);
+    JS_FreeValue(cx, keys);
+
+    return ret;
+}
+
+
+static int
+ngx_http_qjs_headers_out_handler(JSContext *cx, ngx_http_request_t *r,
+    ngx_str_t *name, JSPropertyDescriptor *pdesc, JSValue *value,
+    unsigned flags)
+{
+    u_char              *p;
+    int64_t              length;
+    uint32_t             i;
+    ngx_int_t            rc;
+    ngx_str_t            s;
+    JSValue              v;
+    ngx_list_part_t     *part;
+    ngx_table_elt_t     *header, *h, **ph;
+    ngx_http_js_ctx_t   *ctx;
+
+    if (flags & NJS_HEADER_GET) {
+        return ngx_http_qjs_header_generic(cx, r, &r->headers_out.headers, NULL,
+                                           name, pdesc, flags);
+    }
+
+    part = &r->headers_out.headers.part;
+    header = part->elts;
+
+    for (i = 0; /* void */ ; i++) {
+
+        if (i >= part->nelts) {
+            if (part->next == NULL) {
+                break;
+            }
+
+            part = part->next;
+            header = part->elts;
+            i = 0;
+        }
+
+        h = &header[i];
+
+        if (h->hash == 0
+            || h->key.len != name->len
+            || ngx_strncasecmp(h->key.data, name->data, name->len) != 0)
+        {
+            continue;
+        }
+
+        h->hash = 0;
+        h->next = NULL;
+    }
+
+    if (value == NULL) {
+        return 1;
+    }
+
+    if (JS_IsArray(cx, *value)) {
+        v = JS_GetPropertyStr(cx, *value, "length");
+        if (JS_IsException(v)) {
+            return -1;
+        }
+
+        if (JS_ToInt64(cx, &length, v) < 0) {
+            JS_FreeValue(cx, v);
+            return -1;
+        }
+
+        JS_FreeValue(cx, v);
+
+    } else {
+        v = *value;
+        length = 1;
+    }
+
+    ph = &header;
+    ctx = ngx_http_get_module_ctx(r, ngx_http_js_module);
+
+    for (i = 0; i < (uint32_t) length; i++) {
+        if (JS_IsArray(cx, *value)) {
+            v = JS_GetPropertyUint32(cx, *value, i);
+            if (JS_IsException(v)) {
+                return -1;
+            }
+        }
+
+        rc = ngx_qjs_string(ctx->engine, v, &s);
+
+        if (JS_IsArray(cx, *value)) {
+            JS_FreeValue(cx, v);
+        }
+
+        if (rc != NGX_OK) {
+            return -1;
+        }
+
+        if (s.len == 0) {
+            continue;
+        }
+
+        h = ngx_list_push(&r->headers_out.headers);
+        if (h == NULL) {
+            (void) JS_ThrowOutOfMemory(cx);
+            return -1;
+        }
+
+        p = ngx_pnalloc(r->pool, name->len);
+        if (p == NULL) {
+            h->hash = 0;
+            (void) JS_ThrowOutOfMemory(cx);
+            return -1;
+        }
+
+        ngx_memcpy(p, name->data, name->len);
+
+        h->key.data = p;
+        h->key.len = name->len;
+
+        p = ngx_pnalloc(r->pool, s.len);
+        if (p == NULL) {
+            h->hash = 0;
+            (void) JS_ThrowOutOfMemory(cx);
+            return -1;
+        }
+
+        ngx_memcpy(p, s.data, s.len);
+
+        h->value.data = p;
+        h->value.len = s.len;
+        h->hash = 1;
+
+        *ph = h;
+        ph = &h->next;
+    }
+
+    *ph = NULL;
+
+    return NJS_OK;
+}
+
+
+static int
+ngx_http_qjs_headers_out_special_handler(JSContext *cx, ngx_http_request_t *r,
+    ngx_str_t *name, JSPropertyDescriptor *pdesc, JSValue *value,
+    unsigned flags, ngx_table_elt_t **hh)
+{
+    u_char              *p;
+    uint32_t             length;
+    JSValue              len, setval;
+    ngx_str_t            s;
+    ngx_uint_t           i, rc;
+    ngx_list_t          *headers;
+    ngx_list_part_t     *part;
+    ngx_table_elt_t     *header, *h;
+    ngx_http_js_ctx_t   *ctx;
+
+    if (flags & NJS_HEADER_GET) {
+        return ngx_http_qjs_headers_out_handler(cx, r, name, pdesc, NULL,
+                                                flags | NJS_HEADER_SINGLE);
+    }
+
+    if (value != NULL) {
+        if (JS_IsArray(cx, *value)) {
+            len = JS_GetPropertyStr(cx, *value, "length");
+            if (JS_IsException(len)) {
+                return -1;
+            }
+
+            if (JS_ToUint32(cx, &length, len) < 0) {
+                JS_FreeValue(cx, len);
+                return -1;
+            }
+
+            JS_FreeValue(cx, len);
+
+            setval = JS_GetPropertyUint32(cx, *value, length - 1);
+            if (JS_IsException(setval)) {
+                return -1;
+            }
+
+        } else {
+            setval = *value;
+        }
+
+    } else {
+        setval = JS_UNDEFINED;
+    }
+
+    ctx = ngx_http_get_module_ctx(r, ngx_http_js_module);
+
+    rc = ngx_qjs_string(ctx->engine, setval, &s);
+
+    if (value != NULL && JS_IsArray(cx, *value)) {
+        JS_FreeValue(cx, setval);
+    }
+
+    if (rc != NGX_OK) {
+        return -1;
+    }
+
+    headers = &r->headers_out.headers;
+    part = &headers->part;
+    header = part->elts;
+
+    for (i = 0; /* void */ ; i++) {
+
+        if (i >= part->nelts) {
+            if (part->next == NULL) {
+                break;
+            }
+
+            part = part->next;
+            header = part->elts;
+            i = 0;
+        }
+
+        h = &header[i];
+
+        if (h->hash == 0) {
+            continue;
+        }
+
+        if (h->key.len == name->len
+            && ngx_strncasecmp(h->key.data, name->data, name->len) == 0)
+        {
+            goto done;
+        }
+    }
+
+    h = NULL;
+
+done:
+
+    if (h != NULL && s.len == 0) {
+        h->hash = 0;
+        h = NULL;
+    }
+
+    if (h == NULL && s.len != 0) {
+        h = ngx_list_push(headers);
+        if (h == NULL) {
+            (void) JS_ThrowOutOfMemory(cx);
+            return -1;
+        }
+
+        p = ngx_pnalloc(r->pool, name->len);
+        if (p == NULL) {
+            h->hash = 0;
+            (void) JS_ThrowOutOfMemory(cx);
+            return -1;
+        }
+
+        ngx_memcpy(p, name->data, name->len);
+
+        h->key.data = p;
+        h->key.len = name->len;
+    }
+
+    if (h != NULL) {
+        p = ngx_pnalloc(r->pool, s.len);
+        if (p == NULL) {
+            h->hash = 0;
+            (void) JS_ThrowOutOfMemory(cx);
+            return -1;
+        }
+
+        ngx_memcpy(p, s.data, s.len);
+
+        h->value.data = p;
+        h->value.len = s.len;
+        h->hash = 1;
+    }
+
+    if (hh != NULL) {
+        *hh = h;
+    }
+
+    return 1;
+}
+
+
+static int
+ngx_http_qjs_headers_out_content_encoding(JSContext *cx, ngx_http_request_t *r,
+    ngx_str_t *name, JSPropertyDescriptor *pdesc, JSValue *value,
+    unsigned flags)
+{
+    int               ret;
+    ngx_table_elt_t  *h;
+
+    ret = ngx_http_qjs_headers_out_special_handler(cx, r, name, pdesc, value,
+                                                   flags, &h);
+    if (ret < 0) {
+        return -1;
+    }
+
+    if (!(flags & NJS_HEADER_GET)) {
+        r->headers_out.content_encoding = h;
+    }
+
+    return ret;
+}
+
+
+static int
+ngx_http_qjs_headers_out_content_length(JSContext *cx, ngx_http_request_t *r,
+    ngx_str_t *name, JSPropertyDescriptor *pdesc, JSValue *value,
+    unsigned flags)
+{
+    int               ret;
+    u_char           *p;
+    ngx_int_t         n;
+    ngx_table_elt_t  *h;
+    u_char            content_len[NGX_OFF_T_LEN];
+
+    if (flags & NJS_HEADER_GET) {
+        if (r->headers_out.content_length == NULL
+            && r->headers_out.content_length_n >= 0)
+        {
+            p = ngx_sprintf(content_len, "%O", r->headers_out.content_length_n);
+
+            if (pdesc != NULL) {
+                pdesc->flags = JS_PROP_C_W_E;
+                pdesc->getter = JS_UNDEFINED;
+                pdesc->setter = JS_UNDEFINED;
+                pdesc->value = qjs_string_create(cx, content_len,
+                                                p - content_len);
+                if (JS_IsException(pdesc->value)) {
+                    return -1;
+                }
+            }
+
+            return 1;
+        }
+    }
+
+    ret = ngx_http_qjs_headers_out_special_handler(cx, r, name, pdesc, value,
+                                                   flags, &h);
+    if (ret < 0) {
+        return -1;
+    }
+
+    if (!(flags & NJS_HEADER_GET)) {
+        if (h != NULL) {
+            n = ngx_atoi(h->value.data, h->value.len);
+            if (n == NGX_ERROR) {
+                h->hash = 0;
+                (void) JS_ThrowInternalError(cx, "failed converting argument "
+                                             "to positive integer");
+                return -1;
+            }
+
+            r->headers_out.content_length = h;
+            r->headers_out.content_length_n = n;
+
+        } else {
+            ngx_http_clear_content_length(r);
+        }
+    }
+
+    return ret;
+}
+
+
+static int
+ngx_http_qjs_headers_out_content_type(JSContext *cx, ngx_http_request_t *r,
+    ngx_str_t *name, JSPropertyDescriptor *pdesc, JSValue *value,
+    unsigned flags)
+{
+    uint32_t     length;
+    JSValue     len, setval;
+    ngx_int_t   rc;
+    ngx_str_t  *hdr, s;
+    ngx_http_js_ctx_t  *ctx;
+
+    if (flags & NJS_HEADER_GET) {
+        hdr = &r->headers_out.content_type;
+
+        if (pdesc != NULL) {
+            pdesc->flags = JS_PROP_C_W_E;
+            pdesc->getter = JS_UNDEFINED;
+            pdesc->setter = JS_UNDEFINED;
+
+            if (hdr->len == 0) {
+                pdesc->value = JS_UNDEFINED;
+                return 1;
+            }
+
+            pdesc->value = qjs_string_create(cx, hdr->data, hdr->len);
+            if (JS_IsException(pdesc->value)) {
+                return -1;
+            }
+        }
+
+        return 1;
+    }
+
+    if (value == NULL) {
+        r->headers_out.content_type.len = 0;
+        r->headers_out.content_type_len = 0;
+        r->headers_out.content_type.data = NULL;
+        r->headers_out.content_type_lowcase = NULL;
+        return 1;
+    }
+
+    if (JS_IsArray(cx, *value)) {
+        len = JS_GetPropertyStr(cx, *value, "length");
+        if (JS_IsException(len)) {
+            return -1;
+        }
+
+        if (JS_ToUint32(cx, &length, len) < 0) {
+            JS_FreeValue(cx, len);
+            return -1;
+        }
+
+        JS_FreeValue(cx, len);
+
+        setval = JS_GetPropertyUint32(cx, *value, length - 1);
+        if (JS_IsException(setval)) {
+            return -1;
+        }
+
+    } else {
+        setval = *value;
+    }
+
+    ctx = ngx_http_get_module_ctx(r, ngx_http_js_module);
+
+    rc = ngx_qjs_string(ctx->engine, setval, &s);
+
+    if (JS_IsArray(cx, *value)) {
+        JS_FreeValue(cx, setval);
+    }
+
+    if (rc != NGX_OK) {
+        return -1;
+    }
+
+    r->headers_out.content_type.len = s.len;
+    r->headers_out.content_type_len = r->headers_out.content_type.len;
+    r->headers_out.content_type.data = s.data;
+    r->headers_out.content_type_lowcase = NULL;
+
+    return 1;
+}
+
+
+static int
+ngx_http_qjs_headers_out_date(JSContext *cx, ngx_http_request_t *r,
+    ngx_str_t *name, JSPropertyDescriptor *pdesc, JSValue *value,
+    unsigned flags)
+{
+    int               ret;
+    ngx_table_elt_t  *h;
+
+    ret = ngx_http_qjs_headers_out_special_handler(cx, r, name, pdesc, value,
+                                                   flags, &h);
+    if (ret < 0) {
+        return -1;
+    }
+
+    if (!(flags & NJS_HEADER_GET)) {
+        r->headers_out.date = h;
+    }
+
+    return ret;
+}
+
+
+static int
+ngx_http_qjs_headers_out_last_modified(JSContext *cx, ngx_http_request_t *r,
+    ngx_str_t *name, JSPropertyDescriptor *pdesc, JSValue *value,
+    unsigned flags)
+{
+    int               ret;
+    ngx_table_elt_t  *h;
+
+    ret = ngx_http_qjs_headers_out_special_handler(cx, r, name, pdesc, value,
+                                                   flags, &h);
+    if (ret < 0) {
+        return -1;
+    }
+
+    if (!(flags & NJS_HEADER_GET)) {
+        r->headers_out.last_modified = h;
+    }
+
+    return ret;
+}
+
+
+static int
+ngx_http_qjs_headers_out_location(JSContext *cx, ngx_http_request_t *r,
+    ngx_str_t *name, JSPropertyDescriptor *pdesc, JSValue *value,
+    unsigned flags)
+{
+    int               ret;
+    ngx_table_elt_t  *h;
+
+    ret = ngx_http_qjs_headers_out_special_handler(cx, r, name, pdesc, value,
+                                                   flags, &h);
+    if (ret < 0) {
+        return -1;
+    }
+
+    if (!(flags & NJS_HEADER_GET)) {
+        r->headers_out.location = h;
+    }
+
+    return ret;
+}
+
+
+static int
+ngx_http_qjs_headers_out_server(JSContext *cx, ngx_http_request_t *r,
+    ngx_str_t *name, JSPropertyDescriptor *pdesc, JSValue *value,
+    unsigned flags)
+{
+    int               ret;
+    ngx_table_elt_t  *h;
+
+    ret = ngx_http_qjs_headers_out_special_handler(cx, r, name, pdesc, value,
+                                                   flags, &h);
+    if (ret < 0) {
+        return -1;
+    }
+
+    if (!(flags & NJS_HEADER_GET)) {
+        r->headers_out.server = h;
+    }
+
+    return ret;
+}
+
+
+static int
+ngx_http_qjs_headers_out(JSContext *cx, ngx_http_request_t *r,
+    ngx_str_t *name, JSPropertyDescriptor *pdesc, JSValue *value,
+    unsigned flags)
+{
+    ngx_http_js_header_t  *h;
+
+    static ngx_http_js_header_t headers_out[] = {
+#define header(name, fl, h) { njs_str(name), fl, (uintptr_t) h }
+        header("Age", NJS_HEADER_SINGLE, ngx_http_qjs_headers_out_handler),
+        header("Content-Encoding", 0, ngx_http_qjs_headers_out_content_encoding),
+        header("Content-Length", 0, ngx_http_qjs_headers_out_content_length),
+        header("Content-Type", 0, ngx_http_qjs_headers_out_content_type),
+        header("Date", 0, ngx_http_qjs_headers_out_date),
+        header("Etag", NJS_HEADER_SINGLE, ngx_http_qjs_headers_out_handler),
+        header("Expires", NJS_HEADER_SINGLE, ngx_http_qjs_headers_out_handler),
+        header("Last-Modified", 0, ngx_http_qjs_headers_out_last_modified),
+        header("Location", 0, ngx_http_qjs_headers_out_location),
+        header("Server", 0, ngx_http_qjs_headers_out_server),
+        header("Set-Cookie", NJS_HEADER_ARRAY,
+               ngx_http_qjs_headers_out_handler),
+        header("Retry-After", NJS_HEADER_SINGLE,
+               ngx_http_qjs_headers_out_handler),
+        header("", 0, ngx_http_qjs_headers_out_handler),
+#undef header
+    };
+
+    for (h = headers_out; h->name.len > 0; h++) {
+        if (h->name.len == name->len
+            && ngx_strncasecmp(h->name.data, name->data, name->len) == 0)
+        {
+            break;
+        }
+    }
+
+    return ((njs_http_qjs_header_handler_t) h->handler)(cx,
+                                      r, name, pdesc, value, h->flags | flags);
+}
+
+
+static int
+ngx_http_qjs_headers_out_own_property(JSContext *cx,
+    JSPropertyDescriptor *pdesc, JSValueConst obj, JSAtom prop)
+{
+    int                   ret;
+    ngx_str_t             name;
+    ngx_http_request_t   *r;
+
+    r = JS_GetOpaque(obj, NGX_QJS_CLASS_ID_HTTP_HEADERS_OUT);
+    if (r == NULL) {
+        (void) JS_ThrowInternalError(cx, "\"this\" is not a headers_out"
+                                     " object");
+        return -1;
+    }
+
+    name.data = (u_char *) JS_AtomToCString(cx, prop);
+    if (name.data == NULL) {
+        return -1;
+    }
+
+    name.len = ngx_strlen(name.data);
+
+    ret = ngx_http_qjs_headers_out(cx, r, &name, pdesc, NULL, NJS_HEADER_GET);
+    JS_FreeCString(cx, (char *) name.data);
+
+    return ret;
+}
+
+
+static int
+ngx_http_qjs_headers_out_set_property(JSContext *cx,
+    JSValueConst obj, JSAtom atom, JSValueConst value, JSValueConst receiver,
+    int flags)
+{
+    return ngx_http_qjs_headers_out_define_own_property(cx, obj, atom, value,
+                                JS_UNDEFINED, JS_UNDEFINED, flags);
+}
+
+
+static int
+ngx_http_qjs_headers_out_define_own_property(JSContext *cx,
+    JSValueConst obj, JSAtom prop, JSValueConst value, JSValueConst getter,
+    JSValueConst setter, int flags)
+{
+    int                   ret;
+    ngx_str_t             name;
+    ngx_http_request_t   *r;
+
+    r = JS_GetOpaque(obj, NGX_QJS_CLASS_ID_HTTP_HEADERS_OUT);
+    if (r == NULL) {
+        (void) JS_ThrowInternalError(cx, "\"this\" is not a headers_out"
+                                     " object");
+        return -1;
+    }
+
+    if (!JS_IsUndefined(setter) || !JS_IsUndefined(getter)) {
+        (void) JS_ThrowTypeError(cx, "cannot define getter or setter");
+        return -1;
+    }
+
+    name.data = (u_char *) JS_AtomToCString(cx, prop);
+    if (name.data == NULL) {
+        return -1;
+    }
+
+    name.len = ngx_strlen(name.data);
+
+    if (r->header_sent) {
+        ngx_log_error(NGX_LOG_WARN, r->connection->log, 0,
+                      "ignored setting of response header \"%V\" because"
+                      " headers were already sent", &name);
+    }
+
+    ret = ngx_http_qjs_headers_out(cx, r, &name, NULL, &value, 0);
+    JS_FreeCString(cx, (char *) name.data);
+
+    return ret;
+}
+
+
+static int
+ngx_http_qjs_headers_out_delete_property(JSContext *cx,
+    JSValueConst obj, JSAtom prop)
+{
+    int                   ret;
+    ngx_str_t             name;
+    ngx_http_request_t   *r;
+
+    r = JS_GetOpaque(obj, NGX_QJS_CLASS_ID_HTTP_HEADERS_OUT);
+    if (r == NULL) {
+        (void) JS_ThrowInternalError(cx, "\"this\" is not a headers_out"
+                                     " object");
+        return -1;
+    }
+
+    name.data = (u_char *) JS_AtomToCString(cx, prop);
+    if (name.data == NULL) {
+        return -1;
+    }
+
+    name.len = ngx_strlen(name.data);
+
+    ret = ngx_http_qjs_headers_out(cx, r, &name, NULL, NULL, 0);
+    JS_FreeCString(cx, (char *) name.data);
+
+    return ret;
+}
+
+
+static ngx_int_t
+ngx_http_qjs_body_filter(ngx_http_request_t *r, ngx_http_js_loc_conf_t *jlcf,
+    ngx_http_js_ctx_t *ctx, ngx_chain_t *in)
+{
+    size_t             len;
+    u_char            *p;
+    JSAtom             last_key;
+    JSValue            arguments[3], last;
+    ngx_int_t          rc;
+    njs_int_t          pending;
+    ngx_buf_t         *b;
+    ngx_chain_t       *cl;
+    JSContext         *cx;
+    ngx_connection_t  *c;
+
+    c = r->connection;
+    cx = ctx->engine->u.qjs.ctx;
+
+    arguments[0] = ngx_qjs_arg(ctx->args[0]);
+
+    last_key = JS_NewAtom(cx, "last");
+    if (last_key == JS_ATOM_NULL) {
+        return NGX_ERROR;
+    }
+
+    while (in != NULL) {
+        ctx->buf = in->buf;
+        b = ctx->buf;
+
+        if (!ctx->done) {
+            len = b->last - b->pos;
+
+            p = ngx_pnalloc(r->pool, len);
+            if (p == NULL) {
+                return NJS_ERROR;
+            }
+
+            if (len) {
+                ngx_memcpy(p, b->pos, len);
+            }
+
+            arguments[1] = ngx_qjs_prop(cx, jlcf->buffer_type, p, len);
+            if (JS_IsException(arguments[1])) {
+                JS_FreeAtom(cx, last_key);
+                return NGX_ERROR;
+            }
+
+            last = JS_NewBool(cx, b->last_buf);
+
+            arguments[2] = JS_NewObject(cx);
+            if (JS_IsException(arguments[2])) {
+                JS_FreeAtom(cx, last_key);
+                JS_FreeValue(cx, arguments[1]);
+                return NGX_ERROR;
+            }
+
+            if (JS_SetProperty(cx, arguments[2], last_key, last) < 0) {
+                JS_FreeAtom(cx, last_key);
+                JS_FreeValue(cx, arguments[1]);
+                JS_FreeValue(cx, arguments[2]);
+                return NGX_ERROR;
+            }
+
+            pending = ngx_js_ctx_pending(ctx);
+
+            ngx_log_debug1(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                           "http js body call \"%V\"", &jlcf->body_filter);
+
+            rc = ctx->engine->call((ngx_js_ctx_t *) ctx, &jlcf->body_filter,
+                                   (njs_opaque_value_t *) &arguments[0], 3);
+
+            JS_FreeAtom(cx, last_key);
+            JS_FreeValue(cx, arguments[1]);
+            JS_FreeValue(cx, arguments[2]);
+
+            if (rc == NGX_ERROR) {
+                return NGX_ERROR;
+            }
+
+            if (!pending && rc == NGX_AGAIN) {
+                ngx_log_error(NGX_LOG_ERR, r->connection->log, 0,
+                              "async operation inside \"%V\" body filter",
+                              &jlcf->body_filter);
+                return NGX_ERROR;
+            }
+
+            ctx->buf->pos = ctx->buf->last;
+
+        } else {
+            cl = ngx_alloc_chain_link(c->pool);
+            if (cl == NULL) {
+                return NGX_ERROR;
+            }
+
+            cl->buf = b;
+
+            *ctx->last_out = cl;
+            ctx->last_out = &cl->next;
+        }
+
+        in = in->next;
+    }
+
+    return NGX_OK;
+}
+
+
+static ngx_http_request_t *
+ngx_http_qjs_request(JSValueConst val)
+{
+    ngx_http_qjs_request_t  *req;
+
+    req = JS_GetOpaque(val, NGX_QJS_CLASS_ID_HTTP_REQUEST);
+    if (req == NULL) {
+        return NULL;
+    }
+
+    return req->request;
+}
+
+
+static JSValue
+ngx_http_qjs_request_make(JSContext *cx, ngx_int_t proto_id,
+    ngx_http_request_t *r)
+{
+    JSValue                  request;
+    ngx_http_qjs_request_t  *req;
+
+    request = JS_NewObjectClass(cx, proto_id);
+    if (JS_IsException(request)) {
+        return JS_EXCEPTION;
+    }
+
+    req = js_malloc(cx, sizeof(ngx_http_qjs_request_t));
+    if (req == NULL) {
+        return JS_ThrowOutOfMemory(cx);
+    }
+
+    req->request = r;
+    req->args = JS_UNDEFINED;
+    req->request_body = JS_UNDEFINED;
+    req->response_body = JS_UNDEFINED;
+
+    JS_SetOpaque(request, req);
+
+    return request;
+}
+
+
+static void
+ngx_http_qjs_request_finalizer(JSRuntime *rt, JSValue val)
+{
+    ngx_http_qjs_request_t  *req;
+
+    req = JS_GetOpaque(val, NGX_QJS_CLASS_ID_HTTP_REQUEST);
+    if (req == NULL) {
+        return;
+    }
+
+    JS_FreeValueRT(rt, req->args);
+    JS_FreeValueRT(rt, req->request_body);
+    JS_FreeValueRT(rt, req->response_body);
+
+    js_free_rt(rt, req);
+}
+
+
+static ngx_engine_t *
+ngx_engine_qjs_clone(ngx_js_ctx_t *ctx, ngx_js_loc_conf_t *cf,
+    njs_int_t proto_id, void *external)
+{
+    JSValue             proto;
+    JSContext          *cx;
+    ngx_engine_t       *engine;
+    ngx_http_js_ctx_t  *hctx;
+
+    engine = ngx_qjs_clone(ctx, cf, external);
+    if (engine == NULL) {
+        return NULL;
+    }
+
+    cx = engine->u.qjs.ctx;
+
+    if (!JS_IsRegisteredClass(JS_GetRuntime(cx),
+                              NGX_QJS_CLASS_ID_HTTP_REQUEST))
+    {
+        if (JS_NewClass(JS_GetRuntime(cx), NGX_QJS_CLASS_ID_HTTP_REQUEST,
+                        &ngx_http_qjs_request_class) < 0)
+        {
+            return NULL;
+        }
+
+        proto = JS_NewObject(cx);
+        if (JS_IsException(proto)) {
+            return NULL;
+        }
+
+        JS_SetPropertyFunctionList(cx, proto, ngx_http_qjs_ext_request,
+                                   njs_nitems(ngx_http_qjs_ext_request));
+
+        JS_SetClassProto(cx, NGX_QJS_CLASS_ID_HTTP_REQUEST, proto);
+
+        if (JS_NewClass(JS_GetRuntime(cx), NGX_QJS_CLASS_ID_HTTP_PERIODIC,
+                        &ngx_http_qjs_periodic_class) < 0)
+        {
+            return NULL;
+        }
+
+        proto = JS_NewObject(cx);
+        if (JS_IsException(proto)) {
+            return NULL;
+        }
+
+        JS_SetPropertyFunctionList(cx, proto, ngx_http_qjs_ext_periodic,
+                                   njs_nitems(ngx_http_qjs_ext_periodic));
+
+        JS_SetClassProto(cx, NGX_QJS_CLASS_ID_HTTP_PERIODIC, proto);
+
+        if (JS_NewClass(JS_GetRuntime(cx), NGX_QJS_CLASS_ID_HTTP_VARS,
+                        &ngx_http_qjs_variables_class) < 0)
+        {
+            return NULL;
+        }
+
+        if (JS_NewClass(JS_GetRuntime(cx), NGX_QJS_CLASS_ID_HTTP_HEADERS_IN,
+                        &ngx_http_qjs_headers_in_class) < 0)
+        {
+            return NULL;
+        }
+
+        if (JS_NewClass(JS_GetRuntime(cx), NGX_QJS_CLASS_ID_HTTP_HEADERS_OUT,
+                        &ngx_http_qjs_headers_out_class) < 0)
+        {
+            return NULL;
+        }
+    }
+
+    hctx = (ngx_http_js_ctx_t *) ctx;
+    hctx->body_filter = ngx_http_qjs_body_filter;
+
+    if (proto_id == ngx_http_js_request_proto_id) {
+        proto_id = NGX_QJS_CLASS_ID_HTTP_REQUEST;
+
+    } else if (proto_id == ngx_http_js_periodic_session_proto_id) {
+        proto_id = NGX_QJS_CLASS_ID_HTTP_PERIODIC;
+    }
+
+    ngx_qjs_arg(hctx->args[0]) = ngx_http_qjs_request_make(cx, proto_id,
+                                                           external);
+    if (JS_IsException(ngx_qjs_arg(hctx->args[0]))) {
+        return NULL;
+    }
+
+    return engine;
+}
+
+#endif
+
+
+static ngx_int_t
+ngx_http_js_init_conf_vm(ngx_conf_t *cf, ngx_js_loc_conf_t *conf)
+{
+    ngx_engine_opts_t    options;
+    ngx_js_main_conf_t  *jmcf;
+
+    memset(&options, 0, sizeof(ngx_engine_opts_t));
+
+    options.engine = conf->type;
+
+    jmcf = ngx_http_conf_get_module_main_conf(cf, ngx_http_js_module);
+    ngx_http_js_uptr[NGX_JS_MAIN_CONF_INDEX] = (uintptr_t) jmcf;
+
+    if (conf->type == NGX_ENGINE_NJS) {
+        options.u.njs.metas = &ngx_http_js_metas;
+        options.u.njs.addons = njs_http_js_addon_modules;
+        options.clone = ngx_engine_njs_clone;
+    }
+
+#if (NJS_HAVE_QUICKJS)
+    else if (conf->type == NGX_ENGINE_QJS) {
+        options.u.qjs.metas = ngx_http_js_uptr;
+        options.u.qjs.addons = njs_http_qjs_addon_modules;
+        options.clone = ngx_engine_qjs_clone;
+    }
+#endif
+
     return ngx_js_init_conf_vm(cf, conf, &options);
 }
 
diff --git a/nginx/ngx_js.c b/nginx/ngx_js.c
index ce4988f9..5fe3dc84 100644
--- a/nginx/ngx_js.c
+++ b/nginx/ngx_js.c
@@ -8,6 +8,7 @@
 
 #include <ngx_config.h>
 #include <ngx_core.h>
+#include <math.h>
 #include "ngx_js.h"
 
 
@@ -57,6 +58,53 @@ static ngx_int_t ngx_engine_njs_string(ngx_engine_t *e,
 static void ngx_engine_njs_destroy(ngx_engine_t *e, ngx_js_ctx_t *ctx,
     ngx_js_loc_conf_t *conf);
 
+#if (NJS_HAVE_QUICKJS)
+static ngx_int_t ngx_engine_qjs_init(ngx_engine_t *engine,
+    ngx_engine_opts_t *opts);
+static ngx_int_t ngx_engine_qjs_compile(ngx_js_loc_conf_t *conf, ngx_log_t *log,
+    u_char *start, size_t size);
+static ngx_int_t ngx_engine_qjs_call(ngx_js_ctx_t *ctx, ngx_str_t *fname,
+    njs_opaque_value_t *args, njs_uint_t nargs);
+static void *ngx_engine_qjs_external(ngx_engine_t *engine);
+static ngx_int_t ngx_engine_qjs_pending(ngx_engine_t *engine);
+static ngx_int_t ngx_engine_qjs_string(ngx_engine_t *e,
+    njs_opaque_value_t *value, ngx_str_t *str);
+
+static JSValue ngx_qjs_ext_set_timeout(JSContext *cx, JSValueConst this_val,
+    int argc, JSValueConst *argv, int immediate);
+static JSValue ngx_qjs_ext_clear_timeout(JSContext *cx, JSValueConst this_val,
+    int argc, JSValueConst *argv);
+
+static JSValue ngx_qjs_ext_build(JSContext *cx, JSValueConst this_val);
+static JSValue ngx_qjs_ext_conf_file_path(JSContext *cx, JSValueConst this_val);
+static JSValue ngx_qjs_ext_conf_prefix(JSContext *cx, JSValueConst this_val);
+static JSValue ngx_qjs_ext_constant_integer(JSContext *cx,
+    JSValueConst this_val, int magic);
+static JSValue ngx_qjs_ext_error_log_path(JSContext *cx, JSValueConst this_val);
+static JSValue ngx_qjs_ext_log(JSContext *cx, JSValueConst this_val,
+    int argc, JSValueConst *argv, int level);
+static JSValue ngx_qjs_ext_console_time(JSContext *cx, JSValueConst this_val,
+    int argc, JSValueConst *argv);
+static JSValue ngx_qjs_ext_console_time_end(JSContext *cx,
+    JSValueConst this_val, int argc, JSValueConst *argv);
+static JSValue ngx_qjs_ext_prefix(JSContext *cx, JSValueConst this_val);
+static JSValue ngx_qjs_ext_worker_id(JSContext *cx, JSValueConst this_val);
+
+static void ngx_qjs_console_finalizer(JSRuntime *rt, JSValue val);
+
+static JSModuleDef *ngx_qjs_module_loader(JSContext *ctx,
+    const char *module_name, void *opaque);
+static int ngx_qjs_unhandled_rejection(ngx_js_ctx_t *ctx);
+static void ngx_qjs_rejection_tracker(JSContext *ctx, JSValueConst promise,
+    JSValueConst reason, JS_BOOL is_handled, void *opaque);
+
+static JSValue ngx_qjs_value(JSContext *cx, const ngx_str_t *path);
+static ngx_int_t ngx_qjs_dump_obj(ngx_engine_t *e, JSValueConst val,
+    ngx_str_t *dst);
+
+static JSModuleDef *ngx_qjs_core_init(JSContext *cx, const char *name);
+#endif
+
 static njs_int_t ngx_js_ext_build(njs_vm_t *vm, njs_object_prop_t *prop,
     njs_value_t *value, njs_value_t *setval, njs_value_t *retval);
 static njs_int_t ngx_js_ext_conf_file_path(njs_vm_t *vm,
@@ -377,6 +425,57 @@ njs_module_t *njs_js_addon_modules_shared[] = {
 static njs_int_t      ngx_js_console_proto_id;
 
 
+#if (NJS_HAVE_QUICKJS)
+
+static const JSCFunctionListEntry ngx_qjs_ext_ngx[] = {
+    JS_CGETSET_DEF("build", ngx_qjs_ext_build, NULL),
+    JS_CGETSET_DEF("conf_prefix", ngx_qjs_ext_conf_prefix, NULL),
+    JS_CGETSET_DEF("conf_file_path", ngx_qjs_ext_conf_file_path, NULL),
+    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_CGETSET_MAGIC_DEF("INFO", ngx_qjs_ext_constant_integer, NULL,
+                         NGX_LOG_INFO),
+    JS_CFUNC_MAGIC_DEF("log", 1, ngx_qjs_ext_log, 0),
+    JS_CGETSET_DEF("prefix", ngx_qjs_ext_prefix, NULL),
+    JS_PROP_STRING_DEF("version", NGINX_VERSION, JS_PROP_C_W_E),
+    JS_PROP_INT32_DEF("version_number", nginx_version, JS_PROP_C_W_E),
+    JS_CGETSET_MAGIC_DEF("WARN", ngx_qjs_ext_constant_integer, NULL,
+                         NGX_LOG_WARN),
+    JS_CGETSET_DEF("worker_id", ngx_qjs_ext_worker_id, NULL),
+};
+
+
+static const JSCFunctionListEntry ngx_qjs_ext_console[] = {
+    JS_CFUNC_MAGIC_DEF("error", 1, ngx_qjs_ext_log, NGX_LOG_ERR),
+    JS_CFUNC_MAGIC_DEF("info", 1, ngx_qjs_ext_log, NGX_LOG_INFO),
+    JS_CFUNC_MAGIC_DEF("log", 1, ngx_qjs_ext_log, NGX_LOG_INFO),
+    JS_CFUNC_DEF("time", 1, ngx_qjs_ext_console_time),
+    JS_CFUNC_DEF("timeEnd", 1, ngx_qjs_ext_console_time_end),
+    JS_CFUNC_MAGIC_DEF("warn", 1, ngx_qjs_ext_log, NGX_LOG_WARN),
+};
+
+
+static const JSCFunctionListEntry ngx_qjs_ext_global[] = {
+    JS_CFUNC_MAGIC_DEF("setTimeout", 1, ngx_qjs_ext_set_timeout, 0),
+    JS_CFUNC_MAGIC_DEF("setImmediate", 1, ngx_qjs_ext_set_timeout, 1),
+    JS_CFUNC_DEF("clearTimeout", 1, ngx_qjs_ext_clear_timeout),
+};
+
+
+static JSClassDef ngx_qjs_console_class = {
+    "Console",
+    .finalizer = ngx_qjs_console_finalizer,
+};
+
+
+qjs_module_t  ngx_qjs_ngx_module = {
+    .name = "ngx",
+    .init = ngx_qjs_core_init,
+};
+
+#endif
+
 static ngx_engine_t *
 ngx_create_engine(ngx_engine_opts_t *opts)
 {
@@ -415,6 +514,25 @@ ngx_create_engine(ngx_engine_opts_t *opts)
                                         : ngx_engine_njs_destroy;
         break;
 
+#if (NJS_HAVE_QUICKJS)
+    case NGX_ENGINE_QJS:
+        rc = ngx_engine_qjs_init(engine, opts);
+        if (rc != NGX_OK) {
+            return NULL;
+        }
+
+        engine->name = "QuickJS";
+        engine->type = NGX_ENGINE_QJS;
+        engine->compile = ngx_engine_qjs_compile;
+        engine->call = ngx_engine_qjs_call;
+        engine->external = ngx_engine_qjs_external;
+        engine->pending = ngx_engine_qjs_pending;
+        engine->string = ngx_engine_qjs_string;
+        engine->destroy = opts->destroy ? opts->destroy
+                                        : ngx_engine_qjs_destroy;
+        break;
+#endif
+
     default:
         return NULL;
     }
@@ -493,212 +611,1592 @@ ngx_engine_njs_compile(ngx_js_loc_conf_t *conf, ngx_log_t *log, u_char *start,
             if (value != NULL) {
                 i = njs_value_number(value) - 1;
 
-                if (i < conf->imports->nelts) {
-                    import = conf->imports->elts;
-                    ngx_log_error(NGX_LOG_EMERG, log, 0,
-                                  "%*s, included in %s:%ui", text.length,
-                                  text.start, import[i].file, import[i].line);
-                    return NGX_ERROR;
-                }
-            }
-        }
+                if (i < conf->imports->nelts) {
+                    import = conf->imports->elts;
+                    ngx_log_error(NGX_LOG_EMERG, log, 0,
+                                  "%*s, included in %s:%ui", text.length,
+                                  text.start, import[i].file, import[i].line);
+                    return NGX_ERROR;
+                }
+            }
+        }
+
+        ngx_log_error(NGX_LOG_EMERG, log, 0, "%*s", text.length, text.start);
+        return NGX_ERROR;
+    }
+
+    if (start != end) {
+        ngx_log_error(NGX_LOG_EMERG, log, 0,
+                      "extra characters in js script: \"%*s\"",
+                      end - start, start);
+        return NGX_ERROR;
+    }
+
+    return NGX_OK;
+}
+
+
+ngx_engine_t *
+ngx_njs_clone(ngx_js_ctx_t *ctx, ngx_js_loc_conf_t *cf, void *external)
+{
+    njs_vm_t             *vm;
+    njs_int_t             rc;
+    njs_str_t             key;
+    ngx_str_t             exception;
+    ngx_uint_t            i;
+    ngx_engine_t         *engine;
+    njs_opaque_value_t    retval;
+    ngx_js_named_path_t  *preload;
+
+    vm = njs_vm_clone(cf->engine->u.njs.vm, external);
+    if (vm == NULL) {
+        return NULL;
+    }
+
+    engine = njs_mp_alloc(njs_vm_memory_pool(vm), sizeof(ngx_engine_t));
+    if (engine == NULL) {
+        return NULL;
+    }
+
+    memcpy(engine, cf->engine, sizeof(ngx_engine_t));
+    engine->pool = njs_vm_memory_pool(vm);
+    engine->u.njs.vm = vm;
+
+    /* bind objects from preload vm */
+
+    if (cf->preload_objects != NGX_CONF_UNSET_PTR) {
+        preload = cf->preload_objects->elts;
+
+        for (i = 0; i < cf->preload_objects->nelts; i++) {
+            key.start = preload[i].name.data;
+            key.length = preload[i].name.len;
+
+            rc = njs_vm_value(cf->preload_vm, &key, njs_value_arg(&retval));
+            if (rc != NJS_OK) {
+                return NULL;
+            }
+
+            rc = njs_vm_bind(vm, &key, njs_value_arg(&retval), 0);
+            if (rc != NJS_OK) {
+                return NULL;
+            }
+        }
+    }
+
+    if (njs_vm_start(vm, njs_value_arg(&retval)) == NJS_ERROR) {
+        ngx_js_exception(vm, &exception);
+
+        ngx_log_error(NGX_LOG_ERR, ctx->log, 0, "js exception: %V", &exception);
+
+        return NULL;
+    }
+
+    return engine;
+}
+
+
+static ngx_int_t
+ngx_engine_njs_call(ngx_js_ctx_t *ctx, ngx_str_t *fname,
+    njs_opaque_value_t *args, njs_uint_t nargs)
+{
+    njs_vm_t        *vm;
+    njs_int_t        ret;
+    njs_str_t        name;
+    ngx_str_t        exception;
+    njs_function_t  *func;
+
+    name.start = fname->data;
+    name.length = fname->len;
+
+    vm = ctx->engine->u.njs.vm;
+
+    func = njs_vm_function(vm, &name);
+    if (func == NULL) {
+        ngx_log_error(NGX_LOG_ERR, ctx->log, 0,
+                      "js function \"%V\" not found", fname);
+        return NGX_ERROR;
+    }
+
+    ret = njs_vm_invoke(vm, func, njs_value_arg(args), nargs,
+                        njs_value_arg(&ctx->retval));
+    if (ret == NJS_ERROR) {
+        ngx_js_exception(vm, &exception);
+
+        ngx_log_error(NGX_LOG_ERR, ctx->log, 0,
+                      "js exception: %V", &exception);
+
+        return NGX_ERROR;
+    }
+
+    for ( ;; ) {
+        ret = njs_vm_execute_pending_job(vm);
+        if (ret <= NJS_OK) {
+            if (ret == NJS_ERROR) {
+                ngx_js_exception(vm, &exception);
+
+                ngx_log_error(NGX_LOG_ERR, ctx->log, 0,
+                              "js job exception: %V", &exception);
+                return NGX_ERROR;
+            }
+
+            break;
+        }
+    }
+
+    if (ngx_js_unhandled_rejection(ctx)) {
+        ngx_js_exception(vm, &exception);
+
+        ngx_log_error(NGX_LOG_ERR, ctx->log, 0, "js exception: %V", &exception);
+        return NGX_ERROR;
+    }
+
+    return njs_rbtree_is_empty(&ctx->waiting_events) ? NGX_OK : NGX_AGAIN;
+}
+
+
+static void *
+ngx_engine_njs_external(ngx_engine_t *engine)
+{
+    return njs_vm_external_ptr(engine->u.njs.vm);
+}
+
+static ngx_int_t
+ngx_engine_njs_pending(ngx_engine_t *e)
+{
+    return njs_vm_pending(e->u.njs.vm);
+}
+
+
+static ngx_int_t
+ngx_engine_njs_string(ngx_engine_t *e, njs_opaque_value_t *value,
+    ngx_str_t *str)
+{
+    ngx_int_t  rc;
+    njs_str_t  s;
+
+    rc = ngx_js_string(e->u.njs.vm, njs_value_arg(value), &s);
+
+    str->data = s.start;
+    str->len = s.length;
+
+    return rc;
+}
+
+
+static void
+ngx_engine_njs_destroy(ngx_engine_t *e, ngx_js_ctx_t *ctx,
+    ngx_js_loc_conf_t *conf)
+{
+    ngx_js_event_t     *event;
+    njs_rbtree_node_t  *node;
+
+    if (ctx != NULL) {
+        node = njs_rbtree_min(&ctx->waiting_events);
+
+        while (njs_rbtree_is_there_successor(&ctx->waiting_events, node)) {
+            event = (ngx_js_event_t *) ((u_char *) node
+                                        - offsetof(ngx_js_event_t, node));
+
+            if (event->destructor != NULL) {
+                event->destructor(event);
+            }
+
+            node = njs_rbtree_node_successor(&ctx->waiting_events, node);
+        }
+    }
+
+    njs_vm_destroy(e->u.njs.vm);
+
+    /*
+     * when ctx !=NULL e->pool is vm pool, in such case it is destroyed
+     * by njs_vm_destroy().
+     */
+
+    if (ctx == NULL) {
+        njs_mp_destroy(e->pool);
+    }
+}
+
+
+#if (NJS_HAVE_QUICKJS)
+
+static ngx_int_t
+ngx_engine_qjs_init(ngx_engine_t *engine, ngx_engine_opts_t *opts)
+{
+    JSRuntime  *rt;
+
+    rt = JS_NewRuntime();
+    if (rt == NULL) {
+        return NGX_ERROR;
+    }
+
+    engine->u.qjs.ctx = qjs_new_context(rt, opts->u.qjs.addons);
+    if (engine->u.qjs.ctx == NULL) {
+        return NGX_ERROR;
+    }
+
+    JS_SetRuntimeOpaque(rt, opts->u.qjs.metas);
+    JS_SetContextOpaque(engine->u.qjs.ctx, opts->u.qjs.addons);
+
+    JS_SetModuleLoaderFunc(rt, NULL, ngx_qjs_module_loader, opts->conf);
+
+    return NGX_OK;
+}
+
+
+static ngx_int_t
+ngx_engine_qjs_compile(ngx_js_loc_conf_t *conf, ngx_log_t *log, u_char *start,
+    size_t size)
+{
+    JSValue               code;
+    ngx_str_t             text;
+    JSContext            *cx;
+    ngx_engine_t         *engine;
+    ngx_js_code_entry_t  *pc;
+
+    engine = conf->engine;
+    cx = engine->u.qjs.ctx;
+
+    code = JS_Eval(cx, (char *) start, size, "<main>",
+                   JS_EVAL_TYPE_MODULE | JS_EVAL_FLAG_COMPILE_ONLY);
+
+    if (JS_IsException(code)) {
+        ngx_qjs_exception(engine, &text);
+        ngx_log_error(NGX_LOG_EMERG, log, 0, "js compile %V", &text);
+        return NGX_ERROR;
+    }
+
+    pc = njs_arr_add(engine->precompiled);
+    if (pc == NULL) {
+        JS_FreeValue(cx, code);
+        ngx_log_error(NGX_LOG_EMERG, log, 0, "njs_arr_add() failed");
+        return NGX_ERROR;
+    }
+
+    pc->code = JS_WriteObject(cx, &pc->code_size, code, JS_WRITE_OBJ_BYTECODE);
+    if (pc->code == NULL) {
+        JS_FreeValue(cx, code);
+        ngx_log_error(NGX_LOG_EMERG, log, 0, "JS_WriteObject() failed");
+        return NGX_ERROR;
+    }
+
+    JS_FreeValue(cx, code);
+
+    return NGX_OK;
+}
+
+
+static JSValue
+js_std_await(JSContext *ctx, JSValue obj)
+{
+    int         state, err;
+    JSValue     ret;
+    JSContext  *ctx1;
+
+    for (;;) {
+        state = JS_PromiseState(ctx, obj);
+        if (state == JS_PROMISE_FULFILLED) {
+            ret = JS_PromiseResult(ctx, obj);
+            JS_FreeValue(ctx, obj);
+            break;
+
+        } else if (state == JS_PROMISE_REJECTED) {
+            ret = JS_Throw(ctx, JS_PromiseResult(ctx, obj));
+            JS_FreeValue(ctx, obj);
+            break;
+
+        } else if (state == JS_PROMISE_PENDING) {
+            err = JS_ExecutePendingJob(JS_GetRuntime(ctx), &ctx1);
+            if (err < 0) {
+               /* js_std_dump_error(ctx1); */
+            }
+
+        } else {
+            /* not a promise */
+            ret = obj;
+            break;
+        }
+    }
+
+    return ret;
+}
+
+
+ngx_engine_t *
+ngx_qjs_clone(ngx_js_ctx_t *ctx, ngx_js_loc_conf_t *cf, void *external)
+{
+    JSValue               rv;
+    njs_mp_t             *mp;
+    uint32_t              i, length;
+    JSRuntime            *rt;
+    ngx_str_t             exception;
+    JSContext            *cx;
+    ngx_engine_t         *engine;
+    ngx_js_code_entry_t  *pc;
+
+    mp = njs_mp_fast_create(2 * getpagesize(), 128, 512, 16);
+    if (mp == NULL) {
+        return NULL;
+    }
+
+    engine = njs_mp_alloc(mp, sizeof(ngx_engine_t));
+    if (engine == NULL) {
+        return NULL;
+    }
+
+    memcpy(engine, cf->engine, sizeof(ngx_engine_t));
+    engine->pool = mp;
+
+    if (cf->reuse_queue != NULL) {
+        engine->u.qjs.ctx = ngx_js_queue_pop(cf->reuse_queue);
+        if (engine->u.qjs.ctx != NULL) {
+            ngx_log_debug1(NGX_LOG_DEBUG_HTTP, ctx->log, 0,
+                           "js reused context: %p", engine->u.qjs.ctx);
+            JS_SetContextOpaque(engine->u.qjs.ctx, external);
+            return engine;
+        }
+    }
+
+    rt = JS_NewRuntime();
+    if (rt == NULL) {
+        return NULL;
+    }
+
+    JS_SetRuntimeOpaque(rt, JS_GetRuntimeOpaque(
+                                        JS_GetRuntime(cf->engine->u.qjs.ctx)));
+
+    cx = qjs_new_context(rt, JS_GetContextOpaque(cf->engine->u.qjs.ctx));
+    if (cx == NULL) {
+        JS_FreeRuntime(rt);
+        return NULL;
+    }
+
+    engine->u.qjs.ctx = cx;
+    JS_SetContextOpaque(cx, external);
+
+    JS_SetHostPromiseRejectionTracker(rt, ngx_qjs_rejection_tracker, ctx);
+
+
+    /* TODO: bind objects from preload vm */
+
+    rv = JS_UNDEFINED;
+    pc = engine->precompiled->start;
+    length = engine->precompiled->items;
+
+    for (i = 0; i < length; i++) {
+        rv = JS_ReadObject(cx, pc[i].code, pc[i].code_size,
+                           JS_READ_OBJ_BYTECODE);
+        if (JS_IsException(rv)) {
+            ngx_qjs_exception(engine, &exception);
+
+            ngx_log_error(NGX_LOG_ERR, ctx->log, 0,
+                          "js load module exception: %V", &exception);
+            goto destroy;
+        }
+    }
+
+    if (JS_ResolveModule(cx, rv) < 0) {
+        ngx_log_error(NGX_LOG_ERR, ctx->log, 0, "js resolve module failed");
+        goto destroy;
+    }
+
+    rv = JS_EvalFunction(cx, rv);
+
+    if (JS_IsException(rv)) {
+        ngx_qjs_exception(engine, &exception);
+
+        ngx_log_error(NGX_LOG_ERR, ctx->log, 0, "js eval exception: %V",
+                      &exception);
+        goto destroy;
+    }
+
+    rv = js_std_await(cx, rv);
+    if (JS_IsException(rv)) {
+        ngx_qjs_exception(engine, &exception);
+
+        ngx_log_error(NGX_LOG_ERR, ctx->log, 0, "js eval exception: %V",
+                      &exception);
+        goto destroy;
+    }
+
+    JS_FreeValue(cx, rv);
+
+    return engine;
+
+destroy:
+
+    JS_FreeContext(cx);
+    JS_FreeRuntime(rt);
+    njs_mp_destroy(mp);
+
+    return NULL;
+}
+
+
+static ngx_int_t
+ngx_engine_qjs_call(ngx_js_ctx_t *ctx, ngx_str_t *fname,
+    njs_opaque_value_t *args, njs_uint_t nargs)
+{
+    int         rc;
+    JSValue     fn, val;
+    ngx_str_t   exception;
+    JSRuntime  *rt;
+    JSContext  *cx, *cx1;
+
+    cx = ctx->engine->u.qjs.ctx;
+
+    fn = ngx_qjs_value(cx, fname);
+    if (!JS_IsFunction(cx, fn)) {
+        JS_FreeValue(cx, fn);
+        ngx_log_error(NGX_LOG_ERR, ctx->log, 0, "js function \"%V\" not found",
+                      fname);
+
+        return NGX_ERROR;
+    }
+
+    val = JS_Call(cx, fn, JS_UNDEFINED, nargs, &ngx_qjs_arg(args[0]));
+    JS_FreeValue(cx, fn);
+    if (JS_IsException(val)) {
+        ngx_qjs_exception(ctx->engine, &exception);
+
+        ngx_log_error(NGX_LOG_ERR, ctx->log, 0,
+                      "js call exception: %V", &exception);
+
+        return NGX_ERROR;
+    }
+
+    JS_FreeValue(cx, ngx_qjs_arg(ctx->retval));
+    ngx_qjs_arg(ctx->retval) = val;
+
+    rt = JS_GetRuntime(cx);
+
+    for ( ;; ) {
+        rc = JS_ExecutePendingJob(rt, &cx1);
+        if (rc <= 0) {
+            if (rc == -1) {
+                ngx_qjs_exception(ctx->engine, &exception);
+
+                ngx_log_error(NGX_LOG_ERR, ctx->log, 0,
+                              "js job exception: %V", &exception);
+
+                return NGX_ERROR;
+            }
+
+            break;
+        }
+    }
+
+    if (ngx_qjs_unhandled_rejection(ctx)) {
+        ngx_qjs_exception(ctx->engine, &exception);
+
+        ngx_log_error(NGX_LOG_ERR, ctx->log, 0, "js exception: %V", &exception);
+        return NGX_ERROR;
+    }
+
+    return njs_rbtree_is_empty(&ctx->waiting_events) ? NGX_OK : NGX_AGAIN;
+}
+
+
+static void *
+ngx_engine_qjs_external(ngx_engine_t *e)
+{
+    return JS_GetContextOpaque(e->u.qjs.ctx);
+}
+
+
+static ngx_int_t
+ngx_engine_qjs_pending(ngx_engine_t *e)
+{
+    return JS_IsJobPending(JS_GetRuntime(e->u.qjs.ctx));
+}
+
+
+static ngx_int_t
+ngx_engine_qjs_string(ngx_engine_t *e, njs_opaque_value_t *value,
+    ngx_str_t *str)
+{
+    return ngx_qjs_dump_obj(e, ngx_qjs_arg(*value), str);
+}
+
+
+static void
+ngx_js_cleanup_reuse_ctx(void *data)
+{
+    JSRuntime  *rt;
+    JSContext  *cx;
+
+    ngx_js_queue_t  *reuse = data;
+
+    for ( ;; ) {
+        cx = ngx_js_queue_pop(reuse);
+        if (cx == NULL) {
+            break;
+        }
+
+        rt = JS_GetRuntime(cx);
+        JS_FreeContext(cx);
+        JS_FreeRuntime(rt);
+    }
+}
+
+
+void
+ngx_engine_qjs_destroy(ngx_engine_t *e, ngx_js_ctx_t *ctx,
+    ngx_js_loc_conf_t *conf)
+{
+    uint32_t                    i, length;
+    JSRuntime                  *rt;
+    JSContext                  *cx;
+    JSClassID                   class_id;
+    ngx_qjs_event_t            *event;
+    ngx_js_opaque_t            *opaque;
+    njs_rbtree_node_t          *node;
+    ngx_pool_cleanup_t         *cln;
+    ngx_js_code_entry_t        *pc;
+    ngx_js_rejected_promise_t  *rejected_promise;
+
+    cx = e->u.qjs.ctx;
+
+    if (ctx != NULL) {
+        node = njs_rbtree_min(&ctx->waiting_events);
+
+        while (njs_rbtree_is_there_successor(&ctx->waiting_events, node)) {
+            event = (ngx_qjs_event_t *) ((u_char *) node
+                                         - offsetof(ngx_qjs_event_t, node));
+
+            if (event->destructor != NULL) {
+                event->destructor(event);
+            }
+
+            node = njs_rbtree_node_successor(&ctx->waiting_events, node);
+        }
+
+        if (ctx->rejected_promises != NULL) {
+            rejected_promise = ctx->rejected_promises->start;
+
+            for (i = 0; i < ctx->rejected_promises->items; i++) {
+                JS_FreeValue(cx, ngx_qjs_arg(rejected_promise[i].promise));
+                JS_FreeValue(cx, ngx_qjs_arg(rejected_promise[i].message));
+            }
+        }
+
+        class_id = JS_GetClassID(ngx_qjs_arg(ctx->args[0]));
+        opaque = JS_GetOpaque(ngx_qjs_arg(ctx->args[0]), class_id);
+        opaque->external = NULL;
+
+        JS_FreeValue(cx, ngx_qjs_arg(ctx->args[0]));
+        JS_FreeValue(cx, ngx_qjs_arg(ctx->retval));
+
+    } else if (e->precompiled != NULL) {
+        pc = e->precompiled->start;
+        length = e->precompiled->items;
+
+        for (i = 0; i < length; i++) {
+            js_free(cx, pc[i].code);
+        }
+    }
+
+    njs_mp_destroy(e->pool);
+
+    if (conf != NULL && conf->reuse != 0) {
+        if (conf->reuse_queue == NULL) {
+            conf->reuse_queue = ngx_js_queue_create(ngx_cycle->pool,
+                                                    conf->reuse);
+            if (conf->reuse_queue == NULL) {
+                goto free_ctx;
+            }
+
+            cln = ngx_pool_cleanup_add(ngx_cycle->pool, 0);
+            if (cln == NULL) {
+                goto free_ctx;
+            }
+
+            cln->handler = ngx_js_cleanup_reuse_ctx;
+            cln->data = conf->reuse_queue;
+        }
+
+        if (ngx_js_queue_push(conf->reuse_queue, cx) != NGX_OK) {
+            goto free_ctx;
+        }
+
+        return;
+    }
+
+free_ctx:
+
+    rt = JS_GetRuntime(cx);
+    JS_FreeContext(cx);
+    JS_FreeRuntime(rt);
+}
+
+
+static JSValue
+ngx_qjs_value(JSContext *cx, const ngx_str_t *path)
+{
+    u_char   *start, *p, *end;
+    JSAtom    key;
+    size_t    size;
+    JSValue   value, rv;
+
+    start = path->data;
+    end = start + path->len;
+
+    value = JS_GetGlobalObject(cx);
+
+    for ( ;; ) {
+        p = njs_strlchr(start, end, '.');
+
+        size = ((p != NULL) ? p : end) - start;
+        if (size == 0) {
+            JS_FreeValue(cx, value);
+            return JS_ThrowTypeError(cx, "empty path element");
+        }
+
+        key = JS_NewAtomLen(cx, (char *) start, size);
+        if (key == JS_ATOM_NULL) {
+            JS_FreeValue(cx, value);
+            return JS_ThrowInternalError(cx, "could not create atom");
+        }
+
+        rv = JS_GetProperty(cx, value, key);
+        JS_FreeAtom(cx, key);
+        if (JS_IsException(rv)) {
+            JS_FreeValue(cx, value);
+            return JS_EXCEPTION;
+        }
+
+        JS_FreeValue(cx, value);
+
+        if (p == NULL) {
+            break;
+        }
+
+        start = p + 1;
+        value = rv;
+    }
+
+    return rv;
+}
+
+
+static ngx_int_t
+ngx_qjs_dump_obj(ngx_engine_t *e, JSValueConst val, ngx_str_t *dst)
+{
+    size_t       len, byte_offset, byte_length;
+    u_char      *start, *p;
+    JSValue      buffer, stack;
+    ngx_str_t    str, stack_str;
+    JSContext    *cx;
+
+    if (JS_IsNullOrUndefined(val)) {
+        dst->data = NULL;
+        dst->len = 0;
+        return NGX_OK;
+    }
+
+    cx = e->u.qjs.ctx;
+
+    buffer = JS_GetTypedArrayBuffer(cx, val, &byte_offset, &byte_length, NULL);
+    if (!JS_IsException(buffer)) {
+        start = JS_GetArrayBuffer(cx, &dst->len, buffer);
+
+        JS_FreeValue(cx, buffer);
+
+        if (start != NULL) {
+            start += byte_offset;
+            dst->len = byte_length;
+
+            dst->data = njs_mp_alloc(e->pool, dst->len);
+            if (dst->data == NULL) {
+                return NGX_ERROR;
+            }
+
+            memcpy(dst->data, start, dst->len);
+            return NGX_OK;
+        }
+    }
+
+    str.data = (u_char *) JS_ToCString(cx, val);
+    if (str.data != NULL) {
+        str.len = ngx_strlen(str.data);
+
+        stack = JS_GetPropertyStr(cx, val, "stack");
+
+        stack_str.len = 0;
+        stack_str.data = NULL;
+
+        if (!JS_IsException(stack) && !JS_IsUndefined(stack)) {
+            stack_str.data = (u_char *) JS_ToCString(cx, stack);
+            if (stack_str.data != NULL) {
+                stack_str.len = ngx_strlen(stack_str.data);
+            }
+        }
+
+        len = str.len;
+
+        if (stack_str.len != 0) {
+            len += stack_str.len + njs_length("\n");
+        }
+
+        start = njs_mp_alloc(e->pool, len);
+        if (start == NULL) {
+            JS_FreeCString(cx, (char *) str.data);
+            JS_FreeValue(cx, stack);
+            return NGX_ERROR;
+        }
+
+        p = ngx_cpymem(start, str.data, str.len);
+
+        if (stack_str.len != 0) {
+            *p++ = '\n';
+            (void) ngx_cpymem(p, stack_str.data, stack_str.len);
+            JS_FreeCString(cx, (char *) stack_str.data);
+        }
+
+        JS_FreeCString(cx, (char *) str.data);
+        JS_FreeValue(cx, stack);
+
+    } else {
+        len = njs_length("[exception]");
+
+        start = njs_mp_alloc(e->pool, len);
+        if (start == NULL) {
+            return NGX_ERROR;
+        }
+
+        memcpy(start, "[exception]", len);
+    }
+
+    dst->data = start;
+    dst->len = len;
+
+    return NGX_OK;
+}
+
+
+ngx_int_t
+ngx_qjs_call(ngx_js_ctx_t *ctx, JSValue fn, JSValue *argv, int argc)
+{
+    int         rc;
+    JSValue     ret;
+    ngx_str_t   exception;
+    JSRuntime  *rt;
+    JSContext  *cx, *cx1;
+
+    cx = ctx->engine->u.qjs.ctx;
+
+    ret = JS_Call(cx, fn, JS_UNDEFINED, argc, argv);
+    if (JS_IsException(ret)) {
+        ngx_qjs_exception(ctx->engine, &exception);
+
+        ngx_log_error(NGX_LOG_ERR, ctx->log, 0,
+                      "js call exception: %V", &exception);
+
+        return NGX_ERROR;
+    }
+
+    JS_FreeValue(cx, ret);
+
+    rt = JS_GetRuntime(cx);
+
+    for ( ;; ) {
+        rc = JS_ExecutePendingJob(rt, &cx1);
+        if (rc <= 0) {
+            if (rc == -1) {
+                ngx_qjs_exception(ctx->engine, &exception);
+
+                ngx_log_error(NGX_LOG_ERR, ctx->log, 0,
+                              "js job exception: %V", &exception);
+
+                return NGX_ERROR;
+            }
+
+            break;
+        }
+    }
+
+    return NGX_OK;
+}
+
+
+ngx_int_t
+ngx_qjs_exception(ngx_engine_t *e, ngx_str_t *s)
+{
+    JSValue  exception;
+
+    exception = JS_GetException(e->u.qjs.ctx);
+    if (ngx_qjs_dump_obj(e, exception, s) != NGX_OK) {
+        return NGX_ERROR;
+    }
+
+    JS_FreeValue(e->u.qjs.ctx, exception);
+
+    return NGX_OK;
+}
+
+
+ngx_int_t
+ngx_qjs_integer(JSContext *cx, JSValueConst val, ngx_int_t *n)
+{
+    double  num;
+
+    if (JS_ToFloat64(cx, &num, val)) {
+        return NGX_ERROR;
+    }
+
+    if (isinf(num) || isnan(num)) {
+        (void) JS_ThrowTypeError(cx, "invalid number");
+        return NGX_ERROR;
+    }
+
+    *n = num;
+
+    return NGX_OK;
+}
+
+
+ngx_int_t
+ngx_qjs_string(ngx_engine_t *e, JSValueConst val, ngx_str_t *dst)
+{
+    size_t       len, byte_offset, byte_length;
+    u_char      *start;
+    JSValue      buffer;
+    JSContext   *cx;
+    const char  *str;
+
+    if (JS_IsNullOrUndefined(val)) {
+        dst->data = NULL;
+        dst->len = 0;
+        return NGX_OK;
+    }
+
+    cx = e->u.qjs.ctx;
+
+    buffer = JS_GetTypedArrayBuffer(cx, val, &byte_offset, &byte_length, NULL);
+    if (!JS_IsException(buffer)) {
+        start = JS_GetArrayBuffer(cx, &dst->len, buffer);
+
+        JS_FreeValue(cx, buffer);
+
+        if (start != NULL) {
+            start += byte_offset;
+            dst->len = byte_length;
+
+            dst->data = njs_mp_alloc(e->pool, dst->len);
+            if (dst->data == NULL) {
+                return NGX_ERROR;
+            }
+
+            memcpy(dst->data, start, dst->len);
+            return NGX_OK;
+        }
+    }
+
+    str = JS_ToCString(cx, val);
+    if (str == NULL) {
+        return NGX_ERROR;
+    }
+
+    len = strlen(str);
+
+    start = njs_mp_alloc(e->pool, len);
+    if (start == NULL) {
+        JS_FreeCString(cx, str);
+        return NGX_ERROR;
+    }
+
+    memcpy(start, str, len);
+
+    JS_FreeCString(cx, str);
+
+    dst->data = start;
+    dst->len = len;
+
+    return NGX_OK;
+}
+
+
+static void
+ngx_qjs_timer_handler(ngx_event_t *ev)
+{
+    void             *external;
+    JSContext        *cx;
+    ngx_int_t         rc;
+    ngx_js_ctx_t     *ctx;
+    ngx_qjs_event_t  *event;
+
+    event = (ngx_qjs_event_t *) ((u_char *) ev - offsetof(ngx_qjs_event_t, ev));
+
+    cx = event->ctx;
+    external = JS_GetContextOpaque(cx);
+    ctx = ngx_qjs_external_ctx(cx, external);
+
+    rc = ngx_qjs_call((ngx_js_ctx_t *) ctx, event->function, event->args,
+                      event->nargs);
+
+    ngx_js_del_event(ctx, event);
+
+    ngx_qjs_external_event_finalize(cx)(external, rc);
+}
+
+
+static void
+ngx_qjs_clear_timer(ngx_qjs_event_t *event)
+{
+    int         i;
+    JSContext  *cx;
+
+    cx = event->ctx;
+
+    if (event->ev.timer_set) {
+        ngx_del_timer(&event->ev);
+    }
+
+    JS_FreeValue(cx, event->function);
+
+    for (i = 0; i < (int) event->nargs; i++) {
+        JS_FreeValue(cx, event->args[i]);
+    }
+}
+
+
+static JSValue
+ngx_qjs_ext_set_timeout(JSContext *cx, JSValueConst this_val, int argc,
+    JSValueConst *argv, int immediate)
+{
+    int                i, n;
+    void              *external;
+    uint32_t           delay;
+    ngx_js_ctx_t      *ctx;
+    ngx_qjs_event_t   *event;
+    ngx_connection_t  *c;
+
+    if (!JS_IsFunction(cx, argv[0])) {
+        return JS_ThrowTypeError(cx, "first arg must be a function");
+    }
+
+    delay = 0;
+
+    if (!immediate && argc >= 2) {
+        if (JS_ToUint32(cx, &delay, argv[1]) < 0) {
+            return JS_EXCEPTION;
+        }
+    }
+
+    n = immediate ? 1 : 2;
+    argc = (argc >= n) ? argc - n : 0;
+    external = JS_GetContextOpaque(cx);
+    ctx = ngx_qjs_external_ctx(cx, external);
+
+    event = ngx_pcalloc(ngx_qjs_external_pool(cx, external),
+                        sizeof(ngx_qjs_event_t) + sizeof(JSValue) * argc);
+    if (event == NULL) {
+        return JS_ThrowOutOfMemory(cx);
+    }
+
+    event->ctx = cx;
+    event->function = JS_DupValue(cx, argv[0]);
+    event->nargs = argc;
+    event->args = (JSValue *) &event[1];
+    event->destructor = ngx_qjs_clear_timer;
+    event->fd = ctx->event_id++;
+
+    c = ngx_qjs_external_connection(cx, external);
+
+    event->ev.log = c->log;
+    event->ev.data = event;
+    event->ev.handler = ngx_qjs_timer_handler;
+
+    if (event->nargs != 0) {
+        for (i = 0; i < argc; i++) {
+            event->args[i] = JS_DupValue(cx, argv[n + i]);
+        }
+    }
+
+    ngx_js_add_event(ctx, event);
+
+    ngx_add_timer(&event->ev, delay);
+
+    return JS_NewInt32(cx, event->fd);
+}
+
+
+static JSValue
+ngx_qjs_ext_clear_timeout(JSContext *cx, JSValueConst this_val,
+    int argc, JSValueConst *argv)
+{
+    uint32_t           id;
+    ngx_js_ctx_t       *ctx;
+    ngx_qjs_event_t     event_lookup, *event;
+    njs_rbtree_node_t  *rb;
+
+    if (JS_ToUint32(cx, &id, argv[0]) < 0) {
+        return JS_EXCEPTION;
+    }
+
+    ctx = ngx_qjs_external_ctx(cx, JS_GetContextOpaque(cx));
+    event_lookup.fd = id;
+
+    rb = njs_rbtree_find(&ctx->waiting_events, &event_lookup.node);
+    if (rb == NULL) {
+        return JS_ThrowReferenceError(cx, "failed to find timer");
+    }
+
+    event = (ngx_qjs_event_t *) ((u_char *) rb
+                                 - offsetof(ngx_qjs_event_t, node));
+
+    ngx_js_del_event(ctx, event);
+
+    return JS_UNDEFINED;
+}
+
+
+static JSValue
+ngx_qjs_ext_build(JSContext *cx, JSValueConst this_val)
+{
+    return JS_NewStringLen(cx,
+#ifdef NGX_BUILD
+                           (char *) NGX_BUILD,
+                           njs_strlen(NGX_BUILD)
+#else
+                           (char *) "",
+                           0
+#endif
+                           );
+}
+
+
+static JSValue
+ngx_qjs_ext_conf_prefix(JSContext *cx, JSValueConst this_val)
+{
+    return JS_NewStringLen(cx, (char *) ngx_cycle->prefix.data,
+                           ngx_cycle->prefix.len);
+}
+
+
+static JSValue
+ngx_qjs_ext_conf_file_path(JSContext *cx, JSValueConst this_val)
+{
+    return JS_NewStringLen(cx, (char *) ngx_cycle->conf_file.data,
+                           ngx_cycle->conf_file.len);
+}
+
+
+static JSValue
+ngx_qjs_ext_constant_integer(JSContext *cx, JSValueConst this_val, int magic)
+{
+    return JS_NewInt32(cx, magic);
+}
+
+
+static JSValue
+ngx_qjs_ext_error_log_path(JSContext *cx, JSValueConst this_val)
+{
+    return JS_NewStringLen(cx, (char *) ngx_cycle->error_log.data,
+                           ngx_cycle->error_log.len);
+}
+
+
+static JSValue
+ngx_qjs_ext_prefix(JSContext *cx, JSValueConst this_val)
+{
+    return JS_NewStringLen(cx, (char *) ngx_cycle->prefix.data,
+                           ngx_cycle->prefix.len);
+}
+
+
+static JSValue
+ngx_qjs_ext_worker_id(JSContext *cx, JSValueConst this_val)
+{
+    return JS_NewInt32(cx, ngx_worker);
+}
+
+
+static void
+ngx_qjs_console_finalizer(JSRuntime *rt, JSValue val)
+{
+    ngx_queue_t         *labels, *q, *next;
+    ngx_js_console_t    *console;
+    ngx_js_timelabel_t  *label;
+
+    console = JS_GetOpaque(val, NGX_QJS_CLASS_ID_CONSOLE);
+    if (console == (void *) 1) {
+        return;
+    }
+
+    labels = &console->labels;
+    q = ngx_queue_head(labels);
+
+    for ( ;; ) {
+        if (q == ngx_queue_sentinel(labels)) {
+            break;
+        }
+
+        next = ngx_queue_next(q);
+
+        label = ngx_queue_data(q, ngx_js_timelabel_t, queue);
+        ngx_queue_remove(&label->queue);
+        js_free_rt(rt, label);
+
+        q = next;
+    }
+
+    js_free_rt(rt, console);
+}
+
+
+static JSValue
+ngx_qjs_ext_log(JSContext *cx, JSValueConst this_val, int argc,
+    JSValueConst *argv, int magic)
+{
+    char              *p;
+    uint32_t           level;
+    ngx_str_t          msg;
+    ngx_js_ctx_t      *ctx;
+    ngx_connection_t  *c;
+
+    p = JS_GetContextOpaque(cx);
+    if (p == NULL) {
+        return JS_ThrowInternalError(cx, "external is not set");
+    }
+
+    level = magic & NGX_JS_LOG_MASK;
+
+    if (level == 0) {
+        if (JS_ToUint32(cx, &level, argv[0]) < 0) {
+            return JS_EXCEPTION;
+        }
+
+        argc--;
+        argv++;
+    }
+
+    ctx = ngx_qjs_external_ctx(cx, p);
+    c = ngx_qjs_external_connection(cx, p);
+
+    for ( ; argc > 0; argc--, argv++) {
+        if (ngx_qjs_dump_obj(ctx->engine, argv[0], &msg) != NGX_OK) {
+            return JS_EXCEPTION;
+        }
+
+        ngx_js_logger(c, level, (u_char *) msg.data, msg.len);
+    }
+
+    return JS_UNDEFINED;
+}
+
+
+static JSValue
+ngx_qjs_ext_console_time(JSContext *cx, JSValueConst this_val, int argc,
+    JSValueConst *argv)
+{
+    ngx_str_t            name;
+    ngx_queue_t         *labels, *q;
+    ngx_js_console_t    *console;
+    ngx_connection_t    *c;
+    ngx_js_timelabel_t  *label;
+
+    static const ngx_str_t  default_label = ngx_string("default");
+
+    console = JS_GetOpaque(this_val, NGX_QJS_CLASS_ID_CONSOLE);
+    if (console == NULL) {
+        return JS_ThrowInternalError(cx, "this is not a console object");
+    }
+
+    if (console == (void *) 1) {
+        console = js_malloc(cx, sizeof(ngx_js_console_t));
+        if (console == NULL) {
+            return JS_ThrowOutOfMemory(cx);
+        }
+
+        ngx_queue_init(&console->labels);
+
+        JS_SetOpaque(this_val, console);
+    }
+
+    if (!JS_IsUndefined(argv[0])) {
+        name.data = (u_char *) JS_ToCStringLen(cx, &name.len, argv[0]);
+        if (name.data == NULL) {
+            return JS_EXCEPTION;
+        }
+
+    } else {
+        name = default_label;
+    }
+
+    labels = &console->labels;
+
+    for (q = ngx_queue_head(labels);
+         q != ngx_queue_sentinel(labels);
+         q = ngx_queue_next(q))
+    {
+        label = ngx_queue_data(q, ngx_js_timelabel_t, queue);
+
+        if (name.len == label->name.length
+            && ngx_strncmp(name.data, label->name.start, name.len) == 0)
+        {
+            c = ngx_qjs_external_connection(cx, JS_GetContextOpaque(cx));
+            ngx_log_error(NGX_LOG_INFO, c->log, 0, "js: Timer \"%V\" already"
+                          " exists", &name);
+
+            goto done;
+        }
+    }
+
+    label = js_malloc(cx, sizeof(ngx_js_timelabel_t) + name.len);
+    if (label == NULL) {
+        if (name.data != default_label.data) {
+            JS_FreeCString(cx, (char *) name.data);
+        }
+
+        return JS_ThrowOutOfMemory(cx);
+    }
+
+    label->name.length = name.len;
+    label->name.start = (u_char *) label + sizeof(ngx_js_timelabel_t);
+    memcpy(label->name.start, name.data, name.len);
+
+    label->time = ngx_js_monotonic_time();
+
+    ngx_queue_insert_tail(&console->labels, &label->queue);
+
+done:
+
+    if (name.data != default_label.data) {
+        JS_FreeCString(cx, (char *) name.data);
+    }
+
+    return JS_UNDEFINED;
+}
+
+
+static JSValue
+ngx_qjs_ext_console_time_end(JSContext *cx, JSValueConst this_val, int argc,
+    JSValueConst *argv)
+{
+    uint64_t             ns, ms;
+    ngx_str_t            name;
+    ngx_queue_t         *labels, *q;
+    ngx_js_console_t    *console;
+    ngx_connection_t    *c;
+    ngx_js_timelabel_t  *label;
+
+    static const ngx_str_t  default_label = ngx_string("default");
+
+    ns = ngx_js_monotonic_time();
+
+    console = JS_GetOpaque(this_val, NGX_QJS_CLASS_ID_CONSOLE);
+    if (console == NULL) {
+        return JS_ThrowInternalError(cx, "this is not a console object");
+    }
+
+    if (!JS_IsUndefined(argv[0])) {
+        name.data = (u_char *) JS_ToCStringLen(cx, &name.len, argv[0]);
+        if (name.data == NULL) {
+            return JS_EXCEPTION;
+        }
+
+    } else {
+        name = default_label;
+    }
+
+    if (console == (void *) 1) {
+        goto not_found;
+    }
+
+    labels = &console->labels;
+    q = ngx_queue_head(labels);
+
+    for ( ;; ) {
+        if (q == ngx_queue_sentinel(labels)) {
+            goto not_found;
+        }
+
+        label = ngx_queue_data(q, ngx_js_timelabel_t, queue);
+
+        if (name.len == label->name.length
+            && ngx_strncmp(name.data, label->name.start, name.len) == 0)
+        {
+            ngx_queue_remove(&label->queue);
+            break;
+        }
+
+        q = ngx_queue_next(q);
+    }
+
+    ns = ns - label->time;
+
+    js_free(cx, label);
+
+    ms = ns / 1000000;
+    ns = ns % 1000000;
+
+    c = ngx_qjs_external_connection(cx, JS_GetContextOpaque(cx));
+    ngx_log_error(NGX_LOG_INFO, c->log, 0, "js: %V: %uL.%06uLms",
+                  &name, ms, ns);
 
-        ngx_log_error(NGX_LOG_EMERG, log, 0, "%*s", text.length, text.start);
-        return NGX_ERROR;
+    if (name.data != default_label.data) {
+        JS_FreeCString(cx, (char *) name.data);
     }
 
-    if (start != end) {
-        ngx_log_error(NGX_LOG_EMERG, log, 0,
-                      "extra characters in js script: \"%*s\"",
-                      end - start, start);
-        return NGX_ERROR;
+    return JS_UNDEFINED;
+
+not_found:
+
+    c = ngx_qjs_external_connection(cx, JS_GetContextOpaque(cx));
+    ngx_log_error(NGX_LOG_INFO, c->log, 0, "js: Timer \"%V\" doesn't exist",
+                  &name);
+
+    if (name.data != default_label.data) {
+        JS_FreeCString(cx, (char *) name.data);
     }
 
-    return NGX_OK;
+    return JS_UNDEFINED;
 }
 
 
-ngx_engine_t *
-ngx_njs_clone(ngx_js_ctx_t *ctx, ngx_js_loc_conf_t *cf, void *external)
+static JSModuleDef *
+ngx_qjs_module_loader(JSContext *cx, const char *module_name, void *opaque)
 {
-    njs_vm_t             *vm;
-    njs_int_t             rc;
-    njs_str_t             key;
-    ngx_str_t             exception;
-    ngx_uint_t            i;
-    ngx_engine_t         *engine;
-    njs_opaque_value_t    retval;
-    ngx_js_named_path_t  *preload;
+    JSValue               func_val;
+    njs_int_t             ret;
+    njs_str_t             text;
+    JSModuleDef          *m;
+    njs_module_info_t     info;
+    ngx_js_loc_conf_t    *conf;
+    ngx_js_code_entry_t  *pc;
 
-    vm = njs_vm_clone(cf->engine->u.njs.vm, external);
-    if (vm == NULL) {
-        return NULL;
-    }
+    conf = opaque;
 
-    engine = njs_mp_alloc(njs_vm_memory_pool(vm), sizeof(ngx_engine_t));
-    if (engine == NULL) {
+    njs_memzero(&info, sizeof(njs_module_info_t));
+
+    info.name.start = (u_char *) module_name;
+    info.name.length = njs_strlen(module_name);
+
+    ret = ngx_js_module_lookup(conf, &info);
+    if (ret != NJS_OK) {
+        JS_ThrowReferenceError(cx, "could not load module filename '%s'",
+                               module_name);
         return NULL;
     }
 
-    memcpy(engine, cf->engine, sizeof(ngx_engine_t));
-    engine->pool = njs_vm_memory_pool(vm);
-    engine->u.njs.vm = vm;
+    ret = ngx_js_module_read(conf->engine->pool, info.fd, &text);
 
-    /* bind objects from preload vm */
+    (void) close(info.fd);
 
-    if (cf->preload_objects != NGX_CONF_UNSET_PTR) {
-        preload = cf->preload_objects->elts;
+    if (ret != NJS_OK) {
+        JS_ThrowInternalError(cx, "while reading \"%*s\" module",
+                              (int) info.file.length, info.file.start);
+        return NULL;
+    }
 
-        for (i = 0; i < cf->preload_objects->nelts; i++) {
-            key.start = preload[i].name.data;
-            key.length = preload[i].name.len;
+    func_val = JS_Eval(cx, (char *) text.start, text.length, module_name,
+                       JS_EVAL_TYPE_MODULE | JS_EVAL_FLAG_COMPILE_ONLY);
 
-            rc = njs_vm_value(cf->preload_vm, &key, njs_value_arg(&retval));
-            if (rc != NJS_OK) {
-                return NULL;
-            }
+    njs_mp_free(conf->engine->pool, text.start);
 
-            rc = njs_vm_bind(vm, &key, njs_value_arg(&retval), 0);
-            if (rc != NJS_OK) {
-                return NULL;
-            }
-        }
+    if (JS_IsException(func_val)) {
+        return NULL;
     }
 
-    if (njs_vm_start(vm, njs_value_arg(&retval)) == NJS_ERROR) {
-        ngx_js_exception(vm, &exception);
+    if (conf->engine->precompiled == NULL) {
+        conf->engine->precompiled = njs_arr_create(conf->engine->pool, 4,
+                                                  sizeof(ngx_js_code_entry_t));
+        if (conf->engine->precompiled == NULL) {
+            JS_FreeValue(cx, func_val);
+            JS_ThrowOutOfMemory(cx);
+            return NULL;
+        }
+    }
 
-        ngx_log_error(NGX_LOG_ERR, ctx->log, 0, "js exception: %V", &exception);
+    pc = njs_arr_add(conf->engine->precompiled);
+    if (pc == NULL) {
+        JS_FreeValue(cx, func_val);
+        JS_ThrowOutOfMemory(cx);
+        return NULL;
+    }
 
+    pc->code = JS_WriteObject(cx, &pc->code_size, func_val,
+                              JS_WRITE_OBJ_BYTECODE);
+    if (pc->code == NULL) {
+        JS_FreeValue(cx, func_val);
+        JS_ThrowInternalError(cx, "could not write module bytecode");
         return NULL;
     }
 
-    return engine;
+    m = JS_VALUE_GET_PTR(func_val);
+    JS_FreeValue(cx, func_val);
+
+    return m;
 }
 
 
-static ngx_int_t
-ngx_engine_njs_call(ngx_js_ctx_t *ctx, ngx_str_t *fname,
-    njs_opaque_value_t *args, njs_uint_t nargs)
+static int
+ngx_qjs_unhandled_rejection(ngx_js_ctx_t *ctx)
 {
-    njs_vm_t        *vm;
-    njs_int_t        ret;
-    njs_str_t        name;
-    ngx_str_t        exception;
-    njs_function_t  *func;
+    size_t                      len;
+    uint32_t                    i;
+    JSContext                  *cx;
+    const char                 *str;
+    ngx_js_rejected_promise_t  *rejected_promise;
 
-    name.start = fname->data;
-    name.length = fname->len;
+    if (ctx->rejected_promises == NULL
+        || ctx->rejected_promises->items == 0)
+    {
+        return 0;
+    }
 
-    vm = ctx->engine->u.njs.vm;
+    cx = ctx->engine->u.qjs.ctx;
+    rejected_promise = ctx->rejected_promises->start;
 
-    func = njs_vm_function(vm, &name);
-    if (func == NULL) {
-        ngx_log_error(NGX_LOG_ERR, ctx->log, 0,
-                      "js function \"%V\" not found", fname);
-        return NGX_ERROR;
+    str = JS_ToCStringLen(cx, &len, ngx_qjs_arg(rejected_promise->message));
+    if (njs_slow_path(str == NULL)) {
+        return -1;
     }
 
-    ret = njs_vm_invoke(vm, func, njs_value_arg(args), nargs,
-                        njs_value_arg(&ctx->retval));
-    if (ret == NJS_ERROR) {
-        ngx_js_exception(vm, &exception);
-
-        ngx_log_error(NGX_LOG_ERR, ctx->log, 0,
-                      "js exception: %V", &exception);
+    JS_ThrowTypeError(cx, "unhandled promise rejection: %*s", (int) len, str);
+    JS_FreeCString(cx, str);
 
-        return NGX_ERROR;
+    for (i = 0; i < ctx->rejected_promises->items; i++) {
+        JS_FreeValue(cx, ngx_qjs_arg(rejected_promise[i].promise));
+        JS_FreeValue(cx, ngx_qjs_arg(rejected_promise[i].message));
     }
 
-    for ( ;; ) {
-        ret = njs_vm_execute_pending_job(vm);
-        if (ret <= NJS_OK) {
-            if (ret == NJS_ERROR) {
-                ngx_js_exception(vm, &exception);
+    njs_arr_destroy(ctx->rejected_promises);
+    ctx->rejected_promises = NULL;
 
-                ngx_log_error(NGX_LOG_ERR, ctx->log, 0,
-                              "js job exception: %V", &exception);
-                return NGX_ERROR;
-            }
+    return 1;
+}
 
-            break;
+
+static void
+ngx_qjs_rejection_tracker(JSContext *cx, JSValueConst promise,
+    JSValueConst reason, JS_BOOL is_handled, void *opaque)
+{
+    void                       *promise_obj;
+    uint32_t                    i, length;
+    ngx_js_ctx_t               *ctx;
+    ngx_js_rejected_promise_t  *rejected_promise;
+
+    ctx = opaque;
+
+    if (is_handled && ctx->rejected_promises != NULL) {
+        rejected_promise = ctx->rejected_promises->start;
+        length = ctx->rejected_promises->items;
+
+        promise_obj = JS_VALUE_GET_PTR(promise);
+
+        for (i = 0; i < length; i++) {
+            if (JS_VALUE_GET_PTR(ngx_qjs_arg(rejected_promise[i].promise))
+                == promise_obj)
+            {
+                JS_FreeValue(cx, ngx_qjs_arg(rejected_promise[i].promise));
+                JS_FreeValue(cx, ngx_qjs_arg(rejected_promise[i].message));
+                njs_arr_remove(ctx->rejected_promises, &rejected_promise[i]);
+
+                break;
+            }
         }
+
+        return;
     }
 
-    if (ngx_js_unhandled_rejection(ctx)) {
-        ngx_js_exception(vm, &exception);
+    if (ctx->rejected_promises == NULL) {
+        if (ctx->engine == NULL) {
+            /* Do not track rejections during eval stage. The exception
+             * is lifted by the ngx_qjs_clone() function manually. */
+            return;
+        }
 
-        ngx_log_error(NGX_LOG_ERR, ctx->log, 0, "js exception: %V", &exception);
-        return NGX_ERROR;
+        ctx->rejected_promises = njs_arr_create(ctx->engine->pool, 4,
+                                            sizeof(ngx_js_rejected_promise_t));
+        if (ctx->rejected_promises == NULL) {
+            return;
+        }
     }
 
-    return njs_rbtree_is_empty(&ctx->waiting_events) ? NGX_OK : NGX_AGAIN;
+    rejected_promise = njs_arr_add(ctx->rejected_promises);
+    if (rejected_promise == NULL) {
+        return;
+    }
+
+    ngx_qjs_arg(rejected_promise->promise) = JS_DupValue(cx, promise);
+    ngx_qjs_arg(rejected_promise->message) = JS_DupValue(cx, reason);
 }
 
 
-static void *
-ngx_engine_njs_external(ngx_engine_t *engine)
+static JSModuleDef *
+ngx_qjs_core_init(JSContext *cx, const char *name)
 {
-    return njs_vm_external_ptr(engine->u.njs.vm);
-}
+    int           ret;
+    JSValue       global_obj, proto, obj;
+    JSModuleDef  *m;
 
-static ngx_int_t
-ngx_engine_njs_pending(ngx_engine_t *e)
-{
-    return njs_vm_pending(e->u.njs.vm);
-}
+    if (!JS_IsRegisteredClass(JS_GetRuntime(cx),
+                              NGX_QJS_CLASS_ID_CONSOLE))
+    {
+        if (JS_NewClass(JS_GetRuntime(cx), NGX_QJS_CLASS_ID_CONSOLE,
+                        &ngx_qjs_console_class) < 0)
+        {
+            return NULL;
+        }
 
+        proto = JS_NewObject(cx);
+        if (JS_IsException(proto)) {
+            return NULL;
+        }
 
-static ngx_int_t
-ngx_engine_njs_string(ngx_engine_t *e, njs_opaque_value_t *value,
-    ngx_str_t *str)
-{
-    ngx_int_t  rc;
-    njs_str_t  s;
+        JS_SetPropertyFunctionList(cx, proto, ngx_qjs_ext_console,
+                                   njs_nitems(ngx_qjs_ext_console));
 
-    rc = ngx_js_string(e->u.njs.vm, njs_value_arg(value), &s);
+        JS_SetClassProto(cx, NGX_QJS_CLASS_ID_CONSOLE, proto);
+    }
 
-    str->data = s.start;
-    str->len = s.length;
+    obj = JS_NewObject(cx);
+    if (JS_IsException(obj)) {
+        return NULL;
+    }
 
-    return rc;
-}
+    JS_SetPropertyFunctionList(cx, obj, ngx_qjs_ext_ngx,
+                               njs_nitems(ngx_qjs_ext_ngx));
 
+    global_obj = JS_GetGlobalObject(cx);
 
-static void
-ngx_engine_njs_destroy(ngx_engine_t *e, ngx_js_ctx_t *ctx,
-    ngx_js_loc_conf_t *conf)
-{
-    ngx_js_event_t     *event;
-    njs_rbtree_node_t  *node;
+    JS_SetPropertyFunctionList(cx, global_obj, ngx_qjs_ext_global,
+                               njs_nitems(ngx_qjs_ext_global));
 
-    if (ctx != NULL) {
-        node = njs_rbtree_min(&ctx->waiting_events);
+    ret = JS_SetPropertyStr(cx, global_obj, "ngx", obj);
+    if (ret < 0) {
+        JS_FreeValue(cx, global_obj);
+        return NULL;
+    }
 
-        while (njs_rbtree_is_there_successor(&ctx->waiting_events, node)) {
-            event = (ngx_js_event_t *) ((u_char *) node
-                                        - offsetof(ngx_js_event_t, node));
+    obj = JS_NewObjectClass(cx, NGX_QJS_CLASS_ID_CONSOLE);
+    if (JS_IsException(obj)) {
+        JS_FreeValue(cx, global_obj);
+        return NULL;
+    }
 
-            if (event->destructor != NULL) {
-                event->destructor(event);
-            }
+    JS_SetOpaque(obj, (void *) 1);
 
-            node = njs_rbtree_node_successor(&ctx->waiting_events, node);
-        }
+    ret = JS_SetPropertyStr(cx, global_obj, "console", obj);
+    if (ret < 0) {
+        JS_FreeValue(cx, global_obj);
+        return NULL;
     }
 
-    njs_vm_destroy(e->u.njs.vm);
-
-    /*
-     * when ctx !=NULL e->pool is vm pool, in such case it is destroyed
-     * by njs_vm_destroy().
-     */
+    JS_FreeValue(cx, global_obj);
 
-    if (ctx == NULL) {
-        njs_mp_destroy(e->pool);
+    m = JS_NewCModule(cx, name, NULL);
+    if (m == NULL) {
+        return NULL;
     }
+
+    return m;
 }
 
+#endif
+
 
 ngx_int_t
 ngx_js_call(njs_vm_t *vm, njs_function_t *func, njs_opaque_value_t *args,
@@ -2439,11 +3937,18 @@ ngx_js_create_conf(ngx_conf_t *cf, size_t size)
         return NULL;
     }
 
+    /*
+     * set by ngx_pcalloc():
+     *
+     *     conf->reuse_queue = NULL;
+     */
+
     conf->paths = NGX_CONF_UNSET_PTR;
     conf->type = NGX_CONF_UNSET_UINT;
     conf->imports = NGX_CONF_UNSET_PTR;
     conf->preload_objects = NGX_CONF_UNSET_PTR;
 
+    conf->reuse = NGX_CONF_UNSET_SIZE;
     conf->buffer_size = NGX_CONF_UNSET_SIZE;
     conf->max_response_body_size = NGX_CONF_UNSET_SIZE;
     conf->timeout = NGX_CONF_UNSET_MSEC;
@@ -2507,6 +4012,7 @@ ngx_js_merge_conf(ngx_conf_t *cf, void *parent, void *child,
 
     ngx_conf_merge_uint_value(conf->type, prev->type, NGX_ENGINE_NJS);
     ngx_conf_merge_msec_value(conf->timeout, prev->timeout, 60000);
+    ngx_conf_merge_size_value(conf->reuse, prev->reuse, 128);
     ngx_conf_merge_size_value(conf->buffer_size, prev->buffer_size, 16384);
     ngx_conf_merge_size_value(conf->max_response_body_size,
                               prev->max_response_body_size, 1048576);
@@ -2562,3 +4068,59 @@ ngx_js_monotonic_time(void)
     return (uint64_t) tv.tv_sec * 1000000000 + tv.tv_usec * 1000;
 #endif
 }
+
+
+ngx_js_queue_t *
+ngx_js_queue_create(ngx_pool_t *pool, ngx_uint_t capacity)
+{
+    ngx_js_queue_t  *queue;
+
+    queue = ngx_pcalloc(pool, sizeof(ngx_js_queue_t));
+    if (queue == NULL) {
+        return NULL;
+    }
+
+    queue->data = ngx_pcalloc(pool, sizeof(void *) * capacity);
+    if (queue->data == NULL) {
+        return NULL;
+    }
+
+    queue->head = 0;
+    queue->tail = 0;
+    queue->size = 0;
+    queue->capacity = capacity;
+
+    return queue;
+}
+
+
+ngx_int_t
+ngx_js_queue_push(ngx_js_queue_t *queue, void *item)
+{
+    if (queue->size >= queue->capacity) {
+        return NGX_ERROR;
+    }
+
+    queue->data[queue->tail] = item;
+    queue->tail = (queue->tail + 1) % queue->capacity;
+    queue->size++;
+
+    return NGX_OK;
+}
+
+
+void *
+ngx_js_queue_pop(ngx_js_queue_t *queue)
+{
+    void *item;
+
+    if (queue->size == 0) {
+        return NULL;
+    }
+
+    item = queue->data[queue->head];
+    queue->head = (queue->head + 1) % queue->capacity;
+    queue->size--;
+
+    return item;
+}
diff --git a/nginx/ngx_js.h b/nginx/ngx_js.h
index a3bbd541..8b6fbc85 100644
--- a/nginx/ngx_js.h
+++ b/nginx/ngx_js.h
@@ -19,8 +19,12 @@
 #include "ngx_js_fetch.h"
 #include "ngx_js_shared_dict.h"
 
+#if (NJS_HAVE_QUICKJS)
+#include <qjs.h>
+#endif
 
 #define NGX_ENGINE_NJS      1
+#define NGX_ENGINE_QJS      2
 
 #define NGX_JS_UNSET        0
 #define NGX_JS_DEPRECATED   1
@@ -37,6 +41,26 @@
 
 #define ngx_js_buffer_type(btype) ((btype) & ~NGX_JS_DEPRECATED)
 
+/*
+ * This static table solves the problem of a native QuickJS approach
+ * which uses a static variables of type JSClassID and JS_NewClassID() to
+ * allocate class ids for custom classes. The static variables approach
+ * causes a problem when two modules linked with -Wl,-Bsymbolic-functions flag
+ * are loaded dynamically.
+ */
+
+#define NGX_QJS_CLASS_ID_OFFSET (QJS_CORE_CLASS_ID_LAST)
+#define NGX_QJS_CLASS_ID_CONSOLE (NGX_QJS_CLASS_ID_OFFSET + 1)
+#define NGX_QJS_CLASS_ID_HTTP_REQUEST (NGX_QJS_CLASS_ID_OFFSET + 2)
+#define NGX_QJS_CLASS_ID_HTTP_PERIODIC (NGX_QJS_CLASS_ID_OFFSET + 3)
+#define NGX_QJS_CLASS_ID_HTTP_VARS (NGX_QJS_CLASS_ID_OFFSET + 4)
+#define NGX_QJS_CLASS_ID_HTTP_HEADERS_IN (NGX_QJS_CLASS_ID_OFFSET + 5)
+#define NGX_QJS_CLASS_ID_HTTP_HEADERS_OUT (NGX_QJS_CLASS_ID_OFFSET + 6)
+#define NGX_QJS_CLASS_ID_STREAM_SESSION (NGX_QJS_CLASS_ID_OFFSET + 7)
+#define NGX_QJS_CLASS_ID_STREAM_PERIODIC (NGX_QJS_CLASS_ID_OFFSET + 8)
+#define NGX_QJS_CLASS_ID_STREAM_FLAGS (NGX_QJS_CLASS_ID_OFFSET + 9)
+#define NGX_QJS_CLASS_ID_STREAM_VARS (NGX_QJS_CLASS_ID_OFFSET + 10)
+
 
 typedef struct ngx_js_loc_conf_s ngx_js_loc_conf_t;
 typedef struct ngx_js_event_s ngx_js_event_t;
@@ -76,6 +100,15 @@ struct ngx_js_event_s {
 };
 
 
+typedef struct {
+    void               **data;
+    ngx_uint_t           head;
+    ngx_uint_t           tail;
+    ngx_uint_t           size;
+    ngx_uint_t           capacity;
+} ngx_js_queue_t;
+
+
 #define NGX_JS_COMMON_MAIN_CONF                                               \
     ngx_js_dict_t         *dicts;                                             \
     ngx_array_t           *periodics                                          \
@@ -84,6 +117,8 @@ struct ngx_js_event_s {
 #define _NGX_JS_COMMON_LOC_CONF                                               \
     ngx_uint_t             type;                                              \
     ngx_engine_t          *engine;                                            \
+    ngx_uint_t             reuse;                                             \
+    ngx_js_queue_t        *reuse_queue;                                       \
     ngx_str_t              cwd;                                               \
     ngx_array_t           *imports;                                           \
     ngx_array_t           *paths;                                             \
@@ -157,6 +192,11 @@ struct ngx_js_ctx_s {
 };
 
 
+typedef struct {
+    void                        *external;
+} ngx_js_opaque_t;
+
+
 typedef struct ngx_engine_opts_s {
     unsigned                    engine;
     union {
@@ -164,6 +204,12 @@ typedef struct ngx_engine_opts_s {
             njs_vm_meta_t      *metas;
             njs_module_t      **addons;
         } njs;
+#if (NJS_HAVE_QUICKJS)
+        struct {
+            uintptr_t          *metas;
+            qjs_module_t      **addons;
+        } qjs;
+#endif
     } u;
 
     njs_str_t                   file;
@@ -176,11 +222,22 @@ typedef struct ngx_engine_opts_s {
 } ngx_engine_opts_t;
 
 
+typedef struct {
+    u_char                     *code;
+    size_t                      code_size;
+} ngx_js_code_entry_t;
+
+
 struct ngx_engine_s {
     union {
         struct {
             njs_vm_t           *vm;
         } njs;
+#if (NJS_HAVE_QUICKJS)
+        struct {
+            JSContext          *ctx;
+        } qjs;
+#endif
     } u;
 
     ngx_int_t                 (*compile)(ngx_js_loc_conf_t *conf, ngx_log_t *lg,
@@ -202,6 +259,7 @@ struct ngx_engine_s {
     unsigned                    type;
     const char                 *name;
     njs_mp_t                   *pool;
+    njs_arr_t                  *precompiled;
 };
 
 
@@ -246,6 +304,7 @@ void ngx_js_ctx_init(ngx_js_ctx_t *ctx, ngx_log_t *log);
 #define ngx_js_ctx_external(ctx)                                              \
     ((ctx)->engine->external(ctx->engine))
 
+
 void ngx_js_ctx_destroy(ngx_js_ctx_t *ctx, ngx_js_loc_conf_t *conf);
 ngx_int_t ngx_js_call(njs_vm_t *vm, njs_function_t *func,
     njs_opaque_value_t *args, njs_uint_t nargs);
@@ -253,6 +312,74 @@ ngx_int_t ngx_js_exception(njs_vm_t *vm, ngx_str_t *s);
 ngx_engine_t *ngx_njs_clone(ngx_js_ctx_t *ctx, ngx_js_loc_conf_t *cf,
     void *external);
 
+#if (NJS_HAVE_QUICKJS)
+
+typedef struct ngx_qjs_event_s ngx_qjs_event_t;
+
+typedef union {
+    njs_opaque_value_t opaque;
+    JSValue            value;
+} ngx_qjs_value_t;
+
+struct ngx_qjs_event_s {
+    void                *ctx;
+    JSValue              function;
+    JSValue             *args;
+    ngx_socket_t         fd;
+    NJS_RBTREE_NODE     (node);
+    njs_uint_t           nargs;
+    void               (*destructor)(ngx_qjs_event_t *event);
+    ngx_event_t          ev;
+    void                *data;
+};
+
+#define ngx_qjs_arg(val) (((ngx_qjs_value_t *) &(val))->value)
+ngx_engine_t *ngx_qjs_clone(ngx_js_ctx_t *ctx, ngx_js_loc_conf_t *cf,
+    void *external);
+void ngx_engine_qjs_destroy(ngx_engine_t *e, ngx_js_ctx_t *ctx,
+    ngx_js_loc_conf_t *conf);
+ngx_int_t ngx_qjs_call(ngx_js_ctx_t *ctx, JSValue function,
+    JSValue *argv, int argc);
+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(ngx_engine_t *e, JSValueConst val, ngx_str_t *str);
+
+#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))
+
+#define ngx_qjs_meta(cx, i)                                                  \
+    ((uintptr_t *) JS_GetRuntimeOpaque(JS_GetRuntime(cx)))[i]
+#define ngx_qjs_external_connection(cx, e)                                   \
+    (*((ngx_connection_t **) ((u_char *) (e) + ngx_qjs_meta(cx, 0))))
+#define ngx_qjs_external_pool(cx, e)                                         \
+    ((ngx_external_pool_pt) ngx_qjs_meta(cx, 1))(e)
+#define ngx_qjs_external_resolver(cx, e)                                     \
+    ((ngx_external_resolver_pt) ngx_qjs_meta(vm, 2))(e)
+#define ngx_qjs_external_resolver_timeout(cx, e)                             \
+    ((ngx_external_timeout_pt) ngx_qjs_meta(cx, 3))(e)
+#define ngx_qjs_external_event_finalize(cx)                                  \
+    ((ngx_js_event_finalize_pt) ngx_qjs_meta(cx, 4))
+#define ngx_qjs_external_ssl(cx, e)                                          \
+    ((ngx_external_ssl_pt) ngx_qjs_meta(cx, 5))(e)
+#define ngx_qjs_external_ssl_verify(cx, e)                                   \
+    ((ngx_external_flag_pt) ngx_qjs_meta(cx, 6))(e)
+#define ngx_qjs_external_fetch_timeout(cx, e)                                \
+    ((ngx_external_timeout_pt) ngx_qjs_meta(cx, 7))(e)
+#define ngx_qjs_external_buffer_size(cx, e)                                  \
+    ((ngx_external_size_pt) ngx_qjs_meta(cx, 8))(e)
+#define ngx_qjs_external_max_response_buffer_size(cx, e)                     \
+    ((ngx_external_size_pt) ngx_qjs_meta(cx, 9))(e)
+#define ngx_qjs_main_conf(cx)                                                \
+    ((ngx_js_main_conf_t *) ngx_qjs_meta(cx, NGX_JS_MAIN_CONF_INDEX))
+#define ngx_qjs_external_ctx(cx, e)                                          \
+    ((ngx_js_external_ctx_pt) ngx_qjs_meta(cx, 11))(e)
+
+extern qjs_module_t  qjs_zlib_module;
+extern qjs_module_t  ngx_qjs_ngx_module;
+
+#endif
+
 njs_int_t ngx_js_ext_log(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
     njs_index_t level, njs_value_t *retval);
 void ngx_js_log(njs_vm_t *vm, njs_external_ptr_t external,
@@ -286,6 +413,10 @@ njs_int_t ngx_js_ext_flags(njs_vm_t *vm, njs_object_prop_t *prop,
 ngx_int_t ngx_js_string(njs_vm_t *vm, njs_value_t *value, njs_str_t *str);
 ngx_int_t ngx_js_integer(njs_vm_t *vm, njs_value_t *value, ngx_int_t *n);
 
+ngx_js_queue_t *ngx_js_queue_create(ngx_pool_t *pool, ngx_uint_t capacity);
+ngx_int_t ngx_js_queue_push(ngx_js_queue_t *queue, void *item);
+void *ngx_js_queue_pop(ngx_js_queue_t *queue);
+
 
 extern njs_module_t  ngx_js_ngx_module;
 extern njs_module_t  njs_webcrypto_module;
diff --git a/nginx/ngx_stream_js_module.c b/nginx/ngx_stream_js_module.c
index 565f4e66..98427aae 100644
--- a/nginx/ngx_stream_js_module.c
+++ b/nginx/ngx_stream_js_module.c
@@ -75,6 +75,21 @@ struct ngx_stream_js_ctx_s {
 };
 
 
+#if (NJS_HAVE_QUICKJS)
+
+typedef struct {
+    ngx_str_t               name;
+    ngx_uint_t              data_type;
+    ngx_uint_t              id;
+} ngx_stream_qjs_event_t;
+
+typedef struct {
+    ngx_stream_session_t   *session;
+    JSValue                 callbacks[NGX_JS_EVENT_MAX];
+} ngx_stream_qjs_session_t;
+
+#endif
+
 #define ngx_stream_pending(ctx)                                               \
     (ngx_js_ctx_pending(ctx) || ngx_stream_js_pending_events(ctx))
 
@@ -128,6 +143,57 @@ static njs_int_t ngx_stream_js_periodic_variables(njs_vm_t *vm,
     njs_object_prop_t *prop, njs_value_t *value, njs_value_t *setval,
     njs_value_t *retval);
 
+#if (NJS_HAVE_QUICKJS)
+
+static JSValue ngx_stream_qjs_ext_to_string_tag(JSContext *cx,
+    JSValueConst this_val);
+static JSValue ngx_stream_qjs_ext_done(JSContext *cx, JSValueConst this_val,
+    int argc, JSValueConst *argv, int magic);
+static JSValue ngx_stream_qjs_ext_log(JSContext *cx, JSValueConst this_val,
+    int argc, JSValueConst *argv, int level);
+static JSValue ngx_stream_qjs_ext_on(JSContext *cx, JSValueConst this_val,
+    int argc, JSValueConst *argv);
+static JSValue ngx_stream_qjs_ext_off(JSContext *cx, JSValueConst this_val,
+    int argc, JSValueConst *argv);
+static JSValue ngx_stream_qjs_ext_periodic_to_string_tag(JSContext *cx,
+    JSValueConst this_val);
+static JSValue ngx_stream_qjs_ext_periodic_variables(JSContext *cx,
+    JSValueConst this_val, int type);
+static JSValue ngx_stream_qjs_ext_remote_address(JSContext *cx,
+    JSValueConst this_val);
+static JSValue ngx_stream_qjs_ext_send(JSContext *cx, JSValueConst this_val,
+    int argc, JSValueConst *argv, int from_upstream);
+static JSValue ngx_stream_qjs_ext_set_return_value(JSContext *cx,
+    JSValueConst this_val, int argc, JSValueConst *argv);
+static JSValue ngx_stream_qjs_ext_variables(JSContext *cx,
+    JSValueConst this_val, int type);
+static JSValue ngx_stream_qjs_ext_uint(JSContext *cx, JSValueConst this_val,
+    int offset);
+static JSValue ngx_stream_qjs_ext_flag(JSContext *cx, JSValueConst this_val,
+    int mask);
+
+static int ngx_stream_qjs_variables_own_property(JSContext *cx,
+    JSPropertyDescriptor *pdesc, JSValueConst obj, JSAtom prop);
+static int ngx_stream_qjs_variables_set_property(JSContext *cx,
+    JSValueConst obj, JSAtom atom, JSValueConst value, JSValueConst receiver,
+    int flags);
+static int ngx_stream_qjs_variables_define_own_property(JSContext *cx,
+    JSValueConst obj, JSAtom prop, JSValueConst value, JSValueConst getter,
+    JSValueConst setter, int flags);
+
+static ngx_int_t ngx_stream_qjs_run_event(ngx_stream_session_t *s,
+    ngx_stream_js_ctx_t *ctx, ngx_stream_js_ev_t *event,
+    ngx_uint_t from_upstream);
+static ngx_int_t ngx_stream_qjs_body_filter(ngx_stream_session_t *s,
+    ngx_stream_js_ctx_t *ctx, ngx_chain_t *in, ngx_uint_t from_upstream);
+
+static ngx_stream_session_t *ngx_stream_qjs_session(JSValueConst val);
+static JSValue ngx_stream_qjs_session_make(JSContext *cx, ngx_int_t proto_id,
+    ngx_stream_session_t *s);
+static void ngx_stream_qjs_session_finalizer(JSRuntime *rt, JSValue val);
+
+#endif
+
 static ngx_pool_t *ngx_stream_js_pool(ngx_stream_session_t *s);
 static ngx_resolver_t *ngx_stream_js_resolver(ngx_stream_session_t *s);
 static ngx_msec_t ngx_stream_js_resolver_timeout(ngx_stream_session_t *s);
@@ -167,6 +233,9 @@ static ngx_flag_t ngx_stream_js_ssl_verify(ngx_stream_session_t *s);
 
 static ngx_conf_bitmask_t  ngx_stream_js_engines[] = {
     { ngx_string("njs"), NGX_ENGINE_NJS },
+#if (NJS_HAVE_QUICKJS)
+    { ngx_string("qjs"), NGX_ENGINE_QJS },
+#endif
     { ngx_null_string, 0 }
 };
 
@@ -191,6 +260,13 @@ static ngx_command_t  ngx_stream_js_commands[] = {
       offsetof(ngx_stream_js_srv_conf_t, type),
       &ngx_stream_js_engines },
 
+    { ngx_string("js_context_reuse"),
+      NGX_STREAM_MAIN_CONF|NGX_STREAM_SRV_CONF|NGX_CONF_TAKE1,
+      ngx_conf_set_size_slot,
+      NGX_STREAM_SRV_CONF_OFFSET,
+      offsetof(ngx_stream_js_srv_conf_t, reuse),
+      NULL },
+
     { ngx_string("js_import"),
       NGX_STREAM_MAIN_CONF|NGX_STREAM_SRV_CONF|NGX_CONF_TAKE13,
       ngx_js_import,
@@ -649,9 +725,9 @@ static njs_vm_meta_t ngx_stream_js_metas = {
 static ngx_stream_filter_pt  ngx_stream_next_filter;
 
 
-static njs_int_t    ngx_stream_js_session_proto_id;
-static njs_int_t    ngx_stream_js_periodic_session_proto_id;
-static njs_int_t    ngx_stream_js_session_flags_proto_id;
+static njs_int_t    ngx_stream_js_session_proto_id = 1;
+static njs_int_t    ngx_stream_js_periodic_session_proto_id  = 2;
+static njs_int_t    ngx_stream_js_session_flags_proto_id = 3;
 
 
 njs_module_t  ngx_js_stream_module = {
@@ -682,6 +758,96 @@ njs_module_t *njs_stream_js_addon_modules[] = {
     NULL,
 };
 
+#if (NJS_HAVE_QUICKJS)
+
+static const JSCFunctionListEntry ngx_stream_qjs_ext_session[] = {
+    JS_CGETSET_DEF("[Symbol.toStringTag]", ngx_stream_qjs_ext_to_string_tag,
+                   NULL),
+    JS_CFUNC_MAGIC_DEF("allow", 1, ngx_stream_qjs_ext_done, NGX_OK),
+    JS_CFUNC_MAGIC_DEF("decline", 1, ngx_stream_qjs_ext_done, -NGX_DECLINED),
+    JS_CFUNC_MAGIC_DEF("deny", 1, ngx_stream_qjs_ext_done, -NGX_DONE),
+    JS_CFUNC_MAGIC_DEF("done", 1, ngx_stream_qjs_ext_done, NGX_OK),
+    JS_CFUNC_MAGIC_DEF("error", 1, ngx_stream_qjs_ext_log, NGX_LOG_ERR),
+    JS_CFUNC_MAGIC_DEF("log", 1, ngx_stream_qjs_ext_log, NGX_LOG_INFO),
+    JS_CFUNC_DEF("on", 2, ngx_stream_qjs_ext_on),
+    JS_CFUNC_DEF("off", 1, ngx_stream_qjs_ext_off),
+    JS_CGETSET_MAGIC_DEF("rawVariables", ngx_stream_qjs_ext_variables,
+                   NULL, NGX_JS_BUFFER),
+    JS_CGETSET_DEF("remoteAddress", ngx_stream_qjs_ext_remote_address, NULL),
+    JS_CFUNC_MAGIC_DEF("send", 2, ngx_stream_qjs_ext_send, NGX_JS_BOOL_UNSET),
+    JS_CFUNC_MAGIC_DEF("sendDownstream", 1, ngx_stream_qjs_ext_send,
+                       NGX_JS_BOOL_TRUE),
+    JS_CFUNC_MAGIC_DEF("sendUpstream", 1, ngx_stream_qjs_ext_send,
+                       NGX_JS_BOOL_FALSE),
+    JS_CFUNC_DEF("setReturnValue", 1, ngx_stream_qjs_ext_set_return_value),
+    JS_CGETSET_MAGIC_DEF("status", ngx_stream_qjs_ext_uint, NULL,
+                         offsetof(ngx_stream_session_t, status)),
+    JS_CGETSET_MAGIC_DEF("variables", ngx_stream_qjs_ext_variables,
+                         NULL, NGX_JS_STRING),
+    JS_CFUNC_MAGIC_DEF("warn", 1, ngx_stream_qjs_ext_log, NGX_LOG_WARN),
+};
+
+
+static const JSCFunctionListEntry ngx_stream_qjs_ext_periodic[] = {
+    JS_CGETSET_DEF("[Symbol.toStringTag]",
+                   ngx_stream_qjs_ext_periodic_to_string_tag, NULL),
+    JS_CGETSET_MAGIC_DEF("rawVariables", ngx_stream_qjs_ext_periodic_variables,
+                   NULL, NGX_JS_BUFFER),
+    JS_CGETSET_MAGIC_DEF("variables", ngx_stream_qjs_ext_periodic_variables,
+                         NULL, NGX_JS_STRING),
+};
+
+
+static const JSCFunctionListEntry ngx_stream_qjs_ext_flags[] = {
+    JS_CGETSET_MAGIC_DEF("from_upstream", ngx_stream_qjs_ext_flag, NULL,
+                         2),
+    JS_CGETSET_MAGIC_DEF("last", ngx_stream_qjs_ext_flag, NULL, 1),
+};
+
+
+static JSClassDef ngx_stream_qjs_session_class = {
+    "Session",
+    .finalizer = ngx_stream_qjs_session_finalizer,
+};
+
+
+static JSClassDef ngx_stream_qjs_periodic_class = {
+    "Periodic",
+    .finalizer = NULL,
+};
+
+
+static JSClassDef ngx_stream_qjs_flags_class = {
+    "Stream Flags",
+    .finalizer = NULL,
+};
+
+
+static JSClassDef ngx_stream_qjs_variables_class = {
+    "Variables",
+    .finalizer = NULL,
+    .exotic = & (JSClassExoticMethods) {
+        .get_own_property = ngx_stream_qjs_variables_own_property,
+        .set_property = ngx_stream_qjs_variables_set_property,
+        .define_own_property = ngx_stream_qjs_variables_define_own_property,
+    },
+};
+
+
+qjs_module_t *njs_stream_qjs_addon_modules[] = {
+    &ngx_qjs_ngx_module,
+    /*
+     * Shared addons should be in the same order and the same positions
+     * in all nginx modules.
+     */
+#ifdef NJS_HAVE_ZLIB
+    &qjs_zlib_module,
+#endif
+    NULL,
+};
+
+#endif
+
 
 static ngx_int_t
 ngx_stream_js_access_handler(ngx_stream_session_t *s)
@@ -783,7 +949,6 @@ ngx_stream_js_body_filter(ngx_stream_session_t *s, ngx_chain_t *in,
 {
     ngx_int_t                  rc;
     ngx_chain_t               *out;
-    ngx_connection_t          *c;
     ngx_stream_js_ctx_t       *ctx;
     ngx_stream_js_srv_conf_t  *jscf;
 
@@ -792,10 +957,8 @@ ngx_stream_js_body_filter(ngx_stream_session_t *s, ngx_chain_t *in,
         return ngx_stream_next_filter(s, in, from_upstream);
     }
 
-    c = s->connection;
-
-    ngx_log_debug1(NGX_LOG_DEBUG_STREAM, c->log, 0, "stream js filter u:%ui",
-                   from_upstream);
+    ngx_log_debug1(NGX_LOG_DEBUG_STREAM, s->connection->log, 0,
+                   "stream js filter u:%ui", from_upstream);
 
     rc = ngx_stream_js_init_vm(s, ngx_stream_js_session_proto_id);
 
@@ -810,7 +973,7 @@ ngx_stream_js_body_filter(ngx_stream_session_t *s, ngx_chain_t *in,
     ctx = ngx_stream_get_module_ctx(s, ngx_stream_js_module);
 
     if (!ctx->filter) {
-        ngx_log_debug1(NGX_LOG_DEBUG_STREAM, c->log, 0,
+        ngx_log_debug1(NGX_LOG_DEBUG_STREAM, s->connection->log, 0,
                        "stream js filter call \"%V\"" , &jscf->filter);
 
         rc = ctx->engine->call((ngx_js_ctx_t *) ctx, &jscf->filter,
@@ -990,9 +1153,9 @@ ngx_stream_js_init_vm(ngx_stream_session_t *s, njs_int_t proto_id)
         return NGX_ERROR;
     }
 
-    ngx_log_debug2(NGX_LOG_DEBUG_STREAM, ctx->log, 0,
-                   "stream js vm clone: %p from: %p", ctx->engine,
-                   jscf->engine);
+    ngx_log_debug3(NGX_LOG_DEBUG_STREAM, ctx->log, 0,
+                   "stream js vm clone %s: %p from: %p", jscf->engine->name,
+                   ctx->engine, jscf->engine);
 
     cln = ngx_pool_cleanup_add(s->connection->pool, 0);
     if (cln == NULL) {
@@ -1806,25 +1969,950 @@ ngx_engine_njs_clone(ngx_js_ctx_t *ctx, ngx_js_loc_conf_t *cf,
 }
 
 
-static ngx_int_t
-ngx_stream_js_init_conf_vm(ngx_conf_t *cf, ngx_js_loc_conf_t *conf)
+#if (NJS_HAVE_QUICKJS)
+
+static JSValue
+ngx_stream_qjs_ext_to_string_tag(JSContext *cx, JSValueConst this_val)
 {
-    ngx_engine_opts_t    options;
-    ngx_js_main_conf_t  *jmcf;
+    return JS_NewString(cx, "Stream Session");
+}
 
-    memset(&options, 0, sizeof(ngx_engine_opts_t));
 
-    options.engine = conf->type;
+static JSValue
+ngx_stream_qjs_ext_done(JSContext *cx, JSValueConst this_val, int argc,
+    JSValueConst *argv, int magic)
+{
+    ngx_int_t              status;
+    ngx_stream_js_ctx_t   *ctx;
+    ngx_stream_session_t  *s;
 
-    if (conf->type == NGX_ENGINE_NJS) {
-        jmcf = ngx_stream_conf_get_module_main_conf(cf, ngx_stream_js_module);
-        ngx_stream_js_uptr[NGX_JS_MAIN_CONF_INDEX] = (uintptr_t) jmcf;
+    s = ngx_stream_qjs_session(this_val);
+    if (s == NULL) {
+        return JS_ThrowInternalError(cx, "\"this\" is not a session object");
+    }
 
-        options.u.njs.metas = &ngx_stream_js_metas;
-        options.u.njs.addons = njs_stream_js_addon_modules;
-        options.clone = ngx_engine_njs_clone;
+    status = (ngx_int_t) magic;
+    status = -status;
+
+    if (status == NGX_DONE) {
+        status = NGX_STREAM_FORBIDDEN;
+    }
+
+    if (!JS_IsUndefined(argv[0])) {
+        if (ngx_qjs_integer(cx, argv[0], &status) != NGX_OK) {
+            return JS_EXCEPTION;
+        }
+
+        if (status < NGX_ABORT || status > NGX_STREAM_SERVICE_UNAVAILABLE) {
+            return JS_ThrowInternalError(cx, "code is out of range");
+        }
+    }
+
+    ctx = ngx_stream_get_module_ctx(s, ngx_stream_js_module);
+
+    if (ctx->filter) {
+        return JS_ThrowInternalError(cx, "should not be called while "
+                                     "filtering");
+    }
+
+    ngx_log_debug1(NGX_LOG_DEBUG_STREAM, s->connection->log, 0,
+                   "stream js set status: %i", status);
+
+    ctx->status = status;
+
+    ngx_stream_js_drop_events(ctx);
+
+    return JS_UNDEFINED;
+}
+
+
+static JSValue
+ngx_stream_qjs_ext_log(JSContext *cx, JSValueConst this_val, int argc,
+    JSValueConst *argv, int level)
+{
+    int                    n;
+    const char            *msg;
+    ngx_stream_session_t  *s;
+
+    s = ngx_stream_qjs_session(this_val);
+    if (s == NULL) {
+        return JS_ThrowInternalError(cx, "\"this\" is not a session object");
+    }
+
+    for (n = 0; n < argc; n++) {
+        msg = JS_ToCString(cx, argv[n]);
+
+        ngx_js_logger(s->connection, level, (u_char *) msg, ngx_strlen(msg));
+
+        JS_FreeCString(cx, msg);
+    }
+
+    return JS_UNDEFINED;
+}
+
+
+static const ngx_stream_qjs_event_t *
+ngx_stream_qjs_event(ngx_stream_session_t *s, JSContext *cx, ngx_str_t *event)
+{
+    ngx_uint_t            i, n, type;
+    ngx_stream_js_ctx_t  *ctx;
+
+    static const ngx_stream_qjs_event_t events[] = {
+        {
+            ngx_string("upload"),
+            NGX_JS_STRING,
+            NGX_JS_EVENT_UPLOAD,
+        },
+
+        {
+            ngx_string("download"),
+            NGX_JS_STRING,
+            NGX_JS_EVENT_DOWNLOAD,
+        },
+
+        {
+            ngx_string("upstream"),
+            NGX_JS_BUFFER,
+            NGX_JS_EVENT_UPLOAD,
+        },
+
+        {
+            ngx_string("downstream"),
+            NGX_JS_BUFFER,
+            NGX_JS_EVENT_DOWNLOAD,
+        },
+    };
+
+    ctx = ngx_stream_get_module_ctx(s, ngx_stream_js_module);
+
+    i = 0;
+    n = sizeof(events) / sizeof(events[0]);
+
+    while (i < n) {
+        if (event->len == events[i].name.len
+            && ngx_memcmp(event->data, events[i].name.data, event->len)
+               == 0)
+        {
+            break;
+        }
+
+        i++;
+    }
+
+    if (i == n) {
+        (void) JS_ThrowInternalError(cx, "unknown event \"%*s\"",
+                                     (int) event->len, event->data);
+        return NULL;
+    }
+
+    ctx->events[events[i].id].data_type = events[i].data_type;
+
+    for (n = 0; n < NGX_JS_EVENT_MAX; n++) {
+        type = ctx->events[n].data_type;
+        if (type != NGX_JS_UNSET && type != events[i].data_type) {
+            (void) JS_ThrowInternalError(cx, "mixing string and buffer"
+                                         " events is not allowed");
+            return NULL;
+        }
+    }
+
+    return &events[i];
+}
+
+
+static JSValue
+ngx_stream_qjs_ext_on(JSContext *cx, JSValueConst this_val, int argc,
+    JSValueConst *argv)
+{
+    ngx_str_t                      name;
+    ngx_stream_js_ctx_t           *ctx;
+    ngx_stream_qjs_session_t      *ses;
+    const ngx_stream_qjs_event_t  *e;
+
+    ses = JS_GetOpaque(this_val, NGX_QJS_CLASS_ID_STREAM_SESSION);
+    if (ses == NULL) {
+        return JS_ThrowInternalError(cx, "\"this\" is not a session object");
+    }
+
+    ctx = ngx_stream_get_module_ctx(ses->session, ngx_stream_js_module);
+
+    if (ngx_qjs_string(ctx->engine, argv[0], &name) != NGX_OK) {
+        return JS_EXCEPTION;
+    }
+
+    e = ngx_stream_qjs_event(ses->session, cx, &name);
+    if (e == NULL) {
+        return JS_EXCEPTION;
+    }
+
+    if (JS_IsFunction(cx, ngx_qjs_arg(ctx->events[e->id].function))) {
+        return JS_ThrowInternalError(cx, "event handler \"%s\" is already set",
+                                     name.data);
+    }
+
+    if (!JS_IsFunction(cx, argv[1])) {
+        return JS_ThrowTypeError(cx, "callback is not a function");
+    }
+
+    ngx_qjs_arg(ctx->events[e->id].function) = argv[1];
+
+    JS_FreeValue(cx, ses->callbacks[e->id]);
+    ses->callbacks[e->id] = JS_DupValue(cx, argv[1]);
+
+    return JS_UNDEFINED;
+}
+
+
+static JSValue
+ngx_stream_qjs_ext_off(JSContext *cx, JSValueConst this_val, int argc,
+    JSValueConst *argv)
+{
+    ngx_str_t                      name;
+    ngx_stream_js_ctx_t           *ctx;
+    ngx_stream_session_t          *s;
+    const ngx_stream_qjs_event_t  *e;
+
+    s = ngx_stream_qjs_session(this_val);
+    if (s == NULL) {
+        return JS_ThrowInternalError(cx, "\"this\" is not a session object");
+    }
+
+    ctx = ngx_stream_get_module_ctx(s, ngx_stream_js_module);
+
+    if (ngx_qjs_string(ctx->engine, argv[0], &name) != NGX_OK) {
+        return JS_EXCEPTION;
+    }
+
+    e = ngx_stream_qjs_event(s, cx, &name);
+    if (e == NULL) {
+        return JS_EXCEPTION;
+    }
+
+    ngx_qjs_arg(ctx->events[e->id].function) = JS_NULL;
+    ctx->events[e->id].data_type = NGX_JS_UNSET;
+
+    return JS_UNDEFINED;
+}
+
+
+static JSValue
+ngx_stream_qjs_ext_periodic_to_string_tag(JSContext *cx,
+    JSValueConst this_val)
+{
+    return JS_NewString(cx, "PeriodicSession");
+}
+
+
+static JSValue
+ngx_stream_qjs_ext_periodic_variables(JSContext *cx,
+    JSValueConst this_val, int type)
+{
+    JSValue                    obj;
+    ngx_stream_qjs_session_t  *ses;
+
+    ses = JS_GetOpaque(this_val, NGX_QJS_CLASS_ID_STREAM_PERIODIC);
+    if (ses == NULL) {
+        return JS_ThrowInternalError(cx, "\"this\" is not a periodic object");
+    }
+
+    obj = JS_NewObjectProtoClass(cx, JS_NULL, NGX_QJS_CLASS_ID_STREAM_VARS);
+
+    /*
+     * Using lowest bit of the pointer to store the buffer type.
+     */
+    type = (type == NGX_JS_BUFFER) ? 1 : 0;
+    JS_SetOpaque(obj, (void *) ((uintptr_t) ses->session | (uintptr_t) type));
+
+    return obj;
+}
+
+
+static JSValue
+ngx_stream_qjs_ext_remote_address(JSContext *cx, JSValueConst this_val)
+{
+    ngx_connection_t      *c;
+    ngx_stream_session_t  *s;
+
+    s = ngx_stream_qjs_session(this_val);
+    if (s == NULL) {
+        return JS_ThrowInternalError(cx, "\"this\" is not a session object");
+    }
+
+    c = s->connection;
+
+    return qjs_string_create(cx, c->addr_text.data, c->addr_text.len);
+}
+
+
+static JSValue
+ngx_stream_qjs_ext_send(JSContext *cx, JSValueConst this_val, int argc,
+    JSValueConst *argv, int from_upstream)
+{
+    JSValue                val;
+    unsigned               last_buf, flush;
+    ngx_str_t              buffer;
+    ngx_buf_t             *b;
+    ngx_chain_t           *cl;
+    ngx_connection_t      *c;
+    ngx_stream_js_ctx_t   *ctx;
+    ngx_stream_session_t  *s;
+
+    s = ngx_stream_qjs_session(this_val);
+    if (s == NULL) {
+        return JS_ThrowInternalError(cx, "\"this\" is not a session object");
+    }
+
+    c = s->connection;
+
+    ctx = ngx_stream_get_module_ctx(s, ngx_stream_js_module);
+
+    if (!ctx->filter) {
+        return JS_ThrowInternalError(cx, "cannot send buffer in this handler");
+    }
+
+    if (ngx_qjs_string(ctx->engine, argv[0], &buffer) != NGX_OK) {
+        return JS_EXCEPTION;
     }
 
+    /*
+     * ctx->buf != NULL when s.send() is called while processing incoming
+     * data chunks, otherwise s.send() is called asynchronously
+     */
+
+    if (ctx->buf != NULL) {
+        flush = ctx->buf->flush;
+        last_buf = ctx->buf->last_buf;
+
+    } else {
+        flush = 0;
+        last_buf = 0;
+    }
+
+    if (JS_IsObject(argv[1])) {
+        val = JS_GetPropertyStr(cx, argv[1], "flush");
+        if (JS_IsException(val)) {
+            return JS_EXCEPTION;
+        }
+
+        if (!JS_IsUndefined(val)) {
+            flush = JS_ToBool(cx, val);
+            JS_FreeValue(cx, val);
+        }
+
+        val = JS_GetPropertyStr(cx, argv[1], "last");
+        if (JS_IsException(val)) {
+            return JS_EXCEPTION;
+        }
+
+        if (!JS_IsUndefined(val)) {
+            last_buf = JS_ToBool(cx, val);
+            JS_FreeValue(cx, val);
+        }
+
+        if (from_upstream == NGX_JS_BOOL_UNSET) {
+            val = JS_GetPropertyStr(cx, argv[1], "from_upstream");
+            if (JS_IsException(val)) {
+                return JS_EXCEPTION;
+            }
+
+            if (!JS_IsUndefined(val)) {
+                from_upstream = JS_ToBool(cx, val);
+                JS_FreeValue(cx, val);
+            }
+
+            if (from_upstream == NGX_JS_BOOL_UNSET && ctx->buf == NULL) {
+                return JS_ThrowInternalError(cx, "from_upstream flag is "
+                                             "expected when called "
+                                             "asynchronously");
+            }
+        }
+    }
+
+    cl = ngx_chain_get_free_buf(c->pool, &ctx->free);
+    if (cl == NULL) {
+        return JS_ThrowInternalError(cx, "memory error");
+    }
+
+    b = cl->buf;
+
+    b->flush = flush;
+    b->last_buf = last_buf;
+
+    b->memory = (buffer.len ? 1 : 0);
+    b->sync = (buffer.len ? 0 : 1);
+    b->tag = (ngx_buf_tag_t) &ngx_stream_js_module;
+
+    b->start = buffer.data;
+    b->end = buffer.data + buffer.len;
+
+    b->pos = b->start;
+    b->last = b->end;
+
+    if (from_upstream == NGX_JS_BOOL_UNSET) {
+        *ctx->last_out = cl;
+        ctx->last_out = &cl->next;
+
+    } else {
+
+        if (ngx_stream_js_next_filter(s, ctx, cl, from_upstream) == NGX_ERROR) {
+            return JS_ThrowInternalError(cx, "ngx_stream_js_next_filter() "
+                                         "failed");
+        }
+    }
+
+    return JS_UNDEFINED;
+}
+
+
+static JSValue
+ngx_stream_qjs_ext_set_return_value(JSContext *cx, JSValueConst this_val,
+    int argc, JSValueConst *argv)
+{
+    ngx_js_ctx_t          *ctx;
+    ngx_stream_session_t  *s;
+
+    s = ngx_stream_qjs_session(this_val);
+    if (s == NULL) {
+        return JS_ThrowInternalError(cx, "\"this\" is not a session object");
+    }
+
+    ctx = ngx_stream_get_module_ctx(s, ngx_stream_js_module);
+
+    JS_FreeValue(cx, ngx_qjs_arg(ctx->retval));
+    ngx_qjs_arg(ctx->retval) = JS_DupValue(cx, argv[0]);
+
+    return JS_UNDEFINED;
+}
+
+
+static JSValue
+ngx_stream_qjs_ext_variables(JSContext *cx, JSValueConst this_val, int type)
+{
+    JSValue                obj;
+    ngx_stream_session_t  *s;
+
+    s = ngx_stream_qjs_session(this_val);
+    if (s == NULL) {
+        return JS_ThrowInternalError(cx, "\"this\" is not a session object");
+    }
+
+    obj = JS_NewObjectProtoClass(cx, JS_NULL, NGX_QJS_CLASS_ID_STREAM_VARS);
+
+    /*
+     * Using lowest bit of the pointer to store the buffer type.
+     */
+    type = (type == NGX_JS_BUFFER) ? 1 : 0;
+    JS_SetOpaque(obj, (void *) ((uintptr_t) s | (uintptr_t) type));
+
+    return obj;
+}
+
+
+static JSValue
+ngx_stream_qjs_ext_uint(JSContext *cx, JSValueConst this_val, int offset)
+{
+    ngx_uint_t            *field;
+    ngx_stream_session_t  *s;
+
+    s = ngx_stream_qjs_session(this_val);
+    if (s == NULL) {
+        return JS_ThrowInternalError(cx, "\"this\" is not a session object");
+    }
+
+    field = (ngx_uint_t *) ((u_char *) s + offset);
+
+    return JS_NewUint32(cx, *field);
+}
+
+
+static JSValue
+ngx_stream_qjs_ext_flag(JSContext *cx, JSValueConst this_val, int mask)
+{
+    uintptr_t  flags;
+
+    flags = (uintptr_t) JS_GetOpaque(this_val, NGX_QJS_CLASS_ID_STREAM_FLAGS);
+
+    return JS_NewBool(cx, flags & mask);
+}
+
+
+static int
+ngx_stream_qjs_variables_own_property(JSContext *cx,
+    JSPropertyDescriptor *pdesc, JSValueConst obj, JSAtom prop)
+{
+    uint32_t                      buffer_type;
+    ngx_str_t                     name;
+    ngx_uint_t                    key;
+    ngx_stream_session_t         *s;
+    ngx_stream_variable_value_t  *vv;
+
+    s = JS_GetOpaque(obj, NGX_QJS_CLASS_ID_STREAM_VARS);
+
+    buffer_type = ((uintptr_t) s & 1) ? NGX_JS_BUFFER : NGX_JS_STRING;
+    s = (ngx_stream_session_t *) ((uintptr_t) s & ~(uintptr_t) 1);
+
+    if (s == NULL) {
+        (void) JS_ThrowInternalError(cx, "\"this\" is not a session object");
+        return -1;
+    }
+
+    name.data = (u_char *) JS_AtomToCString(cx, prop);
+    if (name.data == NULL) {
+        return -1;
+    }
+
+    name.len = ngx_strlen(name.data);
+
+    key = ngx_hash_strlow(name.data, name.data, name.len);
+
+    vv = ngx_stream_get_variable(s, &name, key);
+    JS_FreeCString(cx, (char *) name.data);
+    if (vv == NULL || vv->not_found) {
+        return 0;
+    }
+
+    if (pdesc != NULL) {
+        pdesc->flags = JS_PROP_WRITABLE | JS_PROP_CONFIGURABLE;
+        pdesc->getter = JS_UNDEFINED;
+        pdesc->setter = JS_UNDEFINED;
+        pdesc->value = ngx_qjs_prop(cx, buffer_type, vv->data, vv->len);
+    }
+
+    return 1;
+}
+
+
+static int
+ngx_stream_qjs_variables_set_property(JSContext *cx, JSValueConst obj,
+    JSAtom prop, JSValueConst value, JSValueConst receiver, int flags)
+{
+    ngx_str_t                     name, val;
+    ngx_uint_t                    key;
+    ngx_js_ctx_t                 *ctx;
+    ngx_stream_session_t         *s;
+    ngx_stream_variable_t        *v;
+    ngx_stream_variable_value_t  *vv;
+    ngx_stream_core_main_conf_t  *cmcf;
+
+    s = JS_GetOpaque(obj, NGX_QJS_CLASS_ID_STREAM_VARS);
+
+    s = (ngx_stream_session_t *) ((uintptr_t) s & ~(uintptr_t) 1);
+
+    if (s == NULL) {
+        (void) JS_ThrowInternalError(cx, "\"this\" is not a session object");
+        return -1;
+    }
+
+    name.data = (u_char *) JS_AtomToCString(cx, prop);
+    if (name.data == NULL) {
+        return -1;
+    }
+
+    name.len = ngx_strlen(name.data);
+
+    key = ngx_hash_strlow(name.data, name.data, name.len);
+
+    cmcf = ngx_stream_get_module_main_conf(s, ngx_stream_core_module);
+
+    v = ngx_hash_find(&cmcf->variables_hash, key, name.data, name.len);
+    JS_FreeCString(cx, (char *) name.data);
+
+    if (v == NULL) {
+        (void) JS_ThrowInternalError(cx, "variable not found");
+        return -1;
+    }
+
+    ctx = ngx_stream_get_module_ctx(s, ngx_stream_js_module);
+
+    if (ngx_qjs_string(ctx->engine, value, &val) != NGX_OK) {
+        return -1;
+    }
+
+    if (v->set_handler != NULL) {
+        vv = ngx_pcalloc(s->connection->pool,
+                         sizeof(ngx_stream_variable_value_t));
+        if (vv == NULL) {
+            (void) JS_ThrowOutOfMemory(cx);
+            return -1;
+        }
+
+        vv->valid = 1;
+        vv->not_found = 0;
+        vv->data = val.data;
+        vv->len = val.len;
+
+        v->set_handler(s, vv, v->data);
+
+        return 1;
+    }
+
+    if (!(v->flags & NGX_STREAM_VAR_INDEXED)) {
+        (void) JS_ThrowTypeError(cx, "variable is not writable");
+        return -1;
+    }
+
+    vv = &s->variables[v->index];
+
+    vv->valid = 1;
+    vv->not_found = 0;
+
+    vv->data = ngx_pnalloc(s->connection->pool, val.len);
+    if (vv->data == NULL) {
+        vv->valid = 0;
+        (void) JS_ThrowOutOfMemory(cx);
+        return -1;
+    }
+
+    vv->len = val.len;
+    ngx_memcpy(vv->data, val.data, vv->len);
+
+    return 1;
+}
+
+
+static int
+ngx_stream_qjs_variables_define_own_property(JSContext *cx,
+    JSValueConst obj, JSAtom prop, JSValueConst value, JSValueConst getter,
+    JSValueConst setter, int flags)
+{
+    if (!JS_IsUndefined(setter) || !JS_IsUndefined(getter)) {
+        (void) JS_ThrowTypeError(cx, "cannot define getter or setter");
+        return -1;
+    }
+
+    return ngx_stream_qjs_variables_set_property(cx, obj, prop, value, obj,
+                                                 flags);
+}
+
+
+static ngx_int_t
+ngx_stream_qjs_run_event(ngx_stream_session_t *s, ngx_stream_js_ctx_t *ctx,
+    ngx_stream_js_ev_t *event, ngx_uint_t from_upstream)
+{
+    size_t             len;
+    u_char            *p;
+    JSContext         *cx;
+    ngx_int_t          rc;
+    ngx_str_t          exception;
+    ngx_buf_t         *b;
+    uintptr_t          flags;
+    ngx_connection_t  *c;
+    JSValue            argv[2];
+
+    cx = ctx->engine->u.qjs.ctx;
+
+    if (!JS_IsFunction(cx, ngx_qjs_arg(event->function))) {
+        return NGX_OK;
+    }
+
+    c = s->connection;
+    b = ctx->filter ? ctx->buf : c->buffer;
+
+    len = b ? b->last - b->pos : 0;
+
+    p = ngx_pnalloc(c->pool, len);
+    if (p == NULL) {
+        (void) JS_ThrowOutOfMemory(cx);
+        goto error;
+    }
+
+    if (len) {
+        ngx_memcpy(p, b->pos, len);
+    }
+
+    argv[0] = ngx_qjs_prop(cx, event->data_type, p, len);
+    if (JS_IsException(argv[0])) {
+        goto error;
+    }
+
+    argv[1] = JS_NewObjectClass(cx, NGX_QJS_CLASS_ID_STREAM_FLAGS);
+    if (JS_IsException(argv[1])) {
+        JS_FreeValue(cx, argv[0]);
+        goto error;
+    }
+
+    flags = from_upstream << 1 | (uintptr_t) (b && b->last_buf);
+
+    JS_SetOpaque(argv[1], (void *) flags);
+
+    rc = ngx_qjs_call((ngx_js_ctx_t *) ctx, ngx_qjs_arg(event->function),
+                      &argv[0], 2);
+    JS_FreeValue(cx, argv[0]);
+    JS_FreeValue(cx, argv[1]);
+
+    if (rc == NGX_ERROR) {
+error:
+        ngx_qjs_exception(ctx->engine, &exception);
+
+        ngx_log_error(NGX_LOG_ERR, c->log, 0, "js exception: %V",
+                      &exception);
+
+        return NGX_ERROR;
+    }
+
+    return NGX_OK;
+}
+
+
+static ngx_int_t
+ngx_stream_qjs_body_filter(ngx_stream_session_t *s, ngx_stream_js_ctx_t *ctx,
+    ngx_chain_t *in, ngx_uint_t from_upstream)
+{
+    ngx_int_t            rc;
+    JSContext           *cx;
+    ngx_chain_t         *cl;
+    ngx_stream_js_ev_t  *event;
+
+    cx = ctx->engine->u.qjs.ctx;
+
+    while (in != NULL) {
+        ctx->buf = in->buf;
+
+        event = ngx_stream_event(from_upstream);
+
+        if (JS_IsFunction(cx, ngx_qjs_arg(event->function))) {
+            rc = ngx_stream_qjs_run_event(s, ctx, event, from_upstream);
+            if (rc != NGX_OK) {
+                return NGX_ERROR;
+            }
+
+            ctx->buf->pos = ctx->buf->last;
+
+        } else {
+            cl = ngx_alloc_chain_link(s->connection->pool);
+            if (cl == NULL) {
+                return NGX_ERROR;
+            }
+
+            cl->buf = ctx->buf;
+
+            *ctx->last_out = cl;
+            ctx->last_out = &cl->next;
+        }
+
+        in = in->next;
+    }
+
+    return NGX_OK;
+}
+
+
+static ngx_stream_session_t *
+ngx_stream_qjs_session(JSValueConst val)
+{
+    ngx_stream_qjs_session_t  *ses;
+
+    ses = JS_GetOpaque(val, NGX_QJS_CLASS_ID_STREAM_SESSION);
+    if (ses == NULL) {
+        return NULL;
+    }
+
+    return ses->session;
+}
+
+
+static JSValue
+ngx_stream_qjs_session_make(JSContext *cx, ngx_int_t proto_id,
+    ngx_stream_session_t *s)
+{
+    JSValue                    session;
+    ngx_uint_t                 i;
+    ngx_stream_qjs_session_t  *ses;
+
+    session = JS_NewObjectClass(cx, proto_id);
+    if (JS_IsException(session)) {
+        return JS_EXCEPTION;
+    }
+
+    ses = js_malloc(cx, sizeof(ngx_stream_qjs_session_t));
+    if (ses == NULL) {
+        return JS_ThrowOutOfMemory(cx);
+    }
+
+    ses->session = s;
+
+    for (i = 0; i < NGX_JS_EVENT_MAX; i++) {
+        ses->callbacks[i] = JS_UNDEFINED;
+    }
+
+    JS_SetOpaque(session, ses);
+
+    return session;
+}
+
+
+static void
+ngx_stream_qjs_session_finalizer(JSRuntime *rt, JSValue val)
+{
+    ngx_uint_t                 i;
+    ngx_stream_qjs_session_t  *ses;
+
+    ses = JS_GetOpaque(val, NGX_QJS_CLASS_ID_STREAM_SESSION);
+    if (ses == NULL) {
+        return;
+    }
+
+    for (i = 0; i < NGX_JS_EVENT_MAX; i++) {
+        JS_FreeValueRT(rt, ses->callbacks[i]);
+    }
+
+    js_free_rt(rt, ses);
+}
+
+
+static ngx_engine_t *
+ngx_engine_qjs_clone(ngx_js_ctx_t *ctx, ngx_js_loc_conf_t *cf,
+    njs_int_t proto_id, void *external)
+{
+    JSValue               proto;
+    JSContext            *cx;
+    ngx_engine_t         *engine;
+    ngx_stream_js_ctx_t  *sctx;
+
+    engine = ngx_qjs_clone(ctx, cf, external);
+    if (engine == NULL) {
+        return NULL;
+    }
+
+    cx = engine->u.qjs.ctx;
+
+    if (!JS_IsRegisteredClass(JS_GetRuntime(cx),
+                              NGX_QJS_CLASS_ID_STREAM_SESSION))
+    {
+        if (JS_NewClass(JS_GetRuntime(cx), NGX_QJS_CLASS_ID_STREAM_SESSION,
+                        &ngx_stream_qjs_session_class) < 0)
+        {
+            return NULL;
+        }
+
+        proto = JS_NewObject(cx);
+        if (JS_IsException(proto)) {
+            return NULL;
+        }
+
+        JS_SetPropertyFunctionList(cx, proto, ngx_stream_qjs_ext_session,
+                                   njs_nitems(ngx_stream_qjs_ext_session));
+
+        JS_SetClassProto(cx, NGX_QJS_CLASS_ID_STREAM_SESSION, proto);
+
+        if (JS_NewClass(JS_GetRuntime(cx), NGX_QJS_CLASS_ID_STREAM_PERIODIC,
+                        &ngx_stream_qjs_periodic_class) < 0)
+        {
+            return NULL;
+        }
+
+        proto = JS_NewObject(cx);
+        if (JS_IsException(proto)) {
+            return NULL;
+        }
+
+        JS_SetPropertyFunctionList(cx, proto, ngx_stream_qjs_ext_periodic,
+                                   njs_nitems(ngx_stream_qjs_ext_periodic));
+
+        JS_SetClassProto(cx, NGX_QJS_CLASS_ID_STREAM_PERIODIC, proto);
+
+        if (JS_NewClass(JS_GetRuntime(cx), NGX_QJS_CLASS_ID_STREAM_FLAGS,
+                        &ngx_stream_qjs_flags_class) < 0)
+        {
+            return NULL;
+        }
+
+        proto = JS_NewObject(cx);
+        if (JS_IsException(proto)) {
+            return NULL;
+        }
+
+        JS_SetPropertyFunctionList(cx, proto, ngx_stream_qjs_ext_flags,
+                                   njs_nitems(ngx_stream_qjs_ext_flags));
+
+        JS_SetClassProto(cx, NGX_QJS_CLASS_ID_STREAM_FLAGS, proto);
+
+        if (JS_NewClass(JS_GetRuntime(cx), NGX_QJS_CLASS_ID_STREAM_VARS,
+                        &ngx_stream_qjs_variables_class) < 0)
+        {
+            return NULL;
+        }
+    }
+
+    sctx = (ngx_stream_js_ctx_t *) ctx;
+    sctx->run_event = ngx_stream_qjs_run_event;
+    sctx->body_filter = ngx_stream_qjs_body_filter;
+
+    if (proto_id == ngx_stream_js_session_proto_id) {
+        proto_id = NGX_QJS_CLASS_ID_STREAM_SESSION;
+
+    } else if (proto_id == ngx_stream_js_periodic_session_proto_id) {
+        proto_id = NGX_QJS_CLASS_ID_STREAM_PERIODIC;
+    }
+
+    ngx_qjs_arg(ctx->args[0]) = ngx_stream_qjs_session_make(cx, proto_id,
+                                                            external);
+    if (JS_IsException(ngx_qjs_arg(ctx->args[0]))) {
+        return NULL;
+    }
+
+    return engine;
+}
+
+
+static void
+ngx_stream_qjs_destroy(ngx_engine_t *e, ngx_js_ctx_t *ctx,
+    ngx_js_loc_conf_t *conf)
+{
+    ngx_uint_t                 i;
+    JSValue                    cb;
+    ngx_stream_qjs_session_t  *ses;
+
+    if (ctx != NULL) {
+        /*
+         * explicitly freeing the callback functions
+         * to avoid circular references with the session object.
+         */
+        ses = JS_GetOpaque(ngx_qjs_arg(ctx->args[0]),
+                           NGX_QJS_CLASS_ID_STREAM_SESSION);
+        if (ses != NULL) {
+            for (i = 0; i < NGX_JS_EVENT_MAX; i++) {
+                cb = ses->callbacks[i];
+                ses->callbacks[i] = JS_UNDEFINED;
+                JS_FreeValue(e->u.qjs.ctx, cb);
+            }
+        }
+    }
+
+    ngx_engine_qjs_destroy(e, ctx, conf);
+}
+
+#endif
+
+
+static ngx_int_t
+ngx_stream_js_init_conf_vm(ngx_conf_t *cf, ngx_js_loc_conf_t *conf)
+{
+    ngx_engine_opts_t    options;
+    ngx_js_main_conf_t  *jmcf;
+
+    memset(&options, 0, sizeof(ngx_engine_opts_t));
+
+    options.engine = conf->type;
+
+    jmcf = ngx_stream_conf_get_module_main_conf(cf, ngx_stream_js_module);
+    ngx_stream_js_uptr[NGX_JS_MAIN_CONF_INDEX] = (uintptr_t) jmcf;
+
+    if (conf->type == NGX_ENGINE_NJS) {
+        options.u.njs.metas = &ngx_stream_js_metas;
+        options.u.njs.addons = njs_stream_js_addon_modules;
+        options.clone = ngx_engine_njs_clone;
+    }
+
+#if (NJS_HAVE_QUICKJS)
+    else if (conf->type == NGX_ENGINE_QJS) {
+        options.u.qjs.metas = ngx_stream_js_uptr;
+        options.u.qjs.addons = njs_stream_qjs_addon_modules;
+        options.clone = ngx_engine_qjs_clone;
+        options.destroy = ngx_stream_qjs_destroy;
+    }
+#endif
+
     return ngx_js_init_conf_vm(cf, conf, &options);
 }
 
@@ -2392,7 +3480,6 @@ ngx_stream_js_merge_srv_conf(ngx_conf_t *cf, void *parent, void *child)
     ngx_stream_js_srv_conf_t *prev = parent;
     ngx_stream_js_srv_conf_t *conf = child;
 
-    ngx_conf_merge_uint_value(conf->type, prev->type, NGX_ENGINE_NJS);
     ngx_conf_merge_str_value(conf->access, prev->access, "");
     ngx_conf_merge_str_value(conf->preread, prev->preread, "");
     ngx_conf_merge_str_value(conf->filter, prev->filter, "");
diff --git a/nginx/t/js_console.t b/nginx/t/js_console.t
index fcaac3a6..c9499169 100644
--- a/nginx/t/js_console.t
+++ b/nginx/t/js_console.t
@@ -41,6 +41,10 @@ http {
         listen       127.0.0.1:8080;
         server_name  localhost;
 
+        location /engine {
+            js_content test.engine;
+        }
+
         location /dump {
             js_content test.dump;
         }
@@ -74,6 +78,10 @@ http {
 EOF
 
 $t->write_file('test.js', <<EOF);
+    function engine(r) {
+        r.return(200, njs.engine);
+    }
+
     function l(r, method) {
         const data = Buffer.from(r.args.data, 'base64');
         const object = JSON.parse(data);
@@ -116,7 +124,7 @@ $t->write_file('test.js', <<EOF);
         l(r, 'warn');
     }
 
-    export default {dump, error, info, log, time, time_test, warn};
+    export default {engine, dump, error, info, log, time, time_test, warn};
 
 EOF
 
@@ -124,6 +132,8 @@ $t->try_run('no njs console')->plan(7);
 
 ###############################################################################
 
+my $engine = http_get('/engine');
+
 http_get('/dump?data=eyJhIjpbMiwzXX0');
 http_get('/error?data=IldBS0Ei');
 http_get('/info?data=IkJBUiI');
@@ -136,8 +146,16 @@ $t->stop();
 
 like($t->read_file('error.log'), qr/\[error\].*js: WAKA/, 'console.error');
 like($t->read_file('error.log'), qr/\[info\].*js: BAR/, 'console.info');
+
+SKIP: {
+	skip "QuickJS has no console.dump() method.", 1
+		if $engine =~ /QuickJS$/m;
+
 like($t->read_file('error.log'), qr/\[info\].*js: \{a:\['B','C'\]\}/,
 	'console.log with object');
+
+}
+
 like($t->read_file('error.log'), qr/\[warn\].*js: FOO/, 'console.warn');
 like($t->read_file('error.log'), qr/\[info\].*js: foo: \d+\.\d\d\d\d\d\dms/,
 	'console.time foo');
diff --git a/nginx/t/js_dump.t b/nginx/t/js_dump.t
index c00a53a2..a96c2cd4 100644
--- a/nginx/t/js_dump.t
+++ b/nginx/t/js_dump.t
@@ -42,6 +42,10 @@ http {
         listen       127.0.0.1:8080;
         server_name  localhost;
 
+        location /engine {
+            js_content test.engine;
+        }
+
         location /dump {
             js_content test.dump;
         }
@@ -63,6 +67,10 @@ http {
 EOF
 
 $t->write_file('test.js', <<EOF);
+    function engine(r) {
+        r.return(200, njs.engine);
+    }
+
     function dump(r) {
         r.headersOut.baz = 'bar';
         r.return(200, njs.dump(r));
@@ -80,7 +88,7 @@ $t->write_file('test.js', <<EOF);
         });
     }
 
-    export default {dump, stringify, stringify_subrequest};
+    export default {engine, dump, stringify, stringify_subrequest};
 
 EOF
 
@@ -88,6 +96,10 @@ $t->try_run('no njs dump')->plan(3);
 
 ###############################################################################
 
+SKIP: {
+	skip "QuickJS has no njs.dump() method.", 1
+		if http_get('/engine') =~ /QuickJS$/m;
+
 like(http(
 	'GET /dump?v=1&t=x HTTP/1.0' . CRLF
 	. 'Foo: bar' . CRLF
@@ -95,6 +107,12 @@ like(http(
 	. 'Host: localhost' . CRLF . CRLF
 ), qr/method:'GET'/, 'njs.dump(r)');
 
+}
+
+TODO: {
+	local $TODO = 'in QuickJS these are non-enumerable getter/setter props'
+		if http_get('/engine') =~ /^(QuickJS)$/m;
+
 like(http(
 	'GET /stringify?v=1&t=x HTTP/1.0' . CRLF
 	. 'Foo: bar' . CRLF
@@ -107,4 +125,6 @@ like(http(
 	. 'Host: localhost' . CRLF . CRLF
 ), qr/"status":201/, 'JSON.stringify(reply)');
 
+}
+
 ###############################################################################
diff --git a/nginx/t/js_engine.t b/nginx/t/js_engine.t
new file mode 100644
index 00000000..e188ccea
--- /dev/null
+++ b/nginx/t/js_engine.t
@@ -0,0 +1,140 @@
+#!/usr/bin/perl
+
+# (C) Dmitry Volyntsev
+# (C) Nginx, Inc.
+
+# Tests for http njs module, js_engine directive.
+
+###############################################################################
+
+use warnings;
+use strict;
+
+use Test::More;
+
+BEGIN { use FindBin; chdir($FindBin::Bin); }
+
+use lib 'lib';
+use Test::Nginx;
+
+###############################################################################
+
+select STDERR; $| = 1;
+select STDOUT; $| = 1;
+
+my $t = Test::Nginx->new()->has(qw/http proxy/)
+	->write_file_expand('nginx.conf', <<'EOF');
+
+%%TEST_GLOBALS%%
+
+daemon off;
+
+events {
+}
+
+http {
+    %%TEST_GLOBALS_HTTP%%
+
+    js_import test.js;
+
+    server {
+        listen       127.0.0.1:8080;
+        server_name  localhost;
+
+        location /njs {
+            js_content test.njs;
+        }
+
+        location /njs/ {
+            proxy_pass http://127.0.0.1:8081/;
+        }
+
+        location /qjs/ {
+            proxy_pass http://127.0.0.1:8082/;
+        }
+    }
+
+    server {
+        listen       127.0.0.1:8081;
+        server_name  localhost;
+
+        js_engine njs;
+
+        location /test {
+            js_content test.test;
+        }
+
+        location /override {
+            js_engine qjs;
+            js_content test.test;
+        }
+    }
+
+    server {
+        listen       127.0.0.1:8082;
+        server_name  localhost;
+
+        js_engine qjs;
+
+        location /test {
+            js_content test.test;
+        }
+
+        location /override {
+            js_engine njs;
+            js_content test.test;
+        }
+    }
+}
+
+EOF
+
+$t->write_file('test.js', <<EOF);
+    function test_njs(r) {
+        r.return(200, njs.version);
+    }
+
+    function test(r) {
+        r.return(200, njs.engine);
+    }
+
+    export default {njs: test_njs, test};
+
+EOF
+
+$t->try_run('no njs js_engine')->plan(4);
+
+###############################################################################
+
+TODO: {
+local $TODO = 'not yet' unless has_version('0.8.6');
+
+like(http_get('/njs/test'), qr/njs/, 'js_engine njs server');
+like(http_get('/njs/override'), qr/QuickJS/, 'js_engine override');
+like(http_get('/qjs/test'), qr/QuickJS/, 'js_engine qjs server');
+like(http_get('/qjs/override'), qr/njs/, 'js_engine override');
+
+}
+
+$t->stop();
+
+###############################################################################
+
+sub has_version {
+	my $need = shift;
+
+	http_get('/njs') =~ /^([.0-9]+)$/m;
+
+	my @v = split(/\./, $1);
+	my ($n, $v);
+
+	for $n (split(/\./, $need)) {
+		$v = shift @v || 0;
+		return 0 if $n > $v;
+		return 1 if $v > $n;
+	}
+
+	return 1;
+}
+
+###############################################################################
diff --git a/nginx/t/js_fetch.t b/nginx/t/js_fetch.t
index e0763a6a..320e06f5 100644
--- a/nginx/t/js_fetch.t
+++ b/nginx/t/js_fetch.t
@@ -52,6 +52,10 @@ http {
             js_content test.njs;
         }
 
+        location /engine {
+            js_content test.engine;
+        }
+
         location /broken {
             js_content test.broken;
         }
@@ -134,6 +138,10 @@ $t->write_file('test.js', <<EOF);
         r.return(200, njs.version);
     }
 
+    function engine(r) {
+        r.return(200, njs.engine);
+    }
+
     function body(r) {
         var loc = r.args.loc;
         var getter = r.args.getter;
@@ -398,10 +406,14 @@ $t->write_file('test.js', <<EOF);
 
      export default {njs: test_njs, body, broken, broken_response, body_special,
                      chain, chunked_ok, chunked_fail, header, header_iter,
-                     host_header, multi, loc, property};
+                     host_header, multi, loc, property, engine};
 EOF
 
-$t->try_run('no njs.fetch')->plan(36);
+$t->try_run('no njs.fetch');
+
+plan(skip_all => 'not yet') if http_get('/engine') =~ /QuickJS$/m;
+
+$t->plan(36);
 
 $t->run_daemon(\&http_daemon, port(8082));
 $t->waitforsocket('127.0.0.1:' . port(8082));
diff --git a/nginx/t/js_fetch_https.t b/nginx/t/js_fetch_https.t
index 9d4ebb0a..9a44a339 100644
--- a/nginx/t/js_fetch_https.t
+++ b/nginx/t/js_fetch_https.t
@@ -48,6 +48,10 @@ http {
             js_content test.njs;
         }
 
+        location /engine {
+            js_content test.engine;
+        }
+
         location /https {
             js_content test.https;
         }
@@ -102,6 +106,10 @@ $t->write_file('test.js', <<EOF);
         r.return(200, njs.version);
     }
 
+    function engine(r) {
+        r.return(200, njs.engine);
+    }
+
     function https(r) {
         var url = `https://\${r.args.domain}:$p1/loc`;
         var opt = {};
@@ -116,7 +124,7 @@ $t->write_file('test.js', <<EOF);
         .catch(e => r.return(501, e.message))
     }
 
-    export default {njs: test_njs, https};
+    export default {njs: test_njs, https, engine};
 EOF
 
 my $d = $t->testdir();
@@ -186,7 +194,11 @@ foreach my $name ('default.example.com', '1.example.com') {
 		. $t->read_file('intermediate.crt'));
 }
 
-$t->try_run('no njs.fetch')->plan(7);
+$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);
 $t->waitforfile($t->testdir . '/' . port(8981));
diff --git a/nginx/t/js_fetch_objects.t b/nginx/t/js_fetch_objects.t
index d0f47630..1bc88a3d 100644
--- a/nginx/t/js_fetch_objects.t
+++ b/nginx/t/js_fetch_objects.t
@@ -45,6 +45,10 @@ http {
             js_content test.njs;
         }
 
+        location /engine {
+            js_content test.engine;
+        }
+
         location /headers {
             js_content test.headers;
         }
@@ -88,6 +92,10 @@ $t->write_file('test.js', <<EOF);
         r.return(200, njs.version);
     }
 
+    function engine(r) {
+        r.return(200, njs.engine);
+    }
+
     function header(r) {
         r.return(200, r.headersIn.a);
     }
@@ -501,11 +509,15 @@ $t->write_file('test.js', <<EOF);
         run(r, tests);
     }
 
-     export default {njs: test_njs, body, headers, request, response, fetch,
-                     fetch_multi_header};
+     export default {njs: test_njs, engine, body, headers, request, response,
+                     fetch, fetch_multi_header};
 EOF
 
-$t->try_run('no njs')->plan(5);
+$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 8fb6b66f..7cea3386 100644
--- a/nginx/t/js_fetch_resolver.t
+++ b/nginx/t/js_fetch_resolver.t
@@ -50,6 +50,10 @@ http {
             js_content test.njs;
         }
 
+        location /engine {
+            js_content test.engine;
+        }
+
         location /dns {
             js_content test.dns;
 
@@ -104,6 +108,10 @@ $t->write_file('test.js', <<EOF);
         r.return(200, njs.version);
     }
 
+    function engine(r) {
+        r.return(200, njs.engine);
+    }
+
     const p0 = $p0;
     const p1 = $p1;
 
@@ -133,10 +141,14 @@ $t->write_file('test.js', <<EOF);
         r.return(c, `\${v.host}:\${v.request_method}:\${foo}:\${bar}:\${body}`);
     }
 
-     export default {njs: test_njs, dns, loc};
+     export default {njs: test_njs, dns, loc, engine};
 EOF
 
-$t->try_run('no njs.fetch')->plan(5);
+$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);
 $t->waitforfile($t->testdir . '/' . port(8981));
diff --git a/nginx/t/js_fetch_timeout.t b/nginx/t/js_fetch_timeout.t
index 486656d6..1ac1c7aa 100644
--- a/nginx/t/js_fetch_timeout.t
+++ b/nginx/t/js_fetch_timeout.t
@@ -47,6 +47,10 @@ http {
             js_content test.njs;
         }
 
+        location /engine {
+            js_content test.engine;
+        }
+
         location /normal_timeout {
             js_content test.timeout_test;
         }
@@ -80,6 +84,10 @@ $t->write_file('test.js', <<EOF);
         r.return(200, njs.version);
     }
 
+    function engine(r) {
+        r.return(200, njs.engine);
+    }
+
     async function timeout_test(r) {
         let rs = await Promise.allSettled([
             'http://127.0.0.1:$p1/normal_reply',
@@ -102,10 +110,15 @@ $t->write_file('test.js', <<EOF);
         setTimeout((r) => { r.return(200); }, 250, r, 0);
     }
 
-     export default {njs: test_njs, timeout_test, normal_reply, delayed_reply};
+     export default {njs: test_njs, engine, timeout_test, normal_reply,
+                     delayed_reply};
 EOF
 
-$t->try_run('no js_fetch_timeout')->plan(2);
+$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 d6bb1d9e..4c97e04d 100644
--- a/nginx/t/js_fetch_verify.t
+++ b/nginx/t/js_fetch_verify.t
@@ -48,6 +48,10 @@ http {
             js_content test.njs;
         }
 
+        location /engine {
+            js_content test.engine;
+        }
+
         location /https {
             js_content test.https;
         }
@@ -76,6 +80,10 @@ $t->write_file('test.js', <<EOF);
         r.return(200, njs.version);
     }
 
+    function engine(r) {
+        r.return(200, njs.engine);
+    }
+
     function https(r) {
         ngx.fetch(`https://example.com:$p1/loc`)
         .then(reply => reply.text())
@@ -83,7 +91,7 @@ $t->write_file('test.js', <<EOF);
         .catch(e => r.return(501, e.message));
     }
 
-    export default {njs: test_njs, https};
+    export default {njs: test_njs, engine, https};
 EOF
 
 $t->write_file('openssl.conf', <<EOF);
@@ -104,7 +112,11 @@ foreach my $name ('localhost') {
 		or die "Can't create certificate for $name: $!\n";
 }
 
-$t->try_run('no js_fetch_verify')->plan(2);
+$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);
 $t->waitforfile($t->testdir . '/' . port(8981));
diff --git a/nginx/t/js_object.t b/nginx/t/js_object.t
index 97e778a2..8c154010 100644
--- a/nginx/t/js_object.t
+++ b/nginx/t/js_object.t
@@ -42,6 +42,10 @@ http {
         listen       127.0.0.1:8080;
         server_name  localhost;
 
+        location /engine {
+            js_content test.engine;
+        }
+
         location /to_string {
             js_content test.to_string;
         }
@@ -75,6 +79,10 @@ http {
 EOF
 
 $t->write_file('test.js', <<EOF);
+    function engine(r) {
+        r.return(200, njs.engine);
+    }
+
     function to_string(r) {
         r.return(200, r.toString());
     }
@@ -113,7 +121,7 @@ $t->write_file('test.js', <<EOF);
                  Object.getOwnPropertyDescriptors(r)['log'].value === r.log);
     }
 
-    export default {to_string, define_prop, in_operator, redefine_bind,
+    export default {engine, to_string, define_prop, in_operator, redefine_bind,
                     redefine_proxy, redefine_proto, get_own_prop_descs};
 
 EOF
@@ -132,6 +140,12 @@ like(http(
 like(http_get('/redefine_bind'), qr/redefine_bind/, 'redefine_bind');
 like(http_get('/redefine_proxy'), qr/redefine_proxy/, 'redefine_proxy');
 like(http_get('/redefine_proto'), qr/a|b/, 'redefine_proto');
-like(http_get('/get_own_prop_descs'), qr/true/, 'get_own_prop_descs');
+
+SKIP: {
+	skip "In QuickJS methods are in the prototype", 1
+		if http_get('/engine') =~ /QuickJS$/m;
+
+	like(http_get('/get_own_prop_descs'), qr/true/, 'get_own_prop_descs');
+}
 
 ###############################################################################
diff --git a/nginx/t/js_periodic.t b/nginx/t/js_periodic.t
index 8451ce53..63afe379 100644
--- a/nginx/t/js_periodic.t
+++ b/nginx/t/js_periodic.t
@@ -71,6 +71,10 @@ http {
             js_periodic test.timeout_exception interval=30ms;
         }
 
+        location /engine {
+            js_content test.engine;
+        }
+
         location /fetch_ok {
             return 200 'ok';
         }
@@ -120,6 +124,10 @@ my $p0 = port(8080);
 $t->write_file('test.js', <<EOF);
     import fs from 'fs';
 
+    function engine(r) {
+        r.return(200, njs.engine);
+    }
+
     function affinity() {
         ngx.shared.workers.set(ngx.worker_id, 1);
     }
@@ -241,10 +249,14 @@ $t->write_file('test.js', <<EOF);
                      test_file, test_multiple_fetches, test_tick,
                      test_timeout_exception, test_timer, test_vars, tick,
                      tick_exception, timer, timer_exception,
-                     timeout_exception };
+                     timeout_exception, engine };
 EOF
 
-$t->try_run('no js_periodic')->plan(9);
+$t->try_run('no js_periodic');
+
+plan(skip_all => 'not yet') if http_get('/engine') =~ /QuickJS$/m;
+
+$t->plan(9);
 
 ###############################################################################
 
diff --git a/nginx/t/js_preload_object.t b/nginx/t/js_preload_object.t
index 407e97fe..49befd66 100644
--- a/nginx/t/js_preload_object.t
+++ b/nginx/t/js_preload_object.t
@@ -45,6 +45,10 @@ http {
         js_import lib.js;
         js_preload_object lx from l.json;
 
+        location /engine {
+            js_content lib.engine;
+        }
+
         location /test {
             js_content lib.test;
         }
@@ -83,6 +87,10 @@ $t->write_file('lib.js', <<EOF);
         r.return(200, ga + ' ' + g1.c.prop[0].a + ' ' + lx);
     }
 
+    function engine(r) {
+        r.return(200, njs.engine);
+    }
+
     function test_var(r) {
         return g1.b[2];
     }
@@ -123,7 +131,7 @@ $t->write_file('lib.js', <<EOF);
         r.return(200, gg);
     }
 
-    export default {test, test_var, mutate, suffix};
+    export default {engine, test, test_var, mutate, suffix};
 
 EOF
 
@@ -151,7 +159,11 @@ $t->write_file('ga.json', '"ga loaded"');
 $t->write_file('l.json', '"l loaded"');
 $t->write_file('no_suffix', '"no_suffix loaded"');
 
-$t->try_run('no js_preload_object available')->plan(12);
+$t->try_run('no js_preload_object available');
+
+plan(skip_all => 'not yet') if http_get('/engine') =~ /QuickJS$/m;
+
+$t->plan(12);
 
 ###############################################################################
 
diff --git a/nginx/t/js_shared_dict.t b/nginx/t/js_shared_dict.t
index ffc286e2..16128225 100644
--- a/nginx/t/js_shared_dict.t
+++ b/nginx/t/js_shared_dict.t
@@ -51,6 +51,10 @@ http {
             js_content test.njs;
         }
 
+        location /engine {
+            js_content test.engine;
+        }
+
         location /add {
             js_content test.add;
         }
@@ -132,6 +136,10 @@ $t->write_file('test.js', <<'EOF');
         r.return(200, njs.version);
     }
 
+    function engine(r) {
+        r.return(200, njs.engine);
+    }
+
     function convertToValue(dict, v) {
         if (dict.type == 'number') {
             return parseInt(v);
@@ -257,7 +265,7 @@ $t->write_file('test.js', <<'EOF');
 
     function pop(r) {
         var dict = ngx.shared[r.args.dict];
-		var val = dict.pop(r.args.key);
+        var val = dict.pop(r.args.key);
         if (val == '') {
             val = 'empty';
 
@@ -302,10 +310,14 @@ $t->write_file('test.js', <<'EOF');
 
     export default { add, capacity, chain, clear, del, free_space, get, has,
                      incr, items, keys, name, njs: test_njs, pop, replace, set,
-                     set_clear, size, zones };
+                     set_clear, size, zones, engine };
 EOF
 
-$t->try_run('no js_shared_dict_zone')->plan(51);
+$t->try_run('no js_shared_dict_zone');
+
+plan(skip_all => 'not yet') if http_get('/engine') =~ /QuickJS$/m;
+
+$t->plan(51);
 
 ###############################################################################
 
diff --git a/nginx/t/stream_js_console.t b/nginx/t/stream_js_console.t
index c3c22800..0c253289 100644
--- a/nginx/t/stream_js_console.t
+++ b/nginx/t/stream_js_console.t
@@ -34,6 +34,21 @@ daemon off;
 events {
 }
 
+http {
+    %%TEST_GLOBALS_HTTP%%
+
+    js_import test.js;
+
+    server {
+        listen       127.0.0.1:8080;
+        server_name  localhost;
+
+        location /engine {
+            js_content test.engine;
+        }
+    }
+}
+
 stream {
     %%TEST_GLOBALS_STREAM%%
 
@@ -41,7 +56,7 @@ stream {
 
 
     server {
-        listen       127.0.0.1:8080;
+        listen       127.0.0.1:8081;
 
         js_preread test.log;
 
@@ -49,7 +64,7 @@ stream {
     }
 
     server {
-        listen       127.0.0.1:8081;
+        listen       127.0.0.1:8082;
 
         js_preread test.timer;
 
@@ -60,6 +75,10 @@ stream {
 EOF
 
 $t->write_file('test.js', <<EOF);
+    function engine(r) {
+        r.return(200, njs.engine);
+    }
+
     function log(s) {
         s.on('upload', function (data) {
             if (data.length > 0) {
@@ -85,7 +104,7 @@ $t->write_file('test.js', <<EOF);
         });
     }
 
-    export default { log, timer };
+    export default { engine, log, timer };
 EOF
 
 $t->run_daemon(\&stream_daemon, port(8090));
@@ -94,14 +113,23 @@ $t->waitforsocket('127.0.0.1:' . port(8090));
 
 ###############################################################################
 
-is(stream('127.0.0.1:' . port(8080))->io('eyJhIjpbIkIiLCJDIl19'),
+my $engine = http_get('/engine');
+
+is(stream('127.0.0.1:' . port(8081))->io('eyJhIjpbIkIiLCJDIl19'),
 	'eyJhIjpbIkIiLCJDIl19', 'log test');
-is(stream('127.0.0.1:' . port(8081))->io('timer'), 'timer', 'timer test');
+is(stream('127.0.0.1:' . port(8082))->io('timer'), 'timer', 'timer test');
 
 $t->stop();
 
+SKIP: {
+	skip "QuickJS has no console.dump() method.", 1
+		if $engine =~ /QuickJS$/m;
+
 like($t->read_file('error.log'), qr/\[info\].*js: \{a:\['B','C'\]\}/,
 	'console.log with object');
+
+}
+
 like($t->read_file('error.log'), qr/\[info\].*js: foo: \d+\.\d\d\d\d\d\dms/,
 	'console.time foo');
 
diff --git a/nginx/t/stream_js_exit.t b/nginx/t/stream_js_exit.t
index a8bc34ae..01778f0f 100644
--- a/nginx/t/stream_js_exit.t
+++ b/nginx/t/stream_js_exit.t
@@ -45,6 +45,10 @@ http {
         location /njs {
             js_content test.njs;
         }
+
+        location /engine {
+            js_content test.engine;
+        }
     }
 }
 
@@ -74,6 +78,10 @@ $t->write_file('test.js', <<EOF);
         r.return(200, njs.version);
     }
 
+    function engine(r) {
+        r.return(200, njs.engine);
+    }
+
     function access(s) {
         njs.on('exit', () => {
             var v = s.variables;
@@ -95,10 +103,14 @@ $t->write_file('test.js', <<EOF);
         });
     }
 
-    export default {njs: test_njs, access, filter};
+    export default {njs: test_njs, engine, access, filter};
 EOF
 
-$t->try_run('no stream njs available')->plan(2);
+$t->try_run('no stream njs available');
+
+plan(skip_all => 'not yet') if http_get('/engine') =~ /QuickJS$/m;
+
+$t->plan(2);
 
 $t->run_daemon(\&stream_daemon, port(8090));
 $t->waitforsocket('127.0.0.1:' . port(8090));
diff --git a/nginx/t/stream_js_fetch.t b/nginx/t/stream_js_fetch.t
index 106702dc..c57128a8 100644
--- a/nginx/t/stream_js_fetch.t
+++ b/nginx/t/stream_js_fetch.t
@@ -46,6 +46,10 @@ http {
             js_content test.njs;
         }
 
+        location /engine {
+            js_content test.engine;
+        }
+
         location /validate {
             js_content test.validate;
         }
@@ -99,6 +103,10 @@ $t->write_file('test.js', <<EOF);
         r.return(200, njs.version);
     }
 
+    function engine(r) {
+        r.return(200, njs.engine);
+    }
+
     function validate(r) {
         r.return((r.requestText == 'QZ') ? 200 : 403);
     }
@@ -158,10 +166,14 @@ $t->write_file('test.js', <<EOF);
     }
 
     export default {njs: test_njs, validate, preread_verify, filter_verify,
-                    access_ok, access_nok};
+                    access_ok, access_nok, engine};
 EOF
 
-$t->try_run('no stream njs available')->plan(9);
+$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));
 $t->waitforsocket('127.0.0.1:' . port(8090));
diff --git a/nginx/t/stream_js_fetch_https.t b/nginx/t/stream_js_fetch_https.t
index c49b833b..5d7c5c20 100644
--- a/nginx/t/stream_js_fetch_https.t
+++ b/nginx/t/stream_js_fetch_https.t
@@ -47,6 +47,10 @@ http {
         location /njs {
             js_content test.njs;
         }
+
+        location /engine {
+            js_content test.engine;
+        }
     }
 
     server {
@@ -159,6 +163,10 @@ $t->write_file('test.js', <<EOF);
         r.return(200, njs.version);
     }
 
+    function engine(r) {
+        r.return(200, njs.engine);
+    }
+
     function preread(s) {
         s.on('upload', function (data, flags) {
             if (data.startsWith('GO')) {
@@ -193,7 +201,7 @@ $t->write_file('test.js', <<EOF);
         (r.status == 200) ? s.allow(): s.deny();
     }
 
-    export default {njs: test_njs, preread, access_ok, access_nok};
+    export default {njs: test_njs, engine, preread, access_ok, access_nok};
 EOF
 
 my $d = $t->testdir();
@@ -263,7 +271,11 @@ foreach my $name ('default.example.com', '1.example.com') {
 		. $t->read_file('intermediate.crt'));
 }
 
-$t->try_run('no njs.fetch')->plan(6);
+$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);
 $t->waitforfile($t->testdir . '/' . port(8981));
diff --git a/nginx/t/stream_js_fetch_init.t b/nginx/t/stream_js_fetch_init.t
index 6de487da..3f6d7262 100644
--- a/nginx/t/stream_js_fetch_init.t
+++ b/nginx/t/stream_js_fetch_init.t
@@ -58,6 +58,10 @@ http {
             js_content test.njs;
         }
 
+        location /engine {
+            js_content test.engine;
+        }
+
         location /success {
             return 200;
         }
@@ -73,16 +77,24 @@ $t->write_file('test.js', <<EOF);
         r.return(200, njs.version);
     }
 
+    function engine(r) {
+        r.return(200, njs.engine);
+    }
+
     async function access_ok(s) {
         let reply = await ngx.fetch('http://127.0.0.1:$p/success');
 
         (reply.status == 200) ? s.allow(): s.deny();
     }
 
-    export default {njs: test_njs, access_ok};
+    export default {njs: test_njs, engine, access_ok};
 EOF
 
-$t->try_run('no stream njs available')->plan(1);
+$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));
 $t->waitforsocket('127.0.0.1:' . port(8090));
diff --git a/nginx/t/stream_js_object.t b/nginx/t/stream_js_object.t
index 504b9348..571d0a87 100644
--- a/nginx/t/stream_js_object.t
+++ b/nginx/t/stream_js_object.t
@@ -33,35 +33,76 @@ daemon off;
 events {
 }
 
+http {
+    %%TEST_GLOBALS_HTTP%%
+
+    js_import test.js;
+
+    server {
+        listen       127.0.0.1:8080;
+        server_name  localhost;
+
+        location /engine {
+            js_content test.engine;
+        }
+    }
+}
+
 stream {
     %%TEST_GLOBALS_STREAM%%
 
-    js_set $test     test.test;
-
     js_import test.js;
 
+    js_set $to_string            test.to_string;
+    js_set $define_prop          test.define_prop;
+    js_set $in_operator          test.in_operator;
+    js_set $redefine_proto       test.redefine_proto;
+    js_set $get_own_prop_descs   test.get_own_prop_descs;
+
     server {
         listen  127.0.0.1:8081;
-        return  $test$status;
+        return  $to_string;
+    }
+
+    server {
+        listen  127.0.0.1:8082;
+        return  $define_prop$status;
+    }
+
+    server {
+        listen 127.0.0.1:8083;
+        return $in_operator;
+    }
+
+    server {
+        listen 127.0.0.1:8084;
+        return $redefine_proto;
+    }
+
+    server {
+        listen 127.0.0.1:8085;
+        return $get_own_prop_descs;
     }
 }
 
 EOF
 
 $t->write_file('test.js', <<EOF);
+    function engine(r) {
+        r.return(200, njs.engine);
+    }
+
     function to_string(s) {
-        return s.toString() === '[object Stream Session]';
+        return s.toString();
     }
 
     function define_prop(s) {
         Object.defineProperty(s.variables, 'status', {value:400});
-        return s.variables.status == 400;
+        return s.variables.status;
     }
 
     function in_operator(s) {
-        return ['status', 'unknown']
-               .map(v=>v in s.variables)
-               .toString() === 'true,false';
+        return ['status', 'unknown'].map(v=>v in s.variables).toString();
     }
 
     function redefine_proto(s) {
@@ -76,23 +117,27 @@ $t->write_file('test.js', <<EOF);
         return Object.getOwnPropertyDescriptors(s)['on'].value === s.on;
     }
 
-    function test(s) {
-        return [ to_string,
-                 define_prop,
-                 in_operator,
-                 redefine_proto,
-                 get_own_prop_descs,
-               ].every(v=>v(s));
-    }
-
-    export default {test};
+    export default { engine, to_string, define_prop, in_operator,
+                     redefine_proto, get_own_prop_descs };
 
 EOF
 
-$t->try_run('no njs stream session object')->plan(1);
+$t->try_run('no njs stream session object')->plan(5);
 
 ###############################################################################
 
-is(stream('127.0.0.1:' . port(8081))->read(), 'true400', 'var set');
+is(stream('127.0.0.1:' . port(8081))->read(), '[object Stream Session]',
+	'to_string');
+is(stream('127.0.0.1:' . port(8082))->read(), '400400', 'define_prop');
+is(stream('127.0.0.1:' . port(8083))->read(), 'true,false', 'in_operator');
+is(stream('127.0.0.1:' . port(8084))->read(), 'true', 'redefine_proto');
+
+SKIP: {
+	skip "In QuickJS methods are in the prototype", 1
+		if http_get('/engine') =~ /QuickJS$/m;
+
+is(stream('127.0.0.1:' . port(8085))->read(), 'true', 'get_own_prop_descs');
+
+}
 
 ###############################################################################
diff --git a/nginx/t/stream_js_preload_object.t b/nginx/t/stream_js_preload_object.t
index 3c27098d..34ffae2b 100644
--- a/nginx/t/stream_js_preload_object.t
+++ b/nginx/t/stream_js_preload_object.t
@@ -33,6 +33,21 @@ daemon off;
 events {
 }
 
+http {
+    %%TEST_GLOBALS_HTTP%%
+
+    js_import main.js;
+
+    server {
+        listen       127.0.0.1:8080;
+        server_name  localhost;
+
+        location /engine {
+            js_content main.engine;
+        }
+    }
+}
+
 stream {
     %%TEST_GLOBALS_STREAM%%
 
@@ -104,14 +119,22 @@ $t->write_file('lib.js', <<EOF);
 EOF
 
 $t->write_file('main.js', <<EOF);
-    export default {bar: {p(s) {return g1.b[2]}}};
+    function engine(r) {
+        r.return(200, njs.engine);
+    }
+
+    export default {engine, bar: {p(s) {return g1.b[2]}}};
 
 EOF
 
 $t->write_file('g.json',
 	'{"a":1, "b":[1,2,"element",4,5], "c":{"prop":[{"a":3}]}}');
 
-$t->try_run('no js_preload_object available')->plan(2);
+$t->try_run('no js_preload_object available');
+
+plan(skip_all => 'not yet') if http_get('/engine') =~ /QuickJS$/m;
+
+$t->plan(2);
 
 ###############################################################################
 
diff --git a/nginx/t/stream_js_shared_dict.t b/nginx/t/stream_js_shared_dict.t
index e8e482f4..0bdfaeb7 100644
--- a/nginx/t/stream_js_shared_dict.t
+++ b/nginx/t/stream_js_shared_dict.t
@@ -43,6 +43,10 @@ http {
         location / {
             return 200;
         }
+
+        location /engine {
+            js_content test.engine;
+        }
     }
 }
 
@@ -71,6 +75,10 @@ EOF
 $t->write_file('test.js', <<EOF);
     import qs from 'querystring';
 
+    function engine(r) {
+        r.return(200, 'engine');
+    }
+
     function preread_verify(s) {
         var collect = Buffer.from([]);
 
@@ -113,11 +121,15 @@ $t->write_file('test.js', <<EOF);
         });
     }
 
-    export default { preread_verify, control_access };
+    export default { engine, preread_verify, control_access };
 
 EOF
 
-$t->try_run('no js_shared_dict_zone')->plan(9);
+$t->try_run('no js_shared_dict_zone');
+
+plan(skip_all => 'not yet') if http_get('/engine') =~ /QuickJS$/m;
+
+$t->plan(9);
 
 $t->run_daemon(\&stream_daemon, port(8090));
 $t->waitforsocket('127.0.0.1:' . port(8090));
diff --git a/src/qjs.h b/src/qjs.h
index dff5919b..2418e6cd 100644
--- a/src/qjs.h
+++ b/src/qjs.h
@@ -33,6 +33,12 @@
 #include <pthread.h>
 
 
+#define QJS_CORE_CLASS_ID_OFFSET  64
+#define QJS_CORE_CLASS_ID_BUFFER  (QJS_CORE_CLASS_ID_OFFSET)
+#define QJS_CORE_CLASS_ID_UINT8_ARRAY_CTOR (QJS_CORE_CLASS_ID_OFFSET + 1)
+#define QJS_CORE_CLASS_ID_LAST    (QJS_CORE_CLASS_ID_UINT8_ARRAY_CTOR)
+
+
 typedef JSModuleDef *(*qjs_addon_init_pt)(JSContext *ctx, const char *name);
 
 typedef struct {
diff --git a/src/qjs_buffer.c b/src/qjs_buffer.c
index 06574110..2487c633 100644
--- a/src/qjs_buffer.c
+++ b/src/qjs_buffer.c
@@ -262,15 +262,12 @@ static JSClassDef qjs_buffer_class = {
 };
 
 
-static JSClassID qjs_buffer_class_id;
-
 #ifndef NJS_HAVE_QUICKJS_NEW_TYPED_ARRAY
 static JSClassDef qjs_uint8_array_ctor_class = {
     "Uint8ArrayConstructor",
     .finalizer = NULL,
 };
 
-static JSClassID qjs_uint8_array_ctor_id;
 #endif
 
 
@@ -354,7 +351,7 @@ qjs_buffer_ctor(JSContext *ctx, JSValueConst this_val, int argc,
         return ret;
     }
 
-    proto = JS_GetClassProto(ctx, qjs_buffer_class_id);
+    proto = JS_GetClassProto(ctx, QJS_CORE_CLASS_ID_BUFFER);
     JS_SetPrototype(ctx, ret, proto);
     JS_FreeValue(ctx, proto);
 
@@ -725,7 +722,7 @@ qjs_buffer_is_buffer(JSContext *ctx, JSValueConst this_val,
     JSValue proto, buffer_proto, ret;
 
     proto = JS_GetPrototype(ctx, argv[0]);
-    buffer_proto = JS_GetClassProto(ctx, qjs_buffer_class_id);
+    buffer_proto = JS_GetClassProto(ctx, QJS_CORE_CLASS_ID_BUFFER);
 
     ret = JS_NewBool(ctx, JS_VALUE_GET_TAG(argv[0]) == JS_TAG_OBJECT &&
                      JS_VALUE_GET_OBJ(buffer_proto) == JS_VALUE_GET_OBJ(proto));
@@ -2426,7 +2423,7 @@ qjs_buffer_alloc(JSContext *ctx, size_t size)
         return ret;
     }
 
-    proto = JS_GetClassProto(ctx, qjs_buffer_class_id);
+    proto = JS_GetClassProto(ctx, QJS_CORE_CLASS_ID_BUFFER);
     JS_SetPrototype(ctx, ret, proto);
     JS_FreeValue(ctx, proto);
 
@@ -2494,7 +2491,7 @@ qjs_new_uint8_array(JSContext *ctx, int argc, JSValueConst *argv)
 #else
     JSValue ctor;
 
-    ctor = JS_GetClassProto(ctx, qjs_uint8_array_ctor_id);
+    ctor = JS_GetClassProto(ctx, QJS_CORE_CLASS_ID_UINT8_ARRAY_CTOR);
     ret = JS_CallConstructor(ctx, ctor, argc, argv);
     JS_FreeValue(ctx, ctor);
 #endif
@@ -2511,8 +2508,8 @@ qjs_buffer_builtin_init(JSContext *ctx)
     JSValue    global_obj, buffer, proto, ctor, ta, ta_proto, symbol, species;
     JSClassID  u8_ta_class_id;
 
-    JS_NewClassID(&qjs_buffer_class_id);
-    JS_NewClass(JS_GetRuntime(ctx), qjs_buffer_class_id, &qjs_buffer_class);
+    JS_NewClass(JS_GetRuntime(ctx), QJS_CORE_CLASS_ID_BUFFER,
+                &qjs_buffer_class);
 
     global_obj = JS_GetGlobalObject(ctx);
 
@@ -2528,10 +2525,10 @@ qjs_buffer_builtin_init(JSContext *ctx)
      * We use JS_SetClassProto()/JS_GetClassProto() as a key-value store
      * for fast value query by class ID without querying the global object.
      */
-    JS_NewClassID(&qjs_uint8_array_ctor_id);
-    JS_NewClass(JS_GetRuntime(ctx), qjs_uint8_array_ctor_id,
+    JS_NewClass(JS_GetRuntime(ctx), QJS_CORE_CLASS_ID_UINT8_ARRAY_CTOR,
                 &qjs_uint8_array_ctor_class);
-    JS_SetClassProto(ctx, qjs_uint8_array_ctor_id, JS_DupValue(ctx, ctor));
+    JS_SetClassProto(ctx, QJS_CORE_CLASS_ID_UINT8_ARRAY_CTOR,
+                     JS_DupValue(ctx, ctor));
 #endif
 
     ta = JS_CallConstructor(ctx, ctor, 0, NULL);
@@ -2543,7 +2540,7 @@ qjs_buffer_builtin_init(JSContext *ctx)
     JS_SetPrototype(ctx, proto, ta_proto);
     JS_FreeValue(ctx, ta_proto);
 
-    JS_SetClassProto(ctx, qjs_buffer_class_id, proto);
+    JS_SetClassProto(ctx, QJS_CORE_CLASS_ID_BUFFER, proto);
 
     buffer = JS_NewCFunction2(ctx, qjs_buffer, "Buffer", 3,
                               JS_CFUNC_constructor, 0);


More information about the nginx-devel mailing list