Repository navigation
Encoded "?" (%3F) in the request path is cut off when the backend has a path #1612
Description
When a Product uses a Backend with a path, the routing rule that 3scale generates renders the decoded request path ({{uri | remove_first: '<path>'}}). If the client sends a percent-encoded ? in a path segment, APIcast treats it as the start of the query string and everything after it is dropped before the request reaches the upstream. No error is logged and the upstream gets a different resource.
%2F has a related problem: it reaches the upstream as /, so the upstream sees one more path segment than the client sent.
Version
nginx version: openresty/1.27.1.2
quay.io/3scale/apicast:latest created 2026-10-08T08:29:38Z (master 2cef7987)
Same result with quay.io/3scale/apicast:3scale-2.15-stable and 3scale-2.16-stable.
Steps To Reproduce
Test::Nginx case, same rule as the one 3scale generates for a Backend used with the path /REST/svc:
=== TEST 1: backend with a path keeps "%3F" and "%2F" in the upstream request path
--- configuration
{
"services": [
{
"id": 42,
"proxy": {
"policy_chain": [
{
"name": "apicast.policy.routing",
"configuration": {
"rules": [
{
"url": "http://test:$TEST_NGINX_SERVER_PORT/REST/svc",
"replace_path": "{{uri | remove_first: '/REST/svc'}}",
"condition": {
"operations": [
{ "match": "path", "op": "matches", "value": "^(/REST/svc/.*|/REST/svc/?)" }
]
}
}
]
}
}
]
}
}
]
}
--- upstream
location ~ / {
content_by_lua_block {
ngx.say(ngx.var.request_uri)
}
}
--- request eval
["GET /REST/svc/files/a%3Fb", "GET /REST/svc/files/a%3Fb?x=1", "GET /REST/svc/files/a%2Fb"]
--- response_body eval
["/REST/svc/files/a%3Fb\n", "/REST/svc/files/a%3Fb?x=1\n", "/REST/svc/files/a%2Fb\n"]
--- error_code eval
[200, 200, 200]
--- no_error_log
[error]
Current Result
| Client sends | Upstream receives |
|---|---|
/REST/svc/files/a%3Fb |
/REST/svc/files/a |
/REST/svc/files/a%3Fb?x=1 |
/REST/svc/files/a?x=1 |
/REST/svc/files/a%2Fb |
/REST/svc/files/a/b |
Expected Result
The upstream receives the path as the client sent it: /REST/svc/files/a%3Fb, /REST/svc/files/a%3Fb?x=1 and /REST/svc/files/a%2Fb.
Additional Information
- Failing test on master: https://github.com/sergiotravassos/APIcast/actions/runs/40666248684 (branch
proof/master-encoded-path-backend, filet/apicast-policy-routing-backend-path-encoding.t). - Same check with the published images (2.15, 2.16, latest), APIcast in front of an upstream that echoes the raw request target: https://github.com/sergiotravassos/APIcast/actions/runs/40666894234
- The runs are red on purpose: each check fails when the upstream receives a path different from the one the client sent.
- Where it happens for
%3F:upstream_selector.luarendersreplace_pathand callsUpstream:append_path(upstream_selector.lua#L44).append_pathsplits the rendered value withurl_helper.split_path, so the decoded?becomes a query separator. The "query" part is not empty, soappend_pathreturns without keeping it (upstream.lua#L123-L137). - For
%2Fthe information is gone onceuriis decoded.original_request.pathstill has the raw path, but the rule is generated by porta withuri(routing_policy.rb#L77). - Related: Policy: Routing fix URL encoding. #1208 (THREESCALE-5454) and Routing: Fix encoding. #1297 (THREESCALE-5117), which mention that
?/#are hard to get right withngx.var.uri.
PR for the %3F case: #1613 (keeps a decoded ? coming from uri in the path instead of splitting on it). For %2F I'm not sure which way you would prefer, since the template comes from porta.
Activity