Documentation for v2.9.0. Read the latest, v2.12.0.
Operator
Troubleshooting
The messages below are quoted as the service writes them, so a search for the text you see lands
here. Each one has its cause and its fix. Messages that a developer meets in a pipeline (409
with a proposed version, 404 unknown,
410 absent) are explained at the end and
in depth in CI integration.
What changed in 2.6.0
Startup#
-
ERROR: Address 0.0.0.0:3000 already in use. Another instance might be running. -
The port is taken. Stop the other process, or set
BIND_ADDRESS(BIND_ADDRESS=0.0.0.0:3080). There is noPORTvariable. -
CONFIGURATION ERROR: LOG_BUFFER_SIZE="1OO" is not a valid number. Correct it or unset it to use the default (100). - A numeric variable holds something that is not a number. The service refuses to run with a setting it did not apply; fix or unset it. The same shape names any numeric variable.
-
CONFIGURATION ERROR: LOG_FORMAT="yaml" is not recognised. Valid values: json, text. Unset it to use the default (text). -
A fixed-vocabulary variable (
LOG_FORMAT,OTEL_ENABLED,MCP_ENABLED,WEBHOOKS_ENABLED) holds another value. Values are case-insensitive and trimmed. -
CONFIGURATION ERROR: MAX_SPEC_BODY_BYTES=0 would reject every request.,… MCP_CALLS_PER_MINUTE=0 would refuse every MCP call.,… MCP_MAX_RESULT_BYTES=0 would summarise every MCP answer.,… WEBHOOK_WORKER_INTERVAL_SECS and WEBHOOK_DELIVERY_TIMEOUT_SECS must be greater than 0. - A zero that would make a feature useless. Unset the variable or give it a positive value.
-
CONFIGURATION ERROR: PUBLIC_BASE_URL="sanshain.corp" must start with http:// or https://. - Give the full origin:
https://sanshain.corp. ERROR: Invalid bind address '…': …-
BIND_ADDRESSmust behost:portwith a numeric port; IPv6 in brackets. -
ERROR: Can't build the TLS trust store: CA certificate directory '/certs' does not exist -
EXTRA_CA_CERTS_DIRpoints at a path that is not mounted. Mount it or unset the variable. Variants:Could not read CA certificate directory '…': …(permissions) andCould not read CA certificate file '…': …(a file with the right extension that is not a PEM bundle, or holds no certificate). See Additional CA certificates. ERROR: Failed to initialize OpenTelemetry tracing: …-
OTEL_ENABLED=trueand the exporter could not be built, usually a malformedOTEL_EXPORTER_OTLP_ENDPOINT. The collector being down is not this error. -
SECURITY: dev_mode is requested but the ALLOW_INSECURE_DEV_MODE safety gate is not set to 'true'. Refusing to enable dev mode - authentication stays enforced.(stderr), and in the logSECURITY: dev_mode is requested (SANSHAIN_DEV_MODE or persisted dev_mode setting) but the ALLOW_INSECURE_DEV_MODE safety gate is not set to 'true'. Refusing to enable dev mode - authentication stays enforced. Set ALLOW_INSECURE_DEV_MODE=true only on a trusted local machine to enable it. -
Developer mode is wanted (
SANSHAIN_DEV_MODE=trueor the Dev Mode radio) without the gate. Expected in production; on a laptop setALLOW_INSECURE_DEV_MODE=true. See Developer mode. -
SECURITY WARNING: binding to all interfaces (0.0.0.0:3000). Set BIND_ADDRESS=127.0.0.1:3000 to restrict access to localhost. - Informational. In a container this is what you want.
Database#
ERROR: Can't connect to SQLite database: …-
The directory of the file in
DATABASE_URLdoes not exist or is not writable by the service's user (UID 1000 in the manifests).mode=rwccreates the file, not the directory. ERROR: Can't connect to PostgreSQL database: …-
Host, port, credentials or network. Check the URL and that the pod can reach the server;
/readyanswers503for the same reason at runtime. ERROR: Invalid database URL: …-
A SQLite URL the driver cannot parse. The shape is
sqlite:/data/sanshain.db?mode=rwc. -
ERROR: Can't run SQLite migrations: …/… PostgreSQL migrations: … - A migration failed, or the database was touched by a newer version (migrations are forward-only). Restore the backup from before the upgrade, see Backup and restore.
ERROR: Can't normalize stored spec text: …- The one-time rewrite of stored spec text (2.4.0, ADR-0008) failed (a database error mid-way). The next start repeats it; every step is idempotent.
ERROR: Can't claim root usernames: …-
Creating the placeholder accounts for unclaimed
SANSHAIN_ROOT_USERSnames failed; a database error.
Logins, sessions and tokens#
- Lost the initial root password
-
There is no password reset, and
SANSHAIN_ROOT_USERSdoes not help: a name added there is claimed at startup as a placeholder account with a random, unknown password (only a directory login under that name can use it), and registering it is refused. The initial admin is created only while theuserstable is empty, so the options are: restore a backup taken while the password was known; or delete every row ofusers(DELETE FROM users;withsqlite3on the stopped service's file, orpsql), which removes every account, session and API token, so the next start creates the initial admin again withINITIAL_ADMIN_PASSWORDor a new random password on stderr. The Danger Zone's Nuke Users is not a way out: it spares every effective administrator, root included, and needs a working admin session anyway. POST /auth/loginanswers401- Wrong username or password, or an empty password, which is refused before any check; with LDAP, the directory refused and the local check refused too.
POST /auth/loginanswers403-
The account is pending approval (approve it under Users), or the mode is
disabled. Existing sessions still work on the admin routes. 503 Service is in Maintenance Mode / Disabled-
Every API call while the authentication mode is OFF / Maintenance. Select another mode in the
Authentication section or with
PUT /admin/auth-config. 409 This username is reserved-
Registration of a name in
SANSHAIN_ROOT_USERS. See Root users. 409 User already exists- Registration of a taken name, also one that only exists as an LDAP or OIDC shadow account.
- An API call answers a bare
403or401 -
403without a body: noAuthorization: Bearerheader at all.401: the token is invalid, expired or revoked. Mint one on/account.html. -
403 this API token has scope 'read_only' and may not change the catalog; create a token with scope 'full' for provide, require and catalog pushes -
A read-only token on a write route. The scope is fixed at creation; mint a
fullone. -
403 this API token has scope 'read_only' and may not call administrative routes; use a token with scope 'full' of an account holding the permission -
A read-only token on the backup download. Every other admin route answers
401to any API token: they take sessions. -
403 publishing GA for 'orders' requires the 'releaser' role — publish as a snapshot, or ask an administrator to grant the role -
Grant
releaserto the CI user (Roles and permissions). -
A
user_managergrantingadminorreleasergets403 - Both roles are admin-guarded; an admin or root grants them.
-
400 Role 'maintainer' is scoped to specific Producers and cannot be granted instance-wide. - Assign the user or group as a maintainer of a Producer instead.
LDAP#
-
400 LDAP connection failed: Connection failed: LDAP server at ldaps://ldap.example.com:389 accepted the connection but did not respond within 10 s. Check that ldaps:// uses the TLS port and StartTLS the plain one. -
Shown by Test Connection; a login that stalls the same way answers a bare
401after the local password check. The directory took the TCP connection and never answered: almost always a scheme and port mismatch (ldaps://on 389, StartTLS on 636), otherwise a firewall that accepts and drops. The table in Authentication lists the combinations. Test the port from inside the pod, not from a laptop. - Test Connection shows
502 Bad Gateway -
Sanshain never answers 502; the ingress gave up before Sanshain's 10 s did. Raise the ingress
timeout above
LDAP_OPERATION_TIMEOUT_SECS, or lower the variable. -
400 LDAP connection failed: Could not connect to LDAP server at ldap://…: …followed byDNS:,Resolvers:and one line per address -
The directory was never reached. The message names the step:
the hostname could not be resolved,this container has no route to …,the network of … is unreachable from this container,nothing listens on …(host up, port closed) orno answer from … within 5 s(host down or a firewall that drops). The lines under it are the container's own diagnosis; read them as described in When the directory is unreachable.No route to host (os error 113)in the brackets is the kernel saying the same as the first line: the pod has no route to that address today, which is a network change, not a directory one. -
400 LDAP connection failed: Connection failed: Could not connect to LDAP server at ldaps://…: the server certificate is not trusted: … -
A private CA without
EXTRA_CA_CERTS_DIR. The sibling messages name a certificate that is not valid for the URL's host, one that has expired or is not valid yet (check the clocks), a plain LDAP port answering anldaps://URL, a refused handshake, or a connection closed during StartTLS (a TLS port; useldaps://). The rustls text follows in brackets. -
400 LDAP connection failed: Connection failed: LDAP bind failed (check DN/Password): … -
The service account's DN or password.
LDAP bind request failed for cn=…: …is the same step failing at the protocol level. - A directory user logs in but has no admin rights
-
Roles come from the group mapping on every check: the user's
memberOfvalues must equal the name of a directory group that has a role attached (the Bootstrap Admin Group is such a group). A change takes up toSANSHAIN_DIRECTORY_GROUP_TTL_SECS(300) to show. The log lineCould not read directory groups for 'alice'; continuing without directory-derived roles: …means the directory was unreachable at the check; root and local grants still work.
OpenID Connect#
400 OIDC is not configured-
The mode is
oidcor the SSO button was used without anoidc_config. Store one withPUT /admin/auth-config. 500 OIDC discovery failed: …-
<issuer_url>/.well-known/openid-configurationcould not be fetched or parsed from inside the pod. The OIDC client does not useEXTRA_CA_CERTS_DIR. 400 OIDC issuer URL: …,400 OIDC redirect URL: …- One of the two is not a valid URL.
-
400 no OIDC login in progress,400 missing code,400 missing state -
The callback arrived without the flow cookie (older than 10 minutes, another browser, or a
cookie blocked because the site is not served over HTTPS; the cookie is
Secure) or without the provider's parameters. Start again from the SSO button. - The callback answers
401 - The state did not match, the code exchange failed, or the ID token did not verify (nonce, issuer, audience, signature, expiry). Check the client secret and that the redirect URL registered with the provider is exactly the configured one.
-
400 OIDC config required for oidc mode,400 LDAP config required for ldap mode,400 Invalid auth mode: … -
PUT /admin/auth-configswitching to a mode without its configuration, or with a mode name outsidedisabled,dev,local,ldap,oidc.
Webhooks#
-
403 no hosts are allowed for webhooks yet — an administrator has to add the receiver's host to the allow-list (admin page, Webhooks) first - A maintainer registering a hook on an instance whose allow-list is empty. An administrator adds the host, see The allow-list.
-
403 the host 'hooks.example.com' is not in the webhook allow-list — ask an administrator to allow it, or use one of: hooks.slack.com -
The URL's host matches no pattern. For plain
http://the message adds(plain http:// needs a pattern that says http:// itself). -
400 '…' is not a host pattern: use a host name with optional `*`/`?` wildcards, prefixed `http://` only to allow plain HTTP to it -
A line of the allow-list with a path, a port, a scheme other than
http://or another character. -
400 the hook URL must be an http:// or https:// URL with a host,400 the hook URL is not a valid URL: … - The receiver URL itself.
400 webhook.test is sent by the test button only and cannot be subscribed to-
webhook.testinevent_types. Leave the set empty for every event. -
409 webhooks are disabled on this instance (WEBHOOKS_ENABLED=false): a test event would never be delivered -
Send test with the worker off. Set
WEBHOOKS_ENABLED=true(the default) and restart. -
Delivery log:
the hook's host is not in the allow-list (webhook_allowed_hosts) any more - The host was removed after registration. Re-add it; the retries continue until the 24-hour window closes.
- Delivery log:
the receiver answered HTTP 404, or a connection error - The receiver's answer or the network; the body of a non-2xx answer is not read. Fix the receiver and redeliver, or wait for the next retry.
- Delivery log:
given up: 24 hours have passed since the event - Dead. A hook paused for a day finds its deliveries dead on resume. Redeliver sends one more attempt with the same id.
-
409 delivery 12 is in flight — a worker is attempting it right now; try again after that attempt - Redeliver while a worker holds the row. Wait for the attempt.
- Nothing arrives and the delivery log stays
pending -
No worker pass is running (
WEBHOOKS_ENABLED=falsewrites nothing, so pending rows with no attempts mean the worker cannot reach the database or the process is wedged), or the hook is paused. The log lineWebhook worker enabled: every 5 s, 10 s delivery timeout, deep links on https://…confirms the worker started. - The Slack message shows no link, the Teams card no button
-
PUBLIC_BASE_URLis unset and there is no OIDC redirect URL to borrow the origin from.
Backup#
-
409 This instance runs PostgreSQL, which has no snapshot download; take backups with pg_dump (see the deployment guide, Backup and restore) -
Use
pg_dump; the Helm chart switches the CronJob for you. See PostgreSQL. -
409 A database snapshot is already being produced; retry when it has finished - Two downloads at once. Retry.
- The backup CronJob fails with
401or403 -
The token in
backup.tokenSecretexpired or was revoked (401), isread_onlyor belongs to an account withoutrun_destructive_operations(403). Mint afulltoken of an admin and replace the Secret. 500 VACUUM INTO failed: …orsnapshot file: …- The data volume has no room for a second copy, or the directory of the database is not writable.
Settings and administration#
400 days must be at most 36500- Every
*_max_age_dayssetting caps at a hundred years. 400 days must be greater than 0-
dependency_max_age_dayscannot be disabled; the snapshot and trunk ages accept0. 400 Invalid confirmation- A Danger Zone route without its exact phrase, see The Danger Zone.
-
409 'orders' is part of the recorded graph of sanshain-branch(es) release-2026-10 — deleting it would remove those edges from release cuts that already happened. Delete the branch(es) first if the record is no longer needed. - Deleting a Producer or Consumer a branch still references. Delete the named branches first, if their record is no longer needed.
-
400 Only Sanshain's own groups can be renamed; a directory group's name is the directory's.,400 Group '…' comes from the directory, so its membership is the directory's to change. - A directory group is edited in the directory; only its roles are Sanshain's.
400 Supply exactly one of user_id or group_id.- A maintainer assignment with both or neither.
Provides and requires#
409withproposed_version- Refused by the version rules: the number is GA with different content, the change is breaking without a major bump, a snapshot for a number that went GA. Set the spec's version to the proposal and republish. If nothing changed, the difference is whitespace or a comment: content is compared byte-for-byte after line endings and a leading BOM are normalised. See API lifecycle.
-
404 Producer 'orders' has no openapi version 2.1.0 (in either stability) — a missing pinned version is a configuration error, nothing waits for it to appear -
Unknown: the pin names a version that does not
exist, expired unused (snapshot) or was deleted.
GET /producers/{name}/versionslists what exists. 410 openapi version 2.1.0 of 'orders' does not include GET /orders- Absent: the version exists and lacks the endpoint. Check path and method, or pin a version that has it.
400on a provide-
Invalid YAML, a version that is not
MAJOR[.MINOR[.PATCH]](optionallyv-prefixed, no suffix such as-SNAPSHOT), OpenAPI outside 3.0 and 3.1, AsyncAPI outside 2.x and 3.x, a proto file without exactly one// sanshain-version:comment. 413on a provide- The body exceeds
MAX_SPEC_BODY_BYTES(4 MiB). Raise it. - A provide on one replica, a require on another answers the old content
- The 10 s cache window on multi-replica PostgreSQL. See Scaling.
Web pages#
- The dependency graph is empty
-
Dependencies are recorded only by a successful
/requireor/require-bundlewithoutdry_run.scripts/demo.shpopulates an instance. - Download PNG of the graph does nothing
-
The export renders in the browser; a reverse proxy that adds a Content-Security-Policy must
allow
blob:URLs. Sanshain's own headers do. - The audit page shows
Error loading timeline: Session expired - The session is older than 7 days or was revoked. Sign in again.
Checked against v2.6.1.