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

  1. Fix the configured base URL to the final, correct https endpoint so no redirect occurs
  2. Check for a proxy/load balancer issuing redirects and address it (or bypass the proxy)
  3. Re-authenticate if the redirect is an auth-page bounce; the SDK expects 401s, not redirects
  4. 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

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


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)