JuliusBrussee/caveman · error · HTTPError
cave_redirect_not_allowed
Error message
cave_redirect_not_allowed
What it means
The caveman_cloud SDK deliberately blocks HTTP redirects for its API client. A custom urllib handler installs http_error_302 (aliased to 301/303/307/308) that closes the redirect response and raises an HTTPError whose message is the literal 'cave_redirect_not_allowed'. This prevents urllib from forwarding the Authorization header and other credential headers to a different origin, which would leak credentials on cross-origin redirects.
Solutions
- Fix the configured base URL to the final, correct https endpoint so no redirect occurs
- Check for a proxy/load balancer issuing redirects and address it (or bypass the proxy)
- Re-authenticate if the redirect is an auth-page bounce; the SDK expects 401s, not redirects
- Inspect error.headers on the raised HTTPError to see the Location header and identify where the server is redirecting
Example fix
# before client = CloudClient(base_url="http://cave.example.com") # 301 -> https # after client = CloudClient(base_url="https://cave.example.com")
Defensive patterns
Strategy: try-catch
Validate before calling
from urllib.parse import urlparse
def assert_no_redirect_expected(base_url: str) -> None:
parsed = urlparse(base_url)
if parsed.scheme != "https":
raise ValueError(f"base_url should be final https endpoint, got {base_url}") Try / catch
import urllib.error
try:
resp = client.request("/v1/things")
except urllib.error.HTTPError as e:
if e.msg == "cave_redirect_not_allowed":
# do NOT follow; inspect where it wanted to go and fix base_url instead
location = e.headers.get("Location")
raise RuntimeError(f"redirect blocked, server wanted {location}; fix base_url") from e
raise Prevention
- Always configure the final https base URL, never an endpoint known to redirect
- Watch for proxies or SSO gateways that 302 API traffic to login pages
- Handle 401 by re-authenticating rather than expecting a redirect to an auth page
- On this error, read the HTTPError headers' Location to diagnose the redirect source
When it happens
Trigger: Any SDK request (urllib-based) that receives a 301/302/303/307/308 redirect response from the server; the handler immediately raises urllib.error.HTTPError with code and headers and msg 'cave_redirect_not_allowed'.
Common situations: The API base URL was misconfigured (e.g. http instead of https and the server redirects); a proxy or gateway redirects to a login page; a tenant endpoint moved and issues 301s; an expired session causes the server to redirect to an auth page instead of returning 401.
Understand the failure class
Background: "API error: {status}" and "HTTP 401/403/404/429/5xx" errors: non-2xx HTTP responses explained — this error's family across 27 libraries.
Related errors
- device login refused a redirected token endpoint
- awscreds: : http
- caveman: remote gateway key revocation failed
- caveman: remote gateway key revocation was unavailable…
- could not remove credentials from macOS Keychain
AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20).
Data as JSON: /api/errors/d6a0c565a544115d.
Report an issue: GitHub.
Appendix: source
Thrown at packages/sdk/python/caveman_cloud/core.py:28
import threading
import time
import urllib.error
import urllib.request
from contextlib import contextmanager
from urllib.parse import quote, urlsplit
from dataclasses import dataclass, field
from typing import Any, Callable, Iterator, Literal
class _RejectRedirects(urllib.request.HTTPRedirectHandler):
"""Keep gateway and upstream credentials on the configured endpoint."""
def http_error_302(self, req: Any, fp: Any, code: int, msg: str, response_headers: Any) -> Any:
# urllib otherwise forwards Authorization and custom credential headers
# to another origin. Close the redirect response before raising, since
# the caller never receives a response context manager in this case.
fp.close()
raise urllib.error.HTTPError(req.full_url, code, "cave_redirect_not_allowed", response_headers, None)
http_error_301 = http_error_302
http_error_303 = http_error_302
http_error_307 = http_error_302
http_error_308 = http_error_302
def _urlopen(req: urllib.request.Request, *, timeout: float) -> Any:
# A private opener avoids changing urllib's process-wide redirect policy.
return urllib.request.build_opener(_RejectRedirects()).open(req, timeout=timeout)
def _strict_non_negative_int(value: Any) -> int | None:
"""Accept JSON integers only; reject bools, floats, strings, and negatives."""
if isinstance(value, bool) or not isinstance(value, int) or value < 0 or value > 2**53 - 1:
return None
return value
View on GitHub (pinned to 3ee70a1026)