| #!/usr/bin/perl |
| # |
| # (C) Dmitry Volyntsev. |
| # (C) Nginx, Inc. |
| |
| # Tests for subrequests in http njs module. |
| |
| ############################################################################### |
| |
| use warnings; |
| use strict; |
| |
| use Test::More; |
| |
| use Socket qw/ CRLF /; |
| |
| BEGIN { use FindBin; chdir($FindBin::Bin); } |
| |
| use lib 'lib'; |
| use Test::Nginx; |
| |
| ############################################################################### |
| |
| select STDERR; $| = 1; |
| select STDOUT; $| = 1; |
| |
| eval { require JSON::PP; }; |
| plan(skip_all => "JSON::PP not installed") if $@; |
| |
| my $t = Test::Nginx->new()->has(qw/http rewrite proxy cache/) |
| ->write_file_expand('nginx.conf', <<'EOF'); |
| |
| %%TEST_GLOBALS%% |
| |
| daemon off; |
| |
| events { |
| } |
| |
| http { |
| %%TEST_GLOBALS_HTTP%% |
| |
| proxy_cache_path %%TESTDIR%%/cache1 |
| keys_zone=ON:1m use_temp_path=on; |
| |
| js_include test.js; |
| |
| js_set $async_var async_var; |
| js_set $subrequest_var subrequest_var; |
| |
| server { |
| listen 127.0.0.1:8080; |
| server_name localhost; |
| |
| location /njs { |
| js_content test_njs; |
| } |
| |
| location /sr { |
| js_content sr; |
| } |
| |
| location /sr_pr { |
| js_content sr_pr; |
| } |
| |
| location /sr_args { |
| js_content sr_args; |
| } |
| |
| location /sr_options_args { |
| js_content sr_options_args; |
| } |
| |
| location /sr_options_args_pr { |
| js_content sr_options_args_pr; |
| } |
| |
| location /sr_options_method { |
| js_content sr_options_method; |
| } |
| |
| location /sr_options_method_pr { |
| js_content sr_options_method_pr; |
| } |
| |
| location /sr_options_body { |
| js_content sr_options_body; |
| } |
| |
| location /sr_options_method_head { |
| js_content sr_options_method_head; |
| } |
| |
| location /sr_body { |
| js_content sr_body; |
| } |
| |
| location /sr_body_pr { |
| js_content sr_body_pr; |
| } |
| |
| location /sr_body_special { |
| js_content sr_body_special; |
| } |
| |
| location /sr_in_variable_handler { |
| set $_ $async_var; |
| js_content sr_in_variable_handler; |
| } |
| |
| location /sr_detached_in_variable_handler { |
| return 200 $subrequest_var; |
| } |
| |
| location /sr_error_page { |
| set $_ $async_var; |
| error_page 404 /return; |
| return 404; |
| } |
| |
| location /sr_js_in_subrequest { |
| js_content sr_js_in_subrequest; |
| } |
| |
| location /sr_js_in_subrequest_pr { |
| js_content sr_js_in_subrequest_pr; |
| } |
| |
| location /sr_file { |
| js_content sr_file; |
| } |
| |
| location /sr_cache { |
| js_content sr_cache; |
| } |
| |
| |
| location /sr_unavail { |
| js_content sr_unavail; |
| } |
| |
| location /sr_unavail_pr { |
| js_content sr_unavail_pr; |
| } |
| |
| location /sr_broken { |
| js_content sr_broken; |
| } |
| |
| location /sr_too_large { |
| js_content sr_too_large; |
| } |
| |
| location /sr_out_of_order { |
| js_content sr_out_of_order; |
| } |
| |
| location /sr_except_not_a_func { |
| js_content sr_except_not_a_func; |
| } |
| |
| location /sr_except_failed_to_convert_arg { |
| js_content sr_except_failed_to_convert_arg; |
| } |
| |
| location /sr_except_failed_to_convert_options_arg { |
| js_content sr_except_failed_to_convert_options_arg; |
| } |
| |
| location /sr_except_invalid_options_header_only { |
| js_content sr_except_invalid_options_header_only; |
| } |
| |
| location /sr_in_sr_callback { |
| js_content sr_in_sr_callback; |
| } |
| |
| location /sr_uri_except { |
| js_content sr_uri_except; |
| } |
| |
| |
| location /file/ { |
| alias %%TESTDIR%%/; |
| } |
| |
| location /p/ { |
| proxy_cache $arg_c; |
| proxy_pass http://127.0.0.1:8081/; |
| } |
| |
| location /daemon/ { |
| proxy_pass http://127.0.0.1:8082/; |
| } |
| |
| location /too_large/ { |
| subrequest_output_buffer_size 3; |
| proxy_pass http://127.0.0.1:8081/; |
| } |
| |
| location /sr_in_sr { |
| js_content sr_in_sr; |
| } |
| |
| location /unavail { |
| proxy_pass http://127.0.0.1:8084/; |
| } |
| |
| location /sr_parent { |
| js_content sr_parent; |
| } |
| |
| location /js_sub { |
| js_content js_sub; |
| } |
| |
| location /return { |
| return 200 '["$request_method"]'; |
| } |
| } |
| |
| server { |
| listen 127.0.0.1:8081; |
| server_name localhost; |
| |
| location /sub1 { |
| add_header H $arg_h; |
| return 206 '{"a": {"b": 1}}'; |
| } |
| |
| location /sub2 { |
| return 404 '{"e": "msg"}'; |
| } |
| |
| location /method { |
| return 200 '["$request_method"]'; |
| } |
| |
| location /body { |
| js_content body; |
| } |
| |
| location /detached { |
| js_content detached; |
| } |
| |
| location /delayed { |
| js_content delayed; |
| } |
| } |
| |
| server { |
| listen 127.0.0.1:8084; |
| server_name localhost; |
| |
| return 444; |
| } |
| } |
| |
| EOF |
| |
| $t->write_file('test.js', <<EOF); |
| function test_njs(r) { |
| r.return(200, njs.version); |
| } |
| |
| function sr(r) { |
| subrequest_fn(r, ['/p/sub2'], ['uri', 'status']) |
| } |
| |
| function sr_pr(r) { |
| r.subrequest('/p/sub1', 'h=xxx') |
| .then(reply => r.return(200, JSON.stringify({h:reply.headersOut.h}))) |
| } |
| |
| function sr_args(r) { |
| r.subrequest('/p/sub1', 'h=xxx', reply => { |
| r.return(200, JSON.stringify({h:reply.headersOut.h})); |
| }); |
| } |
| |
| function sr_options_args(r) { |
| r.subrequest('/p/sub1', {args:'h=xxx'}, reply => { |
| r.return(200, JSON.stringify({h:reply.headersOut.h})); |
| }); |
| } |
| |
| function sr_options_args_pr(r) { |
| r.subrequest('/p/sub1', {args:'h=xxx'}) |
| .then(reply => r.return(200, JSON.stringify({h:reply.headersOut.h}))) |
| } |
| |
| function sr_options_method(r) { |
| r.subrequest('/p/method', {method:r.args.m}, body_fwd_cb); |
| } |
| |
| function sr_options_method_pr(r) { |
| r.subrequest('/p/method', {method:r.args.m}) |
| .then(body_fwd_cb); |
| } |
| |
| function sr_options_body(r) { |
| r.subrequest('/p/body', {method:'POST', body:'["REQ-BODY"]'}, |
| body_fwd_cb); |
| } |
| |
| function sr_options_method_head(r) { |
| r.subrequest('/p/method', {method:'HEAD'}, reply => { |
| r.return(200, JSON.stringify({c:reply.status})); |
| }); |
| } |
| |
| function sr_body(r) { |
| r.subrequest('/p/sub1', body_fwd_cb); |
| } |
| |
| function sr_body_pr(r) { |
| r.subrequest('/p/sub1') |
| .then(body_fwd_cb); |
| } |
| |
| function sr_body_special(r) { |
| r.subrequest('/p/sub2', body_fwd_cb); |
| } |
| |
| function body(r) { |
| r.return(200, r.variables.request_body); |
| } |
| |
| function delayed(r) { |
| setTimeout(r => r.return(200), 100, r); |
| } |
| |
| function detached(r) { |
| var method = r.variables.request_method; |
| r.log(`DETACHED: \${method} args: \${r.variables.args}`); |
| |
| r.return(200); |
| } |
| |
| function sr_in_variable_handler(r) { |
| } |
| |
| function async_var(r) { |
| r.subrequest('/p/delayed', reply => { |
| r.return(200, JSON.stringify(["CB-VAR"])); |
| }); |
| |
| return ""; |
| } |
| |
| function subrequest_var(r) { |
| r.subrequest('/p/detached', {detached:true}); |
| r.subrequest('/p/detached', {detached:true, args:'a=yyy', |
| method:'POST'}); |
| |
| return "subrequest_var"; |
| } |
| |
| function sr_file(r) { |
| r.subrequest('/file/t', body_fwd_cb); |
| } |
| |
| function sr_cache(r) { |
| r.subrequest('/p/t', body_fwd_cb); |
| } |
| |
| function sr_unavail(req) { |
| subrequest_fn(req, ['/unavail'], ['uri', 'status']); |
| } |
| |
| function sr_unavail_pr(req) { |
| subrequest_fn_pr(req, ['/unavail'], ['uri', 'status']); |
| } |
| |
| function sr_broken(r) { |
| r.subrequest('/daemon/unfinished', reply => { |
| r.return(200, JSON.stringify({code:reply.status})); |
| }); |
| } |
| |
| function sr_too_large(r) { |
| r.subrequest('/too_large/t', body_fwd_cb); |
| } |
| |
| function sr_in_sr(r) { |
| r.subrequest('/sr', body_fwd_cb); |
| } |
| |
| function sr_js_in_subrequest(r) { |
| r.subrequest('/js_sub', body_fwd_cb); |
| } |
| |
| function sr_js_in_subrequest_pr(r) { |
| r.subrequest('/js_sub') |
| .then(body_fwd_cb); |
| } |
| |
| function sr_in_sr_callback(r) { |
| r.subrequest('/return', function (reply) { |
| try { |
| reply.subrequest('/return'); |
| |
| } catch (err) { |
| r.return(200, JSON.stringify({e:err.message})); |
| return; |
| } |
| |
| r.return(200); |
| }); |
| } |
| |
| function sr_parent(r) { |
| try { |
| var parent = r.parent; |
| |
| } catch (err) { |
| r.return(200, JSON.stringify({e:err.message})); |
| return; |
| } |
| |
| r.return(200); |
| } |
| |
| function sr_out_of_order(r) { |
| subrequest_fn(r, ['/p/delayed', '/p/sub1', '/unknown'], |
| ['uri', 'status']); |
| } |
| |
| function collect(replies, props, total, reply) { |
| reply.log(`subrequest handler: \${reply.uri} status: \${reply.status}`) |
| |
| var rep = {}; |
| props.forEach(p => {rep[p] = reply[p]}); |
| |
| replies.push(rep); |
| |
| if (replies.length == total) { |
| reply.parent.return(200, JSON.stringify(replies)); |
| } |
| } |
| |
| function subrequest_fn(r, subs, props) { |
| var replies = []; |
| |
| subs.forEach(sr => |
| r.subrequest(sr, collect.bind(null, replies, |
| props, subs.length))); |
| } |
| |
| function subrequest_fn_pr(r, subs, props) { |
| var replies = []; |
| |
| subs.forEach(sr => r.subrequest(sr) |
| .then(collect.bind(null, replies, props, subs.length))); |
| } |
| |
| function sr_except_not_a_func(r) { |
| r.subrequest('/sub1', 'a=1', 'b'); |
| } |
| |
| function sr_except_failed_to_convert_arg(r) { |
| r.subrequest('/sub1', Symbol.toStringTag, ()=>{}); |
| } |
| |
| function sr_except_failed_to_convert_options_arg(r) { |
| r.subrequest('/sub1', {args:r.args}, ()=>{}); |
| } |
| |
| function sr_uri_except(r) { |
| r.subrequest(Symbol.toStringTag, 'a=1', 'b'); |
| } |
| |
| function body_fwd_cb(r) { |
| r.parent.return(200, JSON.stringify(JSON.parse(r.responseBody))); |
| } |
| |
| function js_sub(r) { |
| r.return(200, '["JS-SUB"]'); |
| } |
| |
| EOF |
| |
| $t->write_file('t', '["SEE-THIS"]'); |
| |
| $t->try_run('no njs available')->plan(31); |
| $t->run_daemon(\&http_daemon); |
| |
| ############################################################################### |
| |
| is(get_json('/sr'), '[{"status":404,"uri":"/p/sub2"}]', 'sr'); |
| is(get_json('/sr_args'), '{"h":"xxx"}', 'sr_args'); |
| is(get_json('/sr_options_args'), '{"h":"xxx"}', 'sr_options_args'); |
| is(get_json('/sr_options_method?m=POST'), '["POST"]', 'sr method POST'); |
| is(get_json('/sr_options_method?m=PURGE'), '["PURGE"]', 'sr method PURGE'); |
| is(get_json('/sr_options_body'), '["REQ-BODY"]', 'sr_options_body'); |
| is(get_json('/sr_options_method_head'), '{"c":200}', 'sr_options_method_head'); |
| is(get_json('/sr_body'), '{"a":{"b":1}}', 'sr_body'); |
| is(get_json('/sr_body_special'), '{"e":"msg"}', 'sr_body_special'); |
| is(get_json('/sr_in_variable_handler'), '["CB-VAR"]', 'sr_in_variable_handler'); |
| |
| $t->todo_alerts() if $t->read_file('nginx.conf') =~ /aio (on|threads)/ |
| and !$t->has_version('1.17.9'); |
| |
| TODO: { |
| local $TODO = 'header already sent' if $t->read_file('nginx.conf') =~ /aio on/ |
| and !$t->has_version('1.17.9'); |
| local $TODO = 'open socket left' if $t->read_file('nginx.conf') =~ /aio thread/ |
| and !$t->has_version('1.17.9'); |
| |
| is(get_json('/sr_file'), '["SEE-THIS"]', 'sr_file'); |
| |
| } |
| |
| is(get_json('/sr_cache?c=1'), '["SEE-THIS"]', 'sr_cache'); |
| is(get_json('/sr_cache?c=1'), '["SEE-THIS"]', 'sr_cached'); |
| is(get_json('/sr_js_in_subrequest'), '["JS-SUB"]', 'sr_js_in_subrequest'); |
| is(get_json('/sr_unavail'), '[{"status":502,"uri":"/unavail"}]', |
| 'sr_unavail'); |
| is(get_json('/sr_out_of_order'), |
| '[{"status":404,"uri":"/unknown"},' . |
| '{"status":206,"uri":"/p/sub1"},' . |
| '{"status":200,"uri":"/p/delayed"}]', |
| 'sr_multi'); |
| |
| my $ver = http_get('/njs'); |
| |
| TODO: { |
| local $TODO = 'not yet' |
| unless $ver =~ /^([.0-9]+)$/m && $1 ge '0.3.8'; |
| |
| is(get_json('/sr_pr'), '{"h":"xxx"}', 'sr_promise'); |
| is(get_json('/sr_options_args_pr'), '{"h":"xxx"}', 'sr_options_args_pr'); |
| is(get_json('/sr_options_method_pr?m=PUT'), '["PUT"]', 'sr method PUT'); |
| is(get_json('/sr_body_pr'), '{"a":{"b":1}}', 'sr_body_pr'); |
| is(get_json('/sr_js_in_subrequest_pr'), '["JS-SUB"]', 'sr_js_in_subrequest_pr'); |
| is(get_json('/sr_unavail_pr'), '[{"status":502,"uri":"/unavail"}]', |
| 'sr_unavail_pr'); |
| |
| } |
| |
| TODO: { |
| local $TODO = 'not yet' |
| unless $ver =~ /^([.0-9]+)$/m && $1 ge '0.3.9'; |
| |
| like(http_get('/sr_detached_in_variable_handler'), qr/subrequest_var/, |
| 'sr_detached_in_variable_handler'); |
| } |
| |
| http_get('/sr_broken'); |
| http_get('/sr_in_sr'); |
| http_get('/sr_in_variable_handler'); |
| http_get('/sr_error_page'); |
| http_get('/sr_too_large'); |
| http_get('/sr_except_not_a_func'); |
| http_get('/sr_except_failed_to_convert_arg'); |
| http_get('/sr_except_failed_to_convert_options_arg'); |
| http_get('/sr_uri_except'); |
| |
| is(get_json('/sr_in_sr_callback'), |
| '{"e":"subrequest can only be created for the primary request"}', |
| 'subrequest for non-primary request'); |
| |
| $t->stop(); |
| |
| ok(index($t->read_file('error.log'), 'callback is not a function') > 0, |
| 'subrequest cb exception'); |
| ok(index($t->read_file('error.log'), 'failed to convert uri arg') > 0, |
| 'subrequest uri exception'); |
| ok(index($t->read_file('error.log'), 'failed to convert args') > 0, |
| 'subrequest invalid args exception'); |
| ok(index($t->read_file('error.log'), 'too big subrequest response') > 0, |
| 'subrequest too large body'); |
| ok(index($t->read_file('error.log'), 'subrequest creation failed') > 0, |
| 'subrequest creation failed'); |
| ok(index($t->read_file('error.log'), |
| 'js subrequest: failed to get the parent context') > 0, |
| 'zero parent ctx'); |
| |
| TODO: { |
| local $TODO = 'not yet' |
| unless $ver =~ /^([.0-9]+)$/m && $1 ge '0.3.9'; |
| |
| ok(index($t->read_file('error.log'), 'DETACHED') > 0, |
| 'detached subrequest'); |
| } |
| |
| ############################################################################### |
| |
| sub recode { |
| my $json; |
| eval { $json = JSON::PP::decode_json(shift) }; |
| |
| if ($@) { |
| return "<failed to parse JSON>"; |
| } |
| |
| JSON::PP->new()->canonical()->encode($json); |
| } |
| |
| sub get_json { |
| http_get(shift) =~ /\x0d\x0a?\x0d\x0a?(.*)/ms; |
| recode($1); |
| } |
| |
| ############################################################################### |
| |
| sub http_daemon { |
| my $server = IO::Socket::INET->new( |
| Proto => 'tcp', |
| LocalAddr => '127.0.0.1:' . port(8082), |
| Listen => 5, |
| Reuse => 1 |
| ) |
| or die "Can't create listening socket: $!\n"; |
| |
| local $SIG{PIPE} = 'IGNORE'; |
| |
| while (my $client = $server->accept()) { |
| $client->autoflush(1); |
| |
| my $headers = ''; |
| my $uri = ''; |
| |
| while (<$client>) { |
| $headers .= $_; |
| last if (/^\x0d?\x0a?$/); |
| } |
| |
| $uri = $1 if $headers =~ /^\S+\s+([^ ]+)\s+HTTP/i; |
| |
| if ($uri eq '/unfinished') { |
| print $client |
| "HTTP/1.1 200 OK" . CRLF . |
| "Transfer-Encoding: chunked" . CRLF . |
| "Content-Length: 100" . CRLF . |
| CRLF . |
| "unfinished" . CRLF; |
| close($client); |
| } |
| } |
| } |
| |
| ############################################################################### |