Finding
NewznabClient and TorznabClient independently own the same HTTP/XML session machinery even though their real protocol differences begin only when mapping a feed item into a SearchResult and when downloading NZB versus torrent content.
The two clients have field-for-field state, byte-equivalent constructors, byte-equivalent fetch_xml implementations, the same caps implementation, the same health-test implementation, the same query dispatch shape, and duplicated negative tests for cancellation, auth failure, rate limiting, oversized bodies, and credential redaction.
Verified against main 65644113e47814c87eaba14a6f322820974b8780.
Evidence
crates/eksetasis/src/client/newznab.rs:21-88 and torznab.rs:21-88 each define { config, http, cf_proxy, timeout, max_body_bytes }, the same constructor, and the same fetch_xml body.
- Both
fetch_xml copies independently own Cloudflare bypass, request timeout, cancellation selection, URL-secret redaction, 401/403 → AuthFailed, 429/Retry-After → RateLimited, and bounded response-body reading.
newznab.rs:161-185 and torznab.rs:170-194 duplicate caps() and test() exactly.
- The test modules repeat the same transport-policy cases. For example, the cancellation, 403, 429-without-header, 500-fallthrough, oversized-stream, and error-redaction tests are structurally identical on both clients.
- Their legitimate divergence is visible immediately after the shared fetch/parse boundary: Newznab maps results to
ReleaseProtocol::Nzb without torrent peer/hash fields, while Torznab maps seeders, leechers, infohash, and ReleaseProtocol::Torrent. Download behavior also differs by protocol.
No open issue owns this shared transport/session layer; #513 concerns Cardigann definition coverage rather than these two native clients.
Why this matters
Authentication, cancellation, response-size bounds, Cloudflare routing, secret redaction, retry metadata, and health semantics are cross-cutting acquisition policy. Maintaining them twice means a security or resilience repair can land for Torznab while Newznab retains the old behavior, or vice versa.
The repeated negative tests do not prevent that structural fork; they themselves are copies that need to be remembered whenever the policy changes. The protocol-specific result mapping does not require duplicating the HTTP transaction that precedes it.
Desired correction
Introduce one narrow XML-indexer transport/session core that owns the common client state and request lifecycle. Newznab and Torznab should remain separate IndexerClient implementations for query/result/download semantics, but delegate fetch, caps retrieval, status mapping, cancellation, body bounds, auth/rate-limit handling, and URL-redaction policy to the shared owner.
Prefer shared behavioral fixtures for the common transport contract and protocol-specific fixtures only for the mapping differences.
Done when:
- one production function owns the XML HTTP request lifecycle for both clients;
- caps/test behavior is derived from that common path;
- auth, cancellation, rate-limit, bounded-body, and secret-redaction tests are written once against the shared contract;
- Newznab/Torznab retain only their genuine result/download differences; and
- changing common transport policy requires one implementation edit.
Finding
NewznabClientandTorznabClientindependently own the same HTTP/XML session machinery even though their real protocol differences begin only when mapping a feed item into aSearchResultand when downloading NZB versus torrent content.The two clients have field-for-field state, byte-equivalent constructors, byte-equivalent
fetch_xmlimplementations, the same caps implementation, the same health-test implementation, the same query dispatch shape, and duplicated negative tests for cancellation, auth failure, rate limiting, oversized bodies, and credential redaction.Verified against
main65644113e47814c87eaba14a6f322820974b8780.Evidence
crates/eksetasis/src/client/newznab.rs:21-88andtorznab.rs:21-88each define{ config, http, cf_proxy, timeout, max_body_bytes }, the same constructor, and the samefetch_xmlbody.fetch_xmlcopies independently own Cloudflare bypass, request timeout, cancellation selection, URL-secret redaction, 401/403 →AuthFailed, 429/Retry-After→RateLimited, and bounded response-body reading.newznab.rs:161-185andtorznab.rs:170-194duplicatecaps()andtest()exactly.ReleaseProtocol::Nzbwithout torrent peer/hash fields, while Torznab mapsseeders,leechers,infohash, andReleaseProtocol::Torrent. Download behavior also differs by protocol.No open issue owns this shared transport/session layer; #513 concerns Cardigann definition coverage rather than these two native clients.
Why this matters
Authentication, cancellation, response-size bounds, Cloudflare routing, secret redaction, retry metadata, and health semantics are cross-cutting acquisition policy. Maintaining them twice means a security or resilience repair can land for Torznab while Newznab retains the old behavior, or vice versa.
The repeated negative tests do not prevent that structural fork; they themselves are copies that need to be remembered whenever the policy changes. The protocol-specific result mapping does not require duplicating the HTTP transaction that precedes it.
Desired correction
Introduce one narrow XML-indexer transport/session core that owns the common client state and request lifecycle. Newznab and Torznab should remain separate
IndexerClientimplementations for query/result/download semantics, but delegate fetch, caps retrieval, status mapping, cancellation, body bounds, auth/rate-limit handling, and URL-redaction policy to the shared owner.Prefer shared behavioral fixtures for the common transport contract and protocol-specific fixtures only for the mapping differences.
Done when: