Skip to content

Commit 760f776

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 d157f56 commit 760f776

13 files changed

+842
-112
lines changed

nginx/ngx_js.c

Lines changed: 189 additions & 96 deletions
Large diffs are not rendered by default.

nginx/ngx_js.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,8 @@ typedef struct {
158158
njs_opaque_value_t retval; \
159159
njs_arr_t *rejected_promises; \
160160
njs_rbtree_t waiting_events; \
161-
ngx_socket_t event_id
161+
ngx_socket_t event_id; \
162+
ngx_int_t broken
162163

163164

164165
#define ngx_js_add_event(ctx, event) \

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');

nginx/t/js_promise_infinite_jobs.t

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
#!/usr/bin/perl
2+
3+
# (C) Test for infinite loop protection in microtask queue processing
4+
5+
# Tests for proper handling of infinite microtask loops similar to Node.js protection.
6+
7+
###############################################################################
8+
9+
use warnings;
10+
use strict;
11+
12+
use Test::More;
13+
14+
BEGIN { use FindBin; chdir($FindBin::Bin); }
15+
16+
use lib 'lib';
17+
use Test::Nginx;
18+
19+
###############################################################################
20+
21+
select STDERR; $| = 1;
22+
select STDOUT; $| = 1;
23+
24+
my $t = Test::Nginx->new()->has(qw/http rewrite/)
25+
->write_file_expand('nginx.conf', <<'EOF');
26+
27+
%%TEST_GLOBALS%%
28+
29+
daemon off;
30+
31+
events {
32+
}
33+
34+
http {
35+
%%TEST_GLOBALS_HTTP%%
36+
37+
js_import test.js;
38+
39+
server {
40+
listen 127.0.0.1:8080;
41+
server_name localhost;
42+
43+
location /njs {
44+
js_content test.njs;
45+
}
46+
47+
location /infinite_microtasks {
48+
js_content test.infinite_microtasks;
49+
}
50+
51+
location /recursive_promises {
52+
js_content test.recursive_promises;
53+
}
54+
55+
location /normal_promise_chain {
56+
js_content test.normal_promise_chain;
57+
}
58+
59+
}
60+
}
61+
62+
EOF
63+
64+
$t->write_file('test.js', <<'EOF');
65+
function test_njs(r) {
66+
r.return(200, njs.version);
67+
}
68+
69+
function infinite_microtasks(r) {
70+
// Create an infinite microtask loop - should trigger protection limit
71+
function infiniteLoop() {
72+
return Promise.resolve().then(() => {
73+
// Recursively create more microtasks
74+
return infiniteLoop();
75+
});
76+
}
77+
78+
return infiniteLoop();
79+
}
80+
81+
function recursive_promises(r) {
82+
// Another variation of infinite microtask generation
83+
let count = 0;
84+
function createPromise() {
85+
return new Promise((resolve) => {
86+
resolve();
87+
}).then(() => {
88+
count++;
89+
// Keep creating more promises indefinitely
90+
return createPromise();
91+
});
92+
}
93+
94+
return createPromise();
95+
}
96+
97+
function normal_promise_chain(r) {
98+
// Normal promise chain that should complete without hitting the limit
99+
let result = Promise.resolve(1);
100+
101+
// Create a reasonable chain of 50 promises (well under the NGX_MAX_JOB_ITERATIONS limit)
102+
for (let i = 0; i < 50; i++) {
103+
result = result.then((value) => value + 1);
104+
}
105+
106+
return result.then((finalValue) => {
107+
r.return(200, "completed with value: " + finalValue);
108+
});
109+
}
110+
111+
export default {njs: test_njs, infinite_microtasks, recursive_promises, normal_promise_chain};
112+
113+
EOF
114+
115+
$t->try_run('no njs available')->plan(5);
116+
117+
###############################################################################
118+
119+
# Test basic functionality
120+
like(http_get('/njs'), qr/\d+\.\d+\.\d+/, 'njs version');
121+
122+
# Test normal promise chain (should work - under the limit)
123+
like(http_get('/normal_promise_chain'), qr/completed with value: 51/, 'normal promise chain completes');
124+
125+
# Test infinite microtasks scenario - should trigger protection limit
126+
my $infinite_response = http_get('/infinite_microtasks');
127+
like($infinite_response, qr/HTTP\/1\.[01] 500|Internal Server Error/, 'infinite microtasks causes error');
128+
129+
# Test recursive promises scenario - should also trigger protection limit
130+
my $recursive_response = http_get('/recursive_promises');
131+
like($recursive_response, qr/HTTP\/1\.[01] 500|Internal Server Error/, 'recursive promises causes error');
132+
133+
$t->stop();
134+
135+
# Check error log for the specific infinite loop protection messages
136+
my $error_log = $t->read_file('error.log');
137+
138+
# Should have error messages for job queue limit exceeded
139+
ok(index($error_log, 'js job queue processing exceeded') > 0,
140+
'job queue limit exceeded message logged');
141+
142+
###############################################################################

nginx/t/js_promise_pending.t

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
#!/usr/bin/perl
2+
3+
# (C) Test for NJS promise pending state handling
4+
5+
# Tests for proper handling of pending promises with no waiting events.
6+
7+
###############################################################################
8+
9+
use warnings;
10+
use strict;
11+
12+
use Test::More;
13+
14+
BEGIN { use FindBin; chdir($FindBin::Bin); }
15+
16+
use lib 'lib';
17+
use Test::Nginx;
18+
19+
###############################################################################
20+
21+
select STDERR; $| = 1;
22+
select STDOUT; $| = 1;
23+
24+
my $t = Test::Nginx->new()->has(qw/http rewrite/)
25+
->write_file_expand('nginx.conf', <<'EOF');
26+
27+
%%TEST_GLOBALS%%
28+
29+
daemon off;
30+
31+
events {
32+
}
33+
34+
http {
35+
%%TEST_GLOBALS_HTTP%%
36+
37+
js_import test.js;
38+
39+
server {
40+
listen 127.0.0.1:8080;
41+
server_name localhost;
42+
43+
location /njs {
44+
js_content test.njs;
45+
}
46+
47+
location /promise_never_resolves {
48+
js_content test.promise_never_resolves;
49+
}
50+
51+
location /promise_with_timeout {
52+
js_content test.promise_with_timeout;
53+
}
54+
55+
56+
}
57+
}
58+
59+
EOF
60+
61+
$t->write_file('test.js', <<'EOF');
62+
function test_njs(r) {
63+
r.return(200, njs.version);
64+
}
65+
66+
function promise_never_resolves(r) {
67+
// Create a promise that never resolves and has no pending events
68+
// This should trigger the condition:
69+
// promise_data->state == NJS_PROMISE_PENDING &&
70+
// njs_rbtree_is_empty(&ctx->waiting_events)
71+
return new Promise((resolve, reject) => {
72+
// Intentionally never call resolve or reject
73+
// No setTimeout, no async operations - truly pending with no events
74+
});
75+
}
76+
77+
function promise_with_timeout(r) {
78+
// Create a promise with a timeout (has waiting events)
79+
const p = new Promise((resolve, reject) => {
80+
setTimeout(() => {
81+
resolve("timeout resolved");
82+
}, 10);
83+
});
84+
85+
// This should NOT trigger pending error because there are waiting events
86+
return p.then((value) => {
87+
r.return(200, "resolved: " + value);
88+
});
89+
}
90+
91+
export default {njs: test_njs, promise_never_resolves, promise_with_timeout};
92+
93+
EOF
94+
95+
$t->try_run('no njs available')->plan(5);
96+
97+
###############################################################################
98+
99+
# Test basic functionality
100+
like(http_get('/njs'), qr/\d+\.\d+\.\d+/, 'njs version');
101+
102+
# Test promise with timeout (should work - has waiting events)
103+
like(http_get('/promise_with_timeout'), qr/resolved: timeout resolved/, 'promise with timeout resolves');
104+
105+
# Test pending promise scenario - should trigger error response
106+
# because it returns a promise that will never resolve with no waiting events
107+
my $never_resolves_response = http_get('/promise_never_resolves');
108+
like($never_resolves_response, qr/HTTP\/1\.[01] 500|Internal Server Error/, 'never resolving promise causes error');
109+
110+
$t->stop();
111+
112+
# Check error log for the specific pending promise error message
113+
my $error_log = $t->read_file('error.log');
114+
115+
# Now that we use ngx_log_error, the specific error message should appear in the log
116+
ok(index($error_log, 'js promise pending, no jobs, no waiting_events') > 0,
117+
'pending promise error message logged');
118+
119+
# Should have no error for promises with waiting events
120+
unlike($error_log, qr/js exception.*timeout resolved/, 'no error for promise with waiting events');
121+
122+
###############################################################################

0 commit comments

Comments
 (0)