Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
17d753b
feat(*): implement kong_pass mediator directive
bungle May 19, 2026
dc1683d
feat(pass): select the upstream HTTP version in kong_pass
bungle Sep 2, 2026
eeb92a8
tests(upstream-tls): give the balancer TLS test its own connection
bungle Sep 2, 2026
18f301a
fix(pass): build kong_pass on nginx older than 1.29.4
bungle Sep 2, 2026
5efc7be
fix(tests): do not use "keepalive 0" to switch off connection reuse
bungle Sep 2, 2026
4c453b8
fix(pass): require the request-local preserve_output patch for version=2
bungle Sep 3, 2026
755b99a
fix(pass): merge the mediator into if/limit_except child configs
bungle Sep 3, 2026
b53d4a8
docs(pass): document the kong_pass directive
bungle Sep 3, 2026
f4d0819
fix(tests): drive the limit_except test from $arg_ variables
bungle Sep 3, 2026
7ab2230
fix(build): only build kong_pass where proxy_pass and grpc_pass exist
bungle Sep 3, 2026
8c4c5e4
fix(tests): let the kong_pass tests skip on a build that cannot run them
bungle Sep 3, 2026
fa5b4aa
fix(pass): reject a literal path that cannot follow the host
bungle Sep 3, 2026
cfb37ea
refactor(pass): read the selector and version through variable indexes
bungle Sep 3, 2026
7cca93c
fix(pass): stop warning per request about a version this build cannot…
bungle Sep 3, 2026
541bef9
fix(pass): name the clashing directive, and accept ${name}
bungle Sep 3, 2026
076d0bc
refactor(vars): accept ${name} wherever this module takes a $variable
bungle Sep 3, 2026
7bdb2dc
fix(vars): re-read a non-cacheable variable instead of serving the cache
bungle Sep 3, 2026
8d6f5ca
fix(vars): invalidate the variables a rewritten header feeds
bungle Sep 3, 2026
2a95cbe
fix(vars): index $uri, and stop caching an absent upstream TLS value
bungle Sep 3, 2026
144c665
fix(vars): bounds-check the FFI variable-get path
bungle Sep 23, 2026
9a8be14
fix(pass): re-read $selector and version= instead of serving their cache
bungle Sep 23, 2026
3cbee85
docs(pass): match the $selector match to how it actually matches
bungle Sep 23, 2026
bf17114
fix(pass): reject a pass slot whose arity no longer matches items
bungle Sep 23, 2026
40e9347
refactor(vars): share the upstream-not-ready fallback
bungle Sep 23, 2026
071cfa9
refactor(pass): check version=="2" once, not per build config
bungle Sep 23, 2026
ba60f0b
fix(build): quote the build-flag comparisons in the kong_pass gate
bungle Sep 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 127 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ Table of Contents
* [lua\_kong\_load\_var\_index](#lua_kong_load_var_index)
* [lua\_kong\_set\_static\_tag](#lua_kong_set_static_tag)
* [lua\_kong\_error\_log\_request\_id](#lua_kong_error_log_request_id)
* [kong\_pass](#kong_pass)
* [Variables](#variables)
* [$kong\_request\_id](#kong_request_id)
* [$kong\_upstream\_ssl\_server\_raw\_cert](#kong_upstream_ssl_server_raw_cert)
Expand Down Expand Up @@ -53,6 +54,13 @@ You may use the [openresty-build-tools](https://github.com/Kong/kong-build-tools
script to automatically build an OpenResty binary with required patches as well as this module
included.

`kong_pass`'s `version=2` parameter additionally requires a patch that defines
`NGX_HTTP_UPSTREAM_PRESERVE_OUTPUT_PATCH` and makes `ngx_http_upstream_t`'s
`preserve_output` flag request-local. Without it, `ngx_http_proxy_v2_handler`
writes that flag into the shared proxy location config instead, so this module
does not call it dynamically; `version=2` falls back to `proxy_http_version`,
the same as on an nginx older than 1.29.4.

Install
=======
This module can be installed just like any ordinary Nginx C module, using the
Expand Down Expand Up @@ -181,6 +189,125 @@ An error log line may look similar to the following:

[Back to TOC](#table-of-contents)

kong\_pass
-------------------------------------------
**syntax:** *kong_pass $selector host path [version=$variable];*

**context:** *location* *if in location* *limit_except*

Mediates a single location between `proxy_pass` and `grpc_pass`, chosen per
request from `$selector`, and, for `proxy_pass`, additionally selects the
upstream HTTP version per request. It exists because both of those are
static, per-location directives: neither can be told "use HTTP/2 for this
request but HTTP/1.1 for the next one on the same location", and a location
can only have one of them configured at a time.

At configuration time, `kong_pass` builds a `<$selector>://<host><path>` URL
and internally invokes `proxy_pass` with it (path included) and `grpc_pass`
with it (path dropped, since gRPC carries the method in the `:path`
pseudo-header and `grpc_pass` does not accept a URI part). `kong_pass` then
installs its own request-time handler in place of whichever one of the two
ran last. Because that URL always embeds `$selector`, `proxy_pass` and
`grpc_pass` always take their own dynamic-URL code path (as they would for
any `proxy_pass`/`grpc_pass` value containing a variable): `host` is not
checked against known `upstream {}` blocks at configuration time, and, if it
does not name one, nginx resolves it as a hostname at request time, which
needs a [`resolver`](https://nginx.org/en/docs/http/ngx_http_core_module.html#resolver)
configured.

`$selector` must be a single variable, written either as `$name` or as
`${name}`, and is evaluated fresh for every request:

- a value of exactly `grpc` or `grpcs`, case-insensitive, dispatches to
`grpc_pass`; the `version=` parameter, if given, is still parsed but has
no effect, since gRPC always speaks HTTP/2
- any other value dispatches to `proxy_pass`

`host` and `path` are plain configuration tokens, not compiled separately:
`kong_pass` concatenates `$selector`, `host` and `path` into that one URL
string at configuration time and hands it, unchanged, to `proxy_pass` (and
to `grpc_pass`, minus `path`, per above). Because `$selector` guarantees the
string contains a variable reference, both directives compile it
dynamically, so a `$variable` written inside `host` or `path` is honoured
too, evaluated per request, the same as it would be in a literal
`proxy_pass`/`grpc_pass` value. `host` must not be empty; `path` may be, in
which case only `<$selector>://<host>` is passed to `proxy_pass`, leaving the
client's URI to the upstream. A `path` written as a literal must start with
`/`, because it is appended straight onto `host` with no separator; a `path`
that starts with `$` is evaluated per request, and so cannot be checked at
configuration time.

Note what dropping `path` for `grpc_pass` means in practice: `path` decides
the upstream URI only for requests `$selector` sends to `proxy_pass`. A
request that goes to `grpc_pass` carries the client's own URI in the `:path`
pseudo-header, because that is where nginx's gRPC module takes it from, so
whatever rewriting `path` expresses does not reach it. A `path` of
`$upstream_uri` holding a rewritten path, for instance, applies to the
HTTP/1.x and HTTP/2 proxy dispatch and not to the gRPC one.

`version=$variable` is optional and, when given, must be a single variable,
in either spelling. It is evaluated per request, only when `$selector`
selected `proxy_pass`:

- an empty value, or `1.1`, uses whatever `proxy_http_version` is configured
for the location, same as not specifying `version=` at all
- `2` proxies to the upstream over HTTP/2 for that request, regardless of
`proxy_http_version`
- anything else is not supported; `kong_pass` logs a warning naming the
value and falls back to `proxy_http_version`, so an unrecognised value
does not silently look like it took effect

`$selector`, `host` and `path` are always required, in that order, and
`kong_pass` may only appear once per location: a second `kong_pass` in the
same location, `if` block or `limit_except` block is a configuration error.
Every argument after `path` must be `name=value`; `version=` is currently
the only one `kong_pass` recognises, and it may only be given once.

A `kong_pass` in a location also covers that location's `if` and
`limit_except` blocks, which nginx runs against separate location configs of
their own: the selector, the `version=` value and both captured handlers are
inherited by such a block that does not set its own. For `limit_except`,
`kong_pass` also becomes the block's content handler, so `$selector` decides
which of `proxy_pass` and `grpc_pass` runs for the requests that block
restricts, even where the block writes one of them itself. Such a directive
still configures whichever handler ends up running — a `proxy_pass` written
there is what a non-`grpc` selector proxies to — but a `grpc` selector sends
those requests to `grpc_pass` regardless of it. Write `kong_pass` in the
`limit_except` block itself to give it a dispatch of its own.

Take care where the selector and `version=` variables come from in that
case. A `limit_except` block runs against a location config that
`ngx_http_rewrite_module` does not inherit into, and `set` is not allowed
inside one either, so no `set` reaches the requests a `limit_except`
restricts: a selector taken from `set $upstream_scheme` evaluates empty for
them, and `proxy_pass` then rejects the URL. Use a variable that any
location config can read, such as one from
[`map`](https://nginx.org/en/docs/http/ngx_http_map_module.html#map), an
`$arg_` variable, or one whose value is assigned at request time.

`kong_pass` is only built when nginx builds both of the modules whose
directives it invokes, `ngx_http_proxy_module` and `ngx_http_grpc_module`, and
the latter needs `ngx_http_v2_module`, which `configure` leaves out unless
`--with-http_v2_module` is given. A build missing any of the three has no
`kong_pass` directive at all, and nginx rejects a configuration using it with
"unknown directive"; the rest of this module is unaffected either way.

Selecting HTTP/2 with `version=` additionally requires:

- nginx 1.29.4 or later, which is when `ngx_http_proxy_module.h` and its
HTTP/2 upstream handler first shipped. On an older nginx, `kong_pass`
still works for HTTP/1.x, but `version=` has no effect: it is still
parsed (so the same configuration works unchanged across nginx versions),
but a warning is logged at startup, and, in the request path, `2` falls
back to `proxy_http_version`. It does so without a warning of its own,
since it is a value `kong_pass` recognises and the startup warning has
already reported that this build cannot honour it
- the patch described under [Description](#description) that defines
`NGX_HTTP_UPSTREAM_PRESERVE_OUTPUT_PATCH`. Without it, `version=2` falls
back the same way, even on nginx 1.29.4 or later

[Back to TOC](#table-of-contents)

Variables
=========

Expand Down
11 changes: 11 additions & 0 deletions config
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,17 @@ ngx_module_srcs=" \
$ngx_addon_dir/src/ssl/ngx_lua_kong_ssl.c \
"

# kong_pass hands its URL to proxy_pass and grpc_pass, so it references
# ngx_http_proxy_module and ngx_http_grpc_module and can only be built when
# nginx builds both of them. ngx_http_grpc_module in turn needs
# ngx_http_v2_module, which configure leaves out unless --with-http_v2_module
# is given, so a default build has no kong_pass and no reference to either
# module.
if [ "$HTTP_PROXY" = YES -a "$HTTP_GRPC" = YES -a "$HTTP_V2" = YES ]; then
ngx_module_srcs="$ngx_module_srcs $ngx_addon_dir/src/ngx_http_lua_kong_pass.c"
have=NGX_HTTP_LUA_KONG_PASS . auto/have
fi

if [ -z "$NGX_LUA_LOC" ]; then
ngx_lua_loc="../ngx_lua-*"
else
Expand Down
91 changes: 75 additions & 16 deletions lualib/resty/kong/var.lua
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,13 @@ local NGX_DECLINED = ngx.DECLINED


local variable_index = {}
local cookie_indexes = {}
local metatable_patched
local str_replace_char
local replace_dashes_lower

local find = string.find

local HTTP_PREFIX = "http_"

--Add back if stream module is implemented to aid readability
Expand All @@ -45,6 +48,9 @@ if subsystem == "http" then
unsigned int index, const unsigned char *value, size_t value_len,
char **err);

int ngx_http_lua_kong_ffi_var_invalidate_by_index(ngx_http_request_t *r,
unsigned int index, char **err);

unsigned int ngx_http_lua_kong_ffi_var_load_indexes(ngx_str_t **names);
]]

Expand Down Expand Up @@ -100,6 +106,16 @@ local function load_indexes()
end
end

-- one Cookie header feeds every $cookie_* variable, so rewriting it
-- makes all of them stale at once
cookie_indexes = {}

for name, index in pairs(variable_index) do
if find(name, "cookie_", 1, true) == 1 then
cookie_indexes[#cookie_indexes + 1] = index
end
end

return variable_index
end

Expand Down Expand Up @@ -129,6 +145,20 @@ local function var_get_by_index(index)
end


local function var_invalidate_by_index(index)
local r = get_request()

local rc = C.ngx_http_lua_kong_ffi_var_invalidate_by_index(r, index,
errmsg)
if rc == NGX_OK then
return
end

assert(rc == NGX_ERROR)
error(ffi_str(errmsg[0]), 2)
end


local function var_set_by_index(index, value)
local r = get_request()

Expand Down Expand Up @@ -160,23 +190,52 @@ local function var_set_by_index(index, value)
end


-- Rewriting a request header leaves the variables fed by it holding the
-- value they cached before the rewrite, so drop those cached values. Only
-- for this request: dropping the index instead, as this once did, would send
-- every later request in the worker down the slower unindexed read as well.
--
-- $args and its relatives need nothing here, even though set_uri_args
-- changes them: nginx marks them non-cacheable, so an indexed read of one
-- re-reads it anyway. $host needs nothing either, because lua-nginx-module
-- invalidates that one itself whenever the Host header is written.
local function invalidate_request_header_vars(name)
name = replace_dashes_lower(name)

local index = variable_index[HTTP_PREFIX .. name]
if index then
var_invalidate_by_index(index)
end

if name == "content_type" or name == "content_length" then
-- a variable of its own, named exactly like the header
index = variable_index[name]
if index then
var_invalidate_by_index(index)
end

elseif name == "cookie" then
for i = 1, #cookie_indexes do
var_invalidate_by_index(cookie_indexes[i])
end
end
end


local function patch_functions()
local orig_set_uri_args = req.set_uri_args

req.set_uri_args = function(...)
variable_index.args = nil
return orig_set_uri_args(...)
end

local orig_set_header = req.set_header

req.set_header = function(name, value)
local normalized_header = replace_dashes_lower(name)
normalized_header = HTTP_PREFIX .. normalized_header
variable_index[normalized_header] = nil

return orig_set_header(name, value)
end
local orig_set_header = req.set_header

req.set_header = function(name, value)
invalidate_request_header_vars(name)
return orig_set_header(name, value)
end

local orig_clear_header = req.clear_header

req.clear_header = function(name)
invalidate_request_header_vars(name)
return orig_clear_header(name)
end
end


Expand Down
8 changes: 6 additions & 2 deletions src/ngx_http_lua_kong_common.h
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,12 @@ typedef struct {


typedef struct {
ngx_str_t tag;
ngx_int_t request_id_var_index;
ngx_str_t tag;
ngx_int_t request_id_var_index;
ngx_int_t pass_selector_index;
ngx_int_t pass_version_index;
ngx_http_handler_pt proxy_handler;
ngx_http_handler_pt grpc_handler;
} ngx_http_lua_kong_loc_conf_t;


Expand Down
15 changes: 15 additions & 0 deletions src/ngx_http_lua_kong_directive.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@
#include "ngx_http_lua_kong_common.h"


ngx_int_t
ngx_http_lua_kong_variable_index(ngx_conf_t *cf, ngx_str_t *value);

char *
ngx_http_lua_kong_load_var_index(ngx_conf_t *cf, ngx_command_t *cmd,
void *conf);
Expand All @@ -30,4 +33,16 @@ char *
ngx_http_lua_kong_error_log_request_id(ngx_conf_t *cf, ngx_command_t *cmd,
void *conf);

#if (NGX_HTTP_LUA_KONG_PASS)

char *
ngx_http_lua_kong_pass(ngx_conf_t *cf, ngx_command_t *cmd,
void *conf);

char *
ngx_http_lua_kong_pass_merge_loc_conf(ngx_conf_t *cf,
ngx_http_lua_kong_loc_conf_t *prev, ngx_http_lua_kong_loc_conf_t *conf);

#endif

#endif /* _NGX_HTTP_LUA_KONG_DIRECTIVE_H_INCLUDED_ */
10 changes: 4 additions & 6 deletions src/ngx_http_lua_kong_log_handler.c
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
* limitations under the License.
*/

#include "ngx_http_lua_kong_common.h"
#include "ngx_http_lua_kong_directive.h"


/*
Expand Down Expand Up @@ -127,16 +127,14 @@ ngx_http_lua_kong_error_log_request_id(ngx_conf_t *cf, ngx_command_t *cmd, void

value = cf->args->elts;

if (value[1].data[0] != '$') {
lcf->request_id_var_index = ngx_http_lua_kong_variable_index(cf, &value[1]);

if (lcf->request_id_var_index == NGX_DECLINED) {
ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
"invalid variable name \"%V\"", &value[1]);
return NGX_CONF_ERROR;
}

value[1].len--;
value[1].data++;

lcf->request_id_var_index = ngx_http_get_variable_index(cf, &value[1]);
if (lcf->request_id_var_index == NGX_ERROR) {
return NGX_CONF_ERROR;
}
Expand Down
Loading
Loading