Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save xeioex/ecdad59bd9cd47adf43f967e5e48da10 to your computer and use it in GitHub Desktop.

Select an option

Save xeioex/ecdad59bd9cd47adf43f967e5e48da10 to your computer and use it in GitHub Desktop.
From 55165c8b2834ca46901b38a2910f0502a35a9bc3 Mon Sep 17 00:00:00 2001
From: Dmitry Volyntsev <xeioex@nginx.com>
Date: Thu, 5 Mar 2026 20:44:02 -0800
Subject: [PATCH 1/2] Modules: added var alias for variables object.
This allows to reference nginx variables with a shorter
syntax: `r.var.uri` or `s.var.bytes_sent`.
---
nginx/ngx_http_js_module.c | 24 ++++++++++++++++++++++++
nginx/ngx_stream_js_module.c | 24 ++++++++++++++++++++++++
nginx/t/js_variables.t | 30 +++++++++++++++++++++++++++---
nginx/t/stream_js_variables.t | 33 +++++++++++++++++++++++++++++++--
4 files changed, 106 insertions(+), 5 deletions(-)
diff --git a/nginx/ngx_http_js_module.c b/nginx/ngx_http_js_module.c
index 20308a81..20bc1a64 100644
--- a/nginx/ngx_http_js_module.c
+++ b/nginx/ngx_http_js_module.c
@@ -990,6 +990,16 @@ static njs_external_t ngx_http_js_ext_request[] = {
}
},
+ {
+ .flags = NJS_EXTERN_OBJECT,
+ .name.string = njs_str("var"),
+ .u.object = {
+ .writable = 1,
+ .prop_handler = ngx_http_js_ext_variables,
+ .magic32 = NGX_JS_STRING,
+ }
+ },
+
{
.flags = NJS_EXTERN_OBJECT,
.name.string = njs_str("variables"),
@@ -1034,6 +1044,16 @@ static njs_external_t ngx_http_js_ext_periodic_session[] = {
}
},
+ {
+ .flags = NJS_EXTERN_OBJECT,
+ .name.string = njs_str("var"),
+ .u.object = {
+ .writable = 1,
+ .prop_handler = ngx_http_js_periodic_session_variables,
+ .magic32 = NGX_JS_STRING,
+ }
+ },
+
{
.flags = NJS_EXTERN_OBJECT,
.name.string = njs_str("variables"),
@@ -1155,6 +1175,8 @@ static const JSCFunctionListEntry ngx_http_qjs_ext_request[] = {
JS_CFUNC_DEF("subrequest", 3, ngx_http_qjs_ext_subrequest),
JS_CGETSET_MAGIC_DEF("uri", ngx_http_qjs_ext_string, NULL,
offsetof(ngx_http_request_t, uri)),
+ JS_CGETSET_MAGIC_DEF("var", ngx_http_qjs_ext_variables,
+ NULL, NGX_JS_STRING),
JS_CGETSET_MAGIC_DEF("variables", ngx_http_qjs_ext_variables,
NULL, NGX_JS_STRING),
JS_CFUNC_MAGIC_DEF("warn", 1, ngx_http_qjs_ext_log, NGX_LOG_WARN),
@@ -1166,6 +1188,8 @@ static const JSCFunctionListEntry ngx_http_qjs_ext_periodic[] = {
JS_PROP_CONFIGURABLE),
JS_CGETSET_MAGIC_DEF("rawVariables", ngx_http_qjs_ext_periodic_variables,
NULL, NGX_JS_BUFFER),
+ JS_CGETSET_MAGIC_DEF("var", ngx_http_qjs_ext_periodic_variables,
+ NULL, NGX_JS_STRING),
JS_CGETSET_MAGIC_DEF("variables", ngx_http_qjs_ext_periodic_variables,
NULL, NGX_JS_STRING),
};
diff --git a/nginx/ngx_stream_js_module.c b/nginx/ngx_stream_js_module.c
index b21b701d..76d8907d 100644
--- a/nginx/ngx_stream_js_module.c
+++ b/nginx/ngx_stream_js_module.c
@@ -685,6 +685,16 @@ static njs_external_t ngx_stream_js_ext_session[] = {
}
},
+ {
+ .flags = NJS_EXTERN_OBJECT,
+ .name.string = njs_str("var"),
+ .u.object = {
+ .writable = 1,
+ .prop_handler = ngx_stream_js_ext_variables,
+ .magic32 = NGX_JS_STRING,
+ }
+ },
+
{
.flags = NJS_EXTERN_OBJECT,
.name.string = njs_str("variables"),
@@ -729,6 +739,16 @@ static njs_external_t ngx_stream_js_ext_periodic_session[] = {
}
},
+ {
+ .flags = NJS_EXTERN_OBJECT,
+ .name.string = njs_str("var"),
+ .u.object = {
+ .writable = 1,
+ .prop_handler = ngx_stream_js_periodic_variables,
+ .magic32 = NGX_JS_STRING,
+ }
+ },
+
{
.flags = NJS_EXTERN_OBJECT,
.name.string = njs_str("variables"),
@@ -853,6 +873,8 @@ static const JSCFunctionListEntry ngx_stream_qjs_ext_session[] = {
JS_CFUNC_DEF("setReturnValue", 1, ngx_stream_qjs_ext_set_return_value),
JS_CGETSET_MAGIC_DEF("status", ngx_stream_qjs_ext_uint, NULL,
offsetof(ngx_stream_session_t, status)),
+ JS_CGETSET_MAGIC_DEF("var", ngx_stream_qjs_ext_variables,
+ NULL, NGX_JS_STRING),
JS_CGETSET_MAGIC_DEF("variables", ngx_stream_qjs_ext_variables,
NULL, NGX_JS_STRING),
JS_CFUNC_MAGIC_DEF("warn", 1, ngx_stream_qjs_ext_log, NGX_LOG_WARN),
@@ -864,6 +886,8 @@ static const JSCFunctionListEntry ngx_stream_qjs_ext_periodic[] = {
JS_PROP_CONFIGURABLE),
JS_CGETSET_MAGIC_DEF("rawVariables", ngx_stream_qjs_ext_periodic_variables,
NULL, NGX_JS_BUFFER),
+ JS_CGETSET_MAGIC_DEF("var", ngx_stream_qjs_ext_periodic_variables,
+ NULL, NGX_JS_STRING),
JS_CGETSET_MAGIC_DEF("variables", ngx_stream_qjs_ext_periodic_variables,
NULL, NGX_JS_STRING),
};
diff --git a/nginx/t/js_variables.t b/nginx/t/js_variables.t
index 6f1eb173..a22bbced 100644
--- a/nginx/t/js_variables.t
+++ b/nginx/t/js_variables.t
@@ -35,7 +35,8 @@ events {
http {
%%TEST_GLOBALS_HTTP%%
- js_set $test_var test.variable;
+ js_set $test_var test.variable;
+ js_set $test_short_var test.short_var;
js_import test.js;
@@ -50,10 +51,18 @@ http {
return 200 $test_var$foo;
}
+ location /var_set_short {
+ return 200 $test_short_var$foo;
+ }
+
location /content_set {
js_content test.content_set;
}
+ location /content_set_short {
+ js_content test.short_content_set;
+ }
+
location /not_found_set {
js_content test.not_found_set;
}
@@ -72,11 +81,21 @@ $t->write_file('test.js', <<EOF);
return 'test_var';
}
+ function short_var(r) {
+ r.var.foo = r.var.arg_a;
+ return 'short_var';
+ }
+
function content_set(r) {
r.variables.foo = r.variables.arg_a;
r.return(200, r.variables.foo);
}
+ function short_content_set(r) {
+ r.var.foo = r.var.arg_a;
+ r.return(200, r.var.foo);
+ }
+
function not_found_set(r) {
try {
r.variables.unknown = 1;
@@ -98,16 +117,21 @@ $t->write_file('test.js', <<EOF);
r.return(200, name);
}
- export default {variable, content_set, not_found_set, variable_lowkey};
+ export default {variable, short_var, content_set, short_content_set,
+ not_found_set, variable_lowkey};
EOF
-$t->try_run('no njs')->plan(5);
+$t->try_run('no njs')->plan(7);
###############################################################################
like(http_get('/var_set?a=bar'), qr/test_varbar/, 'var set');
+like(http_get('/var_set_short?a=bar'), qr/short_varbar/,
+ 'short var set via r.var');
like(http_get('/content_set?a=bar'), qr/bar/, 'content set');
+like(http_get('/content_set_short?a=bar'), qr/bar/,
+ 'short content set via r.var');
like(http_get('/not_found_set'), qr/variable not found/, 'not found exception');
like(http_get('/variable_lowkey'), qr/X{16}/,
'variable name is not overwritten while reading');
diff --git a/nginx/t/stream_js_variables.t b/nginx/t/stream_js_variables.t
index 29e6c33e..ff6804b1 100644
--- a/nginx/t/stream_js_variables.t
+++ b/nginx/t/stream_js_variables.t
@@ -37,7 +37,9 @@ stream {
%%TEST_GLOBALS_STREAM%%
js_set $test_var test.variable;
+ js_set $test_short_var test.short_var;
js_set $test_not_found test.not_found;
+ js_set $test_short_not_found test.short_not_found;
js_import test.js;
@@ -50,6 +52,16 @@ stream {
listen 127.0.0.1:8082;
return $test_not_found;
}
+
+ server {
+ listen 127.0.0.1:8083;
+ return $test_short_var$status;
+ }
+
+ server {
+ listen 127.0.0.1:8084;
+ return $test_short_not_found;
+ }
}
EOF
@@ -60,6 +72,11 @@ $t->write_file('test.js', <<EOF);
return 'test_var';
}
+ function short_var(s) {
+ s.var.status = 401;
+ return 'short_var';
+ }
+
function not_found(s) {
try {
s.variables.unknown = 1;
@@ -68,16 +85,28 @@ $t->write_file('test.js', <<EOF);
}
}
- export default {variable, not_found};
+ function short_not_found(s) {
+ try {
+ s.var.unknown = 1;
+ } catch (e) {
+ return 'short_not_found';
+ }
+ }
+
+ export default {variable, short_var, not_found, short_not_found};
EOF
-$t->try_run('no stream njs available')->plan(2);
+$t->try_run('no stream njs available')->plan(4);
###############################################################################
is(stream('127.0.0.1:' . port(8081))->read(), 'test_var400', 'var set');
is(stream('127.0.0.1:' . port(8082))->read(), 'not_found', 'not found set');
+is(stream('127.0.0.1:' . port(8083))->read(), 'short_var401',
+ 'short var set via s.var');
+is(stream('127.0.0.1:' . port(8084))->read(), 'short_not_found',
+ 'short not found set via s.var');
$t->stop();
--
2.43.0
From 4abe43c4b470cc140e36c0cc949971751d08180f Mon Sep 17 00:00:00 2001
From: Dmitry Volyntsev <xeioex@nginx.com>
Date: Wed, 11 Feb 2026 13:12:44 -0800
Subject: [PATCH 2/2] Modules: js_set inline JavaScript expressions.
Added support for inline JavaScript expressions in the js_set directive.
Previously, js_set only accepted function references:
js_set $var main.handler;
Now it also accepts inline expressions:
js_set $var '(r.uri)';
js_set $var 'r.headersIn["Host"] || "none"';
Additionally, nginx-style $variable references are expanded to
the corresponding JavaScript variable access. For example:
js_set $var '$uri.toUpperCase()';
is equivalent to:
js_set $var 'r.variables.uri.toUpperCase()';
In stream context, $var expands to s.variables.var.
---
nginx/ngx_http_js_module.c | 32 +-
nginx/ngx_js.c | 526 ++++++++++++++++++++++++---
nginx/ngx_js.h | 14 +
nginx/ngx_stream_js_module.c | 31 +-
nginx/t/js_inline.t | 179 +++++++++
nginx/t/js_inline_only.t | 76 ++++
nginx/t/js_variables_location.t | 5 +-
nginx/t/stream_js_variables_server.t | 5 +-
8 files changed, 783 insertions(+), 85 deletions(-)
create mode 100644 nginx/t/js_inline.t
create mode 100644 nginx/t/js_inline_only.t
diff --git a/nginx/ngx_http_js_module.c b/nginx/ngx_http_js_module.c
index 20bc1a64..05aa3121 100644
--- a/nginx/ngx_http_js_module.c
+++ b/nginx/ngx_http_js_module.c
@@ -1644,8 +1644,9 @@ ngx_http_js_variable_set(ngx_http_request_t *r, ngx_http_variable_value_t *v,
if (rc == NGX_DECLINED) {
ngx_log_error(NGX_LOG_ERR, r->connection->log, 0,
- "no \"js_import\" directives found for \"js_set\" handler"
- " \"%V\" in the current scope", fname);
+ "no \"js_import\" or inline expression found"
+ " for \"js_set\" handler \"%V\" at %s:%ui",
+ fname, vdata->file_name, vdata->line);
v->not_found = 1;
return NGX_OK;
}
@@ -1732,6 +1733,7 @@ ngx_http_js_init_vm(ngx_http_request_t *r, njs_int_t proto_id)
}
ngx_js_ctx_init((ngx_js_ctx_t *) ctx, r->connection->log);
+ ctx->conf = (ngx_js_loc_conf_t *) jlcf;
ngx_http_set_ctx(r, ctx, ngx_http_js_module);
}
@@ -8040,9 +8042,12 @@ invalid:
static char *
ngx_http_js_set(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
- ngx_str_t *value;
- ngx_js_set_t *data, *prev;
- ngx_http_variable_t *v;
+ ngx_str_t *value;
+ ngx_js_set_t *data, *prev;
+ ngx_http_variable_t *v;
+ ngx_http_js_loc_conf_t *jlcf;
+
+ static ngx_uint_t ngx_http_js_inline_index;
value = cf->args->elts;
@@ -8065,20 +8070,25 @@ ngx_http_js_set(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
return NGX_CONF_ERROR;
}
- data->fname = value[2];
- data->flags = 0;
- data->file_name = cf->conf_file->file.name.data;
- data->line = cf->conf_file->line;
+ jlcf = ngx_http_conf_get_module_loc_conf(cf, ngx_http_js_module);
+
+ if (ngx_js_set_init(cf, &jlcf->inlines, &ngx_http_js_inline_index,
+ &value[2], "r", data)
+ != NGX_OK)
+ {
+ return NGX_CONF_ERROR;
+ }
if (v->get_handler == ngx_http_js_variable_set) {
prev = (ngx_js_set_t *) v->data;
if (data->fname.len != prev->fname.len
- || ngx_strncmp(data->fname.data, prev->fname.data, data->fname.len) != 0)
+ || ngx_strncmp(data->fname.data, prev->fname.data,
+ data->fname.len) != 0)
{
ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
"variable \"%V\" is redeclared with "
- "different function name", &value[1]);
+ "different handler", &value[1]);
return NGX_CONF_ERROR;
}
}
diff --git a/nginx/ngx_js.c b/nginx/ngx_js.c
index bd1b5f23..25f59f77 100644
--- a/nginx/ngx_js.c
+++ b/nginx/ngx_js.c
@@ -571,6 +571,7 @@ ngx_engine_njs_init(ngx_engine_t *engine, ngx_engine_opts_t *opts)
vm_options.argc = ngx_argc;
vm_options.init = 1;
+ vm_options.file.length = opts->file.length;
vm_options.file.start = njs_mp_alloc(engine->pool, opts->file.length);
if (vm_options.file.start == NULL) {
return NGX_ERROR;
@@ -599,21 +600,154 @@ ngx_engine_njs_init(ngx_engine_t *engine, ngx_engine_opts_t *opts)
}
+/*
+ * Parse line number from JS exception stack trace.
+ *
+ * njs format:
+ * SyntaxError: ...\n at /path:LINE\n
+ *
+ * QuickJS format:
+ * SyntaxError: ...\n at <main>:LINE:COL\n
+ *
+ * Returns 0 if no line number found.
+ */
+static ngx_uint_t
+ngx_js_error_line(u_char *start, size_t len)
+{
+ u_char *p, *end;
+ ngx_uint_t line;
+
+ end = start + len;
+
+ p = ngx_strlchr(start, end, '\n');
+ if (p == NULL) {
+ return 0;
+ }
+
+ p++;
+
+ while (p < end && *p == ' ') {
+ p++;
+ }
+
+ if (end - p < 3 || p[0] != 'a' || p[1] != 't' || p[2] != ' ') {
+ return 0;
+ }
+
+ p += 3;
+
+ /* find first ':' followed by a digit */
+
+ while (p < end && *p != '\n') {
+ if (*p == ':' && p + 1 < end && p[1] >= '0' && p[1] <= '9') {
+ p++;
+
+ line = 0;
+
+ while (p < end && *p >= '0' && *p <= '9') {
+ line = line * 10 + (*p - '0');
+ p++;
+ }
+
+ return line;
+ }
+
+ p++;
+ }
+
+ return 0;
+}
+
+
+static ngx_js_inline_t *
+ngx_js_inline_map(ngx_js_loc_conf_t *conf, u_char *start, size_t len)
+{
+ ngx_uint_t i, line;
+ ngx_js_inline_t *inl;
+
+ line = ngx_js_error_line(start, len);
+
+ if (line == 0 || conf->inlines == NGX_CONF_UNSET_PTR) {
+ return NULL;
+ }
+
+ i = line - 1;
+
+ if (conf->imports != NGX_CONF_UNSET_PTR) {
+ if (i < conf->imports->nelts) {
+ return NULL;
+ }
+
+ i -= conf->imports->nelts;
+ }
+
+ if (i >= conf->inlines->nelts) {
+ return NULL;
+ }
+
+ inl = conf->inlines->elts;
+
+ return &inl[i];
+}
+
+
+static ngx_js_inline_t *
+ngx_js_inline_from_stack(ngx_js_loc_conf_t *conf, u_char *start, size_t len)
+{
+ u_char *p, *end;
+ ngx_uint_t index;
+ ngx_js_inline_t *inl;
+
+ static const u_char prefix[] = "__js_set_";
+
+ if (conf == NULL || conf->inlines == NGX_CONF_UNSET_PTR) {
+ return NULL;
+ }
+
+ end = start + len;
+ p = start;
+
+ while (p + (sizeof(prefix) - 1) <= end) {
+ if (ngx_strncmp(p, prefix, sizeof(prefix) - 1) == 0) {
+ p += sizeof(prefix) - 1;
+
+ if (p >= end || *p < '0' || *p > '9') {
+ return NULL;
+ }
+
+ index = 0;
+
+ while (p < end && *p >= '0' && *p <= '9') {
+ index = index * 10 + (*p - '0');
+ p++;
+ }
+
+ if (index >= conf->inlines->nelts) {
+ return NULL;
+ }
+
+ inl = conf->inlines->elts;
+
+ return &inl[index];
+ }
+
+ p++;
+ }
+
+ return NULL;
+}
+
+
static ngx_int_t
ngx_engine_njs_compile(ngx_js_loc_conf_t *conf, ngx_log_t *log, u_char *start,
size_t size)
{
- u_char *end;
- njs_vm_t *vm;
- njs_int_t rc;
- njs_str_t text;
- ngx_uint_t i;
- njs_value_t *value;
- njs_opaque_value_t exception, lvalue;
- ngx_js_named_path_t *import;
-
- static const njs_str_t line_number_key = njs_str("lineNumber");
- static const njs_str_t file_name_key = njs_str("fileName");
+ u_char *end;
+ njs_vm_t *vm;
+ njs_int_t rc;
+ njs_str_t text;
+ ngx_js_inline_t *inl;
+ njs_opaque_value_t exception;
vm = conf->engine->u.njs.vm;
@@ -633,26 +767,16 @@ ngx_engine_njs_compile(ngx_js_loc_conf_t *conf, ngx_log_t *log, u_char *start,
njs_vm_exception_get(vm, njs_value_arg(&exception));
njs_vm_value_string(vm, &text, njs_value_arg(&exception));
- value = njs_vm_object_prop(vm, njs_value_arg(&exception),
- &file_name_key, &lvalue);
- if (value == NULL) {
- value = njs_vm_object_prop(vm, njs_value_arg(&exception),
- &line_number_key, &lvalue);
+ inl = ngx_js_inline_map(conf, text.start, text.length);
+ if (inl != NULL) {
+ ngx_log_error(NGX_LOG_EMERG, log, 0, "%*s, included at %s:%ui",
+ text.length, text.start, inl->file, inl->line);
- if (value != NULL) {
- i = njs_value_number(value) - 1;
-
- if (i < conf->imports->nelts) {
- import = conf->imports->elts;
- ngx_log_error(NGX_LOG_EMERG, log, 0,
- "%*s, included in %s:%ui", text.length,
- text.start, import[i].file, import[i].line);
- return NGX_ERROR;
- }
- }
+ } else {
+ ngx_log_error(NGX_LOG_EMERG, log, 0, "%*s",
+ text.length, text.start);
}
- ngx_log_error(NGX_LOG_EMERG, log, 0, "%*s", text.length, text.start);
return NGX_ERROR;
}
@@ -763,10 +887,12 @@ static ngx_int_t
ngx_engine_njs_call(ngx_js_ctx_t *ctx, ngx_str_t *fname,
njs_opaque_value_t *args, njs_uint_t nargs)
{
- njs_vm_t *vm;
- njs_int_t ret;
- njs_str_t name;
- njs_function_t *func;
+ njs_vm_t *vm;
+ njs_int_t ret;
+ njs_str_t name, str;
+ ngx_str_t s;
+ ngx_js_inline_t *inl;
+ njs_function_t *func;
name.start = fname->data;
name.length = fname->len;
@@ -783,7 +909,24 @@ ngx_engine_njs_call(ngx_js_ctx_t *ctx, ngx_str_t *fname,
ret = njs_vm_invoke(vm, func, njs_value_arg(args), nargs,
njs_value_arg(&ctx->retval));
if (ret == NJS_ERROR) {
- ngx_js_log_exception(vm, ctx->log, "exception");
+ if (njs_vm_exception_string(vm, &str) != NJS_OK) {
+ ngx_log_error(NGX_LOG_ERR, ctx->log, 0,
+ "js exception");
+ return NGX_ERROR;
+ }
+
+ s.data = str.start;
+ s.len = str.length;
+
+ inl = ngx_js_inline_from_stack(ctx->conf, s.data, s.len);
+ if (inl != NULL) {
+ ngx_log_error(NGX_LOG_ERR, ctx->log, 0,
+ "js exception: %V, included at %s:%ui",
+ &s, inl->file, inl->line);
+ } else {
+ ngx_log_error(NGX_LOG_ERR, ctx->log, 0,
+ "js exception: %V", &s);
+ }
return NGX_ERROR;
}
@@ -963,9 +1106,11 @@ static ngx_int_t
ngx_engine_qjs_compile(ngx_js_loc_conf_t *conf, ngx_log_t *log, u_char *start,
size_t size)
{
- JSValue code;
+ ngx_str_t text;
+ JSValue code, exception;
JSContext *cx;
ngx_engine_t *engine;
+ ngx_js_inline_t *inl;
ngx_js_code_entry_t *pc;
engine = conf->engine;
@@ -975,10 +1120,36 @@ ngx_engine_qjs_compile(ngx_js_loc_conf_t *conf, ngx_log_t *log, u_char *start,
JS_EVAL_TYPE_MODULE | JS_EVAL_FLAG_COMPILE_ONLY);
if (JS_IsException(code)) {
- ngx_qjs_log_exception(engine, log, "compile");
+ exception = JS_GetException(cx);
+
+ if (ngx_qjs_dump_obj(engine, exception, &text) == NGX_OK) {
+ inl = ngx_js_inline_map(conf, text.data, text.len);
+ if (inl != NULL) {
+ ngx_log_error(NGX_LOG_EMERG, log, 0, "%V, included at %s:%ui",
+ &text, inl->file, inl->line);
+
+ } else {
+ ngx_log_error(NGX_LOG_EMERG, log, 0, "js compile: %V", &text);
+ }
+
+ } else {
+ ngx_log_error(NGX_LOG_EMERG, log, 0, "js compile error");
+ }
+
+ JS_FreeValue(cx, exception);
return NGX_ERROR;
}
+ if (engine->precompiled == NULL) {
+ engine->precompiled = njs_arr_create(engine->pool, 4,
+ sizeof(ngx_js_code_entry_t));
+ if (engine->precompiled == NULL) {
+ JS_FreeValue(cx, code);
+ ngx_log_error(NGX_LOG_EMERG, log, 0, "njs_arr_create() failed");
+ return NGX_ERROR;
+ }
+ }
+
pc = njs_arr_add(engine->precompiled);
if (pc == NULL) {
JS_FreeValue(cx, code);
@@ -1120,9 +1291,11 @@ 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)
{
- JSValue fn, val;
- ngx_int_t rc;
- JSContext *cx;
+ JSValue fn, val, exc;
+ ngx_int_t rc;
+ ngx_str_t s;
+ JSContext *cx;
+ ngx_js_inline_t *inl;
cx = ctx->engine->u.qjs.ctx;
@@ -1138,7 +1311,24 @@ ngx_engine_qjs_call(ngx_js_ctx_t *ctx, ngx_str_t *fname,
val = JS_Call(cx, fn, JS_UNDEFINED, nargs, &ngx_qjs_arg(args[0]));
JS_FreeValue(cx, fn);
if (JS_IsException(val)) {
- ngx_qjs_log_exception(ctx->engine, ctx->log, "call exception");
+ exc = JS_GetException(cx);
+
+ if (ngx_qjs_dump_obj(ctx->engine, exc, &s) == NGX_OK) {
+ inl = ngx_js_inline_from_stack(ctx->conf, s.data, s.len);
+ if (inl != NULL) {
+ ngx_log_error(NGX_LOG_ERR, ctx->log, 0,
+ "js exception: %V, included at %s:%ui",
+ &s, inl->file, inl->line);
+ } else {
+ ngx_log_error(NGX_LOG_ERR, ctx->log, 0,
+ "js exception: %V", &s);
+ }
+
+ } else {
+ ngx_log_error(NGX_LOG_ERR, ctx->log, 0, "js exception");
+ }
+
+ JS_FreeValue(cx, exc);
return NGX_ERROR;
}
@@ -3668,6 +3858,131 @@ ngx_js_init_preload_vm(njs_vm_t *vm, ngx_js_loc_conf_t *conf)
}
+ngx_int_t
+ngx_js_is_function_ref(ngx_str_t *str)
+{
+ u_char *p, *end;
+
+ p = str->data;
+ end = p + str->len;
+
+ if (p == end) {
+ return 0;
+ }
+
+ for ( ;; ) {
+ if ((*p < 'a' || *p > 'z')
+ && (*p < 'A' || *p > 'Z')
+ && *p != '_' && *p != '$')
+ {
+ return 0;
+ }
+
+ p++;
+
+ while (p < end && *p != '.') {
+ if ((*p < 'a' || *p > 'z')
+ && (*p < 'A' || *p > 'Z')
+ && (*p < '0' || *p > '9')
+ && *p != '_' && *p != '$')
+ {
+ return 0;
+ }
+
+ p++;
+ }
+
+ if (p == end) {
+ return 1;
+ }
+
+ /* skip '.' */
+ p++;
+
+ if (p == end) {
+ return 0;
+ }
+ }
+}
+
+
+static ngx_int_t
+ngx_js_set_inline(ngx_conf_t *cf, ngx_array_t **inlines, ngx_uint_t *index,
+ ngx_str_t *code, const char *arg, ngx_str_t *fname)
+{
+ size_t arg_len;
+ ngx_js_inline_t *inl;
+
+ if (*inlines == NGX_CONF_UNSET_PTR) {
+ *inlines = ngx_array_create(cf->pool, 4, sizeof(ngx_js_inline_t));
+ if (*inlines == NULL) {
+ return NGX_ERROR;
+ }
+ }
+
+ inl = ngx_array_push(*inlines);
+ if (inl == NULL) {
+ return NGX_ERROR;
+ }
+
+ arg_len = ngx_strlen(arg);
+ inl->code = *code;
+ inl->file = cf->conf_file->file.name.data;
+ inl->line = cf->conf_file->line;
+ inl->arg.len = arg_len;
+ inl->arg.data = (u_char *) arg;
+
+ inl->fname.data = ngx_pnalloc(cf->pool, sizeof("__js_set_65535") - 1);
+ if (inl->fname.data == NULL) {
+ return NGX_ERROR;
+ }
+
+ inl->fname.len = ngx_sprintf(inl->fname.data, "__js_set_%ui", (*index)++)
+ - inl->fname.data;
+
+ *fname = inl->fname;
+
+ return NGX_OK;
+}
+
+
+ngx_int_t
+ngx_js_set_init(ngx_conf_t *cf, ngx_array_t **inlines, ngx_uint_t *index,
+ ngx_str_t *handler, const char *arg, ngx_js_set_t *set)
+{
+ u_char *p, *end;
+
+ set->flags = 0;
+ set->file_name = cf->conf_file->file.name.data;
+ set->line = cf->conf_file->line;
+
+ if (ngx_js_is_function_ref(handler)) {
+
+ p = handler->data;
+ end = p + handler->len;
+
+ while (p < end) {
+ if (*p == '$' && p + 1 < end
+ && ((p[1] >= 'a' && p[1] <= 'z')
+ || (p[1] >= 'A' && p[1] <= 'Z')
+ || p[1] == '_'))
+ {
+ goto inline_expr;
+ }
+
+ p++;
+ }
+
+ set->fname = *handler;
+ return NGX_OK;
+ }
+
+inline_expr:
+
+ return ngx_js_set_inline(cf, inlines, index, handler, arg, &set->fname);
+}
+
+
/*
* Merge configuration values used at configuration time.
*/
@@ -3686,10 +4001,14 @@ ngx_js_merge_vm(ngx_conf_t *cf, ngx_js_loc_conf_t *conf,
{
ngx_str_t *path, *s;
ngx_uint_t i;
- ngx_array_t *imports, *preload_objects, *paths;
+ ngx_array_t *imports, *inlines, *preload_objects, *paths;
+ ngx_js_inline_t *inl, *ili;
ngx_js_named_path_t *import, *pi, *pij, *preload;
- if (prev->imports != NGX_CONF_UNSET_PTR && prev->engine == NULL) {
+ if ((prev->imports != NGX_CONF_UNSET_PTR
+ || prev->inlines != NGX_CONF_UNSET_PTR)
+ && prev->engine == NULL)
+ {
/*
* special handling to preserve conf->engine
* in the "http" or "stream" section to inherit it to all servers
@@ -3703,6 +4022,7 @@ ngx_js_merge_vm(ngx_conf_t *cf, ngx_js_loc_conf_t *conf,
}
if (conf->imports == NGX_CONF_UNSET_PTR
+ && conf->inlines == NGX_CONF_UNSET_PTR
&& conf->type == prev->type
&& conf->paths == NGX_CONF_UNSET_PTR
&& conf->preload_objects == NGX_CONF_UNSET_PTR)
@@ -3710,6 +4030,7 @@ ngx_js_merge_vm(ngx_conf_t *cf, ngx_js_loc_conf_t *conf,
if (prev->engine != NULL) {
conf->preload_objects = prev->preload_objects;
conf->imports = prev->imports;
+ conf->inlines = prev->inlines;
conf->type = prev->type;
conf->paths = prev->paths;
conf->engine = prev->engine;
@@ -3791,6 +4112,43 @@ ngx_js_merge_vm(ngx_conf_t *cf, ngx_js_loc_conf_t *conf,
}
}
+ if (prev->inlines != NGX_CONF_UNSET_PTR) {
+ if (conf->inlines == NGX_CONF_UNSET_PTR) {
+ conf->inlines = prev->inlines;
+
+ } else {
+ inlines = ngx_array_create(cf->pool, 4,
+ sizeof(ngx_js_inline_t));
+ if (inlines == NULL) {
+ return NGX_ERROR;
+ }
+
+ ili = prev->inlines->elts;
+
+ for (i = 0; i < prev->inlines->nelts; i++) {
+ inl = ngx_array_push(inlines);
+ if (inl == NULL) {
+ return NGX_ERROR;
+ }
+
+ *inl = ili[i];
+ }
+
+ ili = conf->inlines->elts;
+
+ for (i = 0; i < conf->inlines->nelts; i++) {
+ inl = ngx_array_push(inlines);
+ if (inl == NULL) {
+ return NGX_ERROR;
+ }
+
+ *inl = ili[i];
+ }
+
+ conf->inlines = inlines;
+ }
+ }
+
if (prev->paths != NGX_CONF_UNSET_PTR) {
if (conf->paths == NGX_CONF_UNSET_PTR) {
conf->paths = prev->paths;
@@ -3827,7 +4185,9 @@ ngx_js_merge_vm(ngx_conf_t *cf, ngx_js_loc_conf_t *conf,
}
}
- if (conf->imports == NGX_CONF_UNSET_PTR) {
+ if (conf->imports == NGX_CONF_UNSET_PTR
+ && conf->inlines == NGX_CONF_UNSET_PTR)
+ {
return NGX_OK;
}
@@ -4155,6 +4515,7 @@ ngx_js_init_conf_vm(ngx_conf_t *cf, ngx_js_loc_conf_t *conf,
size_t size;
ngx_str_t *m, file;
ngx_uint_t i;
+ ngx_js_inline_t *inl;
ngx_pool_cleanup_t *cln;
ngx_js_named_path_t *import;
@@ -4164,14 +4525,34 @@ ngx_js_init_conf_vm(ngx_conf_t *cf, ngx_js_loc_conf_t *conf,
size = 0;
- import = conf->imports->elts;
- for (i = 0; i < conf->imports->nelts; i++) {
+ if (conf->imports != NGX_CONF_UNSET_PTR) {
+ import = conf->imports->elts;
+
+ for (i = 0; i < conf->imports->nelts; i++) {
- /* import <name> from '<path>'; globalThis.<name> = <name>; */
+ /* import <name> from '<path>'; globalThis.<name> = <name>; */
- size += sizeof("import from '';") - 1 + import[i].name.len * 3
- + import[i].path.len
- + sizeof(" globalThis. = ;\n") - 1;
+ size += sizeof("import from '';") - 1 + import[i].name.len * 3
+ + import[i].path.len
+ + sizeof(" globalThis. = ;\n") - 1;
+ }
+ }
+
+ if (conf->inlines != NGX_CONF_UNSET_PTR) {
+ inl = conf->inlines->elts;
+
+ for (i = 0; i < conf->inlines->nelts; i++) {
+
+ /*
+ * function <fname>(<arg>) { return (<code>); }
+ * globalThis.<fname> = <fname>;\n
+ */
+
+ size += sizeof("function () { return (); }") - 1
+ + sizeof(" globalThis. = ;\n") - 1
+ + inl[i].fname.len * 3
+ + inl[i].arg.len + inl[i].code.len;
+ }
}
start = ngx_pnalloc(cf->pool, size + 1);
@@ -4180,20 +4561,44 @@ ngx_js_init_conf_vm(ngx_conf_t *cf, ngx_js_loc_conf_t *conf,
}
p = start;
- import = conf->imports->elts;
- for (i = 0; i < conf->imports->nelts; i++) {
- /* import <name> from '<path>'; globalThis.<name> = <name>; */
+ if (conf->imports != NGX_CONF_UNSET_PTR) {
+ import = conf->imports->elts;
- p = ngx_cpymem(p, "import ", sizeof("import ") - 1);
- p = ngx_cpymem(p, import[i].name.data, import[i].name.len);
- p = ngx_cpymem(p, " from '", sizeof(" from '") - 1);
- p = ngx_cpymem(p, import[i].path.data, import[i].path.len);
- p = ngx_cpymem(p, "'; globalThis.", sizeof("'; globalThis.") - 1);
- p = ngx_cpymem(p, import[i].name.data, import[i].name.len);
- p = ngx_cpymem(p, " = ", sizeof(" = ") - 1);
- p = ngx_cpymem(p, import[i].name.data, import[i].name.len);
- p = ngx_cpymem(p, ";\n", sizeof(";\n") - 1);
+ for (i = 0; i < conf->imports->nelts; i++) {
+
+ /* import <name> from '<path>'; globalThis.<name> = <name>; */
+
+ p = ngx_cpymem(p, "import ", sizeof("import ") - 1);
+ p = ngx_cpymem(p, import[i].name.data, import[i].name.len);
+ p = ngx_cpymem(p, " from '", sizeof(" from '") - 1);
+ p = ngx_cpymem(p, import[i].path.data, import[i].path.len);
+ p = ngx_cpymem(p, "'; globalThis.",
+ sizeof("'; globalThis.") - 1);
+ p = ngx_cpymem(p, import[i].name.data, import[i].name.len);
+ p = ngx_cpymem(p, " = ", sizeof(" = ") - 1);
+ p = ngx_cpymem(p, import[i].name.data, import[i].name.len);
+ p = ngx_cpymem(p, ";\n", sizeof(";\n") - 1);
+ }
+ }
+
+ if (conf->inlines != NGX_CONF_UNSET_PTR) {
+ inl = conf->inlines->elts;
+
+ for (i = 0; i < conf->inlines->nelts; i++) {
+ p = ngx_cpymem(p, "function ", sizeof("function ") - 1);
+ p = ngx_cpymem(p, inl[i].fname.data, inl[i].fname.len);
+ p = ngx_cpymem(p, "(", 1);
+ p = ngx_cpymem(p, inl[i].arg.data, inl[i].arg.len);
+ p = ngx_cpymem(p, ") { return (", sizeof(") { return (") - 1);
+ p = ngx_cpymem(p, inl[i].code.data, inl[i].code.len);
+ p = ngx_cpymem(p, "); } globalThis.",
+ sizeof("); } globalThis.") - 1);
+ p = ngx_cpymem(p, inl[i].fname.data, inl[i].fname.len);
+ p = ngx_cpymem(p, " = ", sizeof(" = ") - 1);
+ p = ngx_cpymem(p, inl[i].fname.data, inl[i].fname.len);
+ p = ngx_cpymem(p, ";\n", sizeof(";\n") - 1);
+ }
}
*p = '\0';
@@ -4288,6 +4693,7 @@ ngx_js_create_conf(ngx_conf_t *cf, size_t size)
conf->paths = NGX_CONF_UNSET_PTR;
conf->type = NGX_CONF_UNSET_UINT;
conf->imports = NGX_CONF_UNSET_PTR;
+ conf->inlines = NGX_CONF_UNSET_PTR;
conf->preload_objects = NGX_CONF_UNSET_PTR;
conf->reuse = NGX_CONF_UNSET_SIZE;
diff --git a/nginx/ngx_js.h b/nginx/ngx_js.h
index a25dc65a..04f95c22 100644
--- a/nginx/ngx_js.h
+++ b/nginx/ngx_js.h
@@ -133,6 +133,7 @@ typedef struct {
ngx_js_queue_t *reuse_queue; \
ngx_str_t cwd; \
ngx_array_t *imports; \
+ ngx_array_t *inlines; \
ngx_array_t *paths; \
\
ngx_array_t *preload_objects; \
@@ -184,6 +185,7 @@ typedef struct {
#define NGX_JS_COMMON_CTX \
ngx_engine_t *engine; \
+ ngx_js_loc_conf_t *conf; \
ngx_log_t *log; \
njs_opaque_value_t args[3]; \
njs_opaque_value_t retval; \
@@ -224,6 +226,15 @@ typedef struct {
} ngx_js_set_t;
+typedef struct {
+ ngx_str_t code;
+ ngx_str_t fname;
+ ngx_str_t arg;
+ u_char *file;
+ ngx_uint_t line;
+} ngx_js_inline_t;
+
+
struct ngx_js_ctx_s {
NGX_JS_COMMON_CTX;
};
@@ -452,6 +463,9 @@ char * ngx_js_preload_object(ngx_conf_t *cf, ngx_command_t *cmd, void *conf);
char * ngx_js_fetch_proxy(ngx_conf_t *cf, ngx_command_t *cmd, void *conf);
ngx_int_t ngx_js_parse_proxy_url(ngx_pool_t *pool, ngx_log_t *log,
ngx_str_t *url_str, ngx_url_t **url_out, ngx_str_t *auth_header_out);
+ngx_int_t ngx_js_is_function_ref(ngx_str_t *str);
+ngx_int_t ngx_js_set_init(ngx_conf_t *cf, ngx_array_t **inlines,
+ ngx_uint_t *index, ngx_str_t *handler, const char *arg, ngx_js_set_t *set);
ngx_int_t ngx_js_merge_vm(ngx_conf_t *cf, ngx_js_loc_conf_t *conf,
ngx_js_loc_conf_t *prev,
ngx_int_t (*init_vm)(ngx_conf_t *cf, ngx_js_loc_conf_t *conf));
diff --git a/nginx/ngx_stream_js_module.c b/nginx/ngx_stream_js_module.c
index 76d8907d..89699cc8 100644
--- a/nginx/ngx_stream_js_module.c
+++ b/nginx/ngx_stream_js_module.c
@@ -1157,8 +1157,9 @@ ngx_stream_js_variable_set(ngx_stream_session_t *s,
if (rc == NGX_DECLINED) {
ngx_log_error(NGX_LOG_ERR, s->connection->log, 0,
- "no \"js_import\" directives found for \"js_set\" handler"
- " \"%V\" in the current scope", fname);
+ "no \"js_import\" or inline expression found"
+ " for \"js_set\" handler \"%V\" at %s:%ui",
+ fname, vdata->file_name, vdata->line);
v->not_found = 1;
return NGX_OK;
}
@@ -1245,6 +1246,7 @@ ngx_stream_js_init_vm(ngx_stream_session_t *s, njs_int_t proto_id)
}
ngx_js_ctx_init((ngx_js_ctx_t *) ctx, s->connection->log);
+ ctx->conf = (ngx_js_loc_conf_t *) jscf;
ngx_stream_set_ctx(s, ctx, ngx_stream_js_module);
}
@@ -3539,9 +3541,12 @@ invalid:
static char *
ngx_stream_js_set(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
- ngx_str_t *value;
- ngx_js_set_t *data, *prev;
- ngx_stream_variable_t *v;
+ ngx_str_t *value;
+ ngx_js_set_t *data, *prev;
+ ngx_stream_variable_t *v;
+ ngx_stream_js_srv_conf_t *jscf;
+
+ static ngx_uint_t ngx_stream_js_inline_index;
value = cf->args->elts;
@@ -3564,19 +3569,25 @@ ngx_stream_js_set(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
return NGX_CONF_ERROR;
}
- data->fname = value[2];
- data->file_name = cf->conf_file->file.name.data;
- data->line = cf->conf_file->line;
+ jscf = ngx_stream_conf_get_module_srv_conf(cf, ngx_stream_js_module);
+
+ if (ngx_js_set_init(cf, &jscf->inlines, &ngx_stream_js_inline_index,
+ &value[2], "s", data)
+ != NGX_OK)
+ {
+ return NGX_CONF_ERROR;
+ }
if (v->get_handler == ngx_stream_js_variable_set) {
prev = (ngx_js_set_t *) v->data;
if (data->fname.len != prev->fname.len
- || ngx_strncmp(data->fname.data, prev->fname.data, data->fname.len) != 0)
+ || ngx_strncmp(data->fname.data, prev->fname.data,
+ data->fname.len) != 0)
{
ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
"variable \"%V\" is redeclared with "
- "different function name", &value[1]);
+ "different handler", &value[1]);
return NGX_CONF_ERROR;
}
}
diff --git a/nginx/t/js_inline.t b/nginx/t/js_inline.t
new file mode 100644
index 00000000..c6de3158
--- /dev/null
+++ b/nginx/t/js_inline.t
@@ -0,0 +1,179 @@
+#!/usr/bin/perl
+
+# (C) Dmitry Volyntsev
+# (C) F5, Inc.
+
+# Tests for http njs module, js_set inline expressions.
+
+###############################################################################
+
+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;
+
+my $t = Test::Nginx->new()->has(qw/http rewrite/)->plan(13);
+
+use constant TEMPLATE_CONF => <<'EOF';
+
+%%TEST_GLOBALS%%
+
+daemon off;
+
+events {
+}
+
+http {
+ %%TEST_GLOBALS_HTTP%%
+
+ # oridinary variable set (function reference)
+ js_set $test_var test.variable;
+
+ js_set $var 'r.var.test_var.toUpperCase()';
+ js_set $header 'r.headersIn["Foo"] || "none"';
+ js_set $template `/p${r.uri}/post`;
+ js_set $expression '1 + 2';
+ js_set $force_expr (r.var.uri);
+
+ js_set $runtime_err '(o.a.a)';
+
+ %%EXTRA_CONF%%
+
+ js_import test.js;
+
+ server {
+ listen 127.0.0.1:8080;
+ server_name localhost;
+
+ location /inline {
+ return 200 "uri=$template foo=$header sum=$expression";
+ }
+
+ location /var {
+ return 200 "var=$var";
+ }
+
+ location /mixed {
+ return 200 "test_var=$test_var uri=$template";
+ }
+
+ location /force_expr {
+ return 200 "uri=$force_expr";
+ }
+
+ location /runtime_err {
+ return 200 "err=$runtime_err";
+ }
+ }
+}
+
+EOF
+
+###############################################################################
+
+$t->write_file('test.js', <<EOF);
+ function variable(r) {
+ return 'from_func';
+ }
+
+ export default {variable};
+
+EOF
+
+write_conf($t, '');
+
+$t->try_run('no njs');
+
+###############################################################################
+
+like(http_get('/inline'), qr/uri=\/p\/inline\/post/, 'template inline');
+like(http_get('/inline'), qr/foo=none/, 'inline headerIn');
+like(http(
+ 'GET /inline HTTP/1.0' . CRLF
+ . 'Foo: bar' . CRLF
+ . 'Host: localhost' . CRLF . CRLF
+), qr/foo=bar/, 'inline headerIn with header');
+
+like(http_get('/inline'), qr/sum=3/, 'inline sum');
+like(http_get('/var'), qr/var=FROM_FUNC/, 'inline var func ref');
+like(http_get('/mixed'), qr/test_var=from_func/, 'mixed func ref');
+like(http_get('/mixed'), qr/uri=\/p\/mixed\/post/, 'mixed inline');
+
+like(http_get('/force_expr'), qr/uri=\/force_expr/, 'forced expression syntax');
+
+http_get('/runtime_err');
+like($t->read_file('error.log'), qr/included at.*nginx\.conf:/s,
+ 'runtime error location');
+
+$t->stop();
+
+###############################################################################
+
+like(check($t, "js_set \$bad 'return 1';"),
+ qr/SyntaxError.*included at.*nginx\.conf:/s,
+ 'inline syntax error location');
+
+like(check($t, "js_set \$bad '1 +';"),
+ qr/SyntaxError.*included at.*nginx\.conf:/s,
+ 'inline syntax error unexpected end');
+
+$t->write_file('bad.js', 'export default {INVALID SYNTAX');
+
+like(check($t, 'js_import bad.js;'),
+ qr/\[emerg\].*SyntaxError/s,
+ 'file syntax error');
+
+unlike(check($t, 'js_import bad.js;'),
+ qr/included at/s,
+ 'file syntax error no inline location');
+
+open my $fh, '>', $t->testdir() . '/error.log';
+close $fh;
+
+###############################################################################
+
+sub write_conf {
+ my ($t, $extra) = @_;
+
+ $t->write_file_expand('nginx.conf',
+ TEMPLATE_CONF =~ s/%%EXTRA_CONF%%/$extra/r);
+}
+
+sub check {
+ my ($t, $extra) = @_;
+
+ $t->stop();
+ unlink $t->testdir() . '/error.log';
+
+ write_conf($t, $extra);
+
+ eval {
+ open OLDERR, ">&", \*STDERR; close STDERR;
+ $t->run();
+ open STDERR, ">&", \*OLDERR;
+ };
+
+ return unless $@;
+
+ my $log = $t->read_file('error.log');
+
+ if ($ENV{TEST_NGINX_VERBOSE}) {
+ map { Test::Nginx::log_core($_) } split(/^/m, $log);
+ }
+
+ return $log;
+}
+
+###############################################################################
diff --git a/nginx/t/js_inline_only.t b/nginx/t/js_inline_only.t
new file mode 100644
index 00000000..f2144822
--- /dev/null
+++ b/nginx/t/js_inline_only.t
@@ -0,0 +1,76 @@
+#!/usr/bin/perl
+
+# (C) Dmitry Volyntsev
+# (C) F5, Inc.
+
+# Tests for http njs module, js_set inline expressions without js_import.
+
+###############################################################################
+
+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_set $template `/p${r.uri}/post`;
+
+ server {
+ listen 127.0.0.1:8080;
+ server_name localhost;
+
+ js_set $a '(r.args.a || "none")';
+ js_set $expanded 'r.var.uri.toUpperCase()';
+
+ location /inline {
+ js_set $method '(r.method)';
+
+ return 200 "uri=$template a=$a method=$method";
+ }
+
+ location /expanded {
+ return 200 "expanded=$expanded";
+ }
+ }
+}
+
+EOF
+
+$t->try_run('no njs')->plan(6);
+
+###############################################################################
+
+like(http_get('/inline'), qr/uri=\/p\/inline\/post/, 'inline only uri');
+like(http_get('/inline'), qr/a=none/, 'inline only args');
+like(http_get('/inline?a=1'), qr/a=1/, 'inline only args with value');
+like(http_get('/inline'), qr/method=GET/, 'inline only method');
+like(http_get('/expanded'), qr/expanded=\/EXPANDED/,
+ 'inline only expanded r.var');
+
+$t->stop();
+
+ok(index($t->read_file('error.log'), 'SyntaxError') < 0, 'no syntax errors');
+
+###############################################################################
diff --git a/nginx/t/js_variables_location.t b/nginx/t/js_variables_location.t
index 02bd85ef..92496a50 100644
--- a/nginx/t/js_variables_location.t
+++ b/nginx/t/js_variables_location.t
@@ -90,7 +90,8 @@ like(http_get('/not_found'), qr/NOT_FOUND:$/, 'not found is empty');
$t->stop();
ok(index($t->read_file('error.log'),
- 'no "js_import" directives found for "js_set" handler "main.variable" '
- . 'in the current scope') > 0, 'log error for js_set without js_import');
+ 'no "js_import" or inline expression found for "js_set" handler '
+ . '"main.variable"') > 0,
+ 'log error for js_set without js_import');
###############################################################################
diff --git a/nginx/t/stream_js_variables_server.t b/nginx/t/stream_js_variables_server.t
index a7d53718..abf8c180 100644
--- a/nginx/t/stream_js_variables_server.t
+++ b/nginx/t/stream_js_variables_server.t
@@ -92,7 +92,8 @@ is(stream('127.0.0.1:' . port(8083))->read(), 'NOT_FOUND:', 'not found var');
$t->stop();
ok(index($t->read_file('error.log'),
- 'no "js_import" directives found for "js_set" handler "main.variable" '
- . 'in the current scope') > 0, 'log error for js_set without js_import');
+ 'no "js_import" or inline expression found for "js_set" handler '
+ . '"main.variable"') > 0,
+ 'log error for js_set without js_import');
###############################################################################
--
2.43.0
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment