From 0673df8d41ae293637c3365b53248e1c26b5dd7a Mon Sep 17 00:00:00 2001 From: Adam Wright Date: Tue, 20 Oct 2026 18:28:58 +0000 Subject: [PATCH 1/2] nginx: send Java the path as sent, so an encoded ';' stays one The /ContentService/ location's proxy_pass had a URI part, so nginx sent Tomcat its decoded copy of the path: %3B arrived as ';', which Tomcat dropped as a matrix parameter. /data/query%3Bx/R-HSA-109581 and /data/database/version%3B -- no node location's -- were answered by Java as node's /data/query/{id} and /data/database/version: a node route answered by Java, which the port rules out, and the refusal of matrix parameters (#526) undone by encoding the ';'. Without a URI part Tomcat gets the path as sent, as the exporter location already does. A spec holds the location to it (shown failing on the old config). On beta, before and after: those two and an encoded ';' in an id are now Java's 404s for the path as sent; ten other URLs (Java's and node's, '//', './', an encoded '/', an export) unchanged. nginx -t ok for dev, release and production. Co-Authored-By: Claude Opus 5.5 --- deploy/nginx/common/routes-core.conf | 12 ++++++++++-- deploy/nginx/routes.spec.ts | 10 ++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/deploy/nginx/common/routes-core.conf b/deploy/nginx/common/routes-core.conf index 45c01db3..7d561846 100644 --- a/deploy/nginx/common/routes-core.conf +++ b/deploy/nginx/common/routes-core.conf @@ -31,7 +31,8 @@ # location is chosen, because the literal locations below do not match a ';' # and would hand it to Java. $request_uri is the path as sent: an encoded # ';' (%3B) is part of an id, as it is to Java, and a ';' in the query string -# is the query's. +# is the query's. That holds only because Java is sent the path as sent, too +# (the /ContentService/ location below). error_page 418 = @content_node_refuses; if ($request_uri ~ "^/ContentService/[^?]*;") { return 418; } location @content_node_refuses { @@ -511,7 +512,14 @@ location ~ ^/gsa(/|$) { return 301 /PathwayBrowser?analysisTab=quantitative; } location /ContentService/ { include /etc/nginx/common/upstream-proxy.conf; - proxy_pass http://content/ContentService/; + # No URI part: Tomcat is sent the request's path as it was sent, not + # nginx's decoded copy. With one, an encoded ';' reached Java as a raw one + # and Java dropped it as a matrix parameter -- so /data/query%3Bx/R-HSA-1, + # which no node location matches, was answered by Java as node's + # /data/query/R-HSA-1: a node route answered by Java, the fallback the + # port rules out. (No braces in comments here: routes.spec.ts reads a + # location's body up to its first closing one.) + proxy_pass http://content; # A diagram or SBML export takes minutes. Apache's default is 300s and the # site relies on it; nginx defaults to 60 and would cut them off. proxy_read_timeout 300s; diff --git a/deploy/nginx/routes.spec.ts b/deploy/nginx/routes.spec.ts index 3efa5f6d..d64f959e 100644 --- a/deploy/nginx/routes.spec.ts +++ b/deploy/nginx/routes.spec.ts @@ -288,6 +288,16 @@ describe('a matrix parameter', () => { ]); }); + it('reaches Java encoded, as sent, so Java cannot drop it as one', () => { + // A proxy_pass with a URI part sends nginx's decoded path: %3B arrives at + // Tomcat as ';', which it drops as a matrix parameter, and + // /data/query%3Bx/{id} -- no node location's -- is answered by Java as + // node's /data/query/{id}. Without one, Tomcat gets the path as sent. + const core = readFileSync(join(__dirname, 'common', 'routes-core.conf'), 'utf8'); + const block = /^location \/ContentService\/ \{([^}]*)\}/m.exec(core)?.[1] ?? ''; + expect(block.match(/^\s*proxy_pass\s+[^;]*;/gm)).toEqual([' proxy_pass http://content;']); + }); + it('is not an encoded one, nor one in the query string', async () => { for (const url of [ '/ContentService/data/query/abridged/R-HSA-109581%3Bx', From 042bb3c6023b51b43feefc1606f4cb4ec0bba020 Mon Sep 17 00:00:00 2001 From: Adam Wright Date: Tue, 20 Oct 2026 18:48:47 +0000 Subject: [PATCH 2/2] Review of #572: the AnalysisService location sends the path as sent too The same rule as /ContentService/: no URI part on its proxy_pass, so the service is sent the request's path as it was sent, never nginx's decoded copy. The spec holds both locations to it (shown failing on the AnalysisService's old line). nginx -t ok for dev, release and production. Co-Authored-By: Claude Opus 5.5 --- deploy/nginx/common/routes-core.conf | 4 +++- deploy/nginx/routes.spec.ts | 15 ++++++++++++--- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/deploy/nginx/common/routes-core.conf b/deploy/nginx/common/routes-core.conf index 7d561846..a907babb 100644 --- a/deploy/nginx/common/routes-core.conf +++ b/deploy/nginx/common/routes-core.conf @@ -528,7 +528,9 @@ location /ContentService/ { location /AnalysisService/ { include /etc/nginx/common/upstream-proxy.conf; - proxy_pass http://analysis/AnalysisService/; + # No URI part, as for /ContentService/: the path as sent, never nginx's + # decoded copy of it. + proxy_pass http://analysis; # An analysis over a large identifier list is not quick either. proxy_read_timeout 300s; proxy_send_timeout 300s; diff --git a/deploy/nginx/routes.spec.ts b/deploy/nginx/routes.spec.ts index d64f959e..d7809bc0 100644 --- a/deploy/nginx/routes.spec.ts +++ b/deploy/nginx/routes.spec.ts @@ -292,10 +292,19 @@ describe('a matrix parameter', () => { // A proxy_pass with a URI part sends nginx's decoded path: %3B arrives at // Tomcat as ';', which it drops as a matrix parameter, and // /data/query%3Bx/{id} -- no node location's -- is answered by Java as - // node's /data/query/{id}. Without one, Tomcat gets the path as sent. + // node's /data/query/{id}. Without one, Tomcat gets the path as sent; + // the AnalysisService's too. const core = readFileSync(join(__dirname, 'common', 'routes-core.conf'), 'utf8'); - const block = /^location \/ContentService\/ \{([^}]*)\}/m.exec(core)?.[1] ?? ''; - expect(block.match(/^\s*proxy_pass\s+[^;]*;/gm)).toEqual([' proxy_pass http://content;']); + for (const [service, upstream] of [ + ['ContentService', 'content'], + ['AnalysisService', 'analysis'], + ]) { + const block = new RegExp(`^location /${service}/ \\{([^}]*)\\}`, 'm').exec(core)?.[1] ?? ''; + expect([service, block.match(/^\s*proxy_pass\s+[^;]*;/gm)]).toEqual([ + service, + [` proxy_pass http://${upstream};`], + ]); + } }); it('is not an encoded one, nor one in the query string', async () => {