Skip to content

feat(http): default User-Agent + treat 403+Retry-After as rate-limit (#1037)#1155

Open
ChrisJr404 wants to merge 1 commit into
anchore:mainfrom
ChrisJr404:http-wrapper-default-ua-and-403-retry-after
Open

feat(http): default User-Agent + treat 403+Retry-After as rate-limit (#1037)#1155
ChrisJr404 wants to merge 1 commit into
anchore:mainfrom
ChrisJr404:http-wrapper-default-ua-and-403-retry-after

Conversation

@ChrisJr404

Copy link
Copy Markdown

Closes the two remaining unchecked items in #1037:

  • always set a User-Agent header so we identify ourselves
  • respect Retry-After on 403s (GitHub returns 403 + Retry-After for secondary rate limits)

The 429 and 503-with-Retry-After items were already shipped.

Default User-Agent

http_wrapper.default_user_agent() returns anchore/vunnel-<version>, falling back to anchore/vunnel-unknown when package metadata is unavailable (e.g. some editable / CI installs). It matches the convention already used by the per-provider helpers in chainguard_libraries, secureos, and fedora — so a future cleanup PR could deduplicate those onto the central helper if desired.

http.get() now sets the default automatically when the caller does not specify one:

user_agent argument Behaviour
None (default) User-Agent: anchore/vunnel-<version>
"" no User-Agent header (explicit opt-out, used by tests)
any non-empty string sent verbatim (unchanged)

A caller that pre-populates headers={"User-Agent": "..."} still wins — setdefault only fills the slot when it's empty.

403 + Retry-After

_is_rate_limited() now treats 403 + Retry-After the same way it already treats 503 + Retry-After. 403 without Retry-After is still a normal permission error (not rate-limit handling), so existing callers that legitimately receive 403s are unaffected.

Tests

tests/unit/utils/test_http_wrapper.py: 47 passed (up from 41).

New tests:

  • test_default_user_agent_identifies_vunnel — covers the new None default.
  • test_caller_supplied_headers_override_default_user_agent — explicit header still wins.
  • test_403_with_retry_after_is_rate_limited — full path through get() with retry handling.
  • test_403_without_retry_after_is_not_rate_limited — plain 403 still raises HTTPError without rate-limit logging.
  • test_403_with_header_is_rate_limited / _without_header_is_not_rate_limited — direct unit tests on _is_rate_limited.

Updated existing tests test_succeeds_if_retries_succeed and test_timeout_is_passed_in to expect the new default UA in outgoing headers. test_empty_user_agent_sets_no_header is unchanged and still passes — the "" opt-out path is preserved exactly.

Full unit suite: 826 passed, no regressions.

…nchore#1037)

Two of the four checkboxes from anchore#1037 ("enhance http_wrapper to be a
better http citizen") were still open:

  - always set a User-Agent header so we identify ourselves
  - respect Retry-After on 403s (GitHub returns 403 + Retry-After
    for secondary rate limits)

This patch closes both.

http_wrapper.default_user_agent()
  Centralised helper that returns 'anchore/vunnel-<version>'
  (and 'anchore/vunnel-unknown' when the package metadata is
  unavailable, e.g. an editable install in CI). The convention
  matches the per-provider helpers in chainguard_libraries,
  secureos, and fedora.

http_wrapper.get()
  Previously a None / empty user_agent argument both meant 'do not
  set a header'. Now:

      user_agent=None  -> default_user_agent() is set automatically
      user_agent=''    -> no User-Agent header (explicit opt-out)
      user_agent='x'   -> 'x' is sent verbatim

  Callers that already set headers={'User-Agent': '...'} keep their
  value untouched (setdefault). Callers that pass user_agent='string'
  keep their existing behaviour.

http_wrapper._is_rate_limited()
  Now also returns True for 403 responses that carry a Retry-After
  header. 403 without Retry-After is still treated as a permission
  error (not rate-limited), so 'genuine 403' callers are unaffected.

Tests
  - test_default_user_agent_identifies_vunnel: None defaults work.
  - test_caller_supplied_headers_override_default_user_agent:
    explicit header wins over the wrapper default.
  - test_403_with_retry_after_is_rate_limited: full integration
    test through get() with retry handling.
  - test_403_without_retry_after_is_not_rate_limited: 403 without
    Retry-After still raises HTTPError without rate-limit logging.
  - test_403_with_header_is_rate_limited / _without_header_is_not:
    direct unit tests on _is_rate_limited.
  - Existing test_succeeds_if_retries_succeed and
    test_timeout_is_passed_in updated to expect the new default UA
    in the outgoing headers (test_empty_user_agent_sets_no_header is
    unchanged and still passes — the empty-string opt-out path).

Full unit suite: 826 passed (47 in test_http_wrapper.py, up from 41).

Signed-off-by: Chris (ChrisJr404) <11917633+ChrisJr404@users.noreply.github.com>
@ChrisJr404 ChrisJr404 force-pushed the http-wrapper-default-ua-and-403-retry-after branch from a3eab0a to 56b197a Compare May 3, 2026 20:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant