Skip to content

Commit a6be845

Browse files
fix: make self-host Agent tab and dashboards work end-to-end (#24)
## Summary - Route Agent tab (`/agent-api`) and backend dashboard generation through host Hermes on `:8787` (`host.docker.internal`, nginx `$http_host`, Vite `changeOrigin: false`) so CSRF and Docker networking stop 502ing. - Switch Hermes profile (cookie jar in `AgentChatClient`, frontend `switchAgentProfile`) before session create so chat/start no longer 404s under `default`. - Add `scripts/self-host/setup-agent.sh` (+ install/smoke/status wiring, e2e check) so a fresh self-host gets MCP SDK, MCP token, and webui without manual Hermes setup; harden `agent/install.sh` for `DEEPSQL_CHAT_*` and nested `HERMES_HOME`. ## Test plan - [ ] `./scripts/self-host/setup-agent.sh` starts webui; `curl -fsS http://127.0.0.1:8787/api/mcp/servers` lists `deepsql` - [ ] Log in at `:3000` → Agent tab boots, runs a SQL question via MCP - [ ] Dashboards → AI generate returns an HTML artifact (no “agent unavailable” 502) - [ ] `./scripts/self-host/smoke-test.sh` passes agent path checks (`profile/switch` + backend session) - [ ] Confirm Brain jobs scheduler no longer NPEs on null `consecutive_failures`
1 parent 2afdb3c commit a6be845

18 files changed

Lines changed: 944 additions & 55 deletions

File tree

.env.example

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,14 @@ DEEPSQL_EMBEDDING_MODEL=text-embedding-3-large
128128
# profile which defaults to false.
129129
EMBEDDING_FAIL_OPEN=false
130130

131+
# ── Hermes agent (Agent tab + AI dashboards) ────────────────────────────────
132+
# Backend → Hermes for dashboards / Slack / CLI. Compose defaults this; only
133+
# override if Hermes listens elsewhere. Requires scripts/self-host/setup-agent.sh
134+
# (also run by install.sh unless DEEPSQL_SKIP_AGENT_SETUP=1).
135+
#AGENT_WEBUI_URL=http://host.docker.internal:8787
136+
#DEEPSQL_SKIP_AGENT_SETUP=0
137+
#DEEPSQL_SMOKE_AGENT=1
138+
131139
# ── The /api/llm/v1 agent gateway ───────────────────────────────────────────
132140
# The DeepSQL CLI agent (`@deepsql/mcp`) points its model at <backend>/api/llm/v1
133141
# and authenticates with a DeepSQL token; the backend forwards those calls

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,3 +100,5 @@ optd-sidecar/target/
100100
.idea/
101101
.vscode/
102102
*.iml
103+
.local-admin-credentials
104+
.local-mcp-token

README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,10 @@ is rejected. Changing width means migrating the column.
176176

177177
The installer generates your JWT secret, the credential-vault encryption key and the vault
178178
DB password, prompts for the first admin account, builds both images, starts the stack, and
179-
verifies pgvector is live.
179+
verifies pgvector is live. Unless you set `DEEPSQL_SKIP_AGENT_SETUP=1`, it also runs
180+
[`scripts/self-host/setup-agent.sh`](scripts/self-host/setup-agent.sh) to install Hermes under
181+
`~/.hermes/`, wire DeepSQL MCP, and start the webui on `0.0.0.0:8787` (required for the
182+
**Agent** tab and AI dashboard generation).
180183

181184
**The first build takes several minutes** — it compiles the Spring Boot backend with Maven
182185
inside the container and bundles the frontend with Vite. It has not hung. Later builds reuse

agent/install.sh

Lines changed: 81 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,55 +3,115 @@
33
# Idempotent: safe to re-run. Source of truth is this repo's agent/ dir.
44
#
55
# Configures:
6-
# - model: existing Azure OpenAI gpt-5.4 via its OpenAI-compatible v1 endpoint
6+
# - model: from DEEPSQL_CHAT_* (or legacy AZURE_OPENAI_*) via OpenAI-compatible endpoint
77
# - mcp_servers.deepsql: the repo's DeepSQL MCP server (read-only DBA tools)
88
# - skills.external_dirs: this repo's agent/skills (source of truth)
99
# - approvals.mode: smart
1010
# - SOUL.md: the DBA persona
1111
# - disables host-affecting toolsets (terminal/file/code/browser/computer_use)
1212
#
1313
# Secrets are read from the environment (or the repo .env), never committed:
14-
# AZURE_OPENAI_KEY, AZURE_OPENAI_ENDPOINT (endpoint defaults to the repo value)
14+
# DEEPSQL_CHAT_API_KEY, DEEPSQL_CHAT_ENDPOINT, DEEPSQL_CHAT_MODEL
15+
# (legacy fallback: AZURE_OPENAI_KEY, AZURE_OPENAI_ENDPOINT)
1516
#
1617
# Upstream note: HERMES_HOME / hermes-agent / hermes CLI are contracts of the
1718
# Nous Hermes Agent runtime this customization runs on — do not rename those.
1819
set -euo pipefail
1920

2021
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
21-
HERMES_HOME="${HERMES_HOME:-$HOME/.hermes}"
22+
# Reject a nested profile home inherited from a live Hermes process.
23+
if [[ "${HERMES_HOME:-}" == */profiles/* ]]; then
24+
unset HERMES_HOME
25+
fi
26+
HERMES_HOME="${DEEPSQL_HERMES_HOME:-${HERMES_HOME:-$HOME/.hermes}}"
2227
AGENT_DIR="${HERMES_AGENT_DIR:-$HERMES_HOME/hermes-agent}"
23-
VENV_PY="$AGENT_DIR/.venv/bin/python"
2428

25-
# Load repo .env for Azure creds if not already in the environment.
29+
# Prefer the same interpreter order as scripts/self-host/setup-agent.sh
30+
if [[ -x "$AGENT_DIR/venv/bin/python" ]]; then
31+
VENV_PY="$AGENT_DIR/venv/bin/python"
32+
elif [[ -x "$AGENT_DIR/.venv/bin/python" ]]; then
33+
VENV_PY="$AGENT_DIR/.venv/bin/python"
34+
else
35+
echo "Agent venv not found under $AGENT_DIR — run scripts/self-host/setup-agent.sh first." >&2
36+
exit 1
37+
fi
38+
39+
# Load repo .env for LLM creds if not already in the environment.
2640
if [[ -f "$REPO_ROOT/.env" ]]; then set -a; . "$REPO_ROOT/.env"; set +a; fi
27-
: "${AZURE_OPENAI_KEY:?Set AZURE_OPENAI_KEY (or add it to repo .env)}"
28-
AZURE_ENDPOINT_HOST="$(printf '%s' "${AZURE_OPENAI_ENDPOINT:-https://your-resource.cognitiveservices.azure.com/}" | sed -E 's#https?://##; s#/.*##; s#\.cognitiveservices\.azure\.com#.openai.azure.com#')"
29-
BASE_URL="https://${AZURE_ENDPOINT_HOST}/openai/v1"
3041

31-
[[ -x "$VENV_PY" ]] || { echo "Agent venv not found at $VENV_PY — install the agent runtime first."; exit 1; }
42+
# Prefer the same BYO-LLM vars the Spring backend uses. Fall back to legacy
43+
# AZURE_OPENAI_* for older checkouts.
44+
API_KEY="${DEEPSQL_CHAT_API_KEY:-${AZURE_OPENAI_KEY:-}}"
45+
ENDPOINT="${DEEPSQL_CHAT_ENDPOINT:-${AZURE_OPENAI_ENDPOINT:-}}"
46+
MODEL="${DEEPSQL_CHAT_MODEL:-gpt-5.4}"
47+
48+
if [[ -z "$API_KEY" ]]; then
49+
echo "Error: set DEEPSQL_CHAT_API_KEY (or AZURE_OPENAI_KEY) in the environment or $REPO_ROOT/.env" >&2
50+
exit 1
51+
fi
52+
if [[ -z "$ENDPOINT" ]]; then
53+
echo "Error: set DEEPSQL_CHAT_ENDPOINT (or AZURE_OPENAI_ENDPOINT)." >&2
54+
exit 1
55+
fi
56+
57+
# Normalize to an OpenAI-compatible …/openai/v1 or …/v1 base URL.
58+
# Azure Cognitive Services / Azure OpenAI hosts need /openai/v1; plain OpenAI
59+
# and OpenAI-compatible servers already expose /v1.
60+
normalize_base_url() {
61+
local ep="$1"
62+
ep="${ep%/}"
63+
if [[ "$ep" == *"/openai/v1" || "$ep" == *"/v1" ]]; then
64+
printf '%s' "$ep"
65+
return
66+
fi
67+
if [[ "$ep" == *".cognitiveservices.azure.com"* || "$ep" == *".openai.azure.com"* || "$ep" == *".azure-api.net"* ]]; then
68+
printf '%s/openai/v1' "$ep"
69+
return
70+
fi
71+
printf '%s/v1' "$ep"
72+
}
73+
BASE_URL="$(normalize_base_url "$ENDPOINT")"
74+
75+
BACKEND_PORT="${DEEPSQL_BACKEND_PORT:-8080}"
3276

3377
echo "→ Repo: $REPO_ROOT"
3478
echo "→ Agent home: $HERMES_HOME"
35-
echo "→ Model base: $BASE_URL"
79+
echo "→ Model: $MODEL @ $BASE_URL"
3680

37-
# Deep-merge the DBA config blocks into ~/.hermes/config.yaml (PyYAML ships with the agent).
38-
REPO_ROOT="$REPO_ROOT" BASE_URL="$BASE_URL" AZURE_OPENAI_KEY="$AZURE_OPENAI_KEY" \
39-
HERMES_HOME="$HERMES_HOME" "$VENV_PY" - <<'PY'
81+
REPO_ROOT="$REPO_ROOT" BASE_URL="$BASE_URL" API_KEY="$API_KEY" MODEL="$MODEL" \
82+
BACKEND_PORT="$BACKEND_PORT" HERMES_HOME="$HERMES_HOME" "$VENV_PY" - <<'PY'
4083
import os, yaml, pathlib
4184
home = pathlib.Path(os.environ["HERMES_HOME"]); repo = os.environ["REPO_ROOT"]
4285
cfg_path = home / "config.yaml"
4386
cfg = yaml.safe_load(cfg_path.read_text()) if cfg_path.exists() else {}
4487
cfg = cfg or {}
4588
cfg["model"] = {
46-
"default": "gpt-5.4", "provider": "custom",
47-
"base_url": os.environ["BASE_URL"], "api_key": os.environ["AZURE_OPENAI_KEY"],
48-
"api_mode": "chat_completions", "context_length": 272000,
89+
"default": os.environ["MODEL"],
90+
"provider": "custom",
91+
"base_url": os.environ["BASE_URL"],
92+
"api_key": os.environ["API_KEY"],
93+
"api_mode": "chat_completions",
94+
"context_length": 272000,
95+
}
96+
cfg.setdefault("providers", {})["custom"] = {
97+
"base_url": os.environ["BASE_URL"],
98+
"api_key": os.environ["API_KEY"],
99+
}
100+
# Keep an existing DEEPSQL_AUTH_TOKEN if a prior setup-agent run wrote one into
101+
# the root config; otherwise leave token unset — setup-agent.sh provisions the
102+
# per-user profile with a minted token.
103+
existing_env = ((cfg.get("mcp_servers") or {}).get("deepsql") or {}).get("env") or {}
104+
mcp_env = {
105+
"DEEPSQL_API_BASE_URL": f"http://localhost:{os.environ['BACKEND_PORT']}/api/",
106+
"DEEPSQL_MCP_USER_ID": existing_env.get("DEEPSQL_MCP_USER_ID", "deepsql-agent"),
107+
"DEEPSQL_MCP_PROJECT_ID": existing_env.get("DEEPSQL_MCP_PROJECT_ID", "deepsql-agent"),
49108
}
109+
if existing_env.get("DEEPSQL_AUTH_TOKEN"):
110+
mcp_env["DEEPSQL_AUTH_TOKEN"] = existing_env["DEEPSQL_AUTH_TOKEN"]
50111
cfg.setdefault("mcp_servers", {})["deepsql"] = {
51112
"command": "node",
52113
"args": [f"{repo}/mcp/deepsql-phase1-server.js"],
53-
"env": {"DEEPSQL_API_BASE_URL": "http://localhost:8080/api/",
54-
"DEEPSQL_MCP_USER_ID": "deepsql-agent", "DEEPSQL_MCP_PROJECT_ID": "deepsql-agent"},
114+
"env": mcp_env,
55115
}
56116
cfg.setdefault("skills", {})["external_dirs"] = [f"{repo}/agent/skills"]
57117
cfg.setdefault("approvals", {})["mode"] = "smart"
@@ -63,10 +123,11 @@ PY
63123
cp "$REPO_ROOT/agent/SOUL.md" "$HERMES_HOME/SOUL.md"
64124
echo " SOUL.md installed"
65125

66-
# Scope to a read-only sandbox: disable host-affecting toolsets.
67126
( cd "$AGENT_DIR" && UV_NO_CONFIG=1 "$VENV_PY" -m hermes_cli.main tools disable \
68127
terminal file code_execution browser computer_use image_gen tts vision web delegation cronjob \
69128
>/dev/null 2>&1 ) || echo " (toolset disable skipped — disable manually with 'hermes tools disable ...')"
70129
echo " host toolsets disabled (read-only deepsql + memory/todo/skills remain)"
71130

72-
echo "✓ DeepSQL Agent customization installed. Verify: (cd $AGENT_DIR && uv run hermes mcp test deepsql)"
131+
echo "✓ DeepSQL Agent customization installed."
132+
echo " Verify: (cd $AGENT_DIR && uv run hermes mcp test deepsql)"
133+
echo " Or run: scripts/self-host/setup-agent.sh (starts webui + provisions MCP token)"

backend/src/main/java/com/dbaagent/service/AgentChatClient.java

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
import java.io.BufferedReader;
1111
import java.io.InputStream;
1212
import java.io.InputStreamReader;
13+
import java.net.CookieManager;
14+
import java.net.CookiePolicy;
1315
import java.net.URI;
1416
import java.net.URLEncoder;
1517
import java.net.http.HttpClient;
@@ -32,12 +34,23 @@
3234
public class AgentChatClient {
3335
private static final Logger log = LoggerFactory.getLogger(AgentChatClient.class);
3436

37+
// Cookie jar: the agent scopes sessions by hermes_profile. Without
38+
// /api/profile/switch first, session/new(profile=u-…) then chat/start
39+
// returns 404 "Session not found" under the default profile.
40+
private final CookieManager cookies = new CookieManager(null, CookiePolicy.ACCEPT_ALL);
3541
private final HttpClient http = HttpClient.newBuilder()
36-
.connectTimeout(Duration.ofSeconds(5)).build();
42+
.connectTimeout(Duration.ofSeconds(5))
43+
.cookieHandler(cookies)
44+
.build();
3745
private final ObjectMapper objectMapper = new ObjectMapper();
3846

39-
/** Internal base URL of the agent webui (compose network). */
40-
@Value("${agent.webui-url:http://deepsql-agent:8787}")
47+
/**
48+
* Base URL of the agent webui. Self-host Compose sets
49+
* {@code AGENT_WEBUI_URL=http://host.docker.internal:8787} (no deepsql-agent
50+
* service in the four-container stack). Override for a dedicated agent
51+
* container on the compose network.
52+
*/
53+
@Value("${agent.webui-url:http://host.docker.internal:8787}")
4154
private String webuiUrl;
4255

4356
/** Hard ceiling on a single agent turn for a channel reply. */
@@ -67,6 +80,13 @@ public String ensureSession(String profile, String existingSessionId) {
6780
}
6881

6982
public SessionAttempt ensureSessionDetailed(String profile, String existingSessionId) {
83+
try {
84+
switchProfile(profile);
85+
} catch (Exception e) {
86+
log.warn("Could not switch agent profile to {} (webui={}): {}",
87+
profile, webuiUrl, e.toString());
88+
return new SessionAttempt(null, describe(e));
89+
}
7090
if (existingSessionId != null && !existingSessionId.isBlank()) {
7191
return new SessionAttempt(existingSessionId, null);
7292
}
@@ -101,6 +121,12 @@ public SessionAttempt ensureSessionDetailed(String profile, String existingSessi
101121
}
102122
}
103123

124+
/** Activate the hermes_profile cookie for subsequent webui API calls. */
125+
private void switchProfile(String profile) throws Exception {
126+
if (profile == null || profile.isBlank()) return;
127+
postJson("/api/profile/switch", Map.of("name", profile));
128+
}
129+
104130
/**
105131
* Start a turn and block until it finishes (or times out), returning the
106132
* assembled assistant text and the tool steps it ran.

backend/src/main/java/com/dbaagent/service/scheduler/BrainJobsService.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,12 @@ private BrainJobStatus toStatus(BrainJob job, String connectionId, ScheduledTask
155155
statusReason = "Recurring job is not registered in db-scheduler";
156156
}
157157

158+
// db-scheduler leaves consecutive_failures NULL until the first
159+
// success/failure is recorded; coerce before unboxing into the int field.
160+
int consecutiveFailures = row != null && row.consecutiveFailures() != null
161+
? row.consecutiveFailures()
162+
: 0;
163+
158164
return new BrainJobStatus(
159165
job.key(),
160166
job.title(),
@@ -165,7 +171,7 @@ private BrainJobStatus toStatus(BrainJob job, String connectionId, ScheduledTask
165171
row != null ? row.executionTime() : null,
166172
row != null ? row.lastSuccess() : null,
167173
row != null ? row.lastFailure() : null,
168-
row != null ? row.consecutiveFailures() : 0,
174+
consecutiveFailures,
169175
status,
170176
statusReason
171177
);

backend/src/test/java/com/dbaagent/service/scheduler/BrainJobsServiceTest.java

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,38 @@ void listJobs_marksMetadataRefreshRunningAndSchemaDriftActiveByDefault() throws
104104
assertEquals("Recurring job is not registered in db-scheduler", driftCheck.statusReason());
105105
}
106106

107+
@Test
108+
void listJobs_toleratesNullConsecutiveFailures() throws Exception {
109+
OffsetDateTime nextRun = OffsetDateTime.of(2026, 4, 8, 9, 30, 0, 0, ZoneOffset.UTC);
110+
111+
when(schemaChangeTrackingService.ensureDefaultDriftConfig("conn-1"))
112+
.thenReturn(new SchemaDriftConfig());
113+
when(jdbcTemplate.query(anyString(), org.mockito.ArgumentMatchers.<RowMapper<Object>>any()))
114+
.thenAnswer(invocation -> {
115+
@SuppressWarnings("unchecked")
116+
RowMapper<Object> mapper = invocation.getArgument(1);
117+
ResultSet rs = mock(ResultSet.class);
118+
when(rs.getString("task_name")).thenReturn("brain-refresh-metadata-lifecycle");
119+
when(rs.getObject("execution_time", OffsetDateTime.class)).thenReturn(nextRun);
120+
when(rs.getBoolean("picked")).thenReturn(false);
121+
when(rs.getObject("last_success", OffsetDateTime.class)).thenReturn(null);
122+
when(rs.getObject("last_failure", OffsetDateTime.class)).thenReturn(null);
123+
when(rs.getObject("consecutive_failures")).thenReturn(null);
124+
when(rs.getObject("last_heartbeat", OffsetDateTime.class)).thenReturn(null);
125+
return List.of(mapper.mapRow(rs, 0));
126+
});
127+
128+
List<BrainJobsService.BrainJobStatus> jobs = service.listJobs("conn-1");
129+
130+
BrainJobsService.BrainJobStatus metadataRefresh = jobs.stream()
131+
.filter(job -> "metadata_refresh".equals(job.key()))
132+
.findFirst()
133+
.orElseThrow();
134+
135+
assertEquals("active", metadataRefresh.status());
136+
assertEquals(0, metadataRefresh.consecutiveFailures());
137+
}
138+
107139
@Test
108140
void runJob_dispatchesMetadataRefreshForConnection() {
109141
BrainJobsService.ManualRunResult result = service.runJob("conn-1", "metadata_refresh");

docker-compose.yml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,10 +86,18 @@ services:
8686
# CORS — allow the frontend service (and optional custom domain)
8787
cors.allowed.origins: ${CORS_ALLOWED_ORIGINS:-http://localhost:3000}
8888

89+
# Dashboard generation + Slack/channels call Hermes server-side (AgentChatClient),
90+
# not via the browser /agent-api proxy. Point at the host webui; there is no
91+
# deepsql-agent Compose service in the four-service self-host stack.
92+
AGENT_WEBUI_URL: ${AGENT_WEBUI_URL:-http://host.docker.internal:8787}
93+
8994
# Self-host should default to the hardened production profile
9095
SPRING_PROFILES_ACTIVE: ${SPRING_PROFILES_ACTIVE:-prod}
9196
# Automatically set by install.sh for pgvector self-host mode
9297
SPRING_AUTOCONFIGURE_EXCLUDE: ${SPRING_AUTOCONFIGURE_EXCLUDE:-}
98+
# So AGENT_WEBUI_URL=http://host.docker.internal:8787 resolves on Linux too.
99+
extra_hosts:
100+
- "host.docker.internal:host-gateway"
93101
ports:
94102
- "${DEEPSQL_BACKEND_PORT:-8080}:8080"
95103
volumes:
@@ -112,6 +120,11 @@ services:
112120
depends_on:
113121
backend:
114122
condition: service_healthy
123+
# nginx proxies /agent-api/ → http://host.docker.internal:8787 (see docker/nginx/default.conf).
124+
# The agent is optional and usually runs on the host (hermes-webui on :8787).
125+
# Docker Desktop injects host.docker.internal; on Linux Compose needs this mapping.
126+
extra_hosts:
127+
- "host.docker.internal:host-gateway"
115128
ports:
116129
- "${DEEPSQL_FRONTEND_PORT:-3000}:80"
117130

docker/nginx/default.conf

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -71,25 +71,27 @@ server {
7171
proxy_set_header Accept "application/json";
7272
}
7373

74-
# Agent chat proxy → the DeepSQL Agent service.
74+
# Agent chat proxy → the DeepSQL Agent service (Hermes webui on :8787).
7575
# Mirrors the dev Vite proxy: strip the /agent-api prefix so
7676
# /agent-api/api/chat/stream reaches the agent's /api/chat/stream.
7777
# SSE buffering MUST be off for token streaming.
7878
location /agent-api/ {
7979
# Require a valid DeepSQL session before reaching the agent.
8080
auth_request /__agent_auth;
8181

82-
# The agent is an optional, separately-run service — the default Compose
83-
# stack does not include it. nginx resolves proxy_pass hostnames at config
84-
# load time, so a literal upstream here makes the whole container refuse to
85-
# start when the agent is absent ("host not found in upstream"). Going
86-
# through a variable defers resolution to request time: without an agent
87-
# this one route returns 502 and everything else keeps working.
88-
resolver 127.0.0.11 ipv6=off valid=10s;
89-
set $deepsql_agent http://deepsql-agent:8787;
90-
proxy_pass $deepsql_agent/;
82+
# The agent is optional and usually runs on the Docker host, not as a
83+
# Compose service. A variable + Docker DNS (127.0.0.11) cannot see
84+
# /etc/hosts entries, so `extra_hosts: deepsql-agent:host-gateway` alone
85+
# still 502s. Literal proxy_pass uses getaddrinfo (hosts file + DNS).
86+
# Compose maps host.docker.internal → host-gateway so this always
87+
# resolves at nginx start; if nothing listens on :8787 this route
88+
# returns 502 and the rest of the UI keeps working.
89+
proxy_pass http://host.docker.internal:8787/;
9190
proxy_http_version 1.1;
92-
proxy_set_header Host $host;
91+
# Hermes CSRF compares Origin host:port to Host. `$host` drops the port
92+
# (localhost vs localhost:3000) and profile/switch returns 403
93+
# "Cross-origin mismatch". `$http_host` preserves the browser Host.
94+
proxy_set_header Host $http_host;
9395
proxy_set_header X-Real-IP $remote_addr;
9496
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
9597
proxy_set_header X-Forwarded-Proto $scheme;

0 commit comments

Comments
 (0)