rohitg00/agentmemory · critical · ViewerConfigError
AGENTMEMORY_VIEWER_HOST=${host} requires VIEWER_ALLOWED_HOST
Error message
AGENTMEMORY_VIEWER_HOST=${host} requires VIEWER_ALLOWED_HOSTS because non-loopback viewer binds only trust explicit Host headers. To fix: set VIEWER_ALLOWED_HOSTS to a comma-separated list of trusted Host header values (e.g. "localhost:3113" for fly proxy), or unset AGENTMEMORY_VIEWER_HOST to keep the safe loopback bind. What it means
startViewerServer throws ViewerConfigError when a non-loopback AGENTMEMORY_VIEWER_HOST bind has a secret but VIEWER_ALLOWED_HOSTS is empty. Non-loopback binds only trust explicit Host header allowlist values to prevent DNS-rebinding/Host-header attacks against the proxy; without an allowlist the server refuses to start.
Source
Thrown at src/viewer/server.ts:224
const resolvedRestPort = restPort ?? port - 2;
const requestedPort = port;
const host = resolveViewerHost();
let inboundSecret: string | null = null;
// Non-loopback bind turns the viewer into a network-reachable
// bearer-authorized proxy. Refuse to start unless the operator has
// both an inbound secret to authenticate callers against and an
// explicit Host header allowlist; otherwise the listening socket
// becomes an open relay to the local REST API.
if (!isLoopbackHost(host)) {
if (!secret) {
throw new ViewerConfigError(
`AGENTMEMORY_VIEWER_HOST=${host} requires AGENTMEMORY_SECRET to be set so the viewer can validate inbound bearer tokens. To fix: unset AGENTMEMORY_VIEWER_HOST to keep the safe loopback bind, or set AGENTMEMORY_SECRET. For Fly images, it is printed on first boot; see deploy/fly/README.md.`,
);
}
if (readAllowedHostsOverride().length === 0) {
throw new ViewerConfigError(
`AGENTMEMORY_VIEWER_HOST=${host} requires VIEWER_ALLOWED_HOSTS because non-loopback viewer binds only trust explicit Host headers. To fix: set VIEWER_ALLOWED_HOSTS to a comma-separated list of trusted Host header values (e.g. "localhost:3113" for fly proxy), or unset AGENTMEMORY_VIEWER_HOST to keep the safe loopback bind.`,
);
}
inboundSecret = secret;
}
// Computed lazily on first request — `port` may be 0 here (OS-assigned)
// or the EADDRINUSE retry loop below may bump us to a different port,
// so we read the actual bound port from server.address() on first hit.
let allowedHosts: Set<string> | null = null;
const server = createServer(async (req, res) => {
if (!allowedHosts) {
const addr = server.address();
const actualPort =
addr && typeof addr === "object" && "port" in addr
? (addr.port as number)
: port;View on GitHub (pinned to e04ba88819)
Solutions
- Set VIEWER_ALLOWED_HOSTS to the exact Host header values clients send, comma-separated (e.g. VIEWER_ALLOWED_HOSTS="localhost:3113" or "your-app.fly.dev")
- Or unset AGENTMEMORY_VIEWER_HOST to keep the safe loopback bind
- Verify parsing: no stray commas/quotes; the list must be non-empty after trim
- Update the allowlist after app renames or custom-domain changes
Example fix
// before AGENTMEMORY_VIEWER_HOST=0.0.0.0 AGENTMEMORY_SECRET=... npm run viewer // ViewerConfigError // after AGENTMEMORY_VIEWER_HOST=0.0.0.0 AGENTMEMORY_SECRET=... VIEWER_ALLOWED_HOSTS="localhost:3113,myapp.fly.dev" npm run viewer
Defensive patterns
Strategy: validation
Validate before calling
const host = process.env.AGENTMEMORY_VIEWER_HOST;
if (host && !isLoopback(host)) {
const allowed = (process.env.VIEWER_ALLOWED_HOSTS ?? '').split(',').map(s => s.trim()).filter(Boolean);
if (allowed.length === 0) throw new Error('Non-loopback viewer bind needs VIEWER_ALLOWED_HOSTS');
} Type guard
function hasAllowedHosts(v: string | undefined): v is string {
return !!v && v.split(',').map(s => s.trim()).filter(Boolean).length > 0;
} Try / catch
try {
await startViewerServer(options);
} catch (e) {
if (e instanceof ViewerConfigError && e.message.includes('VIEWER_ALLOWED_HOSTS')) {
console.error('Set VIEWER_ALLOWED_HOSTS (e.g. "localhost:3113,app.fly.dev") or unset AGENTMEMORY_VIEWER_HOST');
process.exit(1);
}
throw e;
} Prevention
- Mirror the exact Host header the reverse proxy sends (fly proxy sends localhost:3113) into VIEWER_ALLOWED_HOSTS
- Update the allowlist when renaming apps or adding custom domains
- Validate the parsed allowlist is non-empty after trimming/quote-stripping
- Prefer the default loopback bind unless a public viewer is explicitly required
When it happens
Trigger: Starting the viewer with AGENTMEMORY_VIEWER_HOST set to a public host and AGENTMEMORY_SECRET set, while readAllowedHostsOverride() returns [] (VIEWER_ALLOWED_HOSTS unset or only empty/comma tokens) — the second guard in the same isLoopbackHost branch fires.
Common situations: Fly.io deploys where the operator sets the secret but forgets the Host allowlist the fly proxy sends (e.g. 'localhost:3113' or the app's fly.dev hostname); trailing whitespace/quotes or a single comma making the parsed list empty; renaming the app so the old allowed host no longer matches; local .env reused for remote deploys without VIEWER_ALLOWED_HOSTS.
Understand the failure class
Background: "environment variable is not set" and "Missing keys in environment" errors: what missing required env var messages mean and how to fix them — this error's family across 28 libraries.
Related errors
- AGENTMEMORY_VIEWER_HOST=${host} requires AGENTMEMORY_SECRET
- OPENAI_API_KEY is required for the openai provider
- Refusing to read image outside managed store: ${data.raw.ima
- ${envName} must be a positive integer, got: ${override}
- EEXIST
AI-assisted analysis of rohitg00/agentmemory@e04ba88819 (2026-08-30).
Data as JSON: /api/errors/072242d302769aa0.
Report an issue: GitHub.