feat(httpclient): close the platform review's P0/P1/P2 findings
The review found one defect shape repeated across the platform: surfaces that were declared, bound, and documented, but that nothing read. An operator configuring fullUrlRecording, bodyLogging, retry.policy, validatedDnsPinning, timeout.dns, or any of ten declared metric names got a guarantee the code never delivered. Every such surface is now in exactly one of three states -- wired for real, rejected at startup, or registered in a test-enforced gap list with its reason. No silent no-ops remain. P0: - Activate the platform from bootstrap behind app.httpclient.enabled, with a single auto-configuration importing the nine child configurations. - Give the platform a strict, repository-level ENV contract: 74 leaf fields derived from the settings record tree, unknown APP_HTTPCLIENT_* rejected. - Route typed HTTP service clients through the call kernel via KernelHttpExchangeAdapter, so they stop bypassing platform policy. - Pin dynamic-target DNS resolution to the socket for the life of a call, closing the resolve-then-connect TOCTOU / rebinding window. - Actually transmit the idempotency key, and make retry eligibility depend on transmission rather than on merely holding one. - Reject reactive authentication and reactive redirect at startup instead of declaring support that does not function. - Fix the Reactor-only Stable contract row so the lane stops failing. - Stop advertising HTTP/3 on a transport that negotiates HTTP/1. P1 covers execution and retry accounting, redirect security (per-hop target guarding, sensitive-header stripping, 303 body handling), runtime rotation and transport resource ownership keyed by generation, dynamic-target hardening (subdomain matching, global-unicast classification, strict CIDR parsing), protocol intent, pool and timeout wiring, streaming and body limits, observability parity, and OAuth single-flight refresh on a bounded pool with a bounded wait. P2 covers configuration and documentation drift, the Gradle check wiring for the four hermetic lanes, and the CI gate matrix. Two test-quality defects surfaced while closing these: the HTTP/2 stream saturation test ran against cleartext HTTP/1.1 while asserting nothing about the protocol, and an OAuth contention test slept on a latch that could fire before the callers it meant to observe. Both now assert what their names claim. Verification run: :adapter:outbound:httpclient:check and :app-bootstrap:check (checkstyle, spotless, spotbugs, and the four hermetic lanes), verifyCleanArchitectureDependencies, verifyEnvKeys, verifyOneTypePerFile, verifyDependencyLocks, the documentation and gate-matrix verifiers, and the performance lane against a real TLS+ALPN HTTP/2 server. Not executed, and tracked rather than claimed: Docker/Toxiproxy fault injection, JMH, a real QUIC/HTTP3 server, a real Spring Framework 6.2 distribution (now a delegated-pending gate), live OAuth/TLS/proxy/DNS integration, and a whole-repository check. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
5f10b791d3
commit
0cd959a494
@@ -0,0 +1,87 @@
|
||||
# HTTP Client Platform — Repository Adaptation Contract
|
||||
|
||||
**Design source:** `httpclient-superpowers-package/docs/superpowers/specs/2026-08-08-httpclient-platform-design.md`
|
||||
**Plan source:** `httpclient-superpowers-package/docs/superpowers/plans/2026-08-08-httpclient-platform-implementation-plan.md`
|
||||
|
||||
The design package states its own adaptation rule:
|
||||
|
||||
> 실제 Backend Skeleton 저장소가 제공되지 않았으므로 package 경로와 Gradle 구조는 설계서의 명시적
|
||||
> 구현 가정이다. 구현 전 저장소의 기존 convention과 root package에 맞춰 경로만 조정하고 공개 계약과
|
||||
> 정책 의미론은 유지한다.
|
||||
|
||||
This file is the single record of *how* the design's assumed layout was mapped onto this repository.
|
||||
Only paths, build DSL, and composition-root ownership changed. Public contracts, policy order, and
|
||||
error semantics are implemented exactly as specified.
|
||||
|
||||
## 1. Why the module layout differs
|
||||
|
||||
The design assumes a greenfield library with 19 Gradle projects under `modules/httpclient/`.
|
||||
This repository is a Clean Architecture template whose **fail-closed registry**
|
||||
(`src/config/architecture/modules.json`, enforced by `src/settings.gradle` and
|
||||
`verifyCleanArchitectureDependencies`) declares **exactly 19 leaf identities**. Creating 19 more
|
||||
Gradle projects would violate HARD-STOP #5 in `AGENTS.md`.
|
||||
|
||||
Therefore the design's 19 library modules become **package boundaries inside the registered leaf**
|
||||
`:adapter:outbound:httpclient`, with two exceptions driven by this repository's own rules:
|
||||
|
||||
| Design module | Repository home | Reason |
|
||||
|---|---|---|
|
||||
| `httpclient-spring-boot-starter` | `:app-bootstrap` (`dev.caskeleton.bootstrap.autoconfigure.httpclient`) | This repository's composition root owns wiring and canonical activation; an adapter leaf must not auto-configure itself. |
|
||||
| `httpclient-testkit` | `:adapter:outbound:httpclient` `src/test/java/**/testkit` | The design forbids production modules depending on the testkit; a test source set gives the same guarantee without a new Gradle project. |
|
||||
|
||||
The package boundary is enforced by ArchUnit rules (`PublicApiArchitectureTest`,
|
||||
`HttpClientModuleBoundaryTest`) that reproduce the design's module dependency table.
|
||||
|
||||
## 2. Package mapping
|
||||
|
||||
Root package: `io.backend.skeleton.httpclient` → `dev.caskeleton.adapter.outbound.httpclient`.
|
||||
|
||||
| Design module | Design package | Repository package |
|
||||
|---|---|---|
|
||||
| `httpclient-core-api` | `…httpclient.api` (+ `.body`, `.error`, `.operation`, `.result`) | `dev.caskeleton.adapter.outbound.httpclient.api` (+ same subpackages) |
|
||||
| `httpclient-profile` | `…httpclient.profile` | `…outbound.httpclient.profile` |
|
||||
| `httpclient-transport-spi` | `…httpclient.transport` | `…outbound.httpclient.transport` |
|
||||
| `httpclient-transport-apache` | `…httpclient.apache` | `…outbound.httpclient.apache` |
|
||||
| `httpclient-transport-jdk` | `…httpclient.jdk` | `…outbound.httpclient.jdk` |
|
||||
| `httpclient-restclient` | `…httpclient.restclient` | `…outbound.httpclient.restclient` |
|
||||
| `httpclient-resilience` | `…httpclient.resilience` | `…outbound.httpclient.resilience` |
|
||||
| `httpclient-auth` | `…httpclient.auth` | `…outbound.httpclient.auth` |
|
||||
| `httpclient-security` | `…httpclient.security` | `…outbound.httpclient.security` |
|
||||
| `httpclient-observability` | `…httpclient.observation` | `…outbound.httpclient.observation` |
|
||||
| `httpclient-transport-reactor-netty` | `…httpclient.reactor` | `…outbound.httpclient.reactor` |
|
||||
| `httpclient-webclient` | `…httpclient.webclient` | `…outbound.httpclient.webclient` |
|
||||
| `httpclient-service-client` | `…httpclient.service` | `…outbound.httpclient.service` |
|
||||
| `httpclient-dynamic-target` | `…httpclient.dynamic` | `…outbound.httpclient.dynamic` |
|
||||
| `httpclient-resttemplate-migration` | `…httpclient.migration` | `…outbound.httpclient.migration` |
|
||||
| `httpclient-spring7-service-groups` | `…httpclient.spring7` | `…outbound.httpclient.spring7` |
|
||||
| `httpclient-jetty-http3-experimental` | `…httpclient.http3` | `…outbound.httpclient.http3` |
|
||||
| `httpclient-spring-boot-starter` | `…httpclient.autoconfigure` | `dev.caskeleton.bootstrap.autoconfigure.httpclient` |
|
||||
| `httpclient-testkit` | `…httpclient.testkit` | `…outbound.httpclient.testkit` (test source set) |
|
||||
|
||||
## 3. Other deliberate substitutions
|
||||
|
||||
| Design assumption | Repository reality | Adaptation |
|
||||
|---|---|---|
|
||||
| Gradle Kotlin DSL, `build-logic` convention plugin | Groovy DSL, root `build.gradle` conventions, `LockMode.STRICT` dependency locking | Dependencies declared in `src/adapter/outbound/httpclient/build.gradle`; `gradle.lockfile` regenerated. |
|
||||
| Spring Framework 6.2 baseline with 7.0 compatibility | Spring Boot 4.0.0 / Spring Framework 7.0 is the repository baseline | Common code targets the Spring 6.2 **API surface** (no 6.2-only or 7.0-only classes in common packages). The Spring 7 HTTP Service Group integration stays isolated in `…httpclient.spring7`, exactly as the design requires. |
|
||||
| `settings.gradle.kts` module registration | Fail-closed registry | No registry change; leaf identity, gradle path, allowed dependencies unchanged. |
|
||||
| Design §6.2 grades Apache HttpClient 5 as HTTP/2-capable | Spring's blocking factory drives Apache's **classic** client, which is HTTP/1.1 only; HTTP/2 lives in Apache's async client | `ApacheBlockingTransportProvider` declares HTTP/1.1 and rejects an HTTP/2 profile at startup. Blocking HTTP/2 is served by the JDK transport, measured by `NegotiatedProtocolContractTest`. |
|
||||
| Design §28.1 names WireMock for stateful fixtures | WireMock's Jetty modules bind a different Jetty 12 ABI than the Boot-managed one this module already needs for HTTP/3, and fail at server start | `StatefulUpstream` provides path-keyed stateful responses on the existing fixture server; the WireMock dependency was removed rather than worked around with a shaded jar |
|
||||
| Per-task `git commit` | `AGENTS.md`: commit policy is `human-only` | Implementation is delivered unstaged; commits are the human's action. This is the only plan step intentionally not executed, and it is recorded here. |
|
||||
| `docs/httpclient/**`, `.github/workflows/httpclient-*.yml`, `scripts/verify-httpclient-docs.py` | Repository already owns `docs/` and `.github/workflows/` | Created at the same repository-relative paths. |
|
||||
|
||||
## 4. What is unchanged from the design
|
||||
|
||||
- H1 / H2 / H3 / H4 exposure rules and the forbidden native-engine signatures.
|
||||
- `ExecutionEvidence`, `BodyReplayability`, `OperationIdempotency`, `AttemptStage`, `FailureCategory`.
|
||||
- `HttpOperation`, `HttpCallResult`, `BodySource`, `ResponseType`, `BlockingStreamingResponse`.
|
||||
- The complete stable exception hierarchy and `HttpFailureMetadata` redaction rules.
|
||||
- Named Client Profile schema, startup validation codes, and operation override direction.
|
||||
- Effective deadline formula, attempt budget, and streaming setup/idle split.
|
||||
- Retry eligibility inputs, the ordered decision table, retry budget, and backoff rules.
|
||||
- Circuit → Rate Limiter → Bulkhead attempt order and logical admission placement.
|
||||
- OAuth2 cache key, single-flight refresh, and the 401 replay-at-most-once rule.
|
||||
- TLS allow/forbid lists and permanent-failure classification.
|
||||
- Dynamic Target canonicalization → all-answer DNS validation → pinning → redirect revalidation.
|
||||
- Low-cardinality tag allowlist, forbidden labels, trace and logging rules.
|
||||
- Runtime generation swap and drain semantics.
|
||||
Reference in New Issue
Block a user