From 446ecb34527bfdced97df6286d0072ff7abc3c01 Mon Sep 17 00:00:00 2001 From: Manish Kumar Date: Tue, 1 Sep 2026 01:18:19 -0500 Subject: [PATCH 1/3] fix(nginx): correct set/rewrite directive ordering for dynamic upstreams `rewrite ... break;` halts processing of the current rewrite-module directive set, including any `set $..._upstream` that follows it in the same location block. Six locations (workbench, lims, auth, license, policy, hpc) had `set` placed after `rewrite ... break;`, so the variable was never assigned and proxy_pass resolved to `http://` (empty), producing a 500 before the backend was ever contacted. Introduced as a side effect of tonight's resolver-based upstream fix (commit d003395). Reordered set before rewrite in all six locations. Verified live: nginx -t clean, 4/4 heartbeat probes over ~1min watch window, zero uninitialized-variable or invalid-URL-prefix log entries. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_0135NEjymEpbh7fhWwqz9CwL --- docker/nginx-router.conf | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/docker/nginx-router.conf b/docker/nginx-router.conf index bc39999..fd97c8e 100644 --- a/docker/nginx-router.conf +++ b/docker/nginx-router.conf @@ -271,7 +271,15 @@ server { index index.html; autoindex off; } - location ^~ /_svc/workbench { rewrite ^/_svc/workbench(/.*)$ $1 break; set $workbench_upstream workbench:8000; proxy_pass http://$workbench_upstream; } + # NOTE: `set` must come BEFORE `rewrite ... break;`, not after -- `break` + # stops processing of the *entire* current set of ngx_http_rewrite_module + # directives (that's `set` too, not just `rewrite`/`if`), so a `set` + # written after it in the same location never runs. Got this backwards + # on the first pass: `rewrite ...; set $x ...; proxy_pass http://$x;` + # silently left $workbench_upstream (and five siblings below) permanently + # empty -- "using uninitialized ... variable" / "invalid URL prefix in + # http://" / 500, confirmed live in the error log. + location ^~ /_svc/workbench { set $workbench_upstream workbench:8000; rewrite ^/_svc/workbench(/.*)$ $1 break; proxy_pass http://$workbench_upstream; } location ^~ /_svc/lims { # NOTE: this location defines its own proxy_set_header directives, which means # none of the server-level ones above are inherited (nginx behavior: a location @@ -281,8 +289,8 @@ server { proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $router_xfp; - rewrite ^/_svc/lims(/.*)$ $1 break; set $lims_upstream lims:7000; + rewrite ^/_svc/lims(/.*)$ $1 break; proxy_pass http://$lims_upstream; } # LIMS's session login (django.contrib.auth's LoginView/LogoutView, @@ -508,8 +516,8 @@ server { location ^~ /_svc/toolserver { rewrite ^/_svc/toolserver(/.*)$ $1 break; proxy_pass http://toolserver; } location ^~ /_svc/auth { limit_req zone=auth_limit burst=5 nodelay; - rewrite ^/_svc/auth(/.*)$ $1 break; set $auth_upstream auth-service:8001; + rewrite ^/_svc/auth(/.*)$ $1 break; proxy_pass http://$auth_upstream; } # Bare /auth/* — OAuth2 login/callback redirect targets registered with @@ -544,8 +552,8 @@ server { # /api/license/* traffic. location ^~ /api/license/ { limit_req zone=auth_limit burst=5 nodelay; - rewrite ^/api/license(/.*)$ $1 break; set $auth_upstream auth-service:8001; + rewrite ^/api/license(/.*)$ $1 break; proxy_pass http://$auth_upstream; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; @@ -704,16 +712,16 @@ server { location ^~ /ops/ { set $workbench_upstream workbench:8000; proxy_pass http://$workbench_upstream; } location ^~ /plugins/ { set $workbench_upstream workbench:8000; proxy_pass http://$workbench_upstream; } location ^~ /_svc/policy { - rewrite ^/_svc/policy/?(.*)$ /$1 break; set $policy_upstream policy-engine:8002; + rewrite ^/_svc/policy/?(.*)$ /$1 break; proxy_pass http://$policy_upstream; proxy_http_version 1.1; proxy_set_header Connection ""; proxy_buffering off; } location ^~ /_svc/hpc { - rewrite ^/_svc/hpc/?(.*)$ /$1 break; set $hpc_upstream hpc-policy-engine:8003; + rewrite ^/_svc/hpc/?(.*)$ /$1 break; proxy_pass http://$hpc_upstream; proxy_http_version 1.1; proxy_set_header Connection ""; From e81e1696d11a3be7ef05be8be1891f061fd54ea3 Mon Sep 17 00:00:00 2001 From: Manish Kumar Date: Tue, 1 Sep 2026 01:18:23 -0500 Subject: [PATCH 2/3] fix(rag): bake RAGBIO_API_KEY into the frontend build MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Vite env vars are compile-time — the compose environment: block only ever reached the running rag backend container, never the already-built frontend bundle, leaving the key empty client-side and causing RAG's "Not authenticated" state. Adds RAGBIO_API_KEY as a build arg to the rag build block. Verified: rebuilt bundle contains the real key (confirmed non-empty prefix), /_svc/rag/ returns 200 through nginx-router. (An earlier nginx-side fix for this was misdiagnosed and has been reverted — the actual cause was frontend build-time, not routing.) Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_0135NEjymEpbh7fhWwqz9CwL --- docker-compose.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/docker-compose.yml b/docker-compose.yml index 89cb36c..791dda9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -382,6 +382,19 @@ services: # installation commands; it is never passed as a build ARG. secrets: - github_token + # ragbio-ui (the frontend built by the ui-builder stage, which the + # `backend` target still depends on and COPYs dist/ from -- see the + # Dockerfile) bakes this in as import.meta.env.VITE_RAGBIO_API_KEY at + # build time (Vite env vars are compile-time, not runtime -- setting + # RAGBIO_API_KEY under `environment:` below only reaches the running + # backend process, never the already-built frontend bundle). Without + # this build arg the Dockerfile's `ARG RAGBIO_API_KEY` stays empty + # regardless of what .env holds, so every ragbio-ui request sends a + # literal "Authorization: Bearer undefined" and the backend correctly + # (if unhelpfully) reports "Not authenticated" -- confirmed missing + # here, not an nginx routing gap. + args: + RAGBIO_API_KEY: ${RAGBIO_API_KEY} restart: on-failure # image: ghcr.io/omnibioai/omnibioai-rag:latest # Frontend served at http://localhost:8090/ via nginx (port 5175 inside container) From 417092c157059c85d41d6014364fd1de4eed979c Mon Sep 17 00:00:00 2001 From: Manish Kumar Date: Tue, 1 Sep 2026 01:18:27 -0500 Subject: [PATCH 3/3] fix(jobs): attach Authorization header to TES API calls Jobs.jsx's apiFetch() helper set Content-Type but never imported getToken() or attached an Authorization header, causing every TES request to fail with 401 "missing bearer token" despite a valid session. rolesApi.js already had the correct working pattern. Imports getToken from ../lib/session (matches the web-build alias vite.config.js resolves on) and attaches Authorization: Bearer ${token}, consistent with rolesApi.js. Verified: fake-token test now correctly returns "invalid token" instead of "missing bearer token"; real session returns 200. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_0135NEjymEpbh7fhWwqz9CwL --- src/ui/pages/Jobs.jsx | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/ui/pages/Jobs.jsx b/src/ui/pages/Jobs.jsx index 29179ee..2ef1787 100644 --- a/src/ui/pages/Jobs.jsx +++ b/src/ui/pages/Jobs.jsx @@ -1,5 +1,5 @@ import React, { useState, useEffect, useCallback, useRef } from "react"; -import { isElectron } from "../lib/session"; +import { isElectron, getToken } from "../lib/session"; function getHost() { return ( @@ -18,9 +18,19 @@ function tesUrl(path) { } async function apiFetch(path, opts = {}) { + // TES's own auth middleware requires a bearer token ({"detail":"missing + // bearer token"} on :8081) -- this fetch() is a normal same-origin XHR, + // not an iframe navigation (unlike RAG/Control Center), so it's fully + // able to attach a real header itself instead of relying on nginx to + // synthesize one from a cookie. Same pattern as rolesApi.js's request(). + const token = getToken(); const res = await fetch(tesUrl(path), { ...opts, - headers: { "Content-Type": "application/json", ...(opts.headers || {}) }, + headers: { + "Content-Type": "application/json", + ...(token ? { Authorization: `Bearer ${token}` } : {}), + ...(opts.headers || {}), + }, signal: opts.signal || AbortSignal.timeout(10000), }); if (!res.ok) {