]> git.kaiwu.me - njs.git/commitdiff
Modules: improved handling of results of async handlers.
authorVadim Zhestikov <v.zhestikov@f5.com>
Fri, 25 Jul 2025 00:10:36 +0000 (17:10 -0700)
committerVadimZhestikov <108960056+VadimZhestikov@users.noreply.github.com>
Thu, 6 Nov 2025 23:27:06 +0000 (15:27 -0800)
Previously, r.setReturnValue() had to be used when returning a value from async js_set handler.

async function hash(r) {
    let hash = await crypto.subtle.digest('SHA-512', r.headersIn.host);
    r.setReturnValue(Buffer.from(hash).toString('hex'));
}

Now r.setReturnValue() is not needed:

async function hash(r) {
    let hash = await crypto.subtle.digest('SHA-512', r.headersIn.host);
    return Buffer.from(hash).toString('hex');
}

Also added promise handling in global qjs code.

nginx/ngx_js.c
nginx/t/js_async.t
nginx/t/js_promise_top_level_await.t [new file with mode: 0644]
nginx/t/stream_js.t

index 71df075d1dde20e03d5cf98f0344966255017db0..b4e78159ecb310b141b3d8126da59bd20c20f5f0 100644 (file)
@@ -54,7 +54,14 @@ static void ngx_engine_njs_destroy(ngx_engine_t *e, ngx_js_ctx_t *ctx,
     ngx_js_loc_conf_t *conf);
 static ngx_int_t ngx_js_init_preload_vm(njs_vm_t *vm, ngx_js_loc_conf_t *conf);
 
+static ngx_int_t ngx_njs_execute_pending_jobs(njs_vm_t *vm, ngx_log_t *log);
+static njs_int_t ngx_njs_await(njs_vm_t *vm, ngx_log_t *log,
+    njs_value_t *value);
+
 #if (NJS_HAVE_QUICKJS)
+static ngx_int_t ngx_qjs_execute_pending_jobs(JSContext *cx, ngx_log_t *log);
+static ngx_int_t ngx_qjs_await(JSContext *cx, ngx_log_t *log,
+    JSValueConst *value);
 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,
@@ -657,10 +664,61 @@ ngx_engine_njs_compile(ngx_js_loc_conf_t *conf, ngx_log_t *log, u_char *start,
 }
 
 
+static ngx_int_t
+ngx_njs_execute_pending_jobs(njs_vm_t *vm, ngx_log_t *log)
+{
+    njs_int_t  ret;
+    njs_str_t  exception;
+
+    for ( ;; ) {
+        ret = njs_vm_execute_pending_job(vm);
+        if (ret <= NJS_OK) {
+            if (ret == NJS_ERROR) {
+                njs_vm_exception_string(vm, &exception);
+                ngx_log_error(NGX_LOG_ERR, log, 0, "js job exception: %V",
+                              &exception);
+                return NGX_ERROR;
+            }
+
+            break;
+        }
+    }
+
+    return NGX_OK;
+}
+
+
+static njs_int_t
+ngx_njs_await(njs_vm_t *vm, ngx_log_t *log, njs_value_t *value)
+{
+    ngx_int_t            ret;
+    njs_promise_type_t   state;
+
+    ret = ngx_njs_execute_pending_jobs(vm, log);
+    if (ret != NGX_OK) {
+        return NGX_ERROR;
+    }
+
+    if (njs_value_is_promise(value)) {
+        state = njs_promise_state(value);
+
+        if (state == NJS_PROMISE_FULFILL) {
+            njs_value_assign(value, njs_promise_result(value));
+
+        } else if (state == NJS_PROMISE_REJECTED) {
+            njs_vm_throw(vm, njs_promise_result(value));
+        }
+    }
+
+    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            ret;
     ngx_engine_t        *engine;
     njs_opaque_value_t   retval;
 
@@ -671,8 +729,7 @@ ngx_njs_clone(ngx_js_ctx_t *ctx, ngx_js_loc_conf_t *cf, void *external)
 
     engine = njs_mp_alloc(njs_vm_memory_pool(vm), sizeof(ngx_engine_t));
     if (engine == NULL) {
-        njs_vm_destroy(vm);
-        return NULL;
+        goto destroy;
     }
 
     memcpy(engine, cf->engine, sizeof(ngx_engine_t));
@@ -681,13 +738,21 @@ ngx_njs_clone(ngx_js_ctx_t *ctx, ngx_js_loc_conf_t *cf, void *external)
 
     if (njs_vm_start(vm, njs_value_arg(&retval)) == NJS_ERROR) {
         ngx_js_log_exception(vm, ctx->log, "exception");
+        goto destroy;
+    }
 
-        njs_vm_destroy(vm);
-
-        return NULL;
+    ret = ngx_njs_await(vm, ctx->log, njs_value_arg(&retval));
+    if (ret == NGX_ERROR) {
+        goto destroy;
     }
 
     return engine;
+
+destroy:
+
+    njs_vm_destroy(vm);
+
+    return NULL;
 }
 
 
@@ -720,17 +785,9 @@ ngx_engine_njs_call(ngx_js_ctx_t *ctx, ngx_str_t *fname,
         return NGX_ERROR;
     }
 
-    for ( ;; ) {
-        ret = njs_vm_execute_pending_job(vm);
-        if (ret <= NJS_OK) {
-            if (ret == NJS_ERROR) {
-                ngx_js_log_exception(vm, ctx->log, "exception");
-
-                return NGX_ERROR;
-            }
-
-            break;
-        }
+    ret = ngx_njs_await(vm, ctx->log, njs_value_arg(&ctx->retval));
+    if (ret == NGX_ERROR) {
+        return NGX_ERROR;
     }
 
     return njs_rbtree_is_empty(&ctx->waiting_events) ? NGX_OK : NGX_AGAIN;
@@ -807,6 +864,66 @@ ngx_engine_njs_destroy(ngx_engine_t *e, ngx_js_ctx_t *ctx,
 
 #if (NJS_HAVE_QUICKJS)
 
+static ngx_int_t
+ngx_qjs_execute_pending_jobs(JSContext *cx, ngx_log_t *log)
+{
+    int          rc;
+    JSValue      value;
+    JSContext   *cx1;
+    const char  *exception;
+
+    for ( ;; ) {
+        rc = JS_ExecutePendingJob(JS_GetRuntime(cx), &cx1);
+        if (rc <= 0) {
+            if (rc == 0) {
+                break;
+            }
+
+            value = JS_GetException(cx);
+            exception = JS_ToCString(cx, value);
+            JS_FreeValue(cx, value);
+
+            ngx_log_error(NGX_LOG_ERR, log, 0, "js job exception: %s",
+                          exception);
+
+            JS_FreeCString(cx, exception);
+
+            return NGX_ERROR;
+        }
+    }
+
+    return NGX_OK;
+}
+
+
+static ngx_int_t
+ngx_qjs_await(JSContext *cx, ngx_log_t *log, JSValue *value)
+{
+    JSValue             ret;
+    ngx_int_t           rc;
+    JSPromiseStateEnum  state;
+
+    rc = ngx_qjs_execute_pending_jobs(cx, log);
+    if (rc != NGX_OK) {
+        return NGX_ERROR;
+    }
+
+    state = JS_PromiseState(cx, *value);
+    if (state == JS_PROMISE_FULFILLED) {
+        ret = JS_PromiseResult(cx, *value);
+        JS_FreeValue(cx, *value);
+        *value = ret;
+
+    } else if (state == JS_PROMISE_REJECTED) {
+        ret = JS_Throw(cx, JS_PromiseResult(cx, *value));
+        JS_FreeValue(cx, *value);
+        *value = ret;
+    }
+
+    return NGX_OK;
+}
+
+
 static ngx_int_t
 ngx_engine_qjs_init(ngx_engine_t *engine, ngx_engine_opts_t *opts)
 {
@@ -871,48 +988,13 @@ ngx_engine_qjs_compile(ngx_js_loc_conf_t *conf, ngx_log_t *log, u_char *start,
 }
 
 
-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;
+    ngx_int_t             rc;
     JSRuntime            *rt;
     JSContext            *cx;
     ngx_engine_t         *engine;
@@ -990,14 +1072,13 @@ ngx_qjs_clone(ngx_js_ctx_t *ctx, ngx_js_loc_conf_t *cf, void *external)
         goto destroy;
     }
 
-    rv = js_std_await(cx, rv);
-    if (JS_IsException(rv)) {
-        ngx_qjs_log_exception(engine, ctx->log, "eval exception");
+    rc = ngx_qjs_await(cx, ctx->log, &rv);
+    JS_FreeValue(cx, rv);
+    if (rc == NGX_ERROR) {
+        ngx_qjs_log_exception(engine, ctx->log, "await exception");
         goto destroy;
     }
 
-    JS_FreeValue(cx, rv);
-
     return engine;
 
 destroy:
@@ -1014,10 +1095,9 @@ 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;
-    JSRuntime  *rt;
-    JSContext  *cx, *cx1;
+    ngx_int_t   rc;
+    JSContext  *cx;
 
     cx = ctx->engine->u.qjs.ctx;
 
@@ -1041,19 +1121,10 @@ ngx_engine_qjs_call(ngx_js_ctx_t *ctx, ngx_str_t *fname,
     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_log_exception(ctx->engine, ctx->log, "job exception");
-
-                return NGX_ERROR;
-            }
-
-            break;
-        }
+    rc = ngx_qjs_await(cx, ctx->log, &ngx_qjs_arg(ctx->retval));
+    if (rc == NGX_ERROR) {
+        ngx_qjs_log_exception(ctx->engine, ctx->log, "await exception");
+        return NGX_ERROR;
     }
 
     return njs_rbtree_is_empty(&ctx->waiting_events) ? NGX_OK : NGX_AGAIN;
@@ -1365,10 +1436,8 @@ ngx_qjs_dump_obj(ngx_engine_t *e, JSValueConst val, ngx_str_t *dst)
 ngx_int_t
 ngx_qjs_call(JSContext *cx, JSValue fn, JSValue *argv, int argc)
 {
-    int            rc;
     JSValue        ret;
-    JSRuntime     *rt;
-    JSContext     *cx1;
+    ngx_int_t      rc;
     ngx_js_ctx_t  *ctx;
 
     ctx = ngx_qjs_external_ctx(cx, JS_GetContextOpaque(cx));
@@ -1382,19 +1451,9 @@ ngx_qjs_call(JSContext *cx, JSValue fn, JSValue *argv, int argc)
 
     JS_FreeValue(cx, ret);
 
-    rt = JS_GetRuntime(cx);
-
-    for ( ;; ) {
-        rc = JS_ExecutePendingJob(rt, &cx1);
-        if (rc <= 0) {
-            if (rc == -1) {
-                ngx_qjs_log_exception(ctx->engine, ctx->log, "job exception");
-
-                return NGX_ERROR;
-            }
-
-            break;
-        }
+    rc = ngx_qjs_execute_pending_jobs(cx, ctx->log);
+    if (rc != NGX_OK) {
+        return NGX_ERROR;
     }
 
     return NGX_OK;
@@ -2202,6 +2261,7 @@ ngx_js_call(njs_vm_t *vm, njs_function_t *func, njs_opaque_value_t *args,
     njs_uint_t nargs)
 {
     njs_int_t          ret;
+    ngx_js_ctx_t      *ctx;
     ngx_connection_t  *c;
 
     ret = njs_vm_call(vm, func, njs_value_arg(args), nargs);
@@ -2214,19 +2274,11 @@ ngx_js_call(njs_vm_t *vm, njs_function_t *func, njs_opaque_value_t *args,
         return NGX_ERROR;
     }
 
-    for ( ;; ) {
-        ret = njs_vm_execute_pending_job(vm);
-        if (ret <= NJS_OK) {
-            c = ngx_external_connection(vm, njs_vm_external_ptr(vm));
-
-            if (ret == NJS_ERROR) {
-                ngx_js_log_exception(vm, c->log, "job exception");
-
-                return NGX_ERROR;
-            }
+    ctx = ngx_external_ctx(vm, njs_vm_external_ptr(vm));
 
-            break;
-        }
+    ret = ngx_njs_execute_pending_jobs(vm, ctx->log);
+    if (ret != NGX_OK) {
+        return NGX_ERROR;
     }
 
     return NGX_OK;
index 32d1e0a4424ad017783547f5d5dc633af8d924b1..3cbe283a3ec08768b891ab3fe8bcd0b059a53126 100644 (file)
@@ -35,9 +35,10 @@ events {
 http {
     %%TEST_GLOBALS_HTTP%%
 
-    js_set $test_async      test.set_timeout;
-    js_set $context_var     test.context_var;
-    js_set $test_set_rv_var test.set_rv_var;
+    js_set $test_async       test.set_timeout;
+    js_set $context_var      test.context_var;
+    js_set $test_set_rv_var  test.set_rv_var;
+    js_set $test_promise_var test.set_promise_var;
 
     js_import test.js;
 
@@ -85,6 +86,10 @@ http {
             return 200 $test_set_rv_var;
         }
 
+        location /promise_var {
+            return 200 $test_promise_var;
+        }
+
         location /await_reject {
             js_content test.await_reject;
         }
@@ -198,6 +203,13 @@ $t->write_file('test.js', <<EOF);
         r.setReturnValue(`retval: \${a1 + a2}`);
     }
 
+    async function set_promise_var(r) {
+        const a1 = await pr(10);
+        const a2 = await pr(20);
+
+        return `retval: \${a1 + a2}`;
+    }
+
     async function timeout(ms) {
         return new Promise((resolve, reject) => {
             setTimeout(() => {
@@ -213,11 +225,11 @@ $t->write_file('test.js', <<EOF);
 
     export default {njs:test_njs, set_timeout, set_timeout_data,
                     set_timeout_many, context_var, shared_ctx, limit_rate,
-                    async_content, set_rv_var, await_reject};
+                    async_content, set_rv_var, set_promise_var, await_reject};
 
 EOF
 
-$t->try_run('no njs available')->plan(10);
+$t->try_run('no njs available')->plan(11);
 
 ###############################################################################
 
@@ -229,6 +241,7 @@ like(http_get('/limit_rate'), qr/A{50}/, 'limit_rate');
 
 like(http_get('/async_content'), qr/retval: AB/, 'async content');
 like(http_get('/set_rv_var'), qr/retval: 30/, 'set return value variable');
+like(http_get('/promise_var'), qr/retval: 30/, 'fulfilled promise variable');
 
 http_get('/async_var');
 http_get('/await_reject');
diff --git a/nginx/t/js_promise_top_level_await.t b/nginx/t/js_promise_top_level_await.t
new file mode 100644 (file)
index 0000000..29dc632
--- /dev/null
@@ -0,0 +1,92 @@
+#!/usr/bin/perl
+
+# (C) F5, Inc.
+# Vadim Zhestikov
+
+# Tests for top-level await of promises in QuickJS engine.
+
+###############################################################################
+
+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 rewrite/)
+       ->write_file_expand('nginx.conf', <<'EOF');
+
+%%TEST_GLOBALS%%
+
+daemon off;
+
+events {
+}
+
+http {
+    %%TEST_GLOBALS_HTTP%%
+
+    js_import test.js;
+    js_import fulfilled_test.js;
+
+    server {
+        listen       127.0.0.1:8080;
+        server_name  localhost;
+
+        location /resolved {
+            js_content test.test;
+        }
+
+        location /fulfilled {
+            js_content fulfilled_test.test;
+        }
+    }
+}
+
+EOF
+
+$t->write_file('test.js', <<'EOF');
+    var globalResult = await Promise.resolve("resolved value");
+
+    function test(r) {
+        r.return(200, "global result: " + globalResult);
+    }
+
+    export default {test};
+
+EOF
+
+$t->write_file('fulfilled_test.js', <<'EOF');
+    var globalResult = await new Promise((resolve) => {
+        Promise.resolve().then(() => {
+            resolve("fulfilled value");
+        });
+    });
+
+    function test(r) {
+        r.return(200, "fulfilled result: " + globalResult);
+    }
+
+    export default {test};
+
+EOF
+
+$t->try_run('no top-level await support')->plan(2);
+
+###############################################################################
+
+like(http_get('/resolved'), qr/global result: resolved value/,
+    'basic global await works');
+like(http_get('/fulfilled'), qr/fulfilled result: fulfilled value/,
+    'fulfilled promise via microtask works');
+
+###############################################################################
index 0834b68a9d58d452e62491a49e4d17252ae4d737..d442045aeb130da1d79f2a57fa63dd82ced45e17 100644 (file)
@@ -61,14 +61,15 @@ http {
 stream {
     %%TEST_GLOBALS_STREAM%%
 
-    js_set $js_addr      test.addr;
-    js_set $js_var       test.variable;
-    js_set $js_log       test.log;
-    js_set $js_unk       test.unk;
-    js_set $js_req_line  test.req_line;
-    js_set $js_sess_unk  test.sess_unk;
-    js_set $js_async     test.asyncf;
-    js_set $js_buffer    test.buffer;
+    js_set $js_addr          test.addr;
+    js_set $js_var           test.variable;
+    js_set $js_log           test.log;
+    js_set $js_unk           test.unk;
+    js_set $js_req_line      test.req_line;
+    js_set $js_sess_unk      test.sess_unk;
+    js_set $js_async         test.asyncf;
+    js_set $js_async_direct  test.asyncf_direct;
+    js_set $js_buffer        test.buffer;
 
     js_import test.js;
 
@@ -192,6 +193,11 @@ stream {
         return      $js_async;
     }
 
+    server {
+        listen      127.0.0.1:8102;
+        return      $js_async_direct;
+    }
+
     server {
         listen      127.0.0.1:8101;
         return      $js_buffer;
@@ -384,17 +390,24 @@ $t->write_file('test.js', <<EOF);
         s.setReturnValue(`retval: \${a1 + a2}`);
     }
 
+    async function asyncf_direct(s) {
+        const a1 = await pr(10);
+        const a2 = await pr(20);
+
+        return `retval: \${a1 + a2}`;
+    }
+
     export default {njs:test_njs, addr, variable, sess_unk, log, access_step,
                     preread_step, filter_step, access_undecided, access_allow,
                     access_deny, preread_async, preread_data, preread_req_line,
                     req_line, filter_empty, filter_header_inject, filter_search,
                     access_except, preread_except, filter_except, asyncf,
-                    buffer};
+                    asyncf_direct, buffer};
 
 EOF
 
 $t->run_daemon(\&stream_daemon, port(8090));
-$t->try_run('no stream njs available')->plan(25);
+$t->try_run('no stream njs available')->plan(26);
 $t->waitforsocket('127.0.0.1:' . port(8090));
 
 ###############################################################################
@@ -427,7 +440,10 @@ stream('127.0.0.1:' . port(8097))->io('x');
 stream('127.0.0.1:' . port(8098))->io('x');
 stream('127.0.0.1:' . port(8099))->io('x');
 
-is(stream('127.0.0.1:' . port(8100))->read(), 'retval: 30', 'asyncf');
+is(stream('127.0.0.1:' . port(8100))->read(), 'retval: 30',
+        'async var handler setReturnValue');
+is(stream('127.0.0.1:' . port(8102))->read(), 'retval: 30',
+        'async var handler direct');
 
 TODO: {
 local $TODO = 'not yet' unless has_version('0.8.3');