Skip to content

Commit aa38086

Browse files
Modules: improved handling of results of async handlers.
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'); } In addition added: infinite microtask loops detection, promise handling in global qjs code.
1 parent b605a4d commit aa38086

12 files changed

+830
-60
lines changed

nginx/ngx_js.c

Lines changed: 179 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@
1212
#include "ngx_js.h"
1313

1414

15+
#define NGX_MAX_JOB_ITERATIONS 0x1000
16+
17+
1518
typedef struct {
1619
ngx_queue_t labels;
1720
} ngx_js_console_t;
@@ -683,6 +686,42 @@ ngx_njs_clone(ngx_js_ctx_t *ctx, ngx_js_loc_conf_t *cf, void *external)
683686
}
684687

685688

689+
static njs_int_t
690+
ngx_njs_execute_pending_jobs(njs_vm_t *vm, ngx_js_ctx_t *ctx)
691+
{
692+
njs_int_t ret, job_count;
693+
ngx_str_t exception;
694+
695+
job_count = 0;
696+
697+
for ( ;; ) {
698+
ret = njs_vm_execute_pending_job(vm);
699+
if (ret <= NJS_OK) {
700+
if (ret == NJS_ERROR) {
701+
ngx_js_exception(vm, &exception);
702+
703+
ngx_log_error(NGX_LOG_ERR, ctx->log, 0,
704+
"js job exception: %V", &exception);
705+
return NGX_ERROR;
706+
}
707+
708+
break;
709+
}
710+
711+
job_count++;
712+
713+
if (job_count >= NGX_MAX_JOB_ITERATIONS) {
714+
ngx_log_error(NGX_LOG_ERR, ctx->log, 0,
715+
"js job queue processing exceeded %ui iterations",
716+
NGX_MAX_JOB_ITERATIONS);
717+
return NGX_ERROR;
718+
}
719+
}
720+
721+
return NGX_OK;
722+
}
723+
724+
686725
static ngx_int_t
687726
ngx_engine_njs_call(ngx_js_ctx_t *ctx, ngx_str_t *fname,
688727
njs_opaque_value_t *args, njs_uint_t nargs)
@@ -698,6 +737,11 @@ ngx_engine_njs_call(ngx_js_ctx_t *ctx, ngx_str_t *fname,
698737

699738
vm = ctx->engine->u.njs.vm;
700739

740+
ret = ngx_njs_execute_pending_jobs(vm, ctx);
741+
if (ret != NGX_OK) {
742+
return NGX_ERROR;
743+
}
744+
701745
func = njs_vm_function(vm, &name);
702746
if (func == NULL) {
703747
ngx_log_error(NGX_LOG_ERR, ctx->log, 0,
@@ -716,18 +760,27 @@ ngx_engine_njs_call(ngx_js_ctx_t *ctx, ngx_str_t *fname,
716760
return NGX_ERROR;
717761
}
718762

719-
for ( ;; ) {
720-
ret = njs_vm_execute_pending_job(vm);
721-
if (ret <= NJS_OK) {
722-
if (ret == NJS_ERROR) {
723-
ngx_js_exception(vm, &exception);
763+
ret = ngx_njs_execute_pending_jobs(vm, ctx);
764+
if (ret != NGX_OK) {
765+
return NGX_ERROR;
766+
}
724767

725-
ngx_log_error(NGX_LOG_ERR, ctx->log, 0,
726-
"js job exception: %V", &exception);
727-
return NGX_ERROR;
728-
}
768+
njs_value_t *val = njs_value_arg(&ctx->retval);
769+
if (njs_value_is_promise(val)) {
770+
njs_promise_type_t state = njs_promise_state(val);
729771

730-
break;
772+
if (state == NJS_PROMISE_FULFILL) {
773+
njs_value_assign(val, njs_promise_result(val));
774+
775+
} else if (state == NJS_PROMISE_REJECTED) {
776+
njs_vm_throw(vm, njs_promise_result(val));
777+
778+
} else if (state == NJS_PROMISE_PENDING &&
779+
njs_rbtree_is_empty(&ctx->waiting_events))
780+
{
781+
ngx_log_error(NGX_LOG_ERR, ctx->log, 0,
782+
"js promise pending, no jobs, no waiting_events");
783+
return NGX_ERROR;
731784
}
732785
}
733786

@@ -741,6 +794,7 @@ ngx_engine_njs_external(ngx_engine_t *engine)
741794
return njs_vm_external_ptr(engine->u.njs.vm);
742795
}
743796

797+
744798
static ngx_int_t
745799
ngx_engine_njs_pending(ngx_engine_t *e)
746800
{
@@ -754,8 +808,9 @@ ngx_engine_njs_string(ngx_engine_t *e, njs_opaque_value_t *value,
754808
{
755809
ngx_int_t rc;
756810
njs_str_t s;
811+
njs_value_t *val = njs_value_arg(value);
757812

758-
rc = ngx_js_string(e->u.njs.vm, njs_value_arg(value), &s);
813+
rc = ngx_js_string(e->u.njs.vm, val, &s);
759814

760815
str->data = s.start;
761816
str->len = s.length;
@@ -875,38 +930,60 @@ ngx_engine_qjs_compile(ngx_js_loc_conf_t *conf, ngx_log_t *log, u_char *start,
875930

876931

877932
static JSValue
878-
js_std_await(JSContext *ctx, JSValue obj)
933+
ngx_qjs_await(JSContext *ctx, JSValue obj)
879934
{
880-
int state, err;
881-
JSValue ret;
882-
JSContext *ctx1;
935+
int rc, job_count;
936+
JSValue ret;
937+
JSRuntime *rt;
938+
JSContext *ctx1;
939+
ngx_js_ctx_t *js_ctx;
940+
941+
rt = JS_GetRuntime(ctx);
942+
js_ctx = ngx_qjs_external_ctx(ctx, JS_GetContextOpaque(ctx));
943+
944+
job_count = 0;
945+
946+
for ( ;; ) {
947+
rc = JS_ExecutePendingJob(rt, &ctx1);
948+
if (rc <= 0) {
949+
if (rc == -1) {
950+
JS_FreeValue(ctx, obj);
951+
return JS_EXCEPTION;
952+
}
953+
break;
954+
}
955+
956+
job_count++;
957+
958+
if (job_count >= NGX_MAX_JOB_ITERATIONS) {
959+
JS_FreeValue(ctx, obj);
960+
return JS_ThrowInternalError(ctx, "js job queue processing "
961+
"exceeded %u iterations",
962+
NGX_MAX_JOB_ITERATIONS);
963+
}
964+
}
883965

884-
for (;;) {
885-
state = JS_PromiseState(ctx, obj);
966+
if (JS_IsObject(obj)) {
967+
JSPromiseStateEnum state = JS_PromiseState(ctx, obj);
886968
if (state == JS_PROMISE_FULFILLED) {
887969
ret = JS_PromiseResult(ctx, obj);
888970
JS_FreeValue(ctx, obj);
889-
break;
890-
971+
return ret;
891972
} else if (state == JS_PROMISE_REJECTED) {
892-
ret = JS_Throw(ctx, JS_PromiseResult(ctx, obj));
973+
JSValue rejection = JS_PromiseResult(ctx, obj);
893974
JS_FreeValue(ctx, obj);
894-
break;
895-
975+
return JS_Throw(ctx, rejection);
896976
} else if (state == JS_PROMISE_PENDING) {
897-
err = JS_ExecutePendingJob(JS_GetRuntime(ctx), &ctx1);
898-
if (err < 0) {
899-
/* js_std_dump_error(ctx1); */
977+
if (js_ctx && njs_rbtree_is_empty(&js_ctx->waiting_events)) {
978+
JS_FreeValue(ctx, obj);
979+
return JS_ThrowInternalError(ctx, "js promise pending, "
980+
"no jobs, no waiting_events");
900981
}
901-
902-
} else {
903-
/* not a promise */
904-
ret = obj;
905-
break;
982+
return obj;
906983
}
907984
}
908985

909-
return ret;
986+
return obj;
910987
}
911988

912989

@@ -1000,12 +1077,13 @@ ngx_qjs_clone(ngx_js_ctx_t *ctx, ngx_js_loc_conf_t *cf, void *external)
10001077
goto destroy;
10011078
}
10021079

1003-
rv = js_std_await(cx, rv);
1080+
rv = ngx_qjs_await(cx, rv);
10041081
if (JS_IsException(rv)) {
10051082
ngx_qjs_exception(engine, &exception);
10061083

10071084
ngx_log_error(NGX_LOG_ERR, ctx->log, 0, "js eval exception: %V",
10081085
&exception);
1086+
10091087
goto destroy;
10101088
}
10111089

@@ -1023,6 +1101,44 @@ ngx_qjs_clone(ngx_js_ctx_t *ctx, ngx_js_loc_conf_t *cf, void *external)
10231101
}
10241102

10251103

1104+
static ngx_int_t
1105+
ngx_qjs_execute_pending_jobs(ngx_js_ctx_t *ctx, JSRuntime *rt)
1106+
{
1107+
int rc, job_count;
1108+
ngx_str_t exception;
1109+
JSContext *cx;
1110+
1111+
job_count = 0;
1112+
1113+
for ( ;; ) {
1114+
rc = JS_ExecutePendingJob(rt, &cx);
1115+
if (rc <= 0) {
1116+
if (rc == -1) {
1117+
ngx_qjs_exception(ctx->engine, &exception);
1118+
1119+
ngx_log_error(NGX_LOG_ERR, ctx->log, 0,
1120+
"js job exception: %V", &exception);
1121+
1122+
return NGX_ERROR;
1123+
}
1124+
1125+
break;
1126+
}
1127+
1128+
job_count++;
1129+
1130+
if (job_count >= NGX_MAX_JOB_ITERATIONS) {
1131+
ngx_log_error(NGX_LOG_ERR, ctx->log, 0,
1132+
"js job queue processing exceeded %ui iterations",
1133+
NGX_MAX_JOB_ITERATIONS);
1134+
return NGX_ERROR;
1135+
}
1136+
}
1137+
1138+
return NGX_OK;
1139+
}
1140+
1141+
10261142
static ngx_int_t
10271143
ngx_engine_qjs_call(ngx_js_ctx_t *ctx, ngx_str_t *fname,
10281144
njs_opaque_value_t *args, njs_uint_t nargs)
@@ -1031,9 +1147,15 @@ ngx_engine_qjs_call(ngx_js_ctx_t *ctx, ngx_str_t *fname,
10311147
JSValue fn, val;
10321148
ngx_str_t exception;
10331149
JSRuntime *rt;
1034-
JSContext *cx, *cx1;
1150+
JSContext *cx;
10351151

10361152
cx = ctx->engine->u.qjs.ctx;
1153+
rt = JS_GetRuntime(cx);
1154+
1155+
rc = ngx_qjs_execute_pending_jobs(ctx, rt);
1156+
if (rc != NGX_OK) {
1157+
return NGX_ERROR;
1158+
}
10371159

10381160
fn = ngx_qjs_value(cx, fname);
10391161
if (!JS_IsFunction(cx, fn)) {
@@ -1058,21 +1180,31 @@ ngx_engine_qjs_call(ngx_js_ctx_t *ctx, ngx_str_t *fname,
10581180
JS_FreeValue(cx, ngx_qjs_arg(ctx->retval));
10591181
ngx_qjs_arg(ctx->retval) = val;
10601182

1061-
rt = JS_GetRuntime(cx);
1183+
rc = ngx_qjs_execute_pending_jobs(ctx, rt);
1184+
if (rc != NGX_OK) {
1185+
return NGX_ERROR;
1186+
}
10621187

1063-
for ( ;; ) {
1064-
rc = JS_ExecutePendingJob(rt, &cx1);
1065-
if (rc <= 0) {
1066-
if (rc == -1) {
1067-
ngx_qjs_exception(ctx->engine, &exception);
1188+
JSValue retval = ngx_qjs_arg(ctx->retval);
1189+
if (JS_IsObject(retval)) {
1190+
JSPromiseStateEnum state = JS_PromiseState(cx, retval);
10681191

1069-
ngx_log_error(NGX_LOG_ERR, ctx->log, 0,
1070-
"js job exception: %V", &exception);
1192+
if (state == JS_PROMISE_FULFILLED) {
1193+
JSValue result = JS_PromiseResult(cx, retval);
1194+
JS_FreeValue(cx, ngx_qjs_arg(ctx->retval));
1195+
ngx_qjs_arg(ctx->retval) = result;
10711196

1072-
return NGX_ERROR;
1073-
}
1197+
} else if (state == JS_PROMISE_REJECTED) {
1198+
JSValue rejection = JS_PromiseResult(cx, retval);
1199+
JS_FreeValue(cx, ngx_qjs_arg(ctx->retval));
1200+
ngx_qjs_arg(ctx->retval) = JS_Throw(cx, rejection);
10741201

1075-
break;
1202+
} else if (state == JS_PROMISE_PENDING &&
1203+
njs_rbtree_is_empty(&ctx->waiting_events))
1204+
{
1205+
ngx_log_error(NGX_LOG_ERR, ctx->log, 0,
1206+
"js promise pending, no jobs, no waiting_events");
1207+
return NGX_ERROR;
10761208
}
10771209
}
10781210

@@ -1098,7 +1230,9 @@ static ngx_int_t
10981230
ngx_engine_qjs_string(ngx_engine_t *e, njs_opaque_value_t *value,
10991231
ngx_str_t *str)
11001232
{
1101-
return ngx_qjs_dump_obj(e, ngx_qjs_arg(*value), str);
1233+
JSValue val = ngx_qjs_arg(*value);
1234+
1235+
return ngx_qjs_dump_obj(e, val, str);
11021236
}
11031237

11041238

nginx/t/js_async.t

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,10 @@ events {
3535
http {
3636
%%TEST_GLOBALS_HTTP%%
3737
38-
js_set $test_async test.set_timeout;
39-
js_set $context_var test.context_var;
40-
js_set $test_set_rv_var test.set_rv_var;
38+
js_set $test_async test.set_timeout;
39+
js_set $context_var test.context_var;
40+
js_set $test_set_rv_var test.set_rv_var;
41+
js_set $test_promise_var test.set_promise_var;
4142
4243
js_import test.js;
4344
@@ -85,6 +86,10 @@ http {
8586
return 200 $test_set_rv_var;
8687
}
8788
89+
location /promise_var {
90+
return 200 $test_promise_var;
91+
}
92+
8893
location /await_reject {
8994
js_content test.await_reject;
9095
}
@@ -198,6 +203,13 @@ $t->write_file('test.js', <<EOF);
198203
r.setReturnValue(`retval: \${a1 + a2}`);
199204
}
200205
206+
async function set_promise_var(r) {
207+
const a1 = await pr(10);
208+
const a2 = await pr(20);
209+
210+
return `retval: \${a1 + a2}`;
211+
}
212+
201213
async function timeout(ms) {
202214
return new Promise((resolve, reject) => {
203215
setTimeout(() => {
@@ -213,11 +225,11 @@ $t->write_file('test.js', <<EOF);
213225
214226
export default {njs:test_njs, set_timeout, set_timeout_data,
215227
set_timeout_many, context_var, shared_ctx, limit_rate,
216-
async_content, set_rv_var, await_reject};
228+
async_content, set_rv_var, set_promise_var, await_reject};
217229
218230
EOF
219231

220-
$t->try_run('no njs available')->plan(10);
232+
$t->try_run('no njs available')->plan(11);
221233

222234
###############################################################################
223235

@@ -229,6 +241,7 @@ like(http_get('/limit_rate'), qr/A{50}/, 'limit_rate');
229241

230242
like(http_get('/async_content'), qr/retval: AB/, 'async content');
231243
like(http_get('/set_rv_var'), qr/retval: 30/, 'set return value variable');
244+
like(http_get('/promise_var'), qr/retval: 30/, 'fulfilled promise variable');
232245

233246
http_get('/async_var');
234247
http_get('/await_reject');

0 commit comments

Comments
 (0)