Skip to content

fix: stop counting caller-side HTTP errors as dependency failures - #160

Merged
bagowix merged 2 commits into
mainfrom
fix/158-caller-side-exceptions
Aug 12, 2026
Merged

fix: stop counting caller-side HTTP errors as dependency failures#160
bagowix merged 2 commits into
mainfrom
fix/158-caller-side-exceptions

Conversation

@bagowix

@bagowix bagowix commented Aug 12, 2026

Copy link
Copy Markdown
Owner

Summary

HttpStatusClassifier counted any exception raised inside the guarded call as the dependency failing — including the ones the client library raises for the caller's own bug. A burst of them tripped the breaker of a host that was answering fine (rejecting real traffic in blocking mode, polluting the threshold-picking baseline in METRICS_ONLY).

Each copy of the classifier now excludes its own library's caller-side exceptions and takes excluded_exceptions=(...) to replace that set (() restores the old behaviour). Excluded exceptions propagate unchanged; they are recorded as successes, since the window has no third outcome.

Integration Excluded by default Why
httpx2 / httpx UnsupportedProtocol, LocalProtocolError Unsupported URL scheme, local protocol violation — deterministic, no evidence about the dependency
requests InvalidURL (parent of InvalidProxyURL) Request or proxy URL with no host
aiohttp — (empty) aiohttp rejects malformed/non-HTTP URLs before the middleware chain runs, so none reach the classifier; the knob is there for the caller's own middlewares

Two judgement calls worth reviewing:

  • PoolTimeout stays a failure. Pool exhaustion is most often the dependency holding connections open, and shedding load then is the point. Documented, with the one-liner to exclude it when the pool is sized below the caller's own burst.
  • InvalidHeader is not excluded for requests: urllib3 raises it for a malformed header sent by the server (a bad Retry-After, multiple Content-Length), so it is dependency-side.

The typing nit from the issue is fixed too: FailureClassifier.is_failure now declares exception: Exception | None. The engine releases cancellation and shutdown without classifying them (_engine.exit_block, _settle), so the wider annotation only invited classifier authors to write handling that could never run. A classifier still annotated BaseException | None keeps type-checking (contravariance), and griffe check reports no public-API breakage.

Checklist

  • Tests added or updated (suite stays at 100% coverage)
  • uv run ruff format --check and uv run ruff check pass
  • uv run mypy, uv run pyright and uv run pyrefly check pass
  • Docs updated (docs/) for user-facing changes
  • CHANGELOG.md [Unreleased] updated
  • Commits follow Conventional Commits

Tests first: the four classifier suites got failing cases for the caller-side exceptions, the custom/empty exclusion sets, and — for both httpx copies — a transport-level regression test reproducing the issue (a burst of UnsupportedProtocol leaves the breaker CLOSED). griffe check is clean, so no breaking-change label is needed.

Related issues

Closes #158

Added

  • Added configurable excluded_exceptions to HttpStatusClassifier for the httpx, httpx2, requests, and aiohttp extras.
  • Added default exclusions for UnsupportedProtocol and LocalProtocolError in the httpx and httpx2 extras.
  • Added a default InvalidURL exclusion in the requests extra.

Fixed

  • Excluded exceptions now propagate unchanged and count as successes.
  • PoolTimeout remains a failure in the httpx and httpx2 extras.
  • requests.InvalidHeader remains a failure.
  • Cancellation and shutdown exceptions are no longer classified by the engine.

Changed

  • Changed FailureClassifier.is_failure to accept Exception | None instead of BaseException | None.
  • Made excluded_exceptions replace integration defaults. An empty tuple counts all exceptions as failures.
  • Updated the public HttpStatusClassifier constructor API for the httpx, httpx2, requests, and aiohttp extras.

HttpStatusClassifier counted every exception raised inside the guarded
call as the dependency failing, including the ones the client library
raises for the caller's own bug — an unsupported URL scheme or a local
protocol violation in httpx2/httpx, a URL with no host in requests.
A burst of them tripped the breaker of a host that was answering fine
and, in METRICS_ONLY, polluted the baseline used to pick thresholds.

Each copy of the classifier now excludes its own library's caller-side
exceptions and takes excluded_exceptions=(...) to replace that set.
PoolTimeout stays a failure: an exhausted pool usually means the
dependency is holding connections open. aiohttp excludes nothing by
default — it rejects malformed URLs before the middleware chain runs —
so its knob is there for the caller's own middlewares.

FailureClassifier.is_failure now declares exception: Exception | None.
The engine releases cancellation and shutdown without classifying them,
so the wider annotation only invited code that could never run.

Closes #158
@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 937d4994-0954-4299-a86d-66f0ab2be673

📥 Commits

Reviewing files that changed from the base of the PR and between 944ea5a and 4b5ccb6.

📒 Files selected for processing (13)
  • CHANGELOG.md
  • docs/guides/failure-classification.md
  • docs/integrations/index.md
  • docs/llms-full.txt
  • docs/reference.md
  • interlock/integrations/aiohttp.py
  • interlock/integrations/httpx.py
  • interlock/integrations/httpx2.py
  • interlock/integrations/requests.py
  • tests/test_aiohttp.py
  • tests/test_httpx.py
  • tests/test_httpx2.py
  • tests/test_requests.py
🚧 Files skipped from review as they are similar to previous changes (11)
  • docs/integrations/index.md
  • CHANGELOG.md
  • tests/test_httpx.py
  • tests/test_requests.py
  • tests/test_aiohttp.py
  • tests/test_httpx2.py
  • docs/guides/failure-classification.md
  • interlock/integrations/aiohttp.py
  • docs/reference.md
  • docs/llms-full.txt
  • interlock/integrations/httpx2.py
📜 Recent review details
⏰ Context from checks skipped due to timeout. (7)
  • GitHub Check: Platform smoke (macos-latest, Python 3.14)
  • GitHub Check: Platform smoke (windows-latest, Python 3.14)
  • GitHub Check: quality (3.11)
  • GitHub Check: Platform smoke (macos-latest, Python 3.11)
  • GitHub Check: Platform smoke (windows-latest, Python 3.11)
  • GitHub Check: quality (3.14t)
  • GitHub Check: Run benchmarks
⚠️ CI failures not shown inline (2)

GitHub Actions: Code scanning AI findings on PR #160 / 0_github-advanced-security.txt: Code scanning AI findings on PR #160

Conclusion: failure

View job details

##[group]Run set -euo pipefail
 �[36;1mset -euo pipefail�[0m
 �[36;1mecho "RUNNER_TEMP=$RUNNER_TEMP"�[0m
 �[36;1mfind "$RUNNER_TEMP" -maxdepth 1 -type f -name 'git-credentials-*.config' -print -delete�[0m
 �[36;1m�[0m
 �[36;1m# Generate a unique token and stop processing workflow commands to prevent the runtime from injecting commands�[0m
 �[36;1mSTOP_***REDACTED_SECRET_ASSIGNMENT*** /proc/sys/kernel/random/uuid)�[0m
 �[36;1m�[0m
 �[36;1m# Use a trap to ensure we always resume command processing and check for�[0m
 �[36;1m# fallback error annotations, even if the runtime exits with a non-zero code�[0m
 �[36;1m# (which would otherwise cause set -e to abort the shell before we get here).�[0m
 �[36;1m# The trap preserves the original exit code.�[0m
 �[36;1mcopilot_cleanup() {�[0m
 �[36;1m  �[0m
 �[36;1m  if [ -n "${GIT_PROXY_PID:-}" ] && kill -0 "$GIT_PROXY_PID" 2>/dev/null; then�[0m
 �[36;1m    echo "Stopping git-proxy (pid=$GIT_PROXY_PID)..."�[0m
 �[36;1m    kill "$GIT_PROXY_PID" 2>/dev/null || true�[0m
 �[36;1m    for _ in {1..25}; do�[0m
 �[36;1m      if ! kill -0 "$GIT_PROXY_PID" 2>/dev/null; then break; fi�[0m
 �[36;1m      sleep 0.2�[0m
 �[36;1m    done�[0m
 �[36;1m    if kill -0 "$GIT_PROXY_PID" 2>/dev/null; then�[0m
 �[36;1m      echo "git-proxy did not stop gracefully; forcing termination."�[0m
 �[36;1m      kill -KILL "$GIT_PROXY_PID" 2>/dev/null || true�[0m
 �[36;1m    fi�[0m
 �[36;1m    wait "$GIT_PROXY_PID" 2>/dev/null || true�[0m
 �[36;1m  fi�[0m
 �[36;1m  �[0m
 �[36;1m  echo "::$STOP_***REDACTED_SECRET_ASSIGNMENT***
 �[36;1m  FALLBACK_FILE="${RUNNER_TEMP}/copilot-fallback-error.txt"�[0m
 �[36;1m  if [ -f "$FALLBACK_FILE" ]; then�[0m
 �[36;1m    FALLBACK_MSG=$(head -c 500 "$FALLBACK_FILE" | tr -d '\n\r')�[0m
 �[36;1m    echo "::error title=Copilot Error::${FALLBACK_MSG}"�[0m

GitHub Actions: Code scanning AI findings on PR #160 / github-advanced-security: Code scanning AI findings on PR #160

Conclusion: failure

View job details

##[group]Run set -euo pipefail
 �[36;1mset -euo pipefail�[0m
 �[36;1mecho "RUNNER_TEMP=$RUNNER_TEMP"�[0m
 �[36;1mfind "$RUNNER_TEMP" -maxdepth 1 -type f -name 'git-credentials-*.config' -print -delete�[0m
 �[36;1m�[0m
 �[36;1m# Generate a unique token and stop processing workflow commands to prevent the runtime from injecting commands�[0m
 �[36;1mSTOP_***REDACTED_SECRET_ASSIGNMENT*** /proc/sys/kernel/random/uuid)�[0m
 �[36;1m�[0m
 �[36;1m# Use a trap to ensure we always resume command processing and check for�[0m
 �[36;1m# fallback error annotations, even if the runtime exits with a non-zero code�[0m
 �[36;1m# (which would otherwise cause set -e to abort the shell before we get here).�[0m
 �[36;1m# The trap preserves the original exit code.�[0m
 �[36;1mcopilot_cleanup() {�[0m
 �[36;1m  �[0m
 �[36;1m  if [ -n "${GIT_PROXY_PID:-}" ] && kill -0 "$GIT_PROXY_PID" 2>/dev/null; then�[0m
 �[36;1m    echo "Stopping git-proxy (pid=$GIT_PROXY_PID)..."�[0m
 �[36;1m    kill "$GIT_PROXY_PID" 2>/dev/null || true�[0m
 �[36;1m    for _ in {1..25}; do�[0m
 �[36;1m      if ! kill -0 "$GIT_PROXY_PID" 2>/dev/null; then break; fi�[0m
 �[36;1m      sleep 0.2�[0m
 �[36;1m    done�[0m
 �[36;1m    if kill -0 "$GIT_PROXY_PID" 2>/dev/null; then�[0m
 �[36;1m      echo "git-proxy did not stop gracefully; forcing termination."�[0m
 �[36;1m      kill -KILL "$GIT_PROXY_PID" 2>/dev/null || true�[0m
 �[36;1m    fi�[0m
 �[36;1m    wait "$GIT_PROXY_PID" 2>/dev/null || true�[0m
 �[36;1m  fi�[0m
 �[36;1m  �[0m
 �[36;1m  echo "::$STOP_***REDACTED_SECRET_ASSIGNMENT***
 �[36;1m  FALLBACK_FILE="${RUNNER_TEMP}/copilot-fallback-error.txt"�[0m
 �[36;1m  if [ -f "$FALLBACK_FILE" ]; then�[0m
 �[36;1m    FALLBACK_MSG=$(head -c 500 "$FALLBACK_FILE" | tr -d '\n\r')�[0m
 �[36;1m    echo "::error title=Copilot Error::${FALLBACK_MSG}"�[0m
🧰 Additional context used
📓 Path-based instructions (4)
**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

**/*.py: Support Python 3.11 and newer; use Python 3.11+ features where required.
Keep the core zero-dependency and use only the standard library; external dependencies must be isolated behind optional integrations.
Use injected Clock instances for all time access; do not call time.monotonic() or sleep() directly in logic.
Implement the core as an I/O-free state machine with a single threading.Lock around the await-free critical section, never held across the protected call.
Use Protocols for extension points: Clock, SlidingWindow, Storage, FailureClassifier, and EventListener; do not inherit from internal classes.
Expose one public CircuitBreaker class for sync and async operation, with separate internal paths selected by coroutine detection.
Expose the public API through the package __init__.py; keep helpers underscore-prefixed and hidden.
Use absolute imports, placed at the top of the file, ordered as standard library, third-party, then local imports with blank lines between groups.
Use a maximum line length of 100 characters, single-quoted strings, f-strings, and pathlib.Path instead of os.path.
Annotate every parameter and return value; use modern generic syntax and X | None instead of Optional[X].
Use StrEnum or module-level constants instead of magic constants.
When a constructor or function has three or more arguments, pass them by keyword.
Keep functions focused on one job, generally no longer than 20–30 lines, with minimal side effects and extracted repeated loop logic.
Use async/await for I/O-bound work, asyncio.TaskGroup instead of asyncio.gather, and asyncio.to_thread or ProcessPoolExecutor for CPU-bound work.
Do not mix sync and async in one function; never await a sync callable or block on an async callable.
Fail fast on invalid input or state by raising immediately; do not continue with partial results or invented defaults.
Catch only expected exceptions, log them with context, and re-raise; do not use ...

Files:

  • interlock/integrations/requests.py
  • interlock/integrations/httpx.py
{interlock/**/*.py,docs/**/*.md,docs/llms-full.txt,docs/llms.txt}

📄 CodeRabbit inference engine (Custom checks)

When a change affects user-facing behaviour through the public API, integrations, or configuration options, update the relevant page under docs/ and regenerate docs/llms-full.txt; when adding a new documentation page, list it under ## Docs in docs/llms.txt.

Files:

  • interlock/integrations/requests.py
  • interlock/integrations/httpx.py
interlock/**/*.py

📄 CodeRabbit inference engine (Custom checks)

Every production behaviour change in interlock/ must be accompanied by a change under tests/; changes limited to docstrings, comments, or type annotations are exempt. Bug fixes must include at least one regression test that fails without the production fix.

Keep the core dependency-free; external dependencies must belong to extras and be imported lazily.

Files:

  • interlock/integrations/requests.py
  • interlock/integrations/httpx.py

⚙️ CodeRabbit configuration file

Core rules (AGENTS.md is authoritative): (1) Zero-dependency core — anything under interlock/ except interlock/integrations/ may import stdlib only. Flag every third-party import as a blocking issue. (2) No fallbacks, no silent excepts, no a or b or c for required config or data, no hidden retries. Invalid input or state raises immediately. interlock/_notify.py is the one sanctioned swallow (listener hooks are observability, logged with traceback, BaseException still propagates) — do not suggest generalising or "fixing" it. (3) Time comes only from the injected Clock protocol; direct time.monotonic()/time.sleep() in library logic is a bug. (4) Style: 100-char lines, single quotes, f-strings, pathlib, full annotations, X | None never Optional[X], keyword arguments for calls with 3+ arguments, no magic constants (StrEnum or module constants), functions under ~30 lines. (5) Extension points are Protocols (Clock, SlidingWindow, Storage, FailureClassifier, EventListener) — do not propose inheriting internal classes. (6) Sync and async live in one CircuitBreaker with separate internal paths; never propose Sync*/Async* twins and never mix the two paths in one function. (7) Public API is exported from interlock/init.py; everything else is underscore-prefixed. New public symbols need __all__ and a docstring. (8) Python 3.11 is the floor — no 3.12+ syntax or stdlib.

Files:

  • interlock/integrations/requests.py
  • interlock/integrations/httpx.py
interlock/integrations/**/*.py

📄 CodeRabbit inference engine (CONTRIBUTING.md)

Integration dependencies must be optional extras and imported lazily.

Files:

  • interlock/integrations/requests.py
  • interlock/integrations/httpx.py

⚙️ CodeRabbit configuration file

Optional extras. The third-party import must stay inside this package, must never be re-exported from interlock/init.py, and a missing extra must fail with a clear install hint rather than a fallback. Wrap the dependency behind the project's own types so its objects do not leak into core signatures. Check that the extra is declared in pyproject.toml [project.optional-dependencies] and documented under docs/integrations/.

Files:

  • interlock/integrations/requests.py
  • interlock/integrations/httpx.py
🧠 Learnings (2)
📚 Learning: 2026-08-07T15:34:59.595Z
Learnt from: CR
Repo: bagowix/interlock PR: 0
File: coderabbit-custom-pre-merge-checks-unique-id-file-non-traceable-F7F2B60C-1728-4C9A-8889-4F2235E186CA.txt:0-0
Timestamp: 2026-08-07T15:34:59.595Z
Learning: Applies to interlock/**/*.py : Every production behaviour change in `interlock/` must be accompanied by a change under `tests/`; changes limited to docstrings, comments, or type annotations are exempt. Bug fixes must include at least one regression test that fails without the production fix.

Applied to files:

  • interlock/integrations/httpx.py
📚 Learning: 2026-08-07T15:34:59.595Z
Learnt from: CR
Repo: bagowix/interlock PR: 0
File: coderabbit-custom-pre-merge-checks-unique-id-file-non-traceable-F7F2B60C-1728-4C9A-8889-4F2235E186CA.txt:0-0
Timestamp: 2026-08-07T15:34:59.595Z
Learning: Applies to {interlock/*.py,interlock/!(integrations)/**/*.py,pyproject.toml} : Keep the core zero-dependency: files under `interlock/` outside `interlock/integrations/` may import only the standard library or other `interlock` modules; `[project] dependencies` in `pyproject.toml` must remain empty; and `interlock/__init__.py` must not re-export from `interlock.integrations`.

Applied to files:

  • interlock/integrations/httpx.py
🔇 Additional comments (2)
interlock/integrations/httpx.py (1)

79-98: LGTM!

Also applies to: 121-124, 136-136

interlock/integrations/requests.py (1)

70-89: LGTM!

Also applies to: 111-114, 126-126


Walkthrough

The PR narrows FailureClassifier exception annotations and adds configurable excluded-exception policies to HTTP classifiers. Caller-side exceptions count as successes while propagating. Tests cover defaults, overrides, empty exclusions, and circuit state.

Changes

Failure classification and HTTP policies

Layer / File(s) Summary
Narrow classifier exception contracts
interlock/protocols.py, interlock/_classify.py, docs/guides/*, docs/migration.md, docs/integrations/llm.md, docs/reference.md, docs/llms-full.txt, CHANGELOG.md
Classifier APIs and examples use Exception | None. Cancellation and shutdown exceptions bypass classification.
Configure excluded HTTP exceptions
interlock/integrations/*, docs/integrations/*, docs/reference.md, docs/llms-full.txt, CHANGELOG.md
HTTP classifiers accept excluded_exceptions. Defaults exclude caller-side protocol or URL errors where applicable. Excluded exceptions propagate and count as successes.
Validate classification behavior
tests/test_aiohttp.py, tests/test_httpx.py, tests/test_httpx2.py, tests/test_requests.py
Tests cover default exclusions, custom replacement policies, empty exclusions, exception propagation, pool timeouts, and closed circuits.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Suggested labels: bug

🚥 Pre-merge checks | ✅ 9
✅ Passed checks (9 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title follows Conventional Commits and accurately describes the caller-side HTTP error classification change.
Linked Issues check ✅ Passed The implementation satisfies issue [#158] by adding configurable exclusions, correct defaults, narrowed typing, and unclassified cancellation handling.
Out of Scope Changes check ✅ Passed The code, tests, documentation, changelog, and typing updates all support the objectives in issue [#158].
Zero-Dependency Core ✅ Passed PR diff adds external imports only under interlock/integrations; pyproject.toml and interlock/init.py are unchanged, and core changes add no imports.
Changelog Entry ✅ Passed The PR diff adds a Fixed bullet under CHANGELOG.md's ## [Unreleased] section describing caller-side HTTP exceptions and excluded_exceptions behavior.
Docs And Llm Mirror ✅ Passed The PR changes HTTP integration behavior and public classifier options; all four integration pages plus guides/reference and docs/llms-full changed against origin/main, with no new docs page added.
Tests Accompany Behaviour Change ✅ Passed Production classifiers add exclusion-type validation, and tests/test_{aiohttp,httpx,httpx2,requests}.py add matching tests; parent constructors accepted these invalid entries, so the tests fail wit...
Public Api Surface ✅ Passed The PR diff from main contains no changes to interlock/init.py or interlock/pipeline.py, so no exported symbol removal or signature change was introduced.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/158-caller-side-exceptions

Comment @coderabbitai help to get the list of available commands.

@codspeed-hq

codspeed-hq Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 24 untouched benchmarks


Comparing fix/158-caller-side-exceptions (4b5ccb6) with main (d08042e)

Open in CodSpeed

@codecov

codecov Bot commented Aug 12, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ All tests successful. No failed tests found.

📢 Thoughts on this report? Let us know!

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/guides/failure-classification.md`:
- Around line 73-78: Update the exclusion wording in
docs/guides/failure-classification.md lines 73-78 and docs/integrations/index.md
lines 39-45 to state that exclusion defaults differ by integration and that only
non-excluded transport exceptions count as failures; specifically reflect
aiohttp.HttpStatusClassifier’s excluded_exceptions=() default and handler
exceptions counting unless explicitly excluded. Regenerate docs/llms-full.txt
after updating both documentation sites.

In `@docs/reference.md`:
- Around line 173-177: Update all four HttpStatusClassifier signatures in
docs/reference.md to include * before failure_statuses and excluded_exceptions,
documenting both parameters as keyword-only and preventing misleading
positional-call usage.

In `@interlock/integrations/httpx.py`:
- Around line 101-117: Validate every entry in excluded_exceptions during
initialization of the relevant integration classes before assigning
_excluded_exceptions, rejecting anything that is not an Exception subclass
immediately. Apply this consistently in interlock/integrations/httpx.py lines
101-117, interlock/integrations/httpx2.py lines 100-116,
interlock/integrations/requests.py lines 91-107, and
interlock/integrations/aiohttp.py lines 86-102, and add regression coverage for
invalid entries.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 2f206ce0-1376-4891-a247-5a8cd80b92b7

📥 Commits

Reviewing files that changed from the base of the PR and between d08042e and 944ea5a.

📒 Files selected for processing (21)
  • CHANGELOG.md
  • docs/guides/failure-classification.md
  • docs/integrations/aiohttp.md
  • docs/integrations/httpx.md
  • docs/integrations/httpx2.md
  • docs/integrations/index.md
  • docs/integrations/llm.md
  • docs/integrations/requests.md
  • docs/llms-full.txt
  • docs/migration.md
  • docs/reference.md
  • interlock/_classify.py
  • interlock/integrations/aiohttp.py
  • interlock/integrations/httpx.py
  • interlock/integrations/httpx2.py
  • interlock/integrations/requests.py
  • interlock/protocols.py
  • tests/test_aiohttp.py
  • tests/test_httpx.py
  • tests/test_httpx2.py
  • tests/test_requests.py
📜 Review details
⏰ Context from checks skipped due to timeout. (6)
  • GitHub Check: quality (3.14t)
  • GitHub Check: Platform smoke (macos-latest, Python 3.11)
  • GitHub Check: quality (3.11)
  • GitHub Check: Coverage
  • GitHub Check: Platform smoke (macos-latest, Python 3.14)
  • GitHub Check: Run benchmarks
⚠️ CI failures not shown inline (2)

GitHub Actions: Code scanning AI findings on PR #160 / 0_github-advanced-security.txt: Code scanning AI findings on PR #160

Conclusion: failure

View job details

##[group]Run set -euo pipefail
 �[36;1mset -euo pipefail�[0m
 �[36;1mecho "RUNNER_TEMP=$RUNNER_TEMP"�[0m
 �[36;1mfind "$RUNNER_TEMP" -maxdepth 1 -type f -name 'git-credentials-*.config' -print -delete�[0m
 �[36;1m�[0m
 �[36;1m# Generate a unique token and stop processing workflow commands to prevent the runtime from injecting commands�[0m
 �[36;1mSTOP_***REDACTED_SECRET_ASSIGNMENT*** /proc/sys/kernel/random/uuid)�[0m
 �[36;1m�[0m
 �[36;1m# Use a trap to ensure we always resume command processing and check for�[0m
 �[36;1m# fallback error annotations, even if the runtime exits with a non-zero code�[0m
 �[36;1m# (which would otherwise cause set -e to abort the shell before we get here).�[0m
 �[36;1m# The trap preserves the original exit code.�[0m
 �[36;1mcopilot_cleanup() {�[0m
 �[36;1m  �[0m
 �[36;1m  if [ -n "${GIT_PROXY_PID:-}" ] && kill -0 "$GIT_PROXY_PID" 2>/dev/null; then�[0m
 �[36;1m    echo "Stopping git-proxy (pid=$GIT_PROXY_PID)..."�[0m
 �[36;1m    kill "$GIT_PROXY_PID" 2>/dev/null || true�[0m
 �[36;1m    for _ in {1..25}; do�[0m
 �[36;1m      if ! kill -0 "$GIT_PROXY_PID" 2>/dev/null; then break; fi�[0m
 �[36;1m      sleep 0.2�[0m
 �[36;1m    done�[0m
 �[36;1m    if kill -0 "$GIT_PROXY_PID" 2>/dev/null; then�[0m
 �[36;1m      echo "git-proxy did not stop gracefully; forcing termination."�[0m
 �[36;1m      kill -KILL "$GIT_PROXY_PID" 2>/dev/null || true�[0m
 �[36;1m    fi�[0m
 �[36;1m    wait "$GIT_PROXY_PID" 2>/dev/null || true�[0m
 �[36;1m  fi�[0m
 �[36;1m  �[0m
 �[36;1m  echo "::$STOP_***REDACTED_SECRET_ASSIGNMENT***
 �[36;1m  FALLBACK_FILE="${RUNNER_TEMP}/copilot-fallback-error.txt"�[0m
 �[36;1m  if [ -f "$FALLBACK_FILE" ]; then�[0m
 �[36;1m    FALLBACK_MSG=$(head -c 500 "$FALLBACK_FILE" | tr -d '\n\r')�[0m
 �[36;1m    echo "::error title=Copilot Error::${FALLBACK_MSG}"�[0m

GitHub Actions: Code scanning AI findings on PR #160 / github-advanced-security: Code scanning AI findings on PR #160

Conclusion: failure

View job details

##[group]Run set -euo pipefail
 �[36;1mset -euo pipefail�[0m
 �[36;1mecho "RUNNER_TEMP=$RUNNER_TEMP"�[0m
 �[36;1mfind "$RUNNER_TEMP" -maxdepth 1 -type f -name 'git-credentials-*.config' -print -delete�[0m
 �[36;1m�[0m
 �[36;1m# Generate a unique token and stop processing workflow commands to prevent the runtime from injecting commands�[0m
 �[36;1mSTOP_***REDACTED_SECRET_ASSIGNMENT*** /proc/sys/kernel/random/uuid)�[0m
 �[36;1m�[0m
 �[36;1m# Use a trap to ensure we always resume command processing and check for�[0m
 �[36;1m# fallback error annotations, even if the runtime exits with a non-zero code�[0m
 �[36;1m# (which would otherwise cause set -e to abort the shell before we get here).�[0m
 �[36;1m# The trap preserves the original exit code.�[0m
 �[36;1mcopilot_cleanup() {�[0m
 �[36;1m  �[0m
 �[36;1m  if [ -n "${GIT_PROXY_PID:-}" ] && kill -0 "$GIT_PROXY_PID" 2>/dev/null; then�[0m
 �[36;1m    echo "Stopping git-proxy (pid=$GIT_PROXY_PID)..."�[0m
 �[36;1m    kill "$GIT_PROXY_PID" 2>/dev/null || true�[0m
 �[36;1m    for _ in {1..25}; do�[0m
 �[36;1m      if ! kill -0 "$GIT_PROXY_PID" 2>/dev/null; then break; fi�[0m
 �[36;1m      sleep 0.2�[0m
 �[36;1m    done�[0m
 �[36;1m    if kill -0 "$GIT_PROXY_PID" 2>/dev/null; then�[0m
 �[36;1m      echo "git-proxy did not stop gracefully; forcing termination."�[0m
 �[36;1m      kill -KILL "$GIT_PROXY_PID" 2>/dev/null || true�[0m
 �[36;1m    fi�[0m
 �[36;1m    wait "$GIT_PROXY_PID" 2>/dev/null || true�[0m
 �[36;1m  fi�[0m
 �[36;1m  �[0m
 �[36;1m  echo "::$STOP_***REDACTED_SECRET_ASSIGNMENT***
 �[36;1m  FALLBACK_FILE="${RUNNER_TEMP}/copilot-fallback-error.txt"�[0m
 �[36;1m  if [ -f "$FALLBACK_FILE" ]; then�[0m
 �[36;1m    FALLBACK_MSG=$(head -c 500 "$FALLBACK_FILE" | tr -d '\n\r')�[0m
 �[36;1m    echo "::error title=Copilot Error::${FALLBACK_MSG}"�[0m
🧰 Additional context used
📓 Path-based instructions (11)
**/*.md

📄 CodeRabbit inference engine (AGENTS.md)

Document user-facing changes in English Markdown documentation and keep generated documentation mirrors synchronized.

Files:

  • docs/integrations/llm.md
  • docs/integrations/httpx2.md
  • docs/integrations/requests.md
  • docs/integrations/httpx.md
  • docs/integrations/aiohttp.md
  • docs/integrations/index.md
  • CHANGELOG.md
  • docs/migration.md
  • docs/guides/failure-classification.md
  • docs/reference.md
{interlock/**/*.py,docs/**/*.md,docs/llms-full.txt,docs/llms.txt}

📄 CodeRabbit inference engine (Custom checks)

When a change affects user-facing behaviour through the public API, integrations, or configuration options, update the relevant page under docs/ and regenerate docs/llms-full.txt; when adding a new documentation page, list it under ## Docs in docs/llms.txt.

Files:

  • docs/integrations/llm.md
  • interlock/_classify.py
  • docs/integrations/httpx2.md
  • docs/integrations/requests.md
  • docs/integrations/httpx.md
  • interlock/protocols.py
  • docs/integrations/aiohttp.md
  • docs/integrations/index.md
  • docs/migration.md
  • docs/guides/failure-classification.md
  • interlock/integrations/requests.py
  • interlock/integrations/httpx.py
  • interlock/integrations/httpx2.py
  • interlock/integrations/aiohttp.py
  • docs/reference.md
  • docs/llms-full.txt
docs/**/*.{md,mdx}

📄 CodeRabbit inference engine (CONTRIBUTING.md)

User-facing changes must update the relevant documentation page under docs/.

Files:

  • docs/integrations/llm.md
  • docs/integrations/httpx2.md
  • docs/integrations/requests.md
  • docs/integrations/httpx.md
  • docs/integrations/aiohttp.md
  • docs/integrations/index.md
  • docs/migration.md
  • docs/guides/failure-classification.md
  • docs/reference.md
docs/**/*.md

⚙️ CodeRabbit configuration file

User-facing documentation. Check that code samples match the current public API and would actually run. A new page must also be listed in docs/llms.txt under ## Docs. Keep the existing voice: short sentences, no marketing.

Files:

  • docs/integrations/llm.md
  • docs/integrations/httpx2.md
  • docs/integrations/requests.md
  • docs/integrations/httpx.md
  • docs/integrations/aiohttp.md
  • docs/integrations/index.md
  • docs/migration.md
  • docs/guides/failure-classification.md
  • docs/reference.md
**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

**/*.py: Support Python 3.11 and newer; use Python 3.11+ features where required.
Keep the core zero-dependency and use only the standard library; external dependencies must be isolated behind optional integrations.
Use injected Clock instances for all time access; do not call time.monotonic() or sleep() directly in logic.
Implement the core as an I/O-free state machine with a single threading.Lock around the await-free critical section, never held across the protected call.
Use Protocols for extension points: Clock, SlidingWindow, Storage, FailureClassifier, and EventListener; do not inherit from internal classes.
Expose one public CircuitBreaker class for sync and async operation, with separate internal paths selected by coroutine detection.
Expose the public API through the package __init__.py; keep helpers underscore-prefixed and hidden.
Use absolute imports, placed at the top of the file, ordered as standard library, third-party, then local imports with blank lines between groups.
Use a maximum line length of 100 characters, single-quoted strings, f-strings, and pathlib.Path instead of os.path.
Annotate every parameter and return value; use modern generic syntax and X | None instead of Optional[X].
Use StrEnum or module-level constants instead of magic constants.
When a constructor or function has three or more arguments, pass them by keyword.
Keep functions focused on one job, generally no longer than 20–30 lines, with minimal side effects and extracted repeated loop logic.
Use async/await for I/O-bound work, asyncio.TaskGroup instead of asyncio.gather, and asyncio.to_thread or ProcessPoolExecutor for CPU-bound work.
Do not mix sync and async in one function; never await a sync callable or block on an async callable.
Fail fast on invalid input or state by raising immediately; do not continue with partial results or invented defaults.
Catch only expected exceptions, log them with context, and re-raise; do not use ...

Files:

  • interlock/_classify.py
  • interlock/protocols.py
  • tests/test_aiohttp.py
  • tests/test_requests.py
  • interlock/integrations/requests.py
  • interlock/integrations/httpx.py
  • interlock/integrations/httpx2.py
  • tests/test_httpx.py
  • interlock/integrations/aiohttp.py
  • tests/test_httpx2.py
interlock/**/*.py

📄 CodeRabbit inference engine (Custom checks)

Every production behaviour change in interlock/ must be accompanied by a change under tests/; changes limited to docstrings, comments, or type annotations are exempt. Bug fixes must include at least one regression test that fails without the production fix.

Keep the core dependency-free; external dependencies must belong to extras and be imported lazily.

Files:

  • interlock/_classify.py
  • interlock/protocols.py
  • interlock/integrations/requests.py
  • interlock/integrations/httpx.py
  • interlock/integrations/httpx2.py
  • interlock/integrations/aiohttp.py

⚙️ CodeRabbit configuration file

Core rules (AGENTS.md is authoritative): (1) Zero-dependency core — anything under interlock/ except interlock/integrations/ may import stdlib only. Flag every third-party import as a blocking issue. (2) No fallbacks, no silent excepts, no a or b or c for required config or data, no hidden retries. Invalid input or state raises immediately. interlock/_notify.py is the one sanctioned swallow (listener hooks are observability, logged with traceback, BaseException still propagates) — do not suggest generalising or "fixing" it. (3) Time comes only from the injected Clock protocol; direct time.monotonic()/time.sleep() in library logic is a bug. (4) Style: 100-char lines, single quotes, f-strings, pathlib, full annotations, X | None never Optional[X], keyword arguments for calls with 3+ arguments, no magic constants (StrEnum or module constants), functions under ~30 lines. (5) Extension points are Protocols (Clock, SlidingWindow, Storage, FailureClassifier, EventListener) — do not propose inheriting internal classes. (6) Sync and async live in one CircuitBreaker with separate internal paths; never propose Sync*/Async* twins and never mix the two paths in one function. (7) Public API is exported from interlock/init.py; everything else is underscore-prefixed. New public symbols need __all__ and a docstring. (8) Python 3.11 is the floor — no 3.12+ syntax or stdlib.

Files:

  • interlock/_classify.py
  • interlock/protocols.py
  • interlock/integrations/requests.py
  • interlock/integrations/httpx.py
  • interlock/integrations/httpx2.py
  • interlock/integrations/aiohttp.py
{interlock/*.py,interlock/!(integrations)/**/*.py,pyproject.toml}

📄 CodeRabbit inference engine (Custom checks)

Keep the core zero-dependency: files under interlock/ outside interlock/integrations/ may import only the standard library or other interlock modules; [project] dependencies in pyproject.toml must remain empty; and interlock/__init__.py must not re-export from interlock.integrations.

Files:

  • interlock/_classify.py
  • interlock/protocols.py
tests/**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

tests/**/*.py: Use pytest functions rather than test classes, with names formatted as test__unit_of_work__state_under_test__expected_behavior.
Mirror package layout in test filenames, use Arrange-Act-Assert, and create fixtures for repeated setup.
Use injected Clock instances for deterministic tests; do not use sleep() in tests.
Use pytest-asyncio and @pytest.mark.asyncio for asynchronous tests, and use pytest-mock to isolate external dependencies.
Use Hypothesis property-based tests for the state machine and cover all transitions and races.
Write the reproducing test before a bug fix and specify the required behavior before implementing a feature.

Tests must preserve 100% coverage, avoid sleep for time-dependent behavior, and use injected clocks instead.

Files:

  • tests/test_aiohttp.py
  • tests/test_requests.py
  • tests/test_httpx.py
  • tests/test_httpx2.py

⚙️ CodeRabbit configuration file

pytest functions only, never test classes. Names follow test__unit_of_work__state_under_test__expected_behavior in lower case. One behaviour per test, Arrange-Act-Assert. Time is the injected fake Clock — any real sleep or wall-clock read is flakiness, flag it. Async tests use @pytest.mark.asyncio; state-machine work carries hypothesis property tests. Coverage must stay at 100%: point out uncovered branches the diff introduces. Tests run under -n auto, so anything relying on ordering or shared global state is a bug.

Files:

  • tests/test_aiohttp.py
  • tests/test_requests.py
  • tests/test_httpx.py
  • tests/test_httpx2.py
CHANGELOG.md

📄 CodeRabbit inference engine (AGENTS.md)

Add every change to the [Unreleased] section under Added, Fixed, or Changed, explaining user impact rather than only symbol movement.

User-facing changes must update the [Unreleased] section.

Files:

  • CHANGELOG.md

⚙️ CodeRabbit configuration file

Keep a Changelog format. New entries go under ## [Unreleased] in Added / Fixed / Changed. An entry describes what a user could not do before and can now, not which symbol moved. Only the release commit dates a section and updates the link references.

Files:

  • CHANGELOG.md
interlock/integrations/**/*.py

📄 CodeRabbit inference engine (CONTRIBUTING.md)

Integration dependencies must be optional extras and imported lazily.

Files:

  • interlock/integrations/requests.py
  • interlock/integrations/httpx.py
  • interlock/integrations/httpx2.py
  • interlock/integrations/aiohttp.py

⚙️ CodeRabbit configuration file

Optional extras. The third-party import must stay inside this package, must never be re-exported from interlock/init.py, and a missing extra must fail with a clear install hint rather than a fallback. Wrap the dependency behind the project's own types so its objects do not leak into core signatures. Check that the extra is declared in pyproject.toml [project.optional-dependencies] and documented under docs/integrations/.

Files:

  • interlock/integrations/requests.py
  • interlock/integrations/httpx.py
  • interlock/integrations/httpx2.py
  • interlock/integrations/aiohttp.py
docs/llms-full.txt

⚙️ CodeRabbit configuration file

Generated artefact — produced by uv run python scripts/build_llms_full.py. Do not review its content or suggest edits; only confirm it was regenerated together with the docs/ changes in the same PR.

Files:

  • docs/llms-full.txt
🧠 Learnings (6)
📚 Learning: 2026-08-07T19:40:56.830Z
Learnt from: CR
Repo: bagowix/interlock PR: 0
File: CONTRIBUTING.md:0-0
Timestamp: 2026-08-07T19:40:56.830Z
Learning: Keep the project scoped to the circuit-breaker core, resilience pipeline, and thin integrations; retry remains delegated to tenacity, while caching and custom backoff engines are out of scope.

Applied to files:

  • docs/integrations/index.md
  • docs/llms-full.txt
📚 Learning: 2026-08-07T15:34:32.493Z
Learnt from: CR
Repo: bagowix/interlock PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-08-07T15:34:32.493Z
Learning: Applies to **/*.py : Expose one public `CircuitBreaker` class for sync and async operation, with separate internal paths selected by coroutine detection.

Applied to files:

  • docs/integrations/index.md
  • docs/migration.md
  • docs/reference.md
  • docs/llms-full.txt
📚 Learning: 2026-08-07T15:34:32.493Z
Learnt from: CR
Repo: bagowix/interlock PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-08-07T15:34:32.493Z
Learning: Applies to CHANGELOG.md : Add every change to the `[Unreleased]` section under `Added`, `Fixed`, or `Changed`, explaining user impact rather than only symbol movement.

Applied to files:

  • CHANGELOG.md
📚 Learning: 2026-08-07T19:40:56.829Z
Learnt from: CR
Repo: bagowix/interlock PR: 0
File: CONTRIBUTING.md:0-0
Timestamp: 2026-08-07T19:40:56.829Z
Learning: Applies to CHANGELOG.md : User-facing changes must update the `[Unreleased]` section.

Applied to files:

  • CHANGELOG.md
📚 Learning: 2026-08-07T15:34:59.595Z
Learnt from: CR
Repo: bagowix/interlock PR: 0
File: coderabbit-custom-pre-merge-checks-unique-id-file-non-traceable-F7F2B60C-1728-4C9A-8889-4F2235E186CA.txt:0-0
Timestamp: 2026-08-07T15:34:59.595Z
Learning: A changelog entry is required for PRs unless the PR prepares a release, changes only documentation-exempt paths such as `.github/`, `benchmarks/`, `planning/`, tests, non-published developer tooling, or root dot-files. Otherwise, add at least one user-visible bullet under `## [Unreleased]` in `CHANGELOG.md`.

Applied to files:

  • CHANGELOG.md
📚 Learning: 2026-08-07T15:34:59.595Z
Learnt from: CR
Repo: bagowix/interlock PR: 0
File: coderabbit-custom-pre-merge-checks-unique-id-file-non-traceable-F7F2B60C-1728-4C9A-8889-4F2235E186CA.txt:0-0
Timestamp: 2026-08-07T15:34:59.595Z
Learning: Applies to {interlock/**/*.py,docs/**/*.md,docs/llms-full.txt,docs/llms.txt} : When a change affects user-facing behaviour through the public API, integrations, or configuration options, update the relevant page under `docs/` and regenerate `docs/llms-full.txt`; when adding a new documentation page, list it under `## Docs` in `docs/llms.txt`.

Applied to files:

  • docs/llms-full.txt
🔇 Additional comments (10)
docs/guides/failure-classification.md (1)

25-34: LGTM!

Also applies to: 55-64

docs/integrations/llm.md (1)

28-28: LGTM!

docs/llms-full.txt (1)

934-934: 📐 Maintainability & Code Quality

Confirm regeneration of the generated documentation.

Run uv run python scripts/build_llms_full.py after the source Markdown changes. Confirm that the generated output matches docs/llms-full.txt in a clean worktree.

As per path instructions, docs/llms-full.txt is a generated artefact and must stay synchronized with the source documentation.

Source: Path instructions

docs/migration.md (1)

111-111: LGTM!

Also applies to: 264-264

docs/reference.md (1)

140-142: LGTM!

docs/integrations/httpx2.md (1)

170-204: LGTM!

docs/integrations/requests.md (1)

138-150: LGTM!

tests/test_httpx.py (1)

140-169: LGTM!

Also applies to: 171-186

tests/test_httpx2.py (1)

123-152: LGTM!

Also applies to: 154-169

tests/test_requests.py (1)

60-72: LGTM!

Also applies to: 75-80, 83-86

Comment thread docs/guides/failure-classification.md Outdated
Comment thread docs/reference.md Outdated
Comment thread interlock/integrations/httpx.py
An entry that is not an Exception subclass made isinstance() raise from
inside _settle — on the first real failure, masking the exception the
breaker was measuring and leaving the outcome unrecorded. It is now
rejected where it is passed.

Also sharpen the docs from review: the exclusion default differs per
integration (aiohttp excludes nothing, so every handler exception counts
unless excluded), and reference.md marks both classifier parameters
keyword-only.
@bagowix
bagowix merged commit 90f2afd into main Aug 12, 2026
33 of 35 checks passed
@bagowix
bagowix deleted the fix/158-caller-side-exceptions branch August 12, 2026 05:53
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.

HttpStatusClassifier attributes caller-side exceptions to the dependency

1 participant