Files
DongHyeonkaandClaude Opus 5 0cd959a494 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>
2026-08-11 16:49:31 +09:00

2.7 KiB

Migrating from RestTemplate

RestTemplate is permitted only inside …httpclient.migration; RestTemplateBoundaryTest enforces that. New retry, Dynamic Target, and HTTP/3 capabilities are deliberately unreachable from the migration path — a caller that wants them moves to a Named Client Profile.

1. Audit before changing anything

RestTemplateInventory inventory = new RestTemplateInventoryScanner().scan(existingTemplate);

The inventory reports the request factory, message converters, interceptors, error handler, and URI template handler, plus findings:

Code Severity Meaning
SIMPLE_REQUEST_FACTORY blocking no connection pool; unsupported in production
NO_MESSAGE_CONVERTERS blocking the template cannot encode or decode a body
NO_INTERCEPTORS warning confirm where correlation and timeouts are applied
TIMEOUTS_NOT_INTROSPECTABLE informational declare timeouts explicitly on the target profile

2. Bridge without changing behaviour

RestClient client = new RestTemplateToRestClientAdapter().adaptChecked(existingTemplate);

adaptChecked refuses to migrate a template with a blocking finding. The bridge carries the existing converters, interceptors, error handler, and URI handler across, so this step changes the API and nothing else.

3. Move to a Named Client Profile

Turn the platform on with APP_HTTPCLIENT_ENABLED=true — it ships off, and while it is off none of the settings below are bound — then declare the upstream as app.httpclient.clients[N] with its name and an explicit base URL, transport, timeouts, pool, body limits, authentication, retry policy, redirect policy, and TLS profile. Startup validation will tell you exactly which of those is missing. See docs/httpclient/configuration-reference.md for the environment form.

4. Move to a typed client

@HttpClientProfile("payment")
@HttpExchange("/payments")
public interface PaymentClient {

  @PostExchange
  @HttpOperationPolicy(
      name = "create-payment",
      idempotency = OperationIdempotency.IDEMPOTENCY_KEY_REQUIRED,
      retryPolicy = "payment-write")
  PaymentResponse create(
      @RequestHeader("Idempotency-Key") String idempotencyKey, @RequestBody PaymentRequest request);
}

The interface fails startup validation unless it declares a profile, gives every method a stable operation name and an explicit idempotency, supplies a key parameter when the operation requires one, keeps a single execution model, and does not enable retry on a non-idempotent write.

5. Retire the template

Once no production package references RestTemplate, RestTemplateBoundaryTest keeps it that way.