Skip to content

fix(skills): retry ClawHub ZIP download HTTP errors - #57714

Open
VectorPeak wants to merge 1 commit into
NousResearch:mainfrom
VectorPeak:clawhub-download-http-error-retry
Open

fix(skills): retry ClawHub ZIP download HTTP errors#57714
VectorPeak wants to merge 1 commit into
NousResearch:mainfrom
VectorPeak:clawhub-download-http-error-retry

Conversation

@VectorPeak

@VectorPeak VectorPeak commented Jul 3, 2026

Copy link
Copy Markdown

What does this PR do?

This fixes a narrow ClawHub ZIP /download retry gap in ClawHubSource._download_zip(): the function already had a bounded retry loop, but httpx.HTTPError exceptions raised while issuing the ZIP request, such as a temporary ConnectTimeout, returned an empty file map on the first attempt.

What Problem This Solves

ClawHubSource.fetch() resolves a ClawHub skill version, then tries the /download ZIP endpoint first because that path can return the complete skill bundle in one response. If the ZIP path does not produce SKILL.md, fetch() can still fall back to the version metadata/raw-content path.

The retry loop in _download_zip() already declares max_retries = 3, but the previous retry behavior was uneven:

  • A server response with status 429 used the retry loop.
  • A request-time HTTPX exception, such as httpx.ConnectTimeout, skipped the remaining attempts and returned {} immediately.
  • Ordinary non-200 responses that returned a response object followed the existing status-code handling.

That meant a short network interruption before a usable /download response was received could make the primary ZIP path look unavailable after one failed request, even when the next attempt would have succeeded:

hermes skills install <clawhub-skill>
  -> ClawHubSource.fetch(...)
    -> resolve latest ClawHub version
    -> _download_zip(slug, latest_version)
      -> httpx.get("https://clawhub.ai/api/v1/download", ...)
      -> request raises httpx.ConnectTimeout / HTTPError
      -> except httpx.HTTPError
      -> return {}
      -> ZIP path is treated as unavailable after one transient request failure

This is a robustness fix, not a security fix. It does not claim that every ClawHub download failure is recoverable; it only retries httpx.HTTPError exceptions raised while requesting the ZIP bundle.

Changes

  • Retry request-time httpx.HTTPError failures inside ClawHubSource._download_zip() while attempts remain in the existing max_retries = 3 loop.
  • Add a short bounded delay before retrying, matching the function's existing bounded retry shape.
  • Preserve the final-failure contract: after the last failed attempt, _download_zip() still logs and returns {} instead of raising, so ClawHubSource.fetch() can continue to the metadata/raw-content fallback.
  • Leave existing response and ZIP handling on their prior code paths: successful ZIP extraction, 429 handling, non-200 fail-fast behavior, invalid ZIP handling, unsafe ZIP member filtering, large-file skipping, non-UTF-8 skipping, and text decoding behavior are not changed.

The main tradeoff is timing: if the ZIP endpoint is persistently unreachable, the metadata/raw-content fallback is reached after the retry delay rather than immediately. That keeps the fallback intact while allowing short-lived transport failures to recover.

Evidence

The new regression test covers the exact branch that previously returned too early. It simulates one transient ZIP request exception followed by a valid ZIP response:

mock_get.side_effect = [
    httpx.ConnectTimeout("temporary timeout"),
    _MockResponse(status_code=200, content=zip_buffer.getvalue()),
]

Before this change, the first httpx.ConnectTimeout was caught by except httpx.HTTPError, _download_zip() returned {}, and the second mocked response was never requested.

After this change, the same path consumes one retry attempt, requests the ZIP again, extracts the text file, and returns:

{"SKILL.md": "# Skill"}

The test also asserts that httpx.get was called twice and that the retry delay was invoked once. That proves the successful result came from the retry path, not from the metadata/raw-content fallback.

Possible call chain / impact

Skills Hub install/fetch flow
  -> ClawHubSource.fetch(identifier)
    -> _get_json(/skills/{slug})
    -> _resolve_latest_version(slug, skill_data)
    -> _download_zip(slug, version)
      -> first /download request raises transient httpx.HTTPError
      -> retry while attempts remain
      -> next /download response returns a valid ZIP
      -> ZIP members are validated and decoded
      -> files includes SKILL.md
    -> return SkillBundle(...)

The affected path is limited to ClawHub ZIP bundle downloads. This PR does not change GitHub-backed skills, direct URL skills, official optional skills, ClawHub catalog search, ClawHub metadata parsing, the raw-content fallback itself, ZIP member path validation, quarantine/install behavior, or any tool schema.

I also checked nearby PRs before opening this:

Related Issue

No linked issue. Duplicate search performed for ClawHub _download_zip, ZIP download, and HTTPError retry behavior.

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Security fix
  • Documentation update
  • Tests (adding or improving test coverage)
  • Refactor (no behavior change)
  • New skill (bundled or hub)

Changes Made

  • tools/skills_hub.py
    • Retry httpx.HTTPError inside ClawHubSource._download_zip() until the existing retry budget is exhausted.
  • tests/tools/test_skills_hub_clawhub.py
    • Add a regression test proving a transient httpx.ConnectTimeout is retried and the next valid ZIP response is extracted.

How to Test

  1. Run the focused ClawHub test file:
uv run --extra dev pytest tests/tools/test_skills_hub_clawhub.py -q

Observed locally:

17 passed in 0.48s
  1. Run lint on the changed files:
uv run --extra dev ruff check tools/skills_hub.py tests/tools/test_skills_hub_clawhub.py

Observed locally:

All checks passed!
  1. Check whitespace:
git diff --check

Observed locally: passed with no output.

  1. Full-suite attempt on this Windows checkout:
uv run --extra dev pytest tests/ -q

This did not complete because the local environment was missing test-only optional dependencies such as aiohttp during collection.

I then retried with the practical extras needed for the gateway tests:

uv run --extra dev --extra messaging pytest tests/ -q

That cleared the aiohttp import errors but still hit a Windows collection issue in tests/tools/test_search_hidden_dirs.py, where the test calls Unix which rg directly. After adding a temporary local which.exe shim for this process, the full run proceeded further but did not return a final pytest summary before the 30-minute local timeout. Because I do not have a completed passing full-suite result, I left the pytest tests/ -q checklist item unchecked.

Checklist

Code

  • I've read the Contributing Guide
  • My commit messages follow Conventional Commits (fix(scope):, feat(scope):, etc.)
  • I searched for existing PRs to make sure this isn't a duplicate
  • My PR contains only changes related to this fix/feature (no unrelated commits)
  • I've run pytest tests/ -q and all tests pass
  • I've added tests for my changes (required for bug fixes, strongly encouraged for features)
  • I've tested on my platform: Windows 11, focused tests via uv run

Documentation & Housekeeping

  • I've updated relevant documentation (README, docs/, docstrings) - N/A
  • I've updated cli-config.yaml.example if I added/changed config keys - N/A
  • I've updated CONTRIBUTING.md or AGENTS.md if I changed architecture or workflows - N/A
  • I've considered cross-platform impact (Windows, macOS) per the compatibility guide - N/A; this is HTTP retry behavior
  • I've updated tool descriptions/schemas if I changed tool behavior - N/A

Screenshots / Logs

$ uv run --extra dev pytest tests/tools/test_skills_hub_clawhub.py -q
.................                                                        [100%]
17 passed in 0.48s

$ uv run --extra dev ruff check tools/skills_hub.py tests/tools/test_skills_hub_clawhub.py
All checks passed!

$ git diff --check
# no output

Full-suite local attempts did not produce a passing final summary:

$ uv run --extra dev pytest tests/ -q
ERROR tests/gateway/test_api_server.py - ModuleNotFoundError: No module named 'aiohttp'
...
ERROR tests/tools/test_search_hidden_dirs.py - FileNotFoundError: [WinError 2] system cannot find 'which'
$ uv run --all-extras pytest tests/ -q
Failed to build python-olm==3.2.16
help: python-olm was included because hermes-agent[matrix] depends on mautrix[encryption]
$ uv run --extra dev --extra messaging pytest tests/ -q
# with a temporary local which.exe shim for Windows collection
# no final pytest summary before the 30-minute local timeout

@alt-glitch alt-glitch added type/bug Something isn't working P3 Low — cosmetic, nice to have tool/skills Skills system (list, view, manage) labels Jul 3, 2026
@teknium1

Copy link
Copy Markdown
Contributor

Thanks for the focused regression fix. Current main declares a three-attempt ZIP download loop at tools/skills_hub.py:2652-2653, but its httpx.HTTPError branch returns immediately at tools/skills_hub.py:2702-2704; the reported transient-error gap is therefore present. The PR applies the existing bounded retry shape to that branch and covers a timeout followed by a valid ZIP response.

Automated hermes-sweeper review.

@teknium1 teknium1 added the sweeper:blast-contained Sweeper blast radius: contained — one narrow path / opt-in / few users label Jul 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

P3 Low — cosmetic, nice to have sweeper:blast-contained Sweeper blast radius: contained — one narrow path / opt-in / few users tool/skills Skills system (list, view, manage) type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants