# HTTP Client Platform — Support Matrix Grades follow design §6 and §29. A row is **Stable** only when the cross-transport contract suite proves it; anything the suite cannot prove is **Experimental** and says so. ## Spring API | API | Grade | Role | Constraint | |---|---|---|---| | `RestClient` | Stable | Blocking execution | Bounded concurrency and an effective deadline are mandatory | | `WebClient` | Stable | Reactive, streaming, SSE | No blocking work on the event loop | | HTTP Service Client (`@HttpExchange`) | Default | Declarative typed client | Operation metadata is mandatory | | `RestTemplate` | Migration only | Moving existing calls | No new profile or feature | | Generic Exchange (H2) | Restricted | Dynamic method, path, body | Base URL and policy are immutable | | Dynamic Target (H3) | Restricted | User-supplied URL | Separate SSRF policy; inherits no credential | | Native engine | Internal | Engine-specific configuration | Never an application-facing API | ## Transports | Transport | Blocking | Reactive | HTTP/1.1 | HTTP/2 | HTTP/3 | Grade | Verified by | |---|---:|---:|---:|---:|---:|---|---| | Apache HttpClient 5 (classic) | yes | no | yes | **no** | no | Stable (blocking default) | `httpClientStableContractTest`, `NegotiatedProtocolContractTest` | | JDK HttpClient | yes | `sendAsync` | yes | yes (TLS/ALPN) | no | Stable (lightweight, blocking HTTP/2) | `NegotiatedProtocolContractTest` | | Reactor Netty | limited | yes | yes | yes | experimental | Stable (reactive default) | `NegotiatedProtocolContractTest` | | Jetty | facade | yes | yes | yes | yes | **Experimental** | `Http3OptInTest` only | | Simple request factory | yes | no | limited | no | no | Local test only | rejected in production by `ClientProfileValidator` | ### Apache is HTTP/1.1 here, and why Design §6.2 grades Apache HttpClient 5 as HTTP/2-capable, and the library is — in its **async** client. Spring's `HttpComponentsClientHttpRequestFactory` drives the **classic** client, which speaks HTTP/1.1 only. `NegotiatedProtocolContractTest` measures this rather than assuming it: the classic client fails outright against a prior-knowledge h2c server. So `ApacheBlockingTransportProvider.capabilities()` declares HTTP/1.1, and a profile that pairs Apache with `HTTP_2` is rejected at startup instead of quietly running HTTP/1.1 while this table claims otherwise. **Blocking HTTP/2 is served by the JDK transport**; reactive HTTP/2 by Reactor Netty. Both are measured from the client after a real TLS handshake, not read from configuration. The JDK transport declares `routeScopedPool=false`, `boundedPendingAcquireQueue=false`, and `dynamicTargetStable=false`. A profile that needs any of those is rejected at startup rather than served with weaker guarantees. Choosing between Apache and JDK is therefore a real trade: Apache gives route-scoped pooling and Dynamic Target pinning, JDK gives HTTP/2. ## Capability gates | Capability | Gate | |---|---| | Dynamic Target (H3) | Apache and Reactor Netty only; JDK and Jetty are rejected | | HTTP/3 | `experimentalAcknowledgement` must equal `I_ACCEPT_HTTP3_EXPERIMENTAL_SEMANTICS` | | Cross-origin redirect | opt-in per profile; credentials are stripped on the hop | | Retry | evidence-based; never enabled by HTTP method alone | ## CI matrix | Profile | Frequency | Release gate | Task | |---|---|---|---| | Spring Framework 7.0 (repository baseline) | every PR | required | `spring70CompatibilityTest` | | Spring Framework 6.2 API surface | every PR | required | `spring62CompatibilityTest` | | Apache HC5 + RestClient | every PR | required | `httpClientStableContractTest -Phttpclient.contract.transports=apache` | | JDK HttpClient + RestClient | every PR | required | `httpClientStableContractTest -Phttpclient.contract.transports=jdk` | | Reactor Netty + WebClient | every PR | required | `httpClientStableContractTest -Phttpclient.contract.transports=reactor` | | SSRF / cardinality suite | every PR | required | `httpClientSecurityTest` | | Toxiproxy fault suite | nightly, release | required | `httpClientFailureInjectionTest` | | Event-loop blocking (BlockHound) | every PR | required | `httpClientBlockHoundTest` | | Performance certification | nightly, release | required | `httpClientPerformanceTest -Pperformance.assertions.enabled=true` | | Jetty HTTP/3 | nightly | Experimental, non-blocking | `test -Phttp3.tests.enabled=true` | ### Known limitation of the Spring 6.2 lane This repository's Spring Boot 4.0 baseline pins Spring Framework 7, so a real 6.2 runtime cannot be resolved here. `spring62CompatibilityTest` therefore verifies the **API surface**: the common packages must not reference any Spring 7-only type, and `org.springframework.web.service.registry` is confined to `…httpclient.spring7`. Executing the suite against an actual 6.2 distribution requires a host project on that line. This limitation is stated rather than hidden behind a passing check. ## What the suites do not prove Stated so the matrix is read as a measurement rather than an aspiration. | Gap | Why | What is proven instead | |---|---|---| | HTTP/2 frame injection (`REFUSED_STREAM`, arbitrary `GOAWAY`) | The fixture server exposes no frame-level control, and a purpose-built h2 server is a larger dependency than the guarantee is worth here | `Http2EvidenceMapperTest` proves the frame → evidence mapping, and `NegotiatedProtocolContractTest` proves h2 is really negotiated | | Netty buffer-leak detection | Netty reports a leak when an unreferenced buffer is collected, which the suite does not force | `NettyLeakDetectionExtension` asserts the PARANOID detector is live and reports nothing; explicit release assertions in the streaming suites are the primary guarantee | | Spring 6.2 runtime | This repository's Boot 4.0 baseline pins Spring 7 | `spring62CompatibilityTest` confines the common packages to the 6.2 API surface | | Performance latency baseline | Numbers measured on a build agent are not a certification | `httpClientPerformanceTest` asserts structural bounds unconditionally; latency and heap bounds run under `-Pperformance.assertions.enabled=true` |