unslothai/unsloth · error · HTTPException
Failed to decrypt API key. The server public key may have ch
Error message
Failed to decrypt API key. The server public key may have changed — try refreshing the page.
What it means
A 400 raised when resolve_provider_api_key throws while decrypting the client-supplied encrypted_api_key. Keys are RSA-encrypted client-side with a server public key fetched from GET /api/providers/public-key; if the server keypair was regenerated (reinstall, key rotation, new install behind a stale page), old ciphertext cannot be decrypted and this error names the most common cause: refresh the page to pick up the current public key.
Source
Thrown at studio/backend/routes/provider_credentials.py:56
def resolve_provider_api_key_or_400(
provider_id: str | None,
encrypted_api_key: str | None,
*,
allow_saved_key: bool = True,
) -> str:
"""Resolve an explicit key, or a saved key only for an interactive UI session."""
try:
saved_provider_id = provider_id if allow_saved_key else None
return credential_secrets.resolve_provider_api_key(saved_provider_id, encrypted_api_key)
except Exception as exc:
logger.warning(
"external_provider.api_key_decrypt_failed",
error_type = type(exc).__name__,
)
raise HTTPException(
status_code = 400,
detail = (
"Failed to decrypt API key. The server public key may have changed — "
"try refreshing the page."
),
) from exc
View on GitHub (pinned to 203007d190)
Solutions
- Refresh the page so the client re-fetches the public key from GET /api/providers/public-key, then re-enter and resubmit the API key.
- If it persists, verify the server's credential encryption key exists and is stable across restarts (get_or_create_credential_encryption_key should persist, not regenerate).
- Ensure all instances behind a load balancer share the same keypair/storage.
Defensive patterns
Strategy: retry
Validate before calling
const pub = await fetch("/api/providers/public-key").then(r => r.text());
if (pub !== cachedPublicKey) { cachedPublicKey = pub; /* re-encrypt the key before sending */ } Try / catch
try { await submitKey(encrypted); } catch (e) { if (e.status === 400 && /decrypt/i.test(e.detail)) { await refreshPublicKey(); encrypted = await encryptKey(rawKey); return submitKey(encrypted); } throw e; } Prevention
- Fetch the server public key immediately before each encryption, not at page load only.
- Never persist encrypted_api_key ciphertext for later reuse; keep only the raw key in memory.
- Ensure the server's encryption key persists across restarts.
When it happens
Trigger: Submitting a provider form that has been open in a tab since before a server restart or key rotation; a cached SPA bundle holding an old public key; re-using an encrypted_api_key string captured from a previous session; sending malformed ciphertext.
Common situations: Server redeployed or database reset without updating the page; long-lived browser tabs; load-balanced instances where nodes disagree on the keypair; replaying recorded API traffic.
Related errors
- Provider public key is missing.
- Local (stdio) MCP servers can only be configured from the Un
- Invalid or expired API key
- Cannot replace and clear an API key in the same request
- API key cannot be empty
AI-assisted analysis of unslothai/unsloth@203007d190 (2026-08-15).
Data as JSON: /api/errors/f7faad9069e2a3f0.
Report an issue: GitHub.