Skip to content

refactor(library): migrate specialized HTTP clients - #2213

Merged
Pouyanpi merged 0 commit into
pouyanpi/rail-library-stack-13-http-action-migrationsfrom
pouyanpi/rail-library-stack-14-http-specialized-migrations
Jul 24, 2026
Merged

refactor(library): migrate specialized HTTP clients#2213
Pouyanpi merged 0 commit into
pouyanpi/rail-library-stack-13-http-action-migrationsfrom
pouyanpi/rail-library-stack-14-http-specialized-migrations

Conversation

@Pouyanpi

@Pouyanpi Pouyanpi commented Jul 23, 2026

Copy link
Copy Markdown
Collaborator

Description

Migrate the Hugging Face classifier and Clavata integrations to the canonical
HTTP subsystem while preserving their specialized retry and TLS requirements.

Add a transport-neutral HTTPTLSConfig for verification control, custom CA
bundles, and paired client certificate and key paths. TLS configuration is
consumed only at the transport boundary and cannot be combined with an injected
HTTPX client, which keeps ownership and connection construction unambiguous.

Hugging Face and Clavata wrap injected clients with their own retry policies
without taking ownership. Their fallback factories return fully composed
managed clients so http_call can close the entire owned stack. POST retry
behavior is explicit for both integrations.

An AST-based conformance test enforces the library boundary by rejecting direct
imports of HTTPX, aiohttp, Requests, or urllib3 under nemoguardrails/library/.

Impact

  • TLS-sensitive rails use one neutral configuration instead of leaking HTTPX
    construction details.
  • Custom CA and mTLS paths retain their existing validation behavior.
  • Hugging Face and Clavata preserve their integration-specific retries.
  • Injected clients remain caller-owned; fallback transports are closed
    deterministically.
  • New library integrations cannot bypass the canonical transport boundary.

Stack

Layer Branch Base PR
Foundation pouyanpi/rail-library-stack-9-http-contract develop #2209
Observability/lifecycle pouyanpi/rail-library-stack-11-http-observability Stack 9 #2210
Request migrations pouyanpi/rail-library-stack-12-http-request-migrations Stack 11 #2211
Vendor migrations pouyanpi/rail-library-stack-13-http-action-migrations Stack 12 #2212
Specialized clients pouyanpi/rail-library-stack-14-http-specialized-migrations Stack 13 #2213

Related Issue(s)

  • Fixes #
  • Internal tracking: NGUARD-863
  • Issue assignee: @

Verification

  • make test TEST="tests/http tests/test_activefence_rail.py tests/test_fact_checking.py tests/test_gliner.py tests/test_jailbreak_request.py tests/test_polygraf.py tests/test_ai_defense.py tests/test_f5_guardrails.py tests/test_fiddler_rails.py tests/test_pangea_ai_guard.py tests/test_patronus_evaluate_api.py tests/test_policyai_rail.py tests/test_clavata.py tests/test_clavata_models.py tests/test_hf_classifier.py tests/test_llmrails.py tests/guardrails/test_guardrails.py" — 587 passed, 4 skipped.
  • uv run --locked pre-commit run --from-ref origin/develop --to-ref pouyanpi/rail-library-stack-14-http-specialized-migrations — passed.
  • All tests were local and made no live provider calls.

AI Assistance

  • No AI tools were used.
  • AI tools were used; a human reviewed and can explain every change (tool: Codex).

Codex assisted with implementation analysis, validation, stack restructuring,
and this draft. Check the disclosure box after human review.

Checklist

  • I've read the CONTRIBUTING guidelines.
  • This PR links to a triaged issue assigned to me.
  • My PR title follows the project commit convention.
  • I've updated the documentation if applicable.
  • I've added tests if applicable.
  • I've noted any verification beyond CI and any checks I couldn't run.
  • I did not update generated changelog files manually.
  • I addressed all CodeRabbit, Greptile, and other review comments, or replied with why no change is needed.
  • @mentions of the person or team responsible for reviewing proposed changes.

@codecov

codecov Bot commented Jul 23, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 90.58824% with 8 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
nemoguardrails/library/clavata/request.py 70.83% 7 Missing ⚠️
nemoguardrails/library/hf_classifier/backends.py 96.77% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@Pouyanpi
Pouyanpi force-pushed the pouyanpi/rail-library-stack-14-http-specialized-migrations branch from 37f0772 to 694c268 Compare July 23, 2026 14:42
@Pouyanpi
Pouyanpi force-pushed the pouyanpi/rail-library-stack-13-http-action-migrations branch 2 times, most recently from 1f8c466 to 9852187 Compare July 23, 2026 16:21
@Pouyanpi
Pouyanpi force-pushed the pouyanpi/rail-library-stack-14-http-specialized-migrations branch 2 times, most recently from 282faee to e7d4172 Compare July 23, 2026 16:28
@Pouyanpi
Pouyanpi force-pushed the pouyanpi/rail-library-stack-13-http-action-migrations branch from 9852187 to 736a206 Compare July 23, 2026 16:28
@Pouyanpi Pouyanpi added status: triaged Triaged by a maintainer; eligible for automated review (CodeRabbit/Greptile). and removed status: needs triage New issues that have not yet been reviewed or categorized. labels Jul 23, 2026
@Pouyanpi Pouyanpi self-assigned this Jul 23, 2026
@Pouyanpi Pouyanpi added this to the v0.24.0 milestone Jul 23, 2026
@Pouyanpi
Pouyanpi marked this pull request as ready for review July 23, 2026 17:16
@greptile-apps

greptile-apps Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR completes the canonical HTTP subsystem migration for the Hugging Face classifier and Clavata integrations, replacing direct httpx/aiohttp usage with the shared HTTPClient contract. It also introduces HTTPTLSConfig as a transport-neutral value type for TLS verification, CA bundles, and mTLS paths, and adds an AST-based conformance test that prevents future library code from importing HTTP transports directly.

  • HTTPTLSConfig is added to nemoguardrails/http/types.py and threaded through HttpxHTTPClient; TLS settings are rejected when combined with an injected client, keeping ownership unambiguous.
  • Clavata replaces aiohttp + a broken @exponential_backoff decorator (rate-limit exceptions were swallowed before reaching the decorator) with a RetryPolicy(retryable_status_codes={429}, retry_transport_errors=False) applied at the transport layer, which is the correct level for retry decisions.
  • HF classifier replaces per-request httpx.AsyncClient construction with create_http_client + a two-attempt transport-error-only RetryPolicy, preserving the previous retry scope while eliminating the direct httpx dependency.

Confidence Score: 4/5

Safe to merge; all changes stay within the HTTP abstraction layer and the existing 587-test suite covers both the injected-client and factory paths for each integration.

The core transport changes are mechanically correct and well-tested. The two style notes (conditional kwargs indirection and silent discard of http_client for local HF engines) are observability concerns that won't affect production behaviour. The retry-delay in test_clavata_client_uses_shared_retry_policy is a minor test-speed issue. No regressions were identified in the migration.

nemoguardrails/library/hf_classifier/backends.py (get_backend local-engine silent drop) and the two actions.py files (conditional kwargs) are the spots worth a second look before merge.

Important Files Changed

Filename Overview
nemoguardrails/http/types.py Adds HTTPTLSConfig frozen dataclass with paired cert/key validation in post_init; clean addition to the type layer
nemoguardrails/http/transport.py Accepts optional HTTPTLSConfig and correctly rejects TLS config combined with an injected client; verify/cert translation logic is sound
nemoguardrails/http/runtime.py Threads HTTPTLSConfig through to HttpxHTTPClient unchanged; minimal, correct change
nemoguardrails/library/clavata/request.py Replaces aiohttp with canonical HTTP stack and correct RetryPolicy; outer except-Exception wrapper preserves pre-existing behaviour but limits error specificity; conditional kwargs pattern is unnecessarily verbose
nemoguardrails/library/hf_classifier/backends.py Replaces httpx directly with canonical transport; get_backend correctly bypasses cache for injected clients but silently ignores http_client for local engines; retry policy correctly covers transport errors only
nemoguardrails/library/clavata/actions.py Adds http_client injection to evaluate_with_policy and clavata_check; conditional kwargs passthrough is overly defensive but otherwise correct
nemoguardrails/library/hf_classifier/actions.py Adds http_client injection to all three action entry points; same conditional kwargs pattern as Clavata actions
tests/http/test_library_boundary.py New AST-based conformance test that rejects direct httpx/aiohttp/requests/urllib3 imports under the library root; sound approach, correctly walks all AST nodes
tests/http/test_transport.py New TLS tests cover owned-client configuration, injected-client rejection, and paired-credential validation; comprehensive and correct
tests/test_clavata.py Replaces aioresponses mocks with RecordingHTTPClient; cleaner and transport-neutral; first test adds URL/method/header assertions
tests/test_clavata_models.py Adds retry and transport-failure tests for ClavataClient; the retry test incurs a real sleep due to uninjected asyncio.sleep in the policy
tests/test_hf_classifier.py Updates SSL/TLS assertions to match new HTTPTLSConfig field names; adds injected-client and transport-retry tests for VLLMBackend; correct

Sequence Diagram

sequenceDiagram
    participant Action as clavata_check / hf_classifier_check_*
    participant Backend as ClavataClient / _RemoteBackend
    participant Retry as RetryingHTTPClient
    participant Transport as HttpxHTTPClient (owned)
    participant Injected as Injected HTTPClient (caller-owned)

    alt Injected client provided
        Action->>Backend: "call(http_client=injected)"
        Backend->>Retry: wrap(injected, policy)
        Retry->>Injected: request(POST, url)
        Injected-->>Retry: HTTPResponse / HTTPConnectionError
        Retry-->>Backend: response (after policy retries)
        Note over Injected: NOT closed — caller owns it
    else No injected client (factory path)
        Action->>Backend: "call(http_client=None)"
        Backend->>Transport: create_http_client(timeout, tls)
        Backend->>Retry: wrap(Transport, policy)
        Retry->>Transport: request(POST, url)
        Transport-->>Retry: HTTPResponse
        Retry-->>Backend: response
        Backend->>Transport: close() via _resolve_http_client
    end
Loading
Prompt To Fix All With AI
Fix the following 4 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 4
nemoguardrails/library/hf_classifier/backends.py:504-511
**Injected client silently ignored for local engine**

When `http_client` is provided but `config.engine == "local"`, the injected client is silently dropped and the local pipeline backend is created or returned from cache without it. A caller who passes an `http_client` expecting it to influence behaviour receives no warning that their argument was discarded. Logging a debug-level warning here would make this easier to diagnose.

### Issue 2 of 4
nemoguardrails/library/clavata/actions.py:256-257
**Unnecessary conditional kwargs indirection**

`evaluate_with_policy` already declares `http_client: HTTPClient | None = None` as a parameter, so it handles `None` safely. The conditional dict construction adds verbosity without any benefit — passing `http_client=http_client` directly is equivalent and clearer.

```suggestion
    result = await evaluate_with_policy(text, str(policy_id), clavata_config, http_client=http_client)
```

### Issue 3 of 4
nemoguardrails/library/hf_classifier/actions.py:53-54
**Same unnecessary conditional kwargs pattern**

`get_backend` already accepts `http_client=None` and handles it correctly (falls through to caching). Passing it directly simplifies the call site. This pattern appears in all three action functions.

```suggestion
    backend = get_backend(classifier_config, name=classifier_name, http_client=http_client)
```

### Issue 4 of 4
tests/test_clavata_models.py:270-291
**Retry test incurs real sleep**

`_CLAVATA_RETRY_POLICY` sets `initial_delay=0.1`, and `RetryingHTTPClient` uses `asyncio.sleep` for backoff between attempts. Because neither `sleep` nor `random_value` is injected in this test, the first retry waits up to 100 ms of real wall time. Injecting a no-op sleep via `RetryingHTTPClient(transport, _CLAVATA_RETRY_POLICY, sleep=lambda _: asyncio.sleep(0))` would keep the test instant while still verifying the retry count.

Reviews (1): Last reviewed commit: "test(http): enforce the library transpor..." | Re-trigger Greptile

Comment on lines +504 to +511
http_client: HTTPClient | None = None,
) -> ClassifierBackend:
"""Get or create a cached backend instance from classifier config."""
cls = _BACKENDS.get(config.engine)
if cls is None:
raise ValueError(f"Unknown hf_classifier engine: '{config.engine}'. Supported: {', '.join(_BACKENDS)}")
if http_client is not None and config.engine != "local":
return cls(config, http_client=http_client)

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.

P2 Injected client silently ignored for local engine

When http_client is provided but config.engine == "local", the injected client is silently dropped and the local pipeline backend is created or returned from cache without it. A caller who passes an http_client expecting it to influence behaviour receives no warning that their argument was discarded. Logging a debug-level warning here would make this easier to diagnose.

Prompt To Fix With AI
This is a comment left during a code review.
Path: nemoguardrails/library/hf_classifier/backends.py
Line: 504-511

Comment:
**Injected client silently ignored for local engine**

When `http_client` is provided but `config.engine == "local"`, the injected client is silently dropped and the local pipeline backend is created or returned from cache without it. A caller who passes an `http_client` expecting it to influence behaviour receives no warning that their argument was discarded. Logging a debug-level warning here would make this easier to diagnose.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +256 to +257
evaluate_kwargs = {"http_client": http_client} if http_client is not None else {}
result = await evaluate_with_policy(text, str(policy_id), clavata_config, **evaluate_kwargs)

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.

P2 Unnecessary conditional kwargs indirection

evaluate_with_policy already declares http_client: HTTPClient | None = None as a parameter, so it handles None safely. The conditional dict construction adds verbosity without any benefit — passing http_client=http_client directly is equivalent and clearer.

Suggested change
evaluate_kwargs = {"http_client": http_client} if http_client is not None else {}
result = await evaluate_with_policy(text, str(policy_id), clavata_config, **evaluate_kwargs)
result = await evaluate_with_policy(text, str(policy_id), clavata_config, http_client=http_client)
Prompt To Fix With AI
This is a comment left during a code review.
Path: nemoguardrails/library/clavata/actions.py
Line: 256-257

Comment:
**Unnecessary conditional kwargs indirection**

`evaluate_with_policy` already declares `http_client: HTTPClient | None = None` as a parameter, so it handles `None` safely. The conditional dict construction adds verbosity without any benefit — passing `http_client=http_client` directly is equivalent and clearer.

```suggestion
    result = await evaluate_with_policy(text, str(policy_id), clavata_config, http_client=http_client)
```

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Comment on lines +53 to +54
backend_kwargs = {"http_client": http_client} if http_client is not None else {}
backend = get_backend(classifier_config, name=classifier_name, **backend_kwargs)

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.

P2 Same unnecessary conditional kwargs pattern

get_backend already accepts http_client=None and handles it correctly (falls through to caching). Passing it directly simplifies the call site. This pattern appears in all three action functions.

Suggested change
backend_kwargs = {"http_client": http_client} if http_client is not None else {}
backend = get_backend(classifier_config, name=classifier_name, **backend_kwargs)
backend = get_backend(classifier_config, name=classifier_name, http_client=http_client)
Prompt To Fix With AI
This is a comment left during a code review.
Path: nemoguardrails/library/hf_classifier/actions.py
Line: 53-54

Comment:
**Same unnecessary conditional kwargs pattern**

`get_backend` already accepts `http_client=None` and handles it correctly (falls through to caching). Passing it directly simplifies the call site. This pattern appears in all three action functions.

```suggestion
    backend = get_backend(classifier_config, name=classifier_name, http_client=http_client)
```

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Comment on lines +270 to +291
@pytest.mark.asyncio
async def test_clavata_client_uses_shared_retry_policy(self):
response = CreateJobResponse(
job=Job(
status="JOB_STATUS_COMPLETED",
results=[Result(report=Report(result="OUTCOME_FALSE", sectionEvaluationReports=[]))],
)
)
transport = RecordingHTTPClient(
[
HTTPResponse(status_code=429),
HTTPResponse(status_code=200, content=response.model_dump_json().encode()),
]
)
job = await ClavataClient(
"https://clavata.example",
api_key="test-key",
http_client=transport,
).create_job("hello", "policy-id")

assert job.status == "JOB_STATUS_COMPLETED"
assert len(transport.requests) == 2

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.

P2 Retry test incurs real sleep

_CLAVATA_RETRY_POLICY sets initial_delay=0.1, and RetryingHTTPClient uses asyncio.sleep for backoff between attempts. Because neither sleep nor random_value is injected in this test, the first retry waits up to 100 ms of real wall time. Injecting a no-op sleep via RetryingHTTPClient(transport, _CLAVATA_RETRY_POLICY, sleep=lambda _: asyncio.sleep(0)) would keep the test instant while still verifying the retry count.

Prompt To Fix With AI
This is a comment left during a code review.
Path: tests/test_clavata_models.py
Line: 270-291

Comment:
**Retry test incurs real sleep**

`_CLAVATA_RETRY_POLICY` sets `initial_delay=0.1`, and `RetryingHTTPClient` uses `asyncio.sleep` for backoff between attempts. Because neither `sleep` nor `random_value` is injected in this test, the first retry waits up to 100 ms of real wall time. Injecting a no-op sleep via `RetryingHTTPClient(transport, _CLAVATA_RETRY_POLICY, sleep=lambda _: asyncio.sleep(0))` would keep the test instant while still verifying the retry count.

How can I resolve this? If you propose a fix, please make it concise.

@Pouyanpi
Pouyanpi merged commit 644faa4 into pouyanpi/rail-library-stack-13-http-action-migrations Jul 24, 2026
@Pouyanpi
Pouyanpi force-pushed the pouyanpi/rail-library-stack-13-http-action-migrations branch from 736a206 to 644faa4 Compare July 24, 2026 10:14
@Pouyanpi
Pouyanpi deleted the pouyanpi/rail-library-stack-14-http-specialized-migrations branch July 24, 2026 10:14
@Pouyanpi
Pouyanpi force-pushed the pouyanpi/rail-library-stack-14-http-specialized-migrations branch from e7d4172 to 644faa4 Compare July 24, 2026 10:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size: L status: triaged Triaged by a maintainer; eligible for automated review (CodeRabbit/Greptile).

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant