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>
7.3 KiB
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
HttpFailureMetadataredaction 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.