Files
tech-log-backend/docs/httpclient/migration-guide.md
T
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

65 lines
2.7 KiB
Markdown

# 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
```java
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
```java
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
```java
@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.