Skip to content

Instantly share code, notes, and snippets.

@xeioex
Created November 6, 2025 17:25
Show Gist options
  • Save xeioex/6d3eb6c3bc7a0d9975da289b8e72c40f to your computer and use it in GitHub Desktop.
Save xeioex/6d3eb6c3bc7a0d9975da289b8e72c40f to your computer and use it in GitHub Desktop.
commit f97144b9be3fc6957374a11d8e401702cc77e2d6
Author: Dmitry Volyntsev <[email protected]>
Date: Wed Nov 5 21:59:07 2025 -0800
fixup! Modules: improved handling of results of async handlers.
diff --git a/external/njs_shell.c b/external/njs_shell.c
index 570da13b..b29ccee2 100644
--- a/external/njs_shell.c
+++ b/external/njs_shell.c
@@ -2703,7 +2703,7 @@ njs_engine_qjs_destroy(njs_engine_t *engine)
njs_queue_link_t *link;
njs_rejected_promise_t *rejected_promise;
- (void) qjs_call_exit_hook(engine->u.qjs.ctx);
+ qjs_call_exit_hook(engine->u.qjs.ctx);
console = JS_GetRuntimeOpaque(engine->u.qjs.rt);
diff --git a/nginx/ngx_js.c b/nginx/ngx_js.c
index c29b912f..b4e78159 100644
--- a/nginx/ngx_js.c
+++ b/nginx/ngx_js.c
@@ -55,11 +55,13 @@ static void ngx_engine_njs_destroy(ngx_engine_t *e, ngx_js_ctx_t *ctx,
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(ngx_js_ctx_t *ctx, njs_opaque_value_t *retval);
+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 *ctx, ngx_log_t *log);
-static njs_int_t ngx_qjs_await(ngx_js_ctx_t *ctx, JSValue *obj);
+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,
@@ -666,17 +668,18 @@ 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_string;
+ njs_str_t exception;
for ( ;; ) {
ret = njs_vm_execute_pending_job(vm);
- if (ret < NJS_OK) {
- njs_vm_exception_string(vm, &exception_string);
- ngx_log_error(NGX_LOG_ERR, log, 0, "js job exception: %V",
- &exception_string);
- return NGX_ERROR;
- }
- if (ret == NJS_OK) {
+ 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;
}
}
@@ -686,30 +689,24 @@ ngx_njs_execute_pending_jobs(njs_vm_t *vm, ngx_log_t *log)
static njs_int_t
-ngx_njs_await(ngx_js_ctx_t *ctx, njs_opaque_value_t *retval)
+ngx_njs_await(njs_vm_t *vm, ngx_log_t *log, njs_value_t *value)
{
- njs_vm_t *vm;
ngx_int_t ret;
- njs_value_t *val;
njs_promise_type_t state;
- vm = ctx->engine->u.njs.vm;
-
- ret = ngx_njs_execute_pending_jobs(vm, ctx->log);
+ ret = ngx_njs_execute_pending_jobs(vm, log);
if (ret != NGX_OK) {
return NGX_ERROR;
}
- val = njs_value_arg(retval);
- if (njs_value_is_promise(val)) {
- state = njs_promise_state(val);
+ if (njs_value_is_promise(value)) {
+ state = njs_promise_state(value);
if (state == NJS_PROMISE_FULFILL) {
- njs_value_assign(val, njs_promise_result(val));
+ njs_value_assign(value, njs_promise_result(value));
} else if (state == NJS_PROMISE_REJECTED) {
- njs_vm_throw(vm, njs_promise_result(val));
-
+ njs_vm_throw(vm, njs_promise_result(value));
}
}
@@ -732,22 +729,19 @@ 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));
engine->pool = njs_vm_memory_pool(vm);
engine->u.njs.vm = vm;
- ctx->engine = engine;
-
if (njs_vm_start(vm, njs_value_arg(&retval)) == NJS_ERROR) {
ngx_js_log_exception(vm, ctx->log, "exception");
goto destroy;
}
- ret = ngx_njs_await(ctx, &retval);
+ ret = ngx_njs_await(vm, ctx->log, njs_value_arg(&retval));
if (ret == NGX_ERROR) {
goto destroy;
}
@@ -756,7 +750,8 @@ ngx_njs_clone(ngx_js_ctx_t *ctx, ngx_js_loc_conf_t *cf, void *external)
destroy:
- ngx_engine_njs_destroy(engine, ctx, cf);
+ njs_vm_destroy(vm);
+
return NULL;
}
@@ -790,7 +785,7 @@ ngx_engine_njs_call(ngx_js_ctx_t *ctx, ngx_str_t *fname,
return NGX_ERROR;
}
- ret = ngx_njs_await(ctx, &ctx->retval);
+ ret = ngx_njs_await(vm, ctx->log, njs_value_arg(&ctx->retval));
if (ret == NGX_ERROR) {
return NGX_ERROR;
}
@@ -868,67 +863,61 @@ 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 *ctx, ngx_log_t *log)
+ngx_qjs_execute_pending_jobs(JSContext *cx, ngx_log_t *log)
{
int rc;
- JSValue exception_val, exception_str;;
- JSContext *cx;
+ JSValue value;
+ JSContext *cx1;
const char *exception;
for ( ;; ) {
- rc = JS_ExecutePendingJob(JS_GetRuntime(ctx), &cx);
- if (rc < 0) {
- exception_val = JS_GetException(ctx);
-
- exception_str = JS_ToString(ctx, exception_val);
- JS_FreeValue(ctx, exception_val);
+ rc = JS_ExecutePendingJob(JS_GetRuntime(cx), &cx1);
+ if (rc <= 0) {
+ if (rc == 0) {
+ break;
+ }
- exception = JS_ToCString(ctx, exception_str);
- JS_FreeValue(ctx, exception_str);
+ 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(ctx, exception);
+ JS_FreeCString(cx, exception);
return NGX_ERROR;
}
- if (rc == 0) {
- break;
- }
}
return NGX_OK;
}
-static njs_int_t
-ngx_qjs_await(ngx_js_ctx_t *ctx, JSValue *obj)
+static ngx_int_t
+ngx_qjs_await(JSContext *cx, ngx_log_t *log, JSValue *value)
{
- JSValue ret, rejection;
- ngx_int_t rc;
- JSContext *cx;
- JSPromiseStateEnum state;
-
- cx = ctx->engine->u.qjs.ctx;
+ JSValue ret;
+ ngx_int_t rc;
+ JSPromiseStateEnum state;
- rc = ngx_qjs_execute_pending_jobs(cx, ctx->log);
+ rc = ngx_qjs_execute_pending_jobs(cx, log);
if (rc != NGX_OK) {
return NGX_ERROR;
}
- if (JS_IsObject(*obj)) {
- state = JS_PromiseState(cx, *obj);
- if (state == JS_PROMISE_FULFILLED) {
- ret = JS_PromiseResult(cx, *obj);
- JS_FreeValue(cx, *obj);
- *obj = ret;
- } else if (state == JS_PROMISE_REJECTED) {
- rejection = JS_PromiseResult(cx, *obj);
- JS_FreeValue(cx, *obj);
- *obj = JS_Throw(cx, rejection);
- }
+ 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;
@@ -1002,10 +991,10 @@ ngx_engine_qjs_compile(ngx_js_loc_conf_t *conf, ngx_log_t *log, u_char *start,
ngx_engine_t *
ngx_qjs_clone(ngx_js_ctx_t *ctx, ngx_js_loc_conf_t *cf, void *external)
{
- int rc;
JSValue rv;
njs_mp_t *mp;
uint32_t i, length;
+ ngx_int_t rc;
JSRuntime *rt;
JSContext *cx;
ngx_engine_t *engine;
@@ -1076,8 +1065,6 @@ ngx_qjs_clone(ngx_js_ctx_t *ctx, ngx_js_loc_conf_t *cf, void *external)
goto destroy;
}
- ctx->engine = engine;
-
rv = JS_EvalFunction(cx, rv);
if (JS_IsException(rv)) {
@@ -1085,11 +1072,10 @@ ngx_qjs_clone(ngx_js_ctx_t *ctx, ngx_js_loc_conf_t *cf, void *external)
goto destroy;
}
- rc = ngx_qjs_await(ctx, &rv);
-
+ 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;
}
@@ -1109,8 +1095,8 @@ 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_int_t rc;
JSContext *cx;
cx = ctx->engine->u.qjs.ctx;
@@ -1128,15 +1114,16 @@ ngx_engine_qjs_call(ngx_js_ctx_t *ctx, ngx_str_t *fname,
JS_FreeValue(cx, fn);
if (JS_IsException(val)) {
ngx_qjs_log_exception(ctx->engine, ctx->log, "call exception");
+
return NGX_ERROR;
}
JS_FreeValue(cx, ngx_qjs_arg(ctx->retval));
ngx_qjs_arg(ctx->retval) = val;
- rc = ngx_qjs_await(ctx, &ngx_qjs_arg(ctx->retval));
-
+ 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;
}
@@ -1458,6 +1445,7 @@ ngx_qjs_call(JSContext *cx, JSValue fn, JSValue *argv, int argc)
ret = JS_Call(cx, fn, JS_UNDEFINED, argc, argv);
if (JS_IsException(ret)) {
ngx_qjs_log_exception(ctx->engine, ctx->log, "call exception");
+
return NGX_ERROR;
}
@@ -2276,16 +2264,18 @@ ngx_js_call(njs_vm_t *vm, njs_function_t *func, njs_opaque_value_t *args,
ngx_js_ctx_t *ctx;
ngx_connection_t *c;
- ctx = ngx_external_ctx(vm, njs_vm_external_ptr(vm));
-
ret = njs_vm_call(vm, func, njs_value_arg(args), nargs);
if (ret == NJS_ERROR) {
+
c = ngx_external_connection(vm, njs_vm_external_ptr(vm));
ngx_js_log_exception(vm, c->log, "exception");
+
return NGX_ERROR;
}
+ ctx = ngx_external_ctx(vm, njs_vm_external_ptr(vm));
+
ret = ngx_njs_execute_pending_jobs(vm, ctx->log);
if (ret != NGX_OK) {
return NGX_ERROR;
diff --git a/nginx/ngx_js.h b/nginx/ngx_js.h
index 521d1f20..f3c2493b 100644
--- a/nginx/ngx_js.h
+++ b/nginx/ngx_js.h
@@ -438,7 +438,6 @@ void ngx_js_log(njs_vm_t *vm, njs_external_ptr_t external,
ngx_uint_t level, const char *fmt, ...);
void ngx_js_logger(ngx_connection_t *c, ngx_uint_t level,
const u_char *start, size_t length);
-
char * ngx_js_import(ngx_conf_t *cf, ngx_command_t *cmd, void *conf);
char * ngx_js_engine(ngx_conf_t *cf, ngx_command_t *cmd, void *conf);
char * ngx_js_preload_object(ngx_conf_t *cf, ngx_command_t *cmd, void *conf);
diff --git a/nginx/t/js_promise_pending.t b/nginx/t/js_promise_pending.t
deleted file mode 100644
index 521d6454..00000000
--- a/nginx/t/js_promise_pending.t
+++ /dev/null
@@ -1,114 +0,0 @@
-#!/usr/bin/perl
-
-# (C) F5, Inc.
-
-# Tests for proper handling of pending promises with no waiting events.
-
-###############################################################################
-
-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;
-
- server {
- listen 127.0.0.1:8080;
- server_name localhost;
-
- location /njs {
- js_content test.njs;
- }
-
- location /promise_never_resolves {
- js_content test.promise_never_resolves;
- }
-
- location /promise_with_timeout {
- js_content test.promise_with_timeout;
- }
-
-
- }
-}
-
-EOF
-
-$t->write_file('test.js', <<'EOF');
- function test_njs(r) {
- r.return(200, njs.version);
- }
-
- function promise_never_resolves(r) {
- return new Promise((resolve, reject) => {
- });
- }
-
- function promise_with_timeout(r) {
- const p = new Promise((resolve, reject) => {
- setTimeout(() => {
- resolve("timeout resolved");
- }, 10);
- });
-
- return p.then((value) => {
- r.return(200, "resolved: " + value);
- });
- }
-
- export default {njs: test_njs, promise_never_resolves,
- promise_with_timeout};
-
-EOF
-
-$t->try_run('no njs available')->plan(4);
-
-###############################################################################
-
-# Test basic functionality
-like(http_get('/njs'), qr/\d+\.\d+\.\d+/, 'njs version');
-
-# Test promise with timeout (should work - has waiting events)
-like(http_get('/promise_with_timeout'), qr/resolved: timeout resolved/,
- 'promise with timeout resolves');
-
-# Test pending promise scenario - should trigger error response
-# because it returns a promise that will never resolve with no waiting events
-my $never_resolves_response = http_get('/promise_never_resolves');
-like($never_resolves_response, qr/HTTP\/1\.[01] 500|Internal Server Error/,
- 'never resolving promise causes error');
-
-$t->stop();
-
-# Check error log for the specific pending promise error message
-my $error_log = $t->read_file('error.log');
-
-# Should have no error for promises with waiting events
-unlike($error_log, qr/js exception.*timeout resolved/,
- 'no error for promise with waiting events');
-
-###############################################################################
diff --git a/nginx/t/js_promise_top_level_await.t b/nginx/t/js_promise_top_level_await.t
index def5da13..29dc6322 100644
--- a/nginx/t/js_promise_top_level_await.t
+++ b/nginx/t/js_promise_top_level_await.t
@@ -1,14 +1,9 @@
#!/usr/bin/perl
# (C) F5, Inc.
+# Vadim Zhestikov
-# Tests for proper handling of promises in ngx_qjs_await() with:
-# - Job queue processing limits
-# - Waiting events detection
-# - Promise state handling after job processing
-#
-# Note: ngx_qjs_await() is called during global code evaluation,
-# not function calls.
+# Tests for top-level await of promises in QuickJS engine.
###############################################################################
@@ -47,10 +42,6 @@ http {
listen 127.0.0.1:8080;
server_name localhost;
- location /njs {
- js_content test.njs;
- }
-
location /resolved {
js_content test.test;
}
@@ -64,48 +55,38 @@ http {
EOF
$t->write_file('test.js', <<'EOF');
-var globalResult = await Promise.resolve("resolved value");
+ var globalResult = await Promise.resolve("resolved value");
-function test_njs(r) {
- r.return(200, njs.version);
-}
+ function test(r) {
+ r.return(200, "global result: " + globalResult);
+ }
-function test(r) {
- r.return(200, "global result: " + globalResult);
-}
+ export default {test};
-export default {njs: test_njs, test};
EOF
$t->write_file('fulfilled_test.js', <<'EOF');
-var globalResult = await new Promise((resolve) => {
- Promise.resolve().then(() => {
- resolve("fulfilled value");
+ var globalResult = await new Promise((resolve) => {
+ Promise.resolve().then(() => {
+ resolve("fulfilled value");
+ });
});
-});
-function test(r) {
- r.return(200, "fulfilled result: " + globalResult);
-}
+ function test(r) {
+ r.return(200, "fulfilled result: " + globalResult);
+ }
+
+ export default {test};
-export default {test};
EOF
-$t->try_run('no qjs engine available')->plan(3);
+$t->try_run('no top-level await support')->plan(2);
###############################################################################
-# Test basic functionality
-like(http_get('/njs'), qr/\d+\.\d+\.\d+/, 'njs version');
-
-# Test basic global await with resolved promise
like(http_get('/resolved'), qr/global result: resolved value/,
'basic global await works');
-
-# Test global await with fulfilled promise (via microtask)
like(http_get('/fulfilled'), qr/fulfilled result: fulfilled value/,
'fulfilled promise via microtask works');
-$t->stop();
-
###############################################################################
diff --git a/nginx/t/js_promise_top_level_await_pending.t b/nginx/t/js_promise_top_level_await_pending.t
deleted file mode 100644
index 0a237c0e..00000000
--- a/nginx/t/js_promise_top_level_await_pending.t
+++ /dev/null
@@ -1,86 +0,0 @@
-#!/usr/bin/perl
-
-# (C) F5, Inc.
-
-# This test specifically validates the waiting_events checking functionality
-# in ngx_qjs_await() for promises that remain pending with no waiting events.
-
-###############################################################################
-
-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 pending_test.js;
-
- server {
- listen 127.0.0.1:8080;
- server_name localhost;
-
- location /njs {
- js_content pending_test.njs;
- }
-
- location /pending_no_events {
- js_content pending_test.test;
- }
- }
-}
-
-EOF
-
-$t->write_file('pending_test.js', <<'EOF');
-var globalResult = await new Promise((resolve, reject) => {
-});
-
-function test_njs(r) {
- r.return(200, njs.version);
-}
-
-function test(r) {
- r.return(200, "should never reach this: " + globalResult);
-}
-
-export default {njs: test_njs, test};
-EOF
-
-$t->try_run('no qjs engine available')->plan(2);
-
-###############################################################################
-
-# Test basic functionality (this should also fail due to the pending promise
-# in global code)
-my $njs_response = http_get('/njs');
-like($njs_response, qr/HTTP\/1\.[01] 500|Internal Server Error/,
- 'njs version endpoint fails due to pending promise in global code');
-
-# Test pending promise with no waiting events (should cause error)
-my $pending_response = http_get('/pending_no_events');
-like($pending_response, qr/HTTP\/1\.[01] 500|Internal Server Error/,
- 'pending promise with no waiting events causes error');
-
-###############################################################################
diff --git a/nginx/t/stream_js.t b/nginx/t/stream_js.t
index 7105c1e9..56f5699e 100644
--- a/nginx/t/stream_js.t
+++ b/nginx/t/stream_js.t
@@ -441,7 +441,6 @@ 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(8102))->read(), 'retval: 30', 'asyncf1');
TODO: {
diff --git a/src/njs.h b/src/njs.h
index 85e87af9..1592a234 100644
--- a/src/njs.h
+++ b/src/njs.h
@@ -14,6 +14,7 @@
#define NJS_VERSION "0.9.5"
#define NJS_VERSION_NUMBER 0x000905
+
#include <string.h>
#include <njs_types.h>
#include <njs_clang.h>
diff --git a/src/qjs.c b/src/qjs.c
index 47782815..b7899158 100644
--- a/src/qjs.c
+++ b/src/qjs.c
@@ -6,7 +6,6 @@
#include <qjs.h>
#include <njs.h> /* NJS_VERSION */
-#include <njs_rbtree.h>
#include <sys/types.h>
#include <unistd.h>
diff --git a/src/qjs.h b/src/qjs.h
index 85c245fa..df5bfd11 100644
--- a/src/qjs.h
+++ b/src/qjs.h
@@ -52,6 +52,7 @@ typedef struct {
JSContext *qjs_new_context(JSRuntime *rt, qjs_module_t **addons);
JSValue qjs_call_exit_hook(JSContext *ctx);
+
JSValue qjs_new_uint8_array(JSContext *ctx, int argc, JSValueConst *argv);
JSValue qjs_new_array_buffer(JSContext *cx, uint8_t *src, size_t len);
JSValue qjs_buffer_alloc(JSContext *ctx, size_t size);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment