RegistryClient does not follow HTTP redirects (308), breaking resolution against redirecting registries #130

Open
opened 2026-08-10 12:42:30 +00:00 by CoreRasurae · 0 comments
Member

Metadata

  • Commit Message: feat(registry): follow HTTP redirects in RegistryClient
  • Branch: feature/m1-registry-client-follow-redirects

Background and context

The Package Registry Standard does not forbid registry servers from answering with
HTTP redirects. In practice, gateways fronting a registry (e.g. Caddy) normalize URLs —
for example by appending a trailing slash and answering with 308 Permanent Redirect.
RegistryClient (src/cleveractors/registry/client.py) builds its internal
httpx.AsyncClient without follow_redirects=True, and httpx returns a 3xx response
as-is when redirects are disabled. raise_for_status() then raises HTTPStatusError
for that 3xx, which _request converts into an opaque RegistryError("HTTP 308").

The result: a perfectly reachable package on a redirecting registry cannot be resolved
at all.

Current behavior

Resolving a registry reference against a server that answers 308 Permanent Redirect
fails:

Agent 'calculator_builder': failed to resolve agent reference '192.168.1.53:luis-mendes/calculator-app-builder': HTTP 308

Reproduced against http://192.168.1.53 (a Caddy-fronted registry):

GET http://192.168.1.53/agt/luis-mendes/calculator-app-builder?version=latest
  → HTTP/1.1 308 Permanent Redirect
    Location: /agt/luis-mendes/calculator-app-builder/?version=latest

Root cause: RegistryClient._get_client() (src/cleveractors/registry/client.py
~line 82) creates httpx.AsyncClient(base_url=..., headers=..., timeout=...) without
follow_redirects=True. Because redirects are not followed, httpx 0.28.x
raise_for_status() raises HTTPStatusError for the 3xx response, and _request
surfaces it as a RegistryError whose message is the bare string "HTTP 308" — hiding
the real state of the target resource.

Expected behavior

RegistryClient follows HTTP redirects transparently (3xx, including 308 Permanent Redirect). Resolution against a redirecting registry succeeds. If the redirected
target does not exist, the client surfaces the appropriate typed error (e.g.
PackageNotFoundError on a 404) instead of an opaque "HTTP 308".

Acceptance criteria

  • RegistryClient constructs its httpx.AsyncClient with follow_redirects=True.
  • resolve_package / get_package / browse / discover succeed when the server
    answers a 3xx redirect whose target is valid.
  • A 3xx redirect to a missing target surfaces the target's typed error (e.g.
    PackageNotFoundError on 404) rather than an opaque RegistryError("HTTP 308").
  • No regression in existing error mapping (4xx/5xx, timeouts, non-JSON bodies).
  • Verified end-to-end against a live registry fronted by a gateway that normalizes
    URLs with 308 (e.g. Caddy).

Supporting information

  • Reproduced with a Caddy-fronted registry at http://192.168.1.53 (see Current
    behavior for the exact request/response).
  • src/cleveractors/registry/client.py (RegistryClient._get_client, _request)
  • src/cleveractors/registry/reference_resolver.py (_resolve_registry_async)
  • features/registry_http_client.feature / features/steps/registry_http_client_steps.py
    (existing BDD coverage for the client)
  • httpx behavior: with follow_redirects=False a 3xx is returned as-is and
    Response.raise_for_status() raises HTTPStatusError for it.

Subtasks

  • Add follow_redirects=True to the httpx.AsyncClient construction in
    RegistryClient._get_client.
  • Tests (Behave): add a scenario proving 3xx redirects are followed and that a
    redirect-to-404 yields PackageNotFoundError.
  • Verify coverage >=97% via nox -s coverage_report.
  • Run nox (all default sessions), fix any errors.

Definition of Done

This issue is complete when:

  • All subtasks above are completed and checked off.
  • A Git commit is created where the first line of the commit message matches the
    Commit Message in Metadata exactly, followed by a blank line, then additional lines
    providing relevant details about the implementation.
  • The commit is pushed to the remote on the branch matching the Branch in Metadata
    exactly.
  • The commit is submitted as a pull request to master, reviewed, and merged
    before this issue is marked done.
## Metadata - **Commit Message**: `feat(registry): follow HTTP redirects in RegistryClient` - **Branch**: `feature/m1-registry-client-follow-redirects` ## Background and context The Package Registry Standard does not forbid registry servers from answering with HTTP redirects. In practice, gateways fronting a registry (e.g. Caddy) normalize URLs — for example by appending a trailing slash and answering with `308 Permanent Redirect`. `RegistryClient` (`src/cleveractors/registry/client.py`) builds its internal `httpx.AsyncClient` without `follow_redirects=True`, and httpx returns a 3xx response as-is when redirects are disabled. `raise_for_status()` then raises `HTTPStatusError` for that 3xx, which `_request` converts into an opaque `RegistryError("HTTP 308")`. The result: a perfectly reachable package on a redirecting registry cannot be resolved at all. ## Current behavior Resolving a registry reference against a server that answers `308 Permanent Redirect` fails: ``` Agent 'calculator_builder': failed to resolve agent reference '192.168.1.53:luis-mendes/calculator-app-builder': HTTP 308 ``` Reproduced against `http://192.168.1.53` (a Caddy-fronted registry): ``` GET http://192.168.1.53/agt/luis-mendes/calculator-app-builder?version=latest → HTTP/1.1 308 Permanent Redirect Location: /agt/luis-mendes/calculator-app-builder/?version=latest ``` Root cause: `RegistryClient._get_client()` (`src/cleveractors/registry/client.py` `~line 82`) creates `httpx.AsyncClient(base_url=..., headers=..., timeout=...)` without `follow_redirects=True`. Because redirects are not followed, httpx 0.28.x `raise_for_status()` raises `HTTPStatusError` for the 3xx response, and `_request` surfaces it as a `RegistryError` whose message is the bare string `"HTTP 308"` — hiding the real state of the target resource. ## Expected behavior `RegistryClient` follows HTTP redirects transparently (3xx, including `308 Permanent Redirect`). Resolution against a redirecting registry succeeds. If the redirected target does not exist, the client surfaces the appropriate typed error (e.g. `PackageNotFoundError` on a 404) instead of an opaque `"HTTP 308"`. ## Acceptance criteria - [ ] `RegistryClient` constructs its `httpx.AsyncClient` with `follow_redirects=True`. - [ ] `resolve_package` / `get_package` / `browse` / `discover` succeed when the server answers a 3xx redirect whose target is valid. - [ ] A 3xx redirect to a missing target surfaces the target's typed error (e.g. `PackageNotFoundError` on 404) rather than an opaque `RegistryError("HTTP 308")`. - [ ] No regression in existing error mapping (4xx/5xx, timeouts, non-JSON bodies). - [ ] Verified end-to-end against a live registry fronted by a gateway that normalizes URLs with `308` (e.g. Caddy). ## Supporting information - Reproduced with a Caddy-fronted registry at `http://192.168.1.53` (see Current behavior for the exact request/response). - `src/cleveractors/registry/client.py` (`RegistryClient._get_client`, `_request`) - `src/cleveractors/registry/reference_resolver.py` (`_resolve_registry_async`) - `features/registry_http_client.feature` / `features/steps/registry_http_client_steps.py` (existing BDD coverage for the client) - httpx behavior: with `follow_redirects=False` a 3xx is returned as-is and `Response.raise_for_status()` raises `HTTPStatusError` for it. ## Subtasks - [ ] Add `follow_redirects=True` to the `httpx.AsyncClient` construction in `RegistryClient._get_client`. - [ ] Tests (Behave): add a scenario proving 3xx redirects are followed and that a redirect-to-404 yields `PackageNotFoundError`. - [ ] Verify coverage >=97% via `nox -s coverage_report`. - [ ] Run `nox` (all default sessions), fix any errors. ## Definition of Done This issue is complete when: - All subtasks above are completed and checked off. - A Git commit is created where the **first line** of the commit message matches the Commit Message in Metadata exactly, followed by a blank line, then additional lines providing relevant details about the implementation. - The commit is pushed to the remote on the branch matching the **Branch** in Metadata exactly. - The commit is submitted as a **pull request** to `master`, reviewed, and **merged** before this issue is marked done.
CoreRasurae added this to the v2.1.0 milestone 2026-08-10 12:42:30 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
cleveragents/cleveractors-core#130
No description provided.