Files
tech-log-backend/docs/superpowers/specs/2026-07-27-httpclient-production-capability-design.md
T

7031 lines
281 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# HTTP Client Production Capability Deep Design
- 작성일: 2026-07-27
- 상태: 상세 설계 완료, Phase 0/1 기반 및 legacy deadline R1 구현, R2 미구현
- 기준: Java 21, Spring Boot 4.0.0, Gradle 멀티모듈 Clean Architecture
- 대상 leaf: `adapter-outbound-httpclient`
- 구현 추적: typed operation/target foundation, legacy JDK 안전 결함과 active logical deadline
단면까지 적용되었다.
- 상위 문서:
[Production Capability Platform Design](2026-07-26-production-capability-platform-design.md)
## 0. 구현 상태
2026-07-28 기준 구현된 범위:
- `application-core`의 framework-free, monotonic, parent-capped `CallBudget`;
- bounded `HttpDestinationId`, versioned `HttpOperationId`;
- method/route/semantics/request-response mode/success status/retry/physical-attempt/body 상한을
고정하는 immutable `HttpOperationDescriptor`와 closed catalog;
- fixed base authority와 registered relative route/path-segment만 조합하는 target builder;
- absolute/scheme-relative/traversal/pre-encoded target, base URI user-info/query/fragment 거부;
- legacy JDK client redirect `NEVER` 명시;
- legacy streaming의 status-first 검증과 error body callback 차단;
- retry 바깥에 physical-attempt circuit breaker를 두어 각 wire attempt를 독립 집계하는 수정.
- caller/configured monotonic `CallBudget` intersection을 buffered/streaming 실행 경로에 연결;
- blocking logical call과 retry/backoff를 cancellable virtual thread에서 실행하고 cutoff 시
interrupt/`DEPENDENCY_TIMEOUT` 처리;
- client별 bounded live-worker admission, non-cooperative worker의 slot retention, shutdown 시
active task cancellation과 신규 admission 차단;
- worker MDC 복사/정리와 retry ThreadLocal lifecycle 정렬.
아직 구현되지 않은 범위:
- application feature-specific production port와 실제 upstream anti-corruption adapter;
- canonical binding/expected-state/full profile tuple/card registry와 zero-binding resource 0 계약;
- Apache HC5 pool/acquire/lifetime/idle provider;
- Apache engine phase별 deadline 전달, wire hard cancellation과 connection quarantine;
- DNS/address/SSRF/TLS/mTLS/proxy/auth/secret lifecycle;
- wire/decoded/error/streaming body 전체 제한과 codec/media contract;
- OTel 단독 propagation과 manual trace interceptor 제거;
- real-network/TLS/pool/failure qualification 및 R2 readiness card.
따라서 현재 `OutboundHttpClient`는 migration용 JDK R1 이하 facade이며 HTTP capability R2가 아니다.
legacy 실행 경로는 active logical deadline을 사용하지만 operation catalog와 engine phase
deadline을 아직 사용하지 않는다. 이 단면만으로 hard cancellation이나 R2를 주장하지 않는다.
## 1. 설계 판정
현재 HTTP Client 모듈은 timeout, response-size cap, retry, circuit breaker, shutdown guard,
진단 로깅을 가진 유용한 골격이다. 그러나 운영에서 필요한 보장을 실제로 제공하는
production capability는 아니다.
현재 가장 큰 문제는 기능의 수가 아니라 보장의 정확성이다.
1. `globalCallTimeout`은 실제 실행을 중단시키는 total deadline이 아니다.
2. README가 설명한 retry/circuit-breaker 순서와 코드의 실제 합성 순서가 반대다.
3. streaming 경로는 HTTP status를 검사하지 않고 4xx/5xx body도 reader에 전달한다.
4. method만으로 retry 안전성을 판단하며 body replayability와 upstream idempotency 계약이 없다.
5. JDK client의 pool, DNS, TLS, redirect, proxy, cancellation과 lifecycle이 운영 계약에 없다.
6. 모든 dependency가 하나의 global settings를 공유하며 실제 destination registry가 없다.
7. sample의 feature-specific port와 fixture consumer는 있지만, production semantic port 구현과
실제 HTTP operation/binding이 없다.
8. manual trace header가 실제 tracer의 sampling 결정을 훼손하고 tenant baggage를 모든 목적지로
보낼 수 있다.
9. arbitrary URI, header, credential, redirect와 DNS rebinding에 대한 egress 보안 경계가 없다.
10. unknown mutation outcome, partial body, decode failure, pool timeout과 cancellation이
`4xx/5xx/connect/timeout` 몇 개로 합쳐진다.
이번 설계의 목표는 범용 `execute(method, url, body)` SDK가 아니다.
> feature-specific application port 뒤에서, 등록된 destination과 operation만 호출하고,
> 하나의 monotonic deadline 안에서 admission·pool·DNS·connect·TLS·write·response·body·retry를
> 제한하며, retry safety·unknown outcome·보안·관측성·취소를 실행 가능한 계약으로 증명하는
> outbound HTTP capability
선택한 핵심 구조는 다음과 같다.
1. Application은 `OutboundHttpClient`가 아니라 `FraudScreeningPort`,
`PartnerCatalogPort` 같은 feature-specific anti-corruption port에 의존한다.
2. HTTP method, path template, success status, media type, retry safety와 body replayability는
adapter의 typed operation catalog에 등록한다.
3. 호출자는 arbitrary absolute URL이나 raw credential header를 전달하지 않는다.
4. destination binding과 provider activation은 canonical configuration 하나로 결정한다.
5. 하나의 total deadline은 모든 대기와 physical attempt를 포함하며 timeout 때 engine call과
response stream을 적극 취소한다.
6. retry는 method 이름만이 아니라 operation semantics, request identity, replayable body,
failure phase, remaining budget를 모두 만족할 때만 가능하다.
7. mutation이 전송된 뒤 응답을 잃으면 일반 transient failure가 아니라
`INDETERMINATE`로 반환하고 provider-specific inspect/reconcile을 요구한다.
8. circuit breaker는 기본적으로 physical attempt를 집계하고, 논리 호출 집계는 별도 이름의
선택 정책으로만 허용한다.
9. Apache HttpComponents 5 classic + blocking facade를 초기 R2 reference candidate로 삼고,
hard-cancel evidence를 통과할 때만 baseline provider로 승격한다. JDK/HTTP2/reactive engine도
같은 보장을 통과할 때만 R2로 승격한다.
10. OTel instrumentation이 trace propagation을 단독 소유하며 manual MDC `traceparent`
writer는 제거한다.
11. no binding이면 client, pool, evictor, credential refresh, DNS probe가 하나도 생성되지 않는다.
12. capability별 readiness card가 실제 선택된 operation profile과 engine evidence를 검증한다.
## 2. 상위 통합 설계와 이번 심화 범위
상위 설계의 HTTP client 절은 다음 방향을 이미 정했다.
- named client registry와 dependency별 설정;
- pool total/per-route/acquisition/idle/DNS/lifecycle;
- bulkhead와 optional outbound rate limit;
- declared-safe operation만 retry;
- total deadline과 active cancellation;
- redirect와 SSRF 방어;
- TLS, mTLS, proxy, certificate rotation;
- request/response bounds;
- upload/download streaming;
- OTel trace ownership;
- failure injection과 pool exhaustion test;
- application의 feature-specific anti-corruption port.
이번 문서는 위 방향을 구현 계획으로 변환할 수 있도록 다음을 확정한다.
- application port와 adapter-internal kernel의 정확한 경계;
- destination, operation, attempt, logical call의 identity;
- request/response/body 타입과 lifecycle;
- absolute deadline과 phase budget;
- retry, circuit breaker, bulkhead, rate limiter의 정확한 실행 순서;
- safe/idempotent/replayable/unknown outcome의 차이;
- status별 default와 operation override;
- pool과 HTTP/1.1·HTTP/2 capacity model;
- DNS refresh, IP allowlist, rebinding, redirect와 proxy 검증;
- TLS trust, hostname verification, mTLS와 credential rotation;
- buffered, streaming upload/download, compression과 decode limit;
- error taxonomy와 application mapping;
- metrics, span, log, health와 cardinality;
- typed activation/configuration과 zero-side-effect 비활성 상태;
- provider engine 선택과 Gradle dependency ownership;
- real-network, TLS, DNS, proxy, failure-injection test;
- capability별 R2 readiness card와 CI aggregate;
- 기존 `OutboundHttpClient`에서의 단계적 migration.
## 3. 증거 기반 현재 상태
### 3.1 실제 HTTP capability binding이 없다
현재 `adapter-outbound-httpclient``shared-contract`
`adapter-outbound-support`에만 project dependency를 둔다. Registry는 application/domain edge를
허용하지만 production code는 application의 어떤 semantic port도 구현하지 않는다.
`OutboundHttpClient`는 adapter package의 기술 타입이며 기본 bean도 없다. README는 fork가
dependency별 configuration에서 `baseline(...)`을 직접 호출하도록 안내한다. repository 전체에서
이를 호출하는 production consumer는 없다.
다만 “binding 0개”가 HTTP 관련 bean 0개라는 뜻은 아니다. Component scan이 이 configuration을
읽으면 `OutboundHttpSettings`, shutdown guard, `RestClient`/builder 차단 BeanPostProcessor,
error mapper, logger와 retry policy 같은 global infrastructure bean은 생성된다. Named
client/semantic-port binding은 없는데 required global timeout 설정과 전역 부작용은 존재하는
비대칭 상태다.
다만 sample에는 이미 다음 seam이 있다.
- `RepoStatsPort`;
- `GetRepoStatsUseCase`;
- `RepoStatsPortClient` stub;
- `/worklogs/repoStats` endpoint.
이는 application port와 consumer skeleton이 없다는 뜻이 아니다. `RepoStatsPortClient`
`fetchRaw()`가 고정 fixture를 반환하므로 실제 HTTP capability를 소비하지 않는다는 뜻이다.
현재 sample registry edge에도 `sample-portfolio -> adapter-outbound-httpclient`가 없다.
따라서 정확한 현재 상태는 다음과 같다.
```text
HTTP leaf:
settings + generic technical wrapper + tests
sample:
feature-specific port + use case + fixture adapter
but:
no real HTTP operation/binding between them
!=
feature-specific application port + upstream adapter + runtime binding
```
현재 capability level은 R0 seam과 일부 R1 local kernel 사이이며 R2가 아니다.
### 3.2 total deadline이 실행을 제한하지 않는다
`OutboundHttpClient.exchange()`는 다음 deadline을 계산한다.
```java
Instant deadline = Instant.now().plus(settings.globalCallTimeout());
retryPolicy.beginCall(method, deadline);
```
그러나 이 값은 `OutboundRetryPolicy.shouldRetry()`가 다음 retry를 시작할지 확인할 때만 사용한다.
현재 attempt를 중단하지 않고 다음도 포함하지 않는다.
- connection/pool wait;
- DNS;
- connect와 TLS handshake;
- request body write;
- response header wait;
- response body read;
- streaming reader callback;
- retry backoff 자체;
- circuit-breaker/bulkhead wait.
첫 attempt가 `globalCallTimeout`보다 오래 걸려도 read timeout 전까지 계속 실행된다. Retry
predicate가 deadline 직전 true를 반환하면 남은 시간보다 긴 새 attempt도 시작할 수 있다.
`stream()`은 이 deadline을 검사만 하는 수준도 아니며 absolute deadline/retry context/cancel
handle을 아예 만들거나 전달하지 않는다. 그러므로 application.yml의 “whole call including
retries” 주석과 runtime behavior가 일치하지 않는다.
### 3.3 retry와 circuit breaker 합성 설명이 코드와 반대다
현재 코드는 다음 순서로 decorator를 만든다.
```java
Supplier<T> decorated = countingSupplier;
decorated = Retry.decorateSupplier(retry, decorated);
decorated = CircuitBreaker.decorateSupplier(cb, decorated);
```
실행 구조는 다음과 같다.
```text
CircuitBreaker(
Retry(
physical attempt
)
)
```
따라서 circuit breaker는 retry가 끝난 논리 호출 하나를 집계한다. README와 코드 주석은
“retry가 circuit breaker 바깥이고 각 attempt가 독립 집계된다”고 설명한다. 원하는 physical
attempt 집계는 다음 구조여야 한다.
```text
Retry loop(
CircuitBreaker(
one physical attempt
)
)
```
현재 test는 retry hit count와 meter 존재를 확인하지만 circuit-breaker sliding window가 physical
attempt를 몇 건 집계했는지는 검증하지 않는다.
### 3.4 streaming은 status error를 성공 body처럼 전달한다
buffered 경로는 `retrieve()`를 사용해 default 4xx/5xx handler가 예외를 발생시킨다. Streaming
경로는 `exchange(...)` callback을 사용하면서 status를 확인하지 않는다.
Spring Framework는 `RestClient.exchange()`에서 status handler가 자동 적용되지 않는다고
명시한다. Callback이 status를 직접 처리해야 한다.
현재 구현:
```java
streamingClient
.method(method)
.uri(uri)
.exchange((req, res) -> reader.apply(res.getBody()));
```
그 결과 401, 429, 500도 body reader가 정상 결과로 만들 수 있고
`observer.recordSuccess(...)`가 호출된다.
### 3.5 streaming이 size limit의 무제한 우회 경로다
현재 buffered response는 `Content-Length`와 counting stream으로 10 MB 기본 한계를 확인한다.
그러나 streaming 경로에는 다음 제한이 없다.
- maximum wire bytes;
- maximum decoded bytes;
- logical total deadline across retries/backoff;
- 별도의 no-progress body idle deadline;
- content type와 content encoding;
- record/item count;
- reader output size;
- compression expansion ratio.
현재 `JdkClientHttpRequestFactory#setReadTimeout`은 selected Spring Framework 7 구현에서
`sendAsync` completion에 timer를 걸고 timeout 때 future/response body를 cancel/close하는
attempt elapsed timeout에 가깝다. 이를 socket byte-idle timeout이나 logical-call total
deadline이라고 부르지 않는다. Non-cooperative callback의 CPU loop도 제한하지 못한다.
README와 test는 buffered limit을 넘으면 streaming API를 사용하면 된다고 설명하고, test는
oversized response 전체를 성공적으로 읽는 것을 기대한다. Streaming은 heap materialization을
피하는 방법이지 무제한 데이터 허용 권한이 아니다.
### 3.6 request 계약이 너무 넓고 동시에 부족하다
현재 API:
```java
<T> T exchange(HttpMethod method, String uri, Object requestBody, Class<T> responseType)
<T> T stream(HttpMethod method, String uri, Function<InputStream, T> reader)
```
문제:
- raw Spring `HttpMethod`와 transport `InputStream`이 public adapter API에 노출된다.
- `String uri`가 relative path인지 absolute URI인지 강제하지 않는다.
- path/query encoding과 template variable allowlist가 없다.
- caller가 operation ID나 low-cardinality route template을 제공하지 않는다.
- request header, `Accept`, `Content-Type`, API version, conditional header를 표현하지 못한다.
- authentication profile이나 credential ownership이 없다.
- status별 success/domain outcome mapping이 없다.
- `Class<T>`는 generic collection과 versioned envelope를 충분히 표현하지 못한다.
- request body byte limit과 replayability가 없다.
- operation-specific deadline과 resilience policy가 없다.
이 API를 application에 그대로 노출하면 Clean Architecture의 anti-corruption boundary가 사라진다.
### 3.7 retry safety가 method set 하나에 묶여 있다
현재 retry allowlist는 `GET`, `HEAD`, `PUT`, `DELETE`다. `POST`, `PATCH`는 항상 거부한다.
HTTP method의 idempotency는 중요한 출발점이지만 충분하지 않다.
- upstream이 PUT/DELETE를 문서와 다르게 구현할 수 있다.
- request body stream이 다시 열리지 않을 수 있다.
- idempotent effect여도 각 response는 다를 수 있다.
- conditional request가 precondition 없이 재실행될 수 있다.
- POST/PATCH도 upstream이 durable idempotency-key와 replay contract를 제공하면 안전할 수 있다.
- connect reset이 request 전송 전인지 후인지 모르면 mutation outcome은 불명확하다.
RFC 9110도 non-idempotent request 자동 retry는 operation semantics를 실제로 알거나 최초 요청이
적용되지 않았음을 아는 경우가 아니면 하지 말라고 요구한다.
### 3.8 모든 4xx와 많은 transport error가 뭉친다
현재 error mapper는:
- 400~499 전체를 `DEPENDENCY_4XX_CLIENT`;
- 500 이상 전체를 `DEPENDENCY_5XX_SERVER`;
- 알 수 없는 failure를 `DEPENDENCY_CONNECT_FAILED`;
- shutdown reject를 `DEPENDENCY_CIRCUIT_OPEN`;
- connect timeout을 `DEPENDENCY_CONNECT_FAILED`;
- 나머지 timeout을 `DEPENDENCY_TIMEOUT`;
으로 분류한다.
이 분류로는 다음을 결정할 수 없다.
- 401 credential refresh 후 정확히 한 번 replay 가능한가;
- 404가 정상적인 absence인가 API drift인가;
- 408/425/429의 retry 조건은 무엇인가;
- 409/412가 domain conflict/precondition outcome인가;
- 413이 request contract 위반인가;
- 415/406이 media negotiation drift인가;
- 422가 permanent validation인가 idempotency fingerprint mismatch인가;
- 502/503/504와 일반 500의 retry 차이는 무엇인가;
- TLS trust, hostname, certificate expiry, proxy auth failure는 무엇인가;
- pool acquire와 local admission reject를 구분할 수 있는가;
- response가 일부 도착한 뒤 잘렸는가;
- mutation이 적용됐는지 모르는가.
### 3.9 JDK client의 운영 자원 정책이 없다
현재 factory는 destination마다 다음 client를 만든다.
```java
HttpClient.newBuilder()
.connectTimeout(settings.connectTimeout())
.build();
```
명시하지 않은 항목:
- executor;
- HTTP version;
- redirect;
- proxy;
- authenticator/cookie handler;
- SSL context/parameters;
- connection capacity와 per-route isolation;
- acquisition timeout;
- connection TTL/idle validation;
- DNS resolver/TTL/address selection;
- shutdown/close ownership;
- pool metrics.
Java 21 JDK client에는 builder-level total/per-route pool과 connection lease timeout을 구성하는
API가 없다. `jdk.httpclient.connectionPoolSize`는 HTTP/1.1 keep-alive cache의 구현 property이며
application-level admission이나 per-destination active-call bound를 대체하지 않는다.
### 3.10 global settings가 destination 정책을 표현하지 못한다
`app.outbound.http.*` 하나가 모든 dependency에 적용된다.
실제 운영에서는 다음이 destination마다 다르다.
- base URI와 allowed address;
- API version;
- required/optional readiness impact;
- timeout/SLO;
- pool capacity;
- protocol version;
- TLS trust and mTLS identity;
- authentication;
- proxy;
- request/response size;
- retryable statuses와 idempotency contract;
- circuit-breaker threshold;
- rate quota;
- trace/baggage/privacy policy.
현재 required global timeout placeholder는 실제 client binding이 0개여도 bootstrap 설정을
요구한다. 반대로 client가 여러 개여도 서로 다른 정책을 지정할 수 없다.
### 3.11 manual trace propagation은 fork landmine이 아니라 현재 결함이다
`TraceContextPropagationInterceptor`는 MDC의 `trace_id``span_id``traceparent`를 만들고
sampled flag를 항상 `00`으로 기록한다. 실제 tracer가 있어도 먼저 설정된 header를 보존하도록
구성되면 downstream sampling decision을 끊을 수 있다.
추가로 `request_id``tenant_id``baggage`로 모든 destination에 전파한다. Tenant ID가 내부
service boundary에서는 필요할 수 있어도 외부 partner에 보내도 된다는 뜻은 아니다. Destination별
data-sharing policy 없는 global allowlist는 privacy boundary가 아니다.
W3C `traceparent`/`tracestate` mutation과 sampling flag는 tracer propagator가 소유해야 한다.
MDC reconstruction은 fallback으로도 기본 활성화하지 않는다.
### 3.12 observability가 logical call과 attempt를 구분하지 않는다
현재 logger는 dependency, outcome, total duration, retry count를 남기지만:
- operation ID가 없다.
- HTTP status class가 없다.
- pool/DNS/connect/TLS/write/header/body phase가 없다.
- physical attempt별 span과 duration이 없다.
- cancellation과 indeterminate outcome이 없다.
- request/response bytes가 없다.
- actual HTTP client duration meter가 없다.
- pool leased/available/pending meter가 없다.
Resilience4j metric filter는 shared `MeterRegistry`에 전역으로 설치되고
`resilience4j.*` 중 세 meter 외 전부 deny한다. 동일 registry를 사용하는 다른 capability의
Resilience4j meter까지 차단할 수 있다. 현재 singleton configuration에서는 한 번 설치되지만,
다중 ApplicationContext 또는 fork가 config를 수동 구성하면 같은 global filter가 반복 설치될 수
있다.
### 3.13 lifecycle guard가 drain/cancel/close를 수행하지 않는다
`OutboundHttpShutdownGuard.stop()`은 boolean을 변경할 뿐:
- in-flight call count;
- graceful drain deadline;
- pending retry/backoff cancellation;
- response stream close;
- HTTP engine close;
- idle evictor stop;
- credential refresh scheduler stop;
- forced cancellation;
을 수행하지 않는다.
새 호출을 reject하는 것은 필요하지만 resource lifecycle 전체는 아니다. Shutdown을
`DEPENDENCY_CIRCUIT_OPEN`으로 재사용하는 것도 잘못된 operational diagnosis다.
### 3.14 현재 test가 증명하는 범위
2026-07-27 baseline:
```text
./gradlew :adapter:outbound:httpclient:check --rerun-tasks --console=plain
BUILD SUCCESSFUL
```
현재 unit/local test는 다음을 증명한다.
- settings의 일부 numeric validation;
- local JDK HttpServer에 대한 200/4xx/5xx/read-timeout mapping;
- GET retry hit count와 POST no-retry;
- circuit-open short-circuit;
- response `Content-Length`/chunked counting;
- manual MDC header propagation;
- basic logs와 Resilience4j meters;
- shutdown boolean gating.
증명하지 않는 항목:
- real total deadline와 hard cancellation;
- CB physical-attempt count;
- streaming 4xx/5xx rejection;
- connection pool saturation/acquisition/idle/TTL;
- TLS/mTLS/hostname/certificate rotation;
- DNS TTL/rebinding/address failover;
- redirect/proxy/SSRF;
- compressed body expansion;
- streaming cancellation/partial output;
- mutation unknown outcome와 idempotency replay;
- OAuth token refresh;
- HTTP/2 multiplexing/GOAWAY;
- process shutdown drain;
- multi-destination isolation;
- real observability instrumentation.
현재 compile에는 `ThreadLocalUsage`, locale 없는 `toUpperCase`, test default charset 등 기존
ErrorProne warning이 있지만 focused check는 성공한다.
### 3.15 Retry wiring이 object identity에 의존한다
`OutboundHttpClient.baseline(...)``OutboundHttpResilience``OutboundRetryPolicy`를 별도
parameter로 받는다. Client는 전달받은 policy에 ThreadLocal call context를 기록하지만,
Resilience4j retry predicate는 `OutboundHttpResilience`를 만들 때 캡처한 policy를 호출한다.
두 policy instance가 다르면 predicate는 context가 없다고 판단해 retry를 조용히 비활성화한다.
현재 test도 이를 critical wiring constraint로 설명한다. Type system, constructor, composition
validation은 동일 instance를 강제하지 않는다.
R2 state machine은 ThreadLocal/object-identity coupling을 제거한다.
- immutable attempt context를 retry decision에 명시적으로 전달;
- retry loop와 decision policy를 한 aggregate가 소유;
- 동일 instance를 수동으로 맞춰야 하는 factory signature 제거;
- composition mismatch characterization test;
- virtual-thread/async boundary에서도 context loss 없음.
### 3.16 Response-size violation이 retry와 diagnosis를 깨뜨린다
`ResponseSizeBoundingInterceptor`가 size exception을 발생시키지만 현재 error mapper는 이를
stable contract failure로 알지 못한다. Unknown runtime failure fallback이
`DEPENDENCY_CONNECT_FAILED`이며 retryable이므로 retry-enabled safe method는 같은 oversized
response를 반복 다운로드할 수 있다.
추가 영향:
- 최종 size exception이 observer failure path 밖에서 발생해 structured failure log가 빠질 수 있음;
- 큰 4xx/5xx body가 status mapping 전에 size exception으로 바뀌어 원래 status를 잃을 수 있음;
- 현재 size tests는 retry disabled 상태만 검증.
R2는 status/header를 먼저 분기하고 wire/decoded bound failure를
`RESPONSE_TOO_LARGE`/`DECOMPRESSION_LIMIT_EXCEEDED`로 매핑한다. 둘은 default no-retry,
circuit-breaker ignore이며 logical failure observation을 정확히 한 번 남긴다.
## 4. 범위와 명시적 비범위
### 4.1 전체 production capability 설계 범위
- synchronous/virtual-thread-friendly request-response capability;
- JSON과 bounded binary buffered response;
- bounded streaming download callback;
- bounded/reopenable streaming upload callback;
- named destination와 typed operation catalog;
- relative path template와 typed query/header mapping;
- per-destination Apache HttpComponents classic engine;
- explicit HTTP/1.1 connection pool;
- monotonic total deadline와 active cancellation;
- retry, retry budget, physical-attempt circuit breaker;
- logical admission과 attempt bulkhead;
- optional local outbound quota limiter;
- safe/idempotent/idempotency-key/non-retryable operation;
- unknown mutation outcome와 reconciliation hook;
- redirect disabled default와 future-card용 exact allowlisted state-machine;
- DNS/address/SSRF validation;
- HTTPS/TLS hostname verification, custom trust와 mTLS;
- static bearer/API key/OAuth2 client credentials seams; HTTP Basic은 future-card-only;
- proxy allowlist와 proxy-auth profile;
- content type/encoding/header/body bounds;
- OTel/Micrometer observation ownership;
- typed configuration, activation, lifecycle, health;
- real TCP/TLS/DNS/proxy/failure integration tests;
- readiness card와 runbook.
위 목록은 이 문서가 설계하는 전체 surface이지 한 번에 R2로 승인되는 minimum profile이 아니다.
R2는 derived selected card와 exact full effective profile tuple 범위에서만 주장한다.
### 4.2 Minimum R2 static baseline
최소 ACTIVE R2 profile은 `httpclient-static-buffered` 하나와 그 exact compatibility profile만
선택하고 다음으로 제한한다.
- fixed registered destination + relative route;
- synchronous H1;
- bodiless 또는 bounded JSON buffered request/response;
- `SAFE_READ`;
- server TLS/hostname verification;
- auth `none`;
- direct-only, redirect/cookie/engine-hidden-retry disabled;
- finite admission/pool/body/deadline와 hard cancellation;
- safe-read retry budget와 physical-attempt circuit breaker;
- OTel sanitizer, lifecycle, readiness와 runbook.
다음은 minimum profile 밖이다.
- `IDEMPOTENT_MUTATION`/`KEYED_MUTATION` -> `httpclient-idempotent-mutation`;
- `NON_RETRYABLE_MUTATION` -> `httpclient-non-retryable-mutation`;
- streaming download/upload -> 각각의 streaming card;
- mTLS, OAuth2 client credentials, required proxy, HTTP/2, untrusted fetch -> 각각의 card.
Bounded binary, custom server trust, API key와 static bearer는 새 card ID를 만들지 않고
`httpclient-static-buffered`의 conditional effective mode로 지원할 수 있다. 단 profile
compatibility registry에 provider/protocol/request·response body/TLS/auth/proxy/redirect/
operation-semantics 전체 tuple이 exact entry로 존재하고 mode-specific 및 cross-mode
HSEC/HRES/HOBS/HCMP scenario가 모두 포함된 경우에만 활성화한다. 기본 card 시나리오나 축별
지원 합집합만으로 이를 지원했다고 간주하지 않는다.
Redirect-follow, request-signature와 HTTP Basic은 현재 canonical card set에서 지원하지 않는다.
관련 state-machine 설계는 future card를 위한 것이며, mode가 설정되면 card 등록 전에는
startup/readiness가 실패한다. “disabled by default”는 켤 수 있다는 의미가 아니다.
### 4.3 별도 optional capability로 열어둘 항목
- HTTP Service Interface proxy;
- conditional GET/ETag;
- RFC-compliant HTTP cache;
- HTTP/2 multiplexed engine;
- reactive `WebClient` engine;
- OAuth2 token exchange/JWT bearer;
- HTTP message signatures;
- resumable/range download;
- multipart/form-data upload;
- webhook callback registration;
- controlled dynamic public egress through an egress proxy;
- service discovery/load-balancer integration;
- client-side hedging for safe reads;
- response streaming to file/objectstorage workflow;
- provider-specific rate-limit header interpretation.
Optional 항목은 같은 raw API에 boolean을 추가하지 않는다. 요구 guarantee, dependency,
concurrency model이 다르면 별도 readiness card와 provider profile을 갖는다.
### 4.4 이번 범위에서 제외
- controller/inbound DTO와 HTTP server behavior;
- WebSocket, SSE long-lived subscription, gRPC;
- business saga/compensation;
- business-level batch orchestration;
- external API별 domain DTO와 mapping;
- arbitrary user URL fetcher;
- browser cookie/session emulation;
- transparent application-wide retry annotation/AOP;
- distributed transaction;
- service mesh/egress gateway 자체의 설치;
- synthetic benchmark 수치;
- HTTP/3 production baseline.
사용자가 제공한 URL을 fetch해야 하는 feature는 일반 destination client가 아니다. Egress proxy,
quarantine, content scan, strict public-address policy와 별도 threat model을 가진 capability로
설계한다.
## 5. HARD invariants
다음은 구현 편의를 위해 낮출 수 없다.
1. `domain-core``application-core`에 Spring HTTP, Apache/JDK client, Resilience4j,
Micrometer/OTel, URI transport 타입을 노출하지 않는다.
2. Application use case는 generic `OutboundHttpClient`
`execute(method, url, body)`에 의존하지 않는다.
3. Destination ID와 operation ID는 bounded registry 값이며 caller 입력으로 동적 생성하지 않는다.
4. Base URI, host, port, proxy, credential, TLS bundle은 application command에 포함하지 않는다.
5. Normal operation은 relative path template만 사용하며 absolute URI override를 거부한다.
6. Caller input을 raw path/query/header 문자열 연결에 사용하지 않는다.
7. `Authorization`, `Cookie`, proxy credential, API key를 inbound request에서 자동 전달하지 않는다.
8. Redirect는 default disabled다. Enabled hop마다 destination/IP/credential policy를 다시
검증한다.
9. HTTPS production destination에서 trust-all, hostname verification disable,
`NoopHostnameVerifier`를 허용하지 않는다.
10. Plain HTTP는 explicit local/test 또는 승인된 private-network exception 없이는 prod에서
거부한다.
11. 하나의 total deadline이 admission, pool wait, attempt, backoff와 body consumption 전체를
포함한다.
12. Deadline expiry는 flag만 기록하지 않고 transport task와 response body를 적극 취소/close한다.
13. Wall clock을 elapsed deadline 계산에 사용하지 않는다.
14. Method만으로 retry를 허용하지 않는다.
15. Request body를 재생할 수 없으면 automatic retry를 허용하지 않는다.
16. Non-idempotent mutation은 transmission 가능성이 생긴 뒤 upstream이 문서화한
idempotency/reconciliation 계약 없이 replay하지 않는다. Exact `NOT_SENT`가 증명된
pre-send restart는 replay가 아니며 별도 bounded restart policy/budget에서만 허용한다.
17. Mutation request가 전송된 뒤 response를 잃은 경우 ordinary timeout으로 축소하지 않는다.
18. `INDETERMINATE` mutation을 새 operation ID/key로 blind retry하지 않는다.
19. Underlying engine의 hidden automatic retry와 redirect는 끈다.
20. Circuit breaker metric이 physical attempt 정책이라고 문서화되면 실제 physical attempt를
개별 집계한다.
21. Retry backoff 중 physical-attempt bulkhead permit과 connection을 보유하지 않는다.
22. Virtual thread는 bulkhead나 connection pool을 무한대로 만들어도 된다는 근거가 아니다.
23. Pool wait와 network connect timeout을 같은 error로 합치지 않는다.
24. Streaming은 무제한 경로가 아니다.
25. Wire bytes와 decoded bytes의 limit을 구분한다.
26. Status와 headers를 검증하기 전에 response body를 consumer에 넘기지 않는다.
27. Response body/stream은 모든 success, failure, cancellation 경로에서 정확히 한 번 close한다.
28. Error response body도 작은 별도 cap 안에서만 선택적으로 decode하며 log에 남기지 않는다.
29. Content type, charset, content encoding과 JSON constraint mismatch를 ordinary 5xx로 숨기지
않는다.
30. Metric tag와 span name에 raw path, query, user ID, tenant ID, idempotency key, token,
payload를 사용하지 않는다.
31. Trace propagation은 실제 tracer/instrumentation 하나가 소유한다.
32. Baggage는 destination별 explicit allowlist가 없으면 전파하지 않는다.
33. Liveness가 외부 dependency 상태에 의존하지 않는다.
34. Readiness가 모든 pod를 동시에 eject해 outage를 증폭하지 않도록 dependency impact를
명시한다.
35. No binding이면 pool, thread/executor, evictor, DNS lookup, token refresh와 health probe가
생성되지 않는다.
36. Legacy global settings와 canonical binding이 함께 있거나 충돌하면 startup을 실패시킨다.
37. Engine/provider가 요구 보장을 증명하지 못하면 더 약한 guarantee로 조용히 downgrade하지
않는다.
38. Application-level fallback/compensation은 HTTP kernel 안에 넣지 않는다.
39. Adapter는 application/domain DTO를 wire format으로 그대로 serialize하지 않는다.
40. R2 label은 real-network, TLS, pool, cancellation, security failure evidence 없이 부여하지
않는다.
## 6. 대안 검토
### A. 현재 `OutboundHttpClient`에 옵션만 계속 추가
장점:
- 변경량이 작다.
- 기존 tests를 재사용하기 쉽다.
문제:
- raw method/URI/body/class API가 더 커진다.
- application semantic port와 transport kernel이 분리되지 않는다.
- destination/operation policy를 runtime argument로 넘기게 된다.
- replayability, unknown outcome, body lifecycle을 표현하기 어렵다.
- boolean 조합이 잘못된 상태를 만들 수 있다.
선택하지 않는다. 현재 API는 migration facade로만 유지한다.
### B. Application에 하나의 범용 `HttpPort`를 둔다
예:
```java
HttpResponse call(HttpRequest request);
```
이는 HTTP method, status, header, URI, JSON과 transport failure를 application에 유출한다.
Use case가 upstream protocol을 직접 알고 anti-corruption adapter가 사라진다. 선택하지 않는다.
### C. OpenFeign/HTTP Interface annotation을 application port에 직접 붙인다
선언적 API는 편리하지만 annotation과 wire DTO가 application boundary를 오염시킨다. Retry,
auth, status mapping이 proxy magic으로 숨을 수도 있다.
Spring HTTP Service Interface는 허용하되 adapter package의 upstream wire client로만 둔다.
Application port를 별도로 구현한다.
### D. JDK `HttpClient`를 R2 default로 계속 사용
장점:
- JDK 21 기본 제공;
- dependency가 적다;
- HTTP/2와 async cancellation API;
- immutable/thread-safe client.
문제:
- builder API로 per-destination total/per-route pool과 lease timeout을 구성하기 어렵다.
- pool resource와 pending lease observability가 제한된다.
- custom DNS/address admission과 connection lifecycle evidence가 어렵다.
- keep-alive pool 일부가 implementation property에 의존한다.
JDK engine은 R1 compatibility 또는 제한된 profile로 열어두되 초기 R2 default로 선택하지 않는다.
### E. Apache HttpComponents 5 classic + Spring `RestClient`
장점:
- Spring MVC/imperative baseline과 맞는다.
- total/per-route pool, lease timeout, TTL, idle validation, DNS resolver, TLS, proxy를 제어할 수
있다.
- hard cancellation과 connection eviction을 검증할 수 있다.
- broad reactive stack 없이 virtual-thread-friendly blocking facade를 제공할 수 있다.
단점:
- total deadline을 `RestClient` timeout 하나로 얻을 수 없다.
- active cancellation을 위한 execution wrapper와 engine-specific evidence가 필요하다.
- HTTP/2는 별도 provider 판단이 필요하다.
초기 minimum-R2 reference candidate로 선택하되 hard-cancel/card evidence 전에는 R2로 승격하지
않는다.
### F. `WebClient` + Reactor Netty를 모든 호출의 default로 사용
장점:
- cancellation/backpressure/HTTP2/pool 설정이 풍부하다.
- streaming에 강하다.
문제:
- 현재 imperative/virtual-thread template에 reactive runtime과 context semantics를 강제한다.
- `.block()` facade는 cancellation/context/metrics를 잘못 연결하기 쉽다.
- 모든 use case가 reactive일 필요는 없다.
Reactive application이나 HTTP/2 streaming 요구에 대한 별도 provider로 열어둔다.
### G. Apache async engine을 직접 감싼 blocking facade
가장 강한 cancellation과 HTTP/2 확장 경로를 제공하지만 codec, Spring HTTP Service integration,
error mapping을 더 많이 직접 소유한다. 초기 Phase 1은 Apache classic으로 시작하고, classic
hard-cancel evidence가 요구를 만족하지 못하면 이 provider로 승격한다.
### H. Resilience를 Spring annotation/AOP로 적용
Method annotation은 operation descriptor, body replayability, failure phase와 remaining deadline을
알기 어렵다. Decorator order도 proxy order에 숨는다. 선택하지 않는다.
### I. Explicit call state machine에서 Resilience4j primitive를 사용
선택한 방식이다.
- Resilience4j registry/state machine은 재사용한다.
- Retry loop와 ordering은 adapter가 명시적으로 소유한다.
- AOP/annotation은 쓰지 않는다.
- Engine의 automatic retry는 끈다.
- 각 physical attempt의 permission/metric/span을 코드와 test가 검증한다.
## 7. 목표 아키텍처
```mermaid
flowchart LR
UC[Application use case]
PORT[Feature-specific outbound port]
ACL[Upstream anti-corruption adapter]
CAT[Typed operation catalog]
REG[Destination registry]
KERNEL[HTTP execution kernel]
ADMIT[Logical admission]
RETRY[Retry budget and loop]
CB[Physical-attempt circuit breaker]
LIMIT[Attempt bulkhead and local quota]
ENGINE[Apache classic engine]
POOL[Destination-isolated pool]
DNS[Validated DNS/address resolver]
TLS[TLS/mTLS/auth/proxy]
UP[External dependency]
OBS[Observation/error/lifecycle]
UC --> PORT
ACL --> PORT
ACL --> CAT
ACL --> KERNEL
KERNEL --> REG
KERNEL --> ADMIT
ADMIT --> RETRY
RETRY --> LIMIT
LIMIT --> CB
CB --> ENGINE
ENGINE --> POOL
POOL --> DNS
ENGINE --> TLS
DNS --> UP
TLS --> UP
KERNEL --> OBS
```
Dependency direction:
```text
production fork:
application-core
owns feature-specific port and application outcome
adapter-outbound-httpclient
implements that production port
owns external wire DTO, HTTP interface, operation descriptor,
execution kernel, engine, resilience, auth and mapping
skeleton sample:
sample-portfolio
owns RepoStatsPort, use case, wire mapper and sample-local RepoStatsHttpAdapter
consumes the HTTP leaf's adapter-consumer SPI after an explicit registry edge
adapter-outbound-httpclient
never depends on sample-portfolio
adapter-outbound-support
may provide framework-neutral outbound correlation/observation helpers
app-bootstrap
binds selected destinations/providers and validates descriptors
```
`adapter-outbound-httpclient`가 다른 outbound adapter를 직접 호출하지 않는다. OAuth token,
distributed rate quota, secret manager 등이 별도 capability여도 adapter-to-adapter edge를
추가하지 않는다. 필요한 framework-neutral port나 bootstrap composition을 설계하고 registry
승인 뒤 연결한다.
## 8. 모듈과 계층 소유권
### 8.1 `domain-core`
허용:
- 외부 서비스 결과가 실제 domain concept이면 domain value;
- upstream과 무관한 invariant.
금지:
- HTTP status/method/header;
- URI;
- timeout/retry;
- JSON/wire DTO;
- provider ID.
### 8.2 `application-core`
소유:
- `FraudScreeningPort`, `TaxQuotePort`, `PartnerCatalogPort` 같은 semantic port;
- application request/result;
- domain/application-level unavailable/indeterminate outcome;
- use-case deadline 전달을 위한 framework-neutral `CallBudget`가 필요하다면 그 contract;
- compensation/reconciliation use case.
금지:
- `OutboundHttpClient`;
- Spring `HttpMethod`, `HttpHeaders`, `ResponseEntity`;
- Apache/JDK client exception;
- OAuth/SSL bundle;
- raw URL.
예:
```java
public interface PartnerCatalogPort {
CatalogLookupResult find(ProductReference reference, CallBudget budget);
}
```
`CatalogLookupResult``NotFound`는 upstream 404 자체가 아니라 application이 정의한 absence다.
### 8.3 `adapter-outbound-httpclient`
소유:
- production fork의 upstream별 `application-core` port implementation;
- wire request/response DTO와 mapper;
- operation catalog;
- destination binding;
- path/query/header encoding;
- engine SPI와 Apache provider;
- timeout/cancellation;
- resilience;
- TLS/auth/proxy/DNS;
- status/error mapping;
- observability/lifecycle;
- provider tests.
HTTP Service annotation interface를 쓴다면 이 leaf 안에 둔다.
Template sample처럼 feature port가 `sample-portfolio`에 격리된 경우 HTTP leaf가 sample port를
구현하지 않는다. 대신 §10.5의 bounded adapter-consumer SPI를 제공하고 sample-local outbound
adapter가 이를 사용한다.
### 8.4 `shared-contract`
정말 여러 runtime leaf가 동일하게 소비할 때만 다음과 같은 value-only operational contract를
둘 수 있다.
- bounded capability descriptor;
- generic readiness level;
- low-cardinality dependency outcome vocabulary;
- framework-neutral deadline carrier.
HTTP operation, URL, method, credential을 skeleton-wide contract로 올리지 않는다.
### 8.5 `adapter-outbound-support`
유지 가능한 책임:
- correlation value 추출;
- 공통 clock abstraction;
- generic bounded metric naming helper;
- secret-safe diagnostic formatter.
HTTP-specific retry/status/pool/DNS/TLS 정책은 httpclient leaf가 소유한다.
### 8.6 `app-bootstrap`
소유:
- canonical activation;
- typed configuration binding;
- selected provider/destination validation;
- SSL bundle와 secret reference resolution;
- capability descriptor aggregation;
- required readiness policy;
- lifecycle ordering.
Business API operation이나 mapping을 bootstrap에 넣지 않는다.
### 8.7 `sample-portfolio`
Production leaf가 sample을 의존하지 않는다. 현재 sample은 `RepoStatsPort`,
`GetRepoStatsUseCase`, fixture `RepoStatsPortClient`를 이미 가진다.
실제 HTTP reference consumer로 전환할 때:
- `sample-portfolio -> adapter-outbound-httpclient` edge를
`src/config/architecture/modules.json`에 명시적으로 추가;
- sample-local `RepoStatsHttpAdapter``RepoStatsPort`를 구현;
- adapter-consumer SPI는 sample adapter package에서만 사용;
- `repoUrl` raw string을 `RepositoryCoordinates(hostProfile, owner, repository)` 같은 validated
application value로 변경;
- inbound가 허용된 Git provider URL을 parse하되 application/adaptor로 absolute URL을 전달하지
않음;
- registered fixed destination + relative route로 재구성;
- sample-off production build에서는 이 consumer/binding이 없어도 됨;
- sample contract는 provider/card implementation evidence와 deployment ACTIVE readiness를
대신하지 않음.
Registry edge 변경 전에는 sample에서 HTTP leaf type을 import하지 않는다.
### 8.8 두 consumer shape의 선택
```text
실제 fork production:
application-core feature port
<- adapter-outbound-httpclient의 feature adapter
template sample fixture:
sample-local feature port
<- sample-local outbound adapter
-> adapter-outbound-httpclient adapter-consumer SPI
```
새 business concept를 production `application-core`에 demo 목적으로 추가하지 않는다. 반대로
HTTP leaf가 sample business type을 import하지 않는다. 어느 shape든 controller/use case가 generic
HTTP kernel을 직접 호출하는 것은 금지한다.
## 9. Identity와 vocabulary
### 9.1 Destination ID
`HttpDestinationId`는 bounded configuration/registry key다.
예:
```text
partner-catalog
fraud-screening
tax-service
github-api
```
규칙:
- lowercase kebab case;
- committed registry와 typed binding에 존재;
- metric/span/log tag로 사용 가능;
- tenant/user/request에서 동적 생성 금지;
- host name과 동일할 필요 없음;
- credential/TLS/pool isolation 단위.
### 9.2 Operation ID
`HttpOperationId`는 한 upstream API operation의 안정적인 low-cardinality ID다.
```text
partner-catalog.get-product.v1
fraud-screening.evaluate.v2
tax-service.create-quote.v1
```
Operation ID는 다음 정책의 join key다.
- method/path template;
- success/error status;
- body codec;
- retry safety;
- idempotency-key;
- deadline;
- size;
- observability;
- readiness test.
Endpoint path나 Java method name에서 runtime 추론하지 않는다.
### 9.3 Logical call ID
한 application port invocation 안에서 retry/redirect/auth-refresh를 묶는 opaque attempt-group
identity다.
- log/trace correlation용;
- metric tag로 사용 금지;
- idempotency key와 다름;
- application business ID와 다름;
- 한 logical call의 모든 physical attempt에서 동일.
### 9.4 HTTP request attempt와 공통 amplification budget
`HttpRequestAttempt`는 origin으로 wire request 하나를 시작하려는 단위다. 0부터 증가하는
`physicalAttemptOrdinal`은 다음을 모두 같은 연속 번호로 센다.
- initial request;
- ordinary retry;
- confirmed-`NOT_SENT` pre-send restart;
- same-intent/reconciliation 뒤 replay;
- 안전하게 허용한 redirect hop;
- 401 credential refresh 뒤 auth replay;
- provider가 processing evidence로 증명하고 kernel이 새 요청으로 승인한 protocol restart.
사유별 counter는 별도로 유지한다.
```text
ordinaryRetryCount
preSendRestartCount
authReplayCount
redirectHopCount
sameIntentReplayCount
confirmedNotProcessedRestartCount
```
그러나 각 counter/budget의 합이 전체 증폭 상한을 우회해서는 안 된다.
```text
maxProtectedPhysicalAttemptsPerLogicalCall
maxNestedCredentialRequestsPerLogicalCall
maxReconciliationRequestsPerLogicalCall
maxProxyConnectRequestsPerRootCall
maxRevocationHttpRequestsPerRootCall
maxTotalHttpRequestAttemptsPerRootCall
```
Protected destination의 모든 origin request start는 첫 번째와 마지막 shared token을 원자적으로
소비한다. Token endpoint, in-call reconciliation, HTTP CONNECT proxy와 named HTTP OCSP/CRL lookup의
각 실제 HTTP request도 자신의 bounded counter와 같은 root-call total token을 소비한다.
Redirect/auth refresh/retry가 중첩돼도 곱셈 증폭되지 않는다. DNS query와 A/AAAA별 TCP connect는
HTTP request가 아니므로 이 token 대신 resolver/per-address connect cap과 같은 total deadline을
사용한다. Deferred reconciliation use case가 나중에 독립 호출로 시작되면 새 root-call budget을
갖지만 같은 operation identity와 별도 scheduler quota를 유지한다.
`maxAttempts`라는 모호한 이름은 canonical model에서 사용하지 않는다. Operation별 ordinary retry
횟수와 위 physical/root ceiling을 분리한다. Runtime config가 catalog 상한을 낮출 수는 있지만
증가시킬 수 없다. Protected/root 상한은 finite positive이고 비활성일 수 있는 child 상한은 finite
non-negative이며 다음 cross-field invariant를 만족한다.
```text
1 <= maxProtectedPhysicalAttemptsPerLogicalCall
maxNestedCredentialRequestsPerLogicalCall >= 0
maxReconciliationRequestsPerLogicalCall >= 0
maxProxyConnectRequestsPerRootCall >= 0
maxRevocationHttpRequestsPerRootCall >= 0
maxTotalHttpRequestAttemptsPerRootCall >= minimumRequiredRootHttpAttempts(effective profile)
```
`minimumRequiredRootHttpAttempts`는 exact profile의 cold-path scenario마다 계산한다. 최소 initial
protected request, OAuth cache miss/401 refresh, required proxy CONNECT, hard-fail revocation lookup와
advertised auth replay에 필요한 protected request를 포함한다. 예를 들어 one-401 OAuth replay를
지원하면 protected ceiling은 최소 2이고 해당 scenario의 token refresh + 두 protected requests가
root ceiling 안에 들어야 한다. Root ceiling이 reason-specific maxima의 합보다 작을 수는 있지만,
그 때문에 required scenario 하나라도 구조적으로 실행 불가능하면 startup/qualification이 실패한다.
Protected origin 밖에서 실제 HTTP request를 만드는 child path는 공통 authorization protocol을
사용한다.
```text
NestedHttpRequestKind = OAUTH_TOKEN | RECONCILIATION | PROXY_CONNECT | REVOCATION
NestedHttpAuthorizationLease(
kind,
rootCallId,
childProfileFingerprint,
childAttemptOrdinal,
state = AUTHORIZED | BOUND_TO_ENGINE | ABORTED
)
```
`NestedHttpAuthorizationBroker`는 parent root context가 이 lease만 발급하는 최소 capability이며
provider/child adapter에 mutable counter나 refill API를 노출하지 않는다. Broker 생성 시 exact
dependency DAG에서 다음 immutable edge set을 캡처한다.
```text
AllowedChildEdge(
parentProfileFingerprint,
kind,
childProfileFingerprint,
childOperationId,
childAuthorityPolicyFingerprint
)
```
Caller가 전달한 kind/profile/operation edge가 이 set과 exact match하지 않거나 다른 root의 broker/
lease를 재사용하면 child/root token 소비 전에 거절한다. Lease bind 시 provider가 제출한 resolved
child profile, operation, scheme/authority-policy fingerprint도 lease edge와 byte-for-byte 일치해야
한다. 따라서 broker를 generic registered-child/authority oracle로 사용할 수 없다.
`tryAuthorizeNestedHttpRequest`는 deadline/cancellation과 해당 child cap의 순수 availability를 먼저
검사하고, exact child counter token을 원자적으로 한 번 소비해 lease를 만든다. 실제 wire start
직전에는 같은 root-call total HTTP token 하나를 uncommitted reserve하고 cancellation과 engine
handoff를 race한다. Handoff가 이기면 lease/root token을 bind/commit하고, 그 전 실패면 root token은
반납하되 child reason token은 amplification churn을 막기 위해 되살리지 않고 lease를 `ABORTED`
exactly once 닫는다. Cache hit, 기존 CONNECT tunnel 재사용처럼 HTTP request가 발생하지 않는
경로는 lease/token을 소비하지 않는다. Provider가 이 broker를 호출하지 않고 child HTTP request를
시작할 수 있으면 해당 profile은 release-eligible이 아니다.
`TransportConnectAttempt`는 A/AAAA address candidate 하나에 대한 connect 시도이며
`HttpRequestAttempt`와 다르다. Request byte 전 address failover는 connect event/metric일 뿐
`http.request.resend_count`나 physical attempt ordinal을 증가시키지 않는다. First request byte 뒤
재연결은 새 `HttpRequestAttempt`이며 operation state machine 승인이 필요하다.
Engine autonomous resend가 start 전 shared token/deadline/body/CB/bulkhead/span gate를 호출할 수
없으면 hidden resend를 비활성화해야 하며 해당 provider/profile은 release-eligible이 아니다.
사후 관측만으로 공통 상한을 지켰다고 주장하지 않는다.
### 9.5 Operation attempt ID와 idempotency key
Mutation은 application workflow가 생성해 재시도/재시작에도 보존하는 안정적인
`OperationAttemptId`를 가질 수 있다. Adapter는 이를 upstream 형식의 idempotency key로 encode한다.
규칙:
- 첫 network call 전에 생성;
- response loss 뒤에도 caller가 보유;
- payload fingerprint와 결합;
- 다른 intent에 재사용 금지;
- log/metric에 raw value 금지;
- provider가 expiry와 replay semantics를 문서화한 경우에만 retry 근거.
### 9.6 Policy revision
Destination와 operation policy에는 stable ID와 revision/digest가 있다.
```text
destination profile: partner-catalog-r2 / revision 4
operation profile: partner-catalog.get-product.v1 / revision 2
```
같은 revision의 canonical digest가 배포 사이에서 달라지면 startup을 실패시킨다. Rolling
deployment에서 retry semantics가 조용히 변하는 것을 막는다.
## 10. Application semantic port와 adapter boundary
### 10.1 Feature-specific port 원칙
Application port는 upstream HTTP API를 복제하지 않고 use case가 필요한 의미만 노출한다.
좋은 예:
```java
public interface FraudScreeningPort {
ScreeningDecision evaluate(ScreeningSubject subject, CallBudget budget);
}
```
나쁜 예:
```java
public interface HttpPort {
HttpResponse execute(String method, String url, Map<String, String> headers, Object body);
}
```
### 10.2 Wire DTO 격리
Adapter 흐름:
```text
application request
-> upstream request mapper
-> wire DTO
-> registered HTTP operation
-> wire response DTO
-> schema/semantic validation
-> application result
```
Wire DTO는:
- upstream field name/version/null semantics를 소유;
- Jackson annotation을 가질 수 있음;
- application/domain package로 반환하지 않음;
- tolerant read와 required semantic validation을 분리;
- raw problem detail/error body를 application에 노출하지 않음.
### 10.3 Application failure shape
Application port마다 의미 있는 결과를 선택한다.
예:
```text
Found
NotFound
RejectedByPartner
TemporarilyUnavailable
Indeterminate(operationAttemptId)
```
모든 port가 같은 generic exception을 강제로 사용하지 않는다. Kernel failure를 adapter가
operation 의미에 맞게 매핑한다.
### 10.4 Call budget
Use case 전체에 이미 deadline이 있으면 application은 framework-neutral absolute budget을
전달할 수 있다.
```java
public record CallBudget(long monotonicDeadlineNanos) {
}
```
실제 contract는 다음을 만족해야 한다.
- `System.nanoTime()`과 같은 monotonic time domain;
- wall-clock timestamp로 serialize하지 않음;
- destination policy cap과 `min`으로 결합;
- 이미 만료되면 network side effect 없이 reject;
- child call이 parent보다 긴 budget을 만들 수 없음.
`Duration timeout`만 매번 전달하면 nested call이 호출 시점마다 새 budget을 받아 상위 deadline을
넘길 수 있으므로 absolute budget을 선호한다.
### 10.5 Adapter-consumer SPI와 internal kernel
Kernel은 application/controller에 공개하지 않는다. 다만 sample-local outbound adapter나 future
domain-specific outbound leaf가 capability를 재사용할 수 있도록 최소 adapter-consumer SPI를
공개한다.
개념 예:
```java
interface RegisteredHttpOperationInvoker {
<Req, Res> HttpCallResult<Res> execute(
HttpOperation<Req, Res> operation,
Req request,
CallContext context);
}
```
`HttpOperation`은 registry와 fingerprint가 일치하는 immutable descriptor다. Caller는 method,
absolute URL, raw header, credential, retry boolean을 runtime에 제공하지 않는다.
Boundary:
- application/domain/inbound package의 SPI import는 ArchUnit으로 금지;
- adapter/sample outbound package만 사용;
- operation 등록은 startup 전 완료하고 runtime dynamic registration 금지;
- descriptor/codec/wire DTO는 consumer adapter 소유 가능;
- kernel provider/Apache/Spring type은 SPI에 노출하지 않음;
- public-path snapshot으로 accidental generic SDK surface 확장을 검출.
HTTP leaf 안에서 provider와 resilience를 다루는 `HttpTransportEngine`은 계속 internal이다.
### 10.6 HTTP Service Interface 사용
Spring HTTP Service Interface는 다음 조건에서 adapter-internal wire client로 사용할 수 있다.
- interface와 annotation이 adapter package에 위치;
- feature-specific application port를 별도로 구현;
- group/destination이 canonical registry와 1:1로 검증;
- underlying `RestClient`가 동일 kernel의 engine, auth, bounds, observation을 사용;
- proxy가 kernel의 retry/deadline을 우회하지 않음;
- method metadata가 operation catalog와 build-time 대조됨;
- return type이 wire DTO이며 domain/application DTO가 아님.
단순 `@ImportHttpServices` classpath scan으로 새 client를 자동 활성화하지 않는다.
## 11. Typed operation catalog
### 11.1 Catalog가 필요한 이유
동일 destination 안에서도 operation마다 안전성이 다르다.
```text
GET /products/{id}
POST /quotes
POST /payments/{id}/capture
GET /exports/{id}/content
DELETE /sessions/{id}
```
Destination global retry boolean로 이 차이를 표현할 수 없다. Operation catalog는 runtime
request가 정책을 선택하는 것을 막고 code review 가능한 안전 계약을 제공한다.
### 11.2 Operation descriptor
개념적 `HttpOperation<Req, Res>` 필드:
| 필드 | 의미 |
| --- | --- |
| `operationId` | stable low-cardinality ID |
| `destinationId` | exact destination binding |
| `policyRevision` | rolling compatibility revision |
| `method` | fixed HTTP method |
| `routeTemplate` | low-cardinality relative template |
| `semantics` | `SAFE_READ`, `IDEMPOTENT_MUTATION`, `KEYED_MUTATION`, `NON_RETRYABLE_MUTATION` |
| `requestMode` | `NONE`, `BUFFERED`, `REOPENABLE_STREAM`, `SINGLE_USE_STREAM` |
| `responseMode` | `BODILESS`, `BUFFERED`, `STREAM_CALLBACK` |
| `requestCodecId` | wire encoder |
| `responseCodecId` | wire decoder |
| `successContractId` | accepted status/media/schema |
| `errorContractId` | operation-specific status mapping |
| `deadlineProfileId` | phase/total budget profile |
| `retryProfileId` | retry decision/backoff/budget |
| `resilienceGroupId` | CB/bulkhead state-sharing group |
| `authProfileId` | adapter-owned credential profile |
| `egressPolicyId` | URI/address/redirect/proxy policy |
| `observabilityProfileId` | route template and privacy policy |
| `readinessCardIds` | evidence required before R2 |
Runtime caller가 이 필드를 override하지 않는다.
### 11.3 Executable definition과 review registry
다음 이중 구조를 사용한다.
1. Adapter Java code의 immutable descriptor가 executable behavior를 소유한다.
2. `docs/registries/http-operations.yaml`은 operation identity, revision, safety class,
readiness evidence의 review registry다.
3. Build test가 registry와 runtime descriptor를 one-to-one 대조하고 canonical fingerprint를
비교한다.
4. Java code에만 존재하거나 registry에만 존재하는 operation은 실패한다.
5. YAML로 class name을 reflection load하거나 arbitrary expression을 실행하지 않는다.
Template 자체에는 domain operation을 강제하지 않는다. Schema와 fixture operation만 제공하고
fork가 실제 operation을 등록한다.
Illustrative registry:
```yaml
schema_version: 1
operations:
- id: partner-catalog.get-product.v1
destination: partner-catalog
policy_revision: 2
method: GET
route_template: /v1/products/{productRef}
semantics: SAFE_READ
request_mode: NONE
response_mode: BUFFERED
retry_profile: safe-read
resilience_group: partner-catalog-read
readiness_cards:
- httpclient-static-buffered
```
### 11.4 Descriptor validation
Startup/build-time invariant:
- operation/destination ID grammar와 uniqueness;
- route가 relative이며 scheme/authority/userinfo/fragment 없음;
- path variable declaration과 mapper가 정확히 일치;
- query/header key가 allowlist에 존재;
- `KEYED_MUTATION`이면 idempotency key encoder, stable attempt ID, payload fingerprint,
provider replay window와 reconciliation operation이 모두 존재;
- `SINGLE_USE_SOURCE`이면 `maxProtectedPhysicalAttemptsPerLogicalCall=1`;
- `NON_RETRYABLE_MUTATION`이면 default `maxProtectedPhysicalAttemptsPerLogicalCall=1`, redirect/auth replay 금지, transmission
evidence-to-receipt mapping 필수;
- non-retryable pre-send restart를 opt-in하면 `maxProtectedPhysicalAttemptsPerLogicalCall<=2`, buffered/reopenable body,
exact `NOT_SENT` evidence와 별도 restart policy/budget/scenario 필수;
- retryable operation이면 body가 absent/buffered/reopenable;
- streaming response에는 status validator, body cap, cancellation-safe callback contract가 존재;
- success/error status set이 겹치지 않음;
- response codec이 accepted media type마다 존재;
- auth profile과 redirect cross-origin policy가 충돌하지 않음;
- deadline phase 합이 total을 강제로 결정한다는 잘못된 계산을 하지 않음;
- resilience group/card가 registry에 존재;
- readiness card가 operation 요구 feature를 모두 덮음.
### 11.5 Configuration override 제한
Deployment configuration은 다음을 더 보수적으로 만들 수 있다.
- 더 짧은 timeout;
- 더 작은 body/header limit;
- 더 적은 retry;
- 더 낮은 concurrency;
- redirect disable;
- HTTP/2에서 HTTP/1.1로 제한;
- optional dependency를 disabled.
다음은 configuration만으로 넓힐 수 없다.
- method 또는 route;
- safe/idempotent classification;
- accepted destination/redirect host;
- retryable status/exception;
- body replayability;
- auth header 종류;
- media type;
- private CIDR access;
- unknown outcome을 success로 변경.
안전성을 넓히는 변경은 code, registry revision, tests와 review가 필요하다.
## 12. Request target와 URI construction
### 12.1 Base URI
Destination base URI는 startup에 parse/normalize한다.
Required:
- absolute URI;
- default `https`;
- exact lowercase ASCII/IDNA host;
- explicit 또는 scheme default port;
- optional fixed base path;
- empty userinfo;
- empty query/fragment;
- no ambiguous backslash/control/whitespace;
- normalized dot segment 없음;
- allowed scheme/host/port policy와 일치.
Host 비교는 display Unicode가 아니라 canonical ASCII form을 사용한다. IP literal은 canonical
binary address로 비교한다. IPv4-in-IPv6 mapped address와 zone ID도 명시적으로 처리한다.
### 12.2 Relative route only
Normal operation은 catalog의 relative route template만 사용한다.
거부:
```text
https://other.example/path
//other.example/path
file:///etc/passwd
gopher://...
data:...
../admin
%2e%2e/admin
\other
```
Base URI resolution 전에 raw/decoded 두 표현의 ambiguity를 검사한다. Decode 후 다시 decode하는
double-encoding을 허용하지 않는다.
### 12.3 Path variable
각 variable은 한 segment value다.
- URI component encoder로 정확히 한 번 encode;
- `/`, `\`, NUL, control, dot segment 거부;
- 길이와 character profile 제한;
- Unicode normalization policy 고정;
- pre-encoded input 금지;
- empty 허용 여부를 operation이 선언;
- path remainder/wildcard는 별도 typed value와 stricter test 없이는 금지.
String concatenation으로 URI를 만들지 않는다.
### 12.4 Query
Operation이 key와 multiplicity를 선언한다.
- key는 caller가 선택하지 않음;
- value는 component encoding;
- list ordering/canonicalization 명시;
- duplicate 허용 여부;
- blank/null/absent 차이;
- page size/range/total query length bound;
- secret/token/PII query parameter 금지;
- signature provider가 요구하면 canonical order와 exact encoding golden test.
Query는 log, metric tag, span name에 기록하지 않는다.
### 12.5 Dynamic target exception
다음은 일반 URI seam으로 허용하지 않는다.
- user supplied webhook URL;
- arbitrary avatar/document fetch;
- upstream이 응답한 presigned URL;
- pagination `next` absolute link.
필요하면 별도 operation type을 만든다.
- trusted issuer/source 검증;
- allowed scheme/host suffix가 아닌 exact policy;
- resolved IP validation;
- redirect 재검증;
- credential 제거;
- size/type/scan;
- egress proxy;
- bounded lifetime;
- no internal address.
`next` link는 가능한 경우 opaque cursor만 추출해 known route를 재구성한다.
## 13. Header, cookie와 metadata policy
### 13.1 Header ownership
세 그룹으로 나눈다.
1. Engine-owned:
- `Host`/`:authority`;
- `Content-Length`, `Transfer-Encoding`;
- connection/protocol headers;
- proxy authorization.
2. Infrastructure-owned:
- `Authorization`/API key;
- trace context;
- sanitized `User-Agent`;
- `Idempotency-Key`;
- conditional/version headers declared by operation.
3. Operation-owned typed business metadata:
- fixed `Accept`, `Content-Type`;
- provider-documented correlation/reference;
- bounded locale or version enum.
Application caller에게 arbitrary `Map<String,String>`을 주지 않는다.
### 13.2 Forbidden forwarding
Inbound에서 자동 forward 금지:
```text
Authorization
Proxy-Authorization
Cookie
Set-Cookie
X-Forwarded-*
Forwarded
Host
Content-Length
Transfer-Encoding
Connection
Upgrade
TE
Trailer
Keep-Alive
```
사용자 bearer token의 on-behalf-of 전달이 비즈니스 요구라면 별도 credential exchange/delegation
profile을 사용한다. Raw inbound token pass-through를 default로 하지 않는다.
### 13.3 Header limits
Request와 response 모두:
- max field count;
- max single name/value bytes;
- max aggregate bytes;
- duplicate singleton rejection;
- invalid control/obs-fold rejection;
- allowlisted captured response headers;
- trailer allowlist와 aggregate cap;
- header casing에 의미를 두지 않음.
Underlying engine/JVM global header limit만 믿지 않고 destination policy와 test를 둔다.
### 13.4 Cookies
Machine-to-machine default:
- cookie store disabled;
- `Set-Cookie` 저장/재전송 안 함;
- caller cookie 금지.
Cookie-required partner가 있으면:
- destination-exclusive bounded store;
- domain/path/Secure/SameSite policy;
- max cookies/bytes/TTL;
- tenant 간 공유 금지;
- restart persistence 여부;
- secret classification;
- 별도 readiness card.
Browser session emulation은 baseline이 아니다.
### 13.5 Baggage와 correlation
- OTel propagator가 `traceparent``tracestate` 소유;
- `baggage`는 default empty;
- destination별 key allowlist;
- external partner에는 tenant/user/business ID default 금지;
- `X-Request-Id`/`X-Correlation-Id`도 partner contract에 있을 때만 전파;
- inbound value는 syntax/length 검증 후 사용;
- caller-provided trace header override 금지.
## 14. Response contract와 failure taxonomy
### 14.1 Kernel result
Kernel은 raw exception 대신 내부적으로 다음 결과를 만든다.
```text
Completed<T>
Rejected(HttpFailure)
Indeterminate(HttpMutationUncertainty)
Cancelled(HttpCancellation)
```
Adapter가 이를 application-specific result/exception으로 변환한다.
### 14.2 Completed
`Completed<T>`는 단순 2xx가 아니다.
모두 만족해야 한다.
- operation이 success로 선언한 status;
- framing complete;
- body size 안;
- accepted content type/encoding/charset;
- decode 성공;
- required wire field와 semantic invariant 성공;
- stream callback 정상 종료;
- response close 성공 또는 close failure가 결과 신뢰도에 영향 없음을 증명.
Status 204/304/HEAD처럼 body가 없어야 하는 response에 unexpected body가 있으면 engine/framing
policy에 따라 discard cap 안에서 닫고 protocol drift를 기록한다.
### 14.3 Failure stages
가능하면 engine은 다음 stage evidence를 제공한다.
```text
ADMISSION
POOL_ACQUIRE
DNS_RESOLUTION
CONNECT
TLS_HANDSHAKE
REQUEST_HEADERS
REQUEST_BODY
RESPONSE_HEADERS
RESPONSE_BODY
DECODE
CALLBACK
```
Transmission progress와 observation confidence를 분리한다.
```text
TransmissionProgress =
NOT_SENT | MAYBE_SENT | SENT | RESPONSE_STARTED | RESPONSE_COMPLETE
ObservationConfidence =
OBSERVED | UNKNOWN_AFTER_HANDOFF
ProjectedTransmissionEvidence =
progress when sufficient OBSERVED evidence exists, otherwise UNKNOWN
```
정상 provider의 tracker는 progress에 다음 단조 전이만 허용한다.
```text
NOT_SENT
-- immediately before the first possible origin-request byte/frame write --> MAYBE_SENT
-- local request headers/body write completed ----------------------------> SENT
-- first valid response headers observed ---------------------------------> RESPONSE_STARTED
-- required response framing/body contract completed ---------------------> RESPONSE_COMPLETE
```
첫 write는 HTTP/1.1 request line/header byte, HTTP/2 HEADERS frame 제출처럼 upstream이 request를
관측할 수 있는 가장 이른 지점이다. Provider는 그 지점 전에 synchronous tracker callback을
호출해야 한다. Engine ownership transfer 뒤 이 callback을 보장하지 못하거나 start/cancel race의
증거를 회수하지 못하면 progress의 마지막 observed lower bound는 유지하고 confidence만
`UNKNOWN_AFTER_HANDOFF`로 단조 전이한다. Progress는 어느 상태도 downgrade하지 않고 cleanup/cancel이
이를 `NOT_SENT`로 되돌리지 않는다.
한 physical attempt의 authoritative response/failure event와 cancellation은 atomic terminal CAS로
정확히 하나만 승리한다. Response winner가 retry/auth/redirect control disposition을 만들 수 있으므로
항상 logical call terminal을 뜻하지는 않는다. Valid completed/rejection response가 먼저 확정되면
뒤늦은 cancel이 결과를 덮지 않는다. 반대로 mutation이 cancel/deadline 시점에 `MAYBE_SENT` 이상
또는 `UNKNOWN`이고 authoritative terminal result가 없으면 반드시 `INDETERMINATE`다. Exact
`NOT_SENT`일 때만 `Cancelled` 또는 reviewed pre-send restart가 가능하다.
`UNKNOWN_AFTER_HANDOFF`는 progress 순서의 마지막 값이 아니다. 이후 authoritative response
headers/body completion을 직접 관측하면 progress를 `RESPONSE_STARTED|RESPONSE_COMPLETE`로 전진시키고
그 이후 사실의 confidence를 `OBSERVED`로 기록할 수 있다. 다만 response가 없다는 이유로 unknown
request transmission을 `NOT_SENT|SENT`로 추론하지 않는다. Valid terminal response가 있으면 §16의
response semantic outcome이 우선하고, 없으면 projected evidence는 계속 `UNKNOWN`이다.
Provider가 정확히 관측하지 못하면 더 강한 값으로 추측하지 않고 `UNKNOWN`을 사용한다. 이
linearization callback과 race test를 제공하지 못하는 engine은 non-retryable mutation card를
통과하지 못한다.
### 14.4 Stable failure classes
| Failure | 의미 | Generic retry default |
| --- | --- | --- |
| `CALL_BUDGET_EXHAUSTED` | network 전 또는 중 total deadline 만료 | operation policy |
| `ADMISSION_REJECTED` | logical call queue/budget 초과 | false |
| `POOL_ACQUIRE_TIMEOUT` | connection/stream lease 대기 만료 | safe/replayable만 |
| `LOCAL_RATE_LIMITED` | local egress quota 거부 | Retry-After와 budget에 따름 |
| `CIRCUIT_OPEN` | resilience group open | false inside same call |
| `DNS_FAILED` | name resolution 실패 | safe/replayable + bounded |
| `DESTINATION_ADDRESS_REJECTED` | IP/SSRF policy 위반 | false, security alert |
| `CONNECT_TIMEOUT` | socket connect timeout | safe/replayable |
| `CONNECT_REFUSED` | connection 거부 | safe/replayable |
| `TLS_TRUST_FAILED` | trust/chain/revocation | false |
| `TLS_HOSTNAME_FAILED` | hostname mismatch | false |
| `TLS_HANDSHAKE_TIMEOUT` | handshake deadline | safe/replayable if not sent |
| `PROXY_FAILED` | proxy connect/auth/protocol | policy-specific |
| `REQUEST_WRITE_TIMEOUT` | request body write 제한 | mutation may be indeterminate |
| `RESPONSE_HEADER_TIMEOUT` | headers 대기 제한 | operation/transmission-specific |
| `RESPONSE_IDLE_TIMEOUT` | body progress 없음 | safe read may retry from start |
| `RESPONSE_TRUNCATED` | framing/body incomplete | safe/replayable; mutation usually indeterminate |
| `RESPONSE_TOO_LARGE` | wire/decoded cap 초과 | false |
| `UNSUPPORTED_MEDIA_TYPE` | content contract drift | false |
| `UNSUPPORTED_CONTENT_ENCODING` | encoding contract drift | false |
| `DECODE_FAILED` | malformed/schema mismatch | false |
| `UPSTREAM_STATUS` | operation-mapped status | operation-specific |
| `PROTOCOL_VIOLATION` | malformed framing/header/version | false by default |
| `AUTH_MATERIAL_UNAVAILABLE` | local secret/token failure | bounded refresh policy |
| `CANCELLED_BY_CALLER` | caller cancellation | false |
| `SHUTTING_DOWN` | lifecycle reject/cancel | false |
| `CALLBACK_FAILED` | application adapter stream consumer failure | false |
| `INTERNAL_CLIENT_DEFECT` | invariant/programming failure | false, alert |
### 14.5 HTTP status default matrix
Operation mapping이 우선하며 generic default는 보수적이다.
| Status | Default |
| --- | --- |
| 200~299 | declared success set에 있을 때만 success |
| 300~399 | redirect disabled면 explicit upstream status failure |
| 400 | permanent request contract failure |
| 401 | credential profile이 허용하면 one refresh path, 일반 retry 아님 |
| 403 | permanent authz/config failure |
| 404 | operation이 absence로 선언한 경우만 domain absence |
| 408 | safe/replayable operation에서 bounded retry candidate |
| 409 | operation-specific conflict/in-flight/idempotency mapping |
| 410 | operation-specific terminal absence |
| 412 | precondition/domain concurrency outcome |
| 413 | request size/contract failure |
| 415/406 | media negotiation/config drift |
| 422 | application rejection 또는 idempotency fingerprint mismatch |
| 425 | replay-safe operation만 retry candidate |
| 429 | provider quota outcome; valid `Retry-After`와 budget 필요 |
| 500 | default no retry, operation opt-in 가능 |
| 501/505 | permanent capability/protocol mismatch |
| 502/503/504 | safe/replayable bounded retry candidate |
Status code만으로 `retryable=true`를 application 외부 error envelope에 그대로 전달하지 않는다.
Retry가 안전한지는 현재 operation과 body/attempt evidence에 따라 달라진다.
### 14.6 Error body
Default는 discard-and-close다. Operation이 structured error mapping을 요구할 때만:
- 별도 작은 `maxErrorBodyBytes`;
- accepted content type;
- bounded decoder;
- field allowlist;
- message/log 미노출;
- application-safe code로 mapping;
- malformed error body는 original status를 보존한 `ERROR_BODY_DECODE_FAILED` evidence.
### 14.7 `Throwable` 금지
Infrastructure 경계가 모든 `Throwable`을 dependency failure로 바꾸지 않는다.
- `VirtualMachineError`, `LinkageError`, `ThreadDeath` 등은 통과;
- `InterruptedException`은 interrupt flag 복구 후 cancellation/shutdown으로 분류;
- `CancellationException`은 별도;
- adapter programmer exception은 `INTERNAL_CLIENT_DEFECT`;
- engine/network exception만 taxonomy mapping;
- Error mapper 자체 failure가 original failure를 덮지 않음.
## 15. Total deadline와 active cancellation
### 15.1 Deadline 정의
Effective deadline:
```text
min(
inherited application deadline,
operation total-deadline cap,
destination maximum call duration
)
```
Elapsed time은 monotonic clock으로 계산한다.
```text
remaining = deadlineNanos - monotonicNowNanos
```
`Instant.now()`는 NTP/clock adjustment 영향을 받으므로 elapsed control에 사용하지 않는다.
`Retry-After` HTTP date 해석에만 wall clock을 사용하고 최종 sleep은 monotonic remaining으로
제한한다.
### 15.2 포함 범위
Total deadline에는 모두 포함된다.
```text
logical admission wait
+ credential acquisition/refresh wait
+ request body encode/open/spool wait
+ local outbound quota wait
+ physical-attempt bulkhead wait
+ circuit permission acquisition
+ DNS
+ connection/HTTP2 stream lease
+ connect
+ TLS
+ request write
+ response headers
+ response body/callback
+ retry decision
+ Retry-After/backoff
+ every physical attempt
```
### 15.3 Phase cap
Caller-visible absolute deadline을 `D`라 하고 positive finite `cleanupReserve`를 둔다.
```text
executionCutoff = D - cleanupReserve
remainingExecution = executionCutoff - monotonicNow
remainingReturn = D - monotonicNow
```
Normal admission, backoff와 새 attempt는 `executionCutoff`까지만 허용한다. 각 실행 phase는:
```text
effectivePhaseTimeout = min(configuredPhaseCap, remainingExecution)
```
을 사용한다.
필수 cap:
- admission acquire;
- credential acquire/refresh;
- body encode/open/spool;
- local quota acquire;
- physical bulkhead acquire;
- circuit permission acquire;
- pool acquire;
- DNS;
- connect;
- TLS handshake;
- request write/idle;
- response header;
- response body idle;
- total body/callback;
Phase cap의 합을 total deadline으로 오해하지 않는다. 실제 phase는 순차/중첩되고 retry가 있으므로
total은 별도 상한이다.
Cleanup은 일반 phase가 아니라 reserve를 사용한다.
```text
synchronousCleanupBudget = max(0, min(cleanupReserve, remainingReturn))
```
`D <= now + cleanupReserve`이면 새 network side effect를 시작하지 않는다. Scheduler tolerance를
cleanup budget으로 사용하지 않는다.
### 15.4 Attempt 시작 조건
모든 physical attempt의 공통 조건:
- remaining > `minimumAttemptBudget`;
- execution cutoff 전이며 cleanup reserve가 보존됨;
- 필요한 admission/connection acquisition 뒤에도 meaningful budget;
- logical call not cancelled;
- protected physical ceiling과 shared root-call total capacity가 남음;
- shutdown state가 이 call lease를 허용.
최초 `attempt=0`은 retry가 아니다.
- retry budget token을 요구하거나 소비하지 않음;
- operation/body가 replayable일 필요 없음;
- `SINGLE_USE_SOURCE``NON_RETRYABLE_MUTATION`도 실행 가능;
- validation/encoding에서 network side effect 전 거절될 수 있음.
`attempt>0`은 공통 조건에 더해 다음을 모두 만족한다.
- previous `AttemptDisposition`이 정확히 후속 attempt를 허용;
- body가 동일 bytes/intent로 reopen 가능;
- applicable retry/pre-send-restart/auth-replay/operation-specific replay budget의 순수 availability 확인;
- backoff/`Retry-After` 뒤에도 meaningful budget;
- `physicalAttemptOrdinal < maxProtectedPhysicalAttemptsPerLogicalCall`이고 shared root-call total
token이 남음;
- identity-preserving replay라면 key/fingerprint/scope 동일.
`NON_RETRYABLE_MUTATION`은 기본 `maxProtectedPhysicalAttemptsPerLogicalCall=1`이다. 별도
reviewed pre-send restart policy가
`RESTART_CONFIRMED_NOT_SENT`만 허용하고 body가 buffered/reopenable일 때 한 번의 attempt를
추가로 허용할 수 있다. 이는 mutation replay/retry가 아니며 별도 restart budget/metric을
사용한다. `MAYBE_SENT` 이상은 후속 mutation attempt를 절대 시작하지 않는다.
Deadline 직전에 성공 가능성이 없는 attempt를 시작하지 않는다.
### 15.5 Active cancellation 구현
Apache classic R2 provider는 blocking `RestClient` call을 adapter-owned virtual-thread task로
실행할 수 있다.
```text
caller
-> submit virtual-thread attempt task
-> wait only until execution cutoff
-> cutoff/cancel:
Future.cancel(true)
cancel request execution
close response/entity stream
hard-cancel connection when required
await cleanup only until caller-visible deadline D
if still active at D:
return to caller
quarantine connection/generation
hand off to bounded orphan reaper
```
구현 시 검증할 사항:
- interrupt가 Apache request cancellation으로 실제 연결;
- `hardCancellationEnabled`의 connection 폐기 semantics;
- cancelled connection이 pool로 정상 반환되지 않음;
- stream callback 중 network read가 해제됨;
- executor shutdown이 in-flight task를 유실하지 않음;
- virtual-thread task 수가 logical admission으로 bounded;
- cancellation race에서 response가 정확히 한 번 close.
Thread interrupt flag만 세우고 active cancellation이라 주장하지 않는다.
### 15.6 Callback 한계
Streaming callback이 무한 CPU loop를 돌거나 interrupt를 무시하면 transport close만으로 callback
종료를 보장할 수 없다.
계약:
- callback은 blocking read/write interrupt와 cancellation token을 존중;
- adapter는 network stream을 close;
- callback에 checkpoint/cancellation view 제공 가능;
- non-cooperative callback hard kill은 보장하지 않음;
- test가 cooperative/non-cooperative behavior와 shutdown impact를 구분.
### 15.7 Cancellation outcome
Cancellation 원인을 구분한다.
```text
PARENT_DEADLINE
OPERATION_DEADLINE
CALLER_CANCELLED
SHUTDOWN_DRAIN_EXPIRED
HEDGE_LOSER
```
OTel convention상 의도된 caller cancellation은 자동으로 dependency error로 기록하지 않는다.
그러나 mutation이 이미 전송됐으면 application outcome은 cancellation보다
`INDETERMINATE`가 우선할 수 있다.
### 15.8 Cleanup reserve와 orphan reaper
Normal path는 execution cutoff에서 cancellation을 시작해 `D` 전에 response close, connection
discard, circuit/bulkhead permit 정리를 끝내는 것을 목표로 한다.
`D`까지 cleanup이 끝나지 않으면 caller latency를 더 늘리지 않는다.
- incomplete connection/stream은 reusable pool로 반환 금지;
- engine generation을 quarantine;
- physical-attempt permit는 실제 task termination까지 reaper가 보유해 capacity를 과다 판매하지
않음;
- logical call은 caller 반환 뒤 release하되 orphan count가 새 admission capacity에 반영됨;
- orphan registry와 reaper worker/queue/deadline은 finite;
- `orphanCleanupTimeout` 뒤에도 종료되지 않으면 generation을 `DEGRADED/NOT_READY`로 만들고 새
call을 받지 않음;
- Java task를 강제 kill했다고 주장하지 않음;
- provider close/rollover/runbook escalation;
- synchronous logical latency SLO와 asynchronous cleanup SLO를 별도 metric으로 기록.
따라서 보장은 다음처럼 구분한다.
```text
caller return <= D + scheduler tolerance
normal cleanup target <= D
quarantined cleanup <= orphanCleanupTimeout
no quarantined resource reuse at any time
```
Parent cancellation이 이미 `D`를 지나 도착하면 synchronous cleanup budget은 0이며 즉시
quarantine/reaper 경로를 사용한다.
## 16. Replayability, idempotency와 unknown outcome
### 16.1 세 개념을 분리한다
- HTTP method idempotency: 같은 intended effect를 반복해도 추가 effect가 없음.
- Body replayability: client가 동일 bytes/semantics를 다시 전송할 수 있음.
- Operation deduplication: upstream이 stable key/fingerprint로 같은 mutation을 식별하고 이전
결과를 replay함.
하나가 다른 둘을 암시하지 않는다.
### 16.2 Request body mode
```text
NONE
BUFFERED_IMMUTABLE
REOPENABLE_SOURCE
SINGLE_USE_SOURCE
```
`BUFFERED_IMMUTABLE`:
- size cap 안;
- canonical encoding 후 bytes/digest 고정;
- attempt마다 새 publisher/stream.
`REOPENABLE_SOURCE`:
- `open()`마다 처음부터 같은 content;
- stable length/checksum 또는 canonical fingerprint;
- concurrent open 허용 여부;
- 실패 시 close;
- source revision drift 검증.
`SINGLE_USE_SOURCE`:
- `maxProtectedPhysicalAttemptsPerLogicalCall=1`;
- redirect/auth replay 금지;
- response loss는 transmission evidence에 따라 indeterminate.
### 16.3 Operation semantics
```text
SAFE_READ
IDEMPOTENT_MUTATION
KEYED_MUTATION
NON_RETRYABLE_MUTATION
```
`GET`이라고 자동으로 `SAFE_READ`가 되지 않고 catalog review가 필요하다. `PUT`/`DELETE`
provider 문서와 precondition을 검증한다.
### 16.4 Keyed mutation
`KEYED_MUTATION` 요구:
- upstream 문서화된 key contract;
- key format/entropy/length;
- uniqueness scope;
- retention/replay window;
- same key + different payload 처리;
- concurrent same-key 처리;
- success/error replay semantics;
- inspection/reconciliation endpoint;
- client-side stable operation attempt ID;
- canonical payload fingerprint;
- credential/tenant scope;
- real failure tests.
2026-07 기준 IETF Idempotency-Key 문서는 만료된 Internet-Draft이며 표준 RFC로 취급하지 않는다.
Provider가 실제 지원하는 header와 semantics를 contract로 검증한다.
### 16.5 Unknown mutation state
다음 상황은 operation이 적용됐을 가능성이 있다.
- request body 일부/전체 전송 후 connection reset;
- response header timeout;
- response body truncate;
- caller/shutdown cancellation after send;
- proxy/gateway가 upstream response를 잃음;
- success response decode 실패.
결과:
```text
Indeterminate(
operationAttemptId,
destinationId,
operationId,
requestFingerprint,
lastAttempt,
transmissionEvidence,
reconciliationHint
)
```
Raw URL, credential, payload는 receipt에 넣지 않는다.
### 16.6 Exhaustive attempt disposition
Kernel은 `mutation outcome unresolved` 같은 loose boolean을 사용하지 않는다.
입력:
```text
operationSemantics
transmissionEvidence
processingEvidence
cancellationOutcome
responseIntegrity
responseSemanticClass
bodyReplayability
reconciliationContract
credentialGeneration
authChallengeDecision
authReplayCount
remainingBudget
```
출력은 정확히 하나다.
```text
RETURN_COMPLETED
RETURN_DECLARED_REJECTION
RETURN_CANCELLED(reason)
RESTART_CONFIRMED_NOT_SENT
RESTART_CONFIRMED_NOT_PROCESSED
RETRY_SAFE_READ
REPLAY_SAME_INTENT
REFRESH_CREDENTIAL_AND_REPLAY
FOLLOW_DECLARED_REDIRECT
RECONCILE_SAME_OPERATION
RETURN_INDETERMINATE
RETURN_PERMANENT_FAILURE
```
응답의 byte/framing 무결성과 operation 의미를 한 enum에 섞지 않는다.
```text
ResponseIntegrity =
VALID_COMPLETE
| VALID_HEADERS_ONLY
| NONE_OR_INVALID
ResponseSemanticClass =
COMPLETED
| DOMAIN_REJECTION
| RETRY_CONTROL
| STALE_CREDENTIAL_CHALLENGE
| DECLARED_REDIRECT
| RECONCILIATION_SIGNAL
| UNKNOWN
```
`VALID_HEADERS_ONLY`는 catalog가 status/header만으로 해당 control outcome을 authoritative하게
판단할 수 있을 때만 사용한다. Success가 required body를 요구하는데 truncate/decode/schema
failure가 발생하면 `NONE_OR_INVALID`이며 status가 2xx라는 이유로 `COMPLETED`가 되지 않는다.
반대로 bounded error body decode가 실패해도 catalog가 429 status와 valid `Retry-After`만으로
retry control을 선언했다면 원래 status를 보존할 수 있다.
Terminal-result/cancellation CAS에서 cancellation이 먼저 이겼다면 response table보다 먼저
분기한다. `SAFE_READ` 또는 exact `NOT_SENT` operation은 `RETURN_CANCELLED(reason)`이며 reason은
caller/deadline/shutdown을 보존한다. Mutation이 `MAYBE_SENT` 이상 또는 `UNKNOWN`이면 cancellation을
permanent failure로 축소하지 않고 `RETURN_INDETERMINATE`다. Authoritative response가 먼저 CAS를
이겼다면 뒤늦은 cancellation은 아래 semantic result를 덮지 않는다.
CAS 시점은 required integrity에 따라 다르다. Exact `VALID_HEADERS_ONLY` response outcome은
status/header/framing 검증 직후 response winner를 시도하고, body-required outcome은 bounded
body/decode/semantic 검증이 끝난 직후 시도한다. Header-authoritative winner 뒤의 body
drain/discard는 cleanup일 뿐 outcome 확정을 늦추지 않는다. Response body task, provider failure와
cancellation callback은 §20의 단일 `AttemptTerminalCoordinator`에 event를 제출하며 winner와 cleanup
owner를 각각 정확히 하나만 선출한다.
Authoritative completed/rejection response가 없을 때는 `processingEvidence`를 semantic
`UNKNOWN`/transmission fallback보다 먼저 평가한다.
| Provider processing evidence | Required contract | Disposition |
| --- | --- | --- |
| `CONFIRMED_NOT_PROCESSED` | exact RFC/provider evidence + operation H2 opt-in + replayable body + same identity + finite protocol-restart/protected/root budget | `RESTART_CONFIRMED_NOT_PROCESSED` |
| `MAYBE_PROCESSED` 또는 `UNKNOWN` | mutation | processing evidence로 restart 금지; semantic/fallback matrix 계속 평가 |
따라서 request progress가 `SENT`인 mutation이라도 authoritative `CONFIRMED_NOT_PROCESSED`가 있으면
아래 generic transmission fallback이 먼저 `RETURN_INDETERMINATE`로 종결하지 않는다. 반대로 valid
completed/domain-rejection response가 있으면 processing hint가 그 authoritative result를 덮지 않는다.
그 다음 response semantic disposition precedence는 다음과 같다.
| Semantic class | Required integrity/contract | Disposition |
| --- | --- | --- |
| `COMPLETED` | operation success contract의 required integrity 충족 | `RETURN_COMPLETED` |
| `DOMAIN_REJECTION` | operation이 status/header/body 중 요구한 rejection evidence 충족 | `RETURN_DECLARED_REJECTION` |
| `RETRY_CONTROL` | exact status profile + operation/body/transmission/budget gate | safe read는 `RETRY_SAFE_READ`; mutation은 authoritative same-intent/reconciliation 계약이 있을 때만 해당 state, 그 외 indeterminate/permanent |
| `STALE_CREDENTIAL_CHALLENGE` | exact configured challenge + §17.5 gate | `REFRESH_CREDENTIAL_AND_REPLAY`, 아니면 terminal auth rejection |
| `DECLARED_REDIRECT` | future redirect card + §24.3 hop/body/origin gate | `FOLLOW_DECLARED_REDIRECT`; current cards에서는 terminal reject |
| `RECONCILIATION_SIGNAL` | provider-specific inspect/reconcile contract | `RECONCILE_SAME_OPERATION` 또는 authoritative terminal result |
| `UNKNOWN` 또는 required integrity 미충족 | 아래 fallback matrix | transmission evidence에 따른 disposition |
임의의 401, 403, malformed challenge 또는 provider가 선언하지 않은 error body를 refresh 신호로
추측하지 않는다. 408/425/429/5xx가 framing-complete response라는 이유만으로 terminal rejection이
되는 것도 아니며, operation catalog의 exact `RETRY_CONTROL` mapping을 통과해야 한다.
`UNKNOWN`/required-integrity-failure fallback 결정표:
| Semantics | `NOT_SENT` | `MAYBE_SENT` / `SENT` / `RESPONSE_STARTED` / `RESPONSE_COMPLETE` / `UNKNOWN` |
| --- | --- | --- |
| `SAFE_READ` | failure/status profile이 허용하면 `RESTART_CONFIRMED_NOT_SENT` | failure/status profile + replayable body + budget이 모두 허용하면 `RETRY_SAFE_READ`, 아니면 permanent failure |
| `IDEMPOTENT_MUTATION` | policy가 허용하면 `RESTART_CONFIRMED_NOT_SENT` | catalog가 same-intent replay 결과도 authoritative라고 증명한 경우만 `REPLAY_SAME_INTENT`; 그 외 `RETURN_INDETERMINATE` |
| `KEYED_MUTATION` | policy가 허용하면 `RESTART_CONFIRMED_NOT_SENT` | generic retry 금지, contract가 있으면 `RECONCILE_SAME_OPERATION`, 없으면 `RETURN_INDETERMINATE` |
| `NON_RETRYABLE_MUTATION` | protected physical ceiling 1이 기본. buffered/reopenable body + explicit ceiling 2 restart policy가 exact `NOT_SENT`에만 opt-in한 경우 `RESTART_CONFIRMED_NOT_SENT`, 아니면 permanent before-send failure | 항상 `RETURN_INDETERMINATE` |
`RESPONSE_COMPLETE`라도 terminal semantic result가 invalid하면 mutation effect를 부정하지 못한다.
`MAYBE_SENT``UNKNOWN``NOT_SENT`로 낮추지 않는다.
`RESTART_CONFIRMED_NOT_SENT`는 upstream mutation transmission이 없었다는 engine evidence 뒤의
pre-send restart다. Retry/replay metric과 budget에 섞지 않지만 새 physical attempt, deadline,
quota/bulkhead/CB/span에는 그대로 집계한다.
HTTP/2 provider가 §30.2의 exact `CONFIRMED_NOT_PROCESSED`를 반환하면
`RESTART_CONFIRMED_NOT_PROCESSED` 후보가 된다. Operation catalog의 explicit H2 restart opt-in,
replayable body, same identity, finite protocol-restart budget, shared physical/root ceiling과 deadline을
모두 요구한다. 이는 engine autonomous resend가 아니라 provider가 현재 attempt를 종료하고 kernel이
새 physical request를 승인하는 state다. `MAYBE_PROCESSED|UNKNOWN`은 이 disposition을 만들지 않는다.
Non-retryable mutation도 provider/RFC evidence가 authoritative `NOT_PROCESSED`이고 operation이
opt-in한 경우에만 허용하며 one-shot body에는 허용하지 않는다.
`FOLLOW_DECLARED_REDIRECT`도 engine auto-follow가 아니다. Prior 3xx response/resource를 닫고 future
redirect card의 hop token과 attempt authorization을 얻은 뒤, target origin/DNS/credential/body를
다시 검증해 같은 kernel loop로 들어간다. Current canonical card set에서는 항상 terminal reject다.
`REFRESH_CREDENTIAL_AND_REPLAY`는 다음 조건을 모두 만족할 때만 선택한다.
```text
configured stale/expired-token challenge exactly matched
AND challenged credential generation is older than the currently usable generation
OR a bounded single-flight refresh for that generation is required
AND authReplayCount == 0
AND operation semantics is SAFE_READ
OR operation contract states that this exact 401 authoritatively means NOT_APPLIED
AND operation semantics is not NON_RETRYABLE_MUTATION
AND body is ABSENT, BUFFERED_REPLAYABLE or REOPENABLE_SOURCE
AND body mode is not SINGLE_USE_SOURCE
AND same operation-attempt identity and credential scope can be preserved
AND auth replay/amplification budget is available
AND caller is not cancelled and execution cutoff leaves the minimum attempt budget
```
401을 받았다는 사실만으로 mutation 미적용을 추론하지 않는다. `IDEMPOTENT_MUTATION` 또는
`KEYED_MUTATION`은 provider contract가 그 challenge를 authoritative `NOT_APPLIED`로 선언하고
같은 operation ID/key/fingerprint를 유지할 때만 auth replay할 수 있다.
`NON_RETRYABLE_MUTATION``SINGLE_USE_SOURCE`는 auth replay를 항상 금지한다.
Auth replay는 ordinary retry나 pre-send restart가 아니지만 새 physical attempt다. 따라서
attempt/resend count, total deadline, local quota, physical bulkhead, circuit permission과 attempt
span을 다시 거치며 total amplification dashboard에 포함한다. Ordinary retry token을 소비하지
않고 별도의 one-token auth-replay budget을 원자적으로 소비한다.
`REPLAY_SAME_INTENT``RECONCILE_SAME_OPERATION`은 다음 identity를 그대로 유지한다.
```text
operationAttemptId
idempotencyKey
requestFingerprint
operationId
tenant/credential scope
```
새 key/fingerprint로 mutation을 재시작하지 않는다. Authoritative reconciliation이
`CONFIRMED_NOT_APPLIED`를 반환한 뒤에만 같은 logical identity로 새 physical mutation attempt를
시작할 수 있다.
### 16.7 Reconciliation
Operation이 다음 중 하나를 제공해야 automatic mutation retry를 허용할 수 있다.
- same idempotency key replay가 authoritative result를 반환;
- operation ID로 status inspection;
- provider resource ID + conditional lookup;
- externally visible durable receipt;
- verified `NOT_APPLIED` evidence.
Reconciliation outcome:
```text
CONFIRMED_APPLIED(result/reference)
CONFIRMED_NOT_APPLIED
STILL_IN_PROGRESS
UNKNOWN
KEY_EXPIRED
PAYLOAD_MISMATCH
```
`CONFIRMED_NOT_APPLIED`일 때만 새 attempt 여부를 policy가 결정한다. `UNKNOWN`을 success/failure로
추측하지 않는다.
Mapping:
| Reconciliation result | Kernel disposition |
| --- | --- |
| `CONFIRMED_APPLIED` | `RETURN_COMPLETED` with authoritative result/reference |
| `CONFIRMED_NOT_APPLIED` | budget/replay policy가 허용하면 same-identity new physical attempt |
| `STILL_IN_PROGRESS` | bounded later reconciliation, caller에는 indeterminate receipt |
| `UNKNOWN` | `RETURN_INDETERMINATE` |
| `KEY_EXPIRED` | `RETURN_INDETERMINATE`, blind new key 금지 |
| `PAYLOAD_MISMATCH` | permanent local/provider contract failure, security alert |
In-call reconciliation은 다음 exact state를 따른다.
1. prior mutation response/handle과 circuit permission을 완료하고 bulkhead를 반납한다. Committed
local quota handle은 닫되 소비한 token을 환불하지 않는다.
2. catalog에 등록된 `SAFE_READ` reconciliation operation만 선택한다. Raw URL/임의 operation은
허용하지 않고 별도 resilience group과 pool/admission 상한을 사용한다.
3. 같은 parent execution cutoff, cancellation token과 root-call HTTP-request-attempt budget을
전달한다. Poll 횟수/간격과 `maxReconciliationRequestsPerLogicalCall`은 finite하다.
4. 각 poll wait 직후 common gate와 registered child operation의 pure eligibility를 재검사하고
`tryAuthorizeNestedHttpRequest(RECONCILIATION, childProfileFingerprint)`를 정확히 한 번 호출한다.
반환된 `NestedHttpAuthorizationLease`는 child physical attempt까지 carrying하며 같은 poll에서
다시 acquire하지 않는다. Child-level safe-read retry를 허용하면 새 wire request마다 새 poll
ordinal/lease가 필요하고 child retry budget availability도 같은 atomic authorization에 포함한다.
5. Child request의 body/pool/quota/bulkhead/CB acquisition 뒤 wire start 직전에 shared root HTTP token을
reserve한다. Cancellation/start race에서 start가 이길 때만 lease/root token을 bind/commit한다.
Pre-bind exit는 lease를 `ABORTED`로 exactly once 닫고 uncommitted root/quota를 반납한다. 현재
logical call에서 detached background task를 만들지 않는다.
6. `CONFIRMED_NOT_APPLIED`만 same operation identity의 `REPLAY_SAME_INTENT`로 돌아가며 별도 replay
budget과 protected/root physical token을 소비한다. 나머지는 위 mapping대로 terminal/receipt다.
Deadline 안에 확인되지 않으면 kernel은 `Indeterminate` receipt를 반환한다. 이후 scheduler/use
case가 수행하는 reconciliation은 새 root-call budget과 별도 scheduler quota를 갖고 원래
operation-attempt identity를 이어받는다.
### 16.8 Application orchestration
HTTP kernel이 business compensation을 수행하지 않는다.
```text
use case
-> stable operation attempt ID 저장/보유
-> outbound port mutation
-> Indeterminate
-> reconciliation use case/scheduler
-> confirmed state
-> next business transition/compensation
```
DB transaction 안에서 remote HTTP mutation을 호출하고 rollback이 remote effect도 취소한다고
가정하지 않는다.
## 17. Retry policy와 retry budget
### 17.1 Pure eligibility와 exactly-once attempt authorization
후속 attempt 판정은 state를 바꾸지 않는 pure eligibility와 token을 한 번만 소비하는 authorization을
분리한다. 최초 request는 ordinary reason token을 요구하지 않지만 shared protected/root physical
token은 engine handoff 직전에 소비한다.
Pure eligibility:
```text
operation policy allows this exact AttemptDisposition
AND request body replayable when the disposition needs replay
AND failure/status/processing-evidence profile allows this disposition
AND call not cancelled/shutting down
AND absolute deadline has minimum attempt budget
AND reason-specific counter, protected physical ceiling and root-call total capacity are available
AND exact reason budget currently has a token
```
Pure 함수는 budget/CB/quota/ordinal을 획득하거나 증가시키지 않는다. Eligibility가 true인 뒤
state machine이 backoff 등 disposition별 wait를 마치고 gate를 재검사한 다음 단 한 번
`tryAuthorizeNextAttempt`를 호출한다.
```text
AttemptAuthorizationLease(
disposition,
reasonBudgetId,
logicalCallId,
operationIdentityDigest,
state = AUTHORIZED | BOUND_TO_PHYSICAL_ATTEMPT | ABORTED
)
```
`tryAuthorizeNextAttempt`는 exact reason token과 counter를 원자적으로 한 번 소비한다. Lease가 다음
loop로 carrying되므로 auth/retry/replay 분기가 다시 token을 소비하지 않는다. Initial request에는
`INITIAL` authorization marker만 있고 reason token은 없다. Authorized 뒤 deadline, refresh 또는
final protected/root reservation이 실패하면 lease는 `ABORTED`로 exactly once 닫고 default로 token을
환불하지 않아 churn이 amplification budget을 되살리지 못하게 한다. Circuit permission, local
quota와 protected/root physical token은 이 lease와 별도로 §20의 실제 attempt 경계에서 획득한다.
Disposition/semantics gate:
| Semantics | Ordinary subsequent attempt |
| --- | --- |
| `SAFE_READ` | `RESTART_CONFIRMED_NOT_SENT` 또는 `RETRY_SAFE_READ` |
| `IDEMPOTENT_MUTATION` | `RESTART_CONFIRMED_NOT_SENT`; same-intent/H2-not-processed는 별도 state |
| `KEYED_MUTATION` | `RESTART_CONFIRMED_NOT_SENT`; reconciliation/same-intent/H2-not-processed는 별도 state |
| `NON_RETRYABLE_MUTATION` | default 금지; explicit protected ceiling 2 + reopenable body + restart budget의 `RESTART_CONFIRMED_NOT_SENT`, 또는 H2 exact opt-in의 `RESTART_CONFIRMED_NOT_PROCESSED`만 |
`REPLAY_SAME_INTENT`, `RESTART_CONFIRMED_NOT_PROCESSED`, `REFRESH_CREDENTIAL_AND_REPLAY`
`FOLLOW_DECLARED_REDIRECT`는 ordinary retry predicate를 재사용하지 않고 동일 pure-eligibility +
reason-specific `AttemptAuthorizationLease` protocol을 사용한다. `RECONCILE_SAME_OPERATION`은 prior
protected attempt를 반복하지 않고 §16.7의 registered child state로 들어가며, 각 실제 reconciliation
HTTP request는 별도 `NestedHttpAuthorizationLease`에 bind된다. 모든 state는 동일
key/fingerprint/scope와 shared root ceiling을 유지한다.
### 17.2 Backoff
Default:
- exponential backoff;
- full jitter 또는 decorrelated jitter를 명시;
- zero busy-loop 금지;
- upper cap;
- absolute deadline cap;
- retry budget cap.
예시 full jitter:
```text
cap_n = min(maxBackoff, initialBackoff * 2^n)
sleep_n = random(0, cap_n)
sleep_n = min(sleep_n, remaining - minimumAttemptBudget)
```
Random source는 test에서 deterministic injection 가능해야 한다.
### 17.3 `Retry-After`
408/429/503 등 operation이 허용한 status에서만 해석한다.
- delta-seconds와 HTTP-date만 허용하고 multiple/mixed 값은 invalid;
- malformed, negative 또는 overflow delta는 server hint를 버리고 bounded client jitter로 fallback;
- 문법상 valid하지만 과거인 HTTP-date는 `serverMinimumDelay=0`으로 두되 client jitter는 유지;
- maximum cap;
- wall-clock skew tolerance;
- remaining deadline보다 길면 현재 logical call에서는 retry하지 않음;
- provider-specific rate-reset header는 별도 parser;
- invalid header는 bounded metric, raw value log 금지.
Server hint와 client jitter 조합은:
```text
delay = max(serverMinimumDelay, clientBackoffWithJitter)
```
를 기본으로 하되 operation contract가 다르면 명시한다.
### 17.4 Retry budget
대규모 장애 때 모든 최초 호출이 N번 retry하면 outage를 증폭한다. Destination/resilience group별
retry budget을 둔다.
가능한 정책:
- token bucket;
- 성공 call 비율 기반 token replenish;
- rolling retry/original ratio cap;
- minimum reserved original-call capacity.
Metric:
```text
http.client.retry.attempts
http.client.retry.exhausted
http.client.retry.budget.rejected
http.client.presend.restarts
http.client.presend.restart.exhausted
```
Retry budget은 provider quota의 정확한 cluster-wide enforcement를 주장하지 않는다. Pod별 local
보호 장치다. Pre-send restart budget/metric은 retry budget과 분리하되 둘 다 total amplification
dashboard에 포함한다.
### 17.5 Credential refresh는 retry와 분리
401 처리 순서는 다음과 같다.
1. operation/auth profile에 등록된 invalid/expired-token status, challenge와 bounded error
contract가 정확히 일치하는지 확인한다.
2. 401 response body/connection을 drain-or-discard policy로 닫고 기존 attempt의 circuit
permission을 완료한다. Bulkhead는 반납하고 committed quota handle은 exactly once 닫되 token은
환불하지 않는다. Protected destination의 해당
401은 breaker-ignore이며 token endpoint failure와 섞지 않는다.
3. §16.6의 `REFRESH_CREDENTIAL_AND_REPLAY` pure eligibility만 평가한다. 여기서는 token을
소비하지 않는다. 조건이 하나라도 거짓이면 401을 terminal auth failure로 반환한다.
4. §17.1의 `tryAuthorizeNextAttempt`가 one-token auth-replay
`AttemptAuthorizationLease`를 정확히 한 번 만든 뒤 common deadline/cancellation/shutdown gate를
다시 확인하고 bounded single-flight refresh에 참여한다.
5. 각 waiter는 자신의 absolute execution cutoff와 cancellation token을 사용한다. Waiter가
timeout/cancel되면 shared refresh에서 detach하고 즉시 반환하며, shared refresh는 active
waiter/owner가 0일 때만 취소한다. Waiter bound를 넘으면 local admission failure다.
6. 실제 token fetch는 별도 named destination, pool, quota/bulkhead, circuit breaker와 token
operation policy를 사용한다. Protected destination의 attempt permit/connection을 보유한 채
refresh하지 않으며 자기 자신을 재귀 호출하지 않는다.
7. refresh wait 직후 common gate와 minimum attempt budget을 다시 확인한다. 성공한 새
generation의 scope/audience/tenant binding을 검증하고 원래 operation-attempt identity,
idempotency key와 payload fingerprint를 유지한다.
8. 다음 요청은 ordinary physical-attempt loop의 body-open, quota, bulkhead, circuit permission,
attempt span과 engine-start gate를 모두 새로 거친다. 정확히 한 번만 auth replay하고 두 번째
401은 refresh 없이 terminal이다.
Single-flight key는 `TokenCacheKey + challengedCredentialGeneration`이다. 여러 root call이 같은
flight에 합류할 때 실제 token network call의 owner와 deadline을 고정한다.
```text
RefreshFlight(
key,
immutableFlightDeadline,
creatorRootCallId,
creatorRefreshBudgetLease,
activeWaiters,
state
)
```
- creator election winner만 자신의 remaining nested-credential/root HTTP capacity에서 finite
token-operation attempt slice를 고정해 `creatorRefreshBudgetLease`로 flight에 이전한다. 이 slice는
상한 소유권이며 아직 wire token을 소비하지 않는다;
- `immutableFlightDeadline = min(creatorExecutionCutoff, now + tokenCallCap)`이며 joiner가 연장하지
못한다;
- 실제 child attempt마다 §9.4의 `NestedHttpAuthorizationLease(OAUTH_TOKEN)`를 하나 만들고 token
write 직전에 creator root의 shared HTTP token과 bind/commit한다. 사용하지 않은 slice capacity는
flight terminal close 때 해제하지만 이미 authorize/실행한 attempt token은 환불하지 않는다;
- joiner는 존재하지 않는 shared network attempt를 자기 root count에 중복 기록하지 않는다. 대신
자신의 auth `AttemptAuthorizationLease`, waiter admission과 후속 protected/root replay capacity를
보유해야 한다;
- creator가 cancel/detach돼도 다른 waiter가 있으면 이미 이전된 lease/deadline으로 flight가
계속된다. active waiter가 0이면 cancel하고, deadline owner를 다른 root로 바꾸거나 늘리지 않는다;
- 짧은 creator deadline으로 flight가 실패하면 joiner는 terminal refresh failure를 받고 같은 auth
lease로 새 flight를 반복 생성하지 않는다.
Refresh call의 자체 retry는 token operation이 선언한 semantics와 budget만 사용하며 protected
operation의 retry count에 합산하지 않는다. 반면 protected request의 auth replay는
`http.request.resend_count`, logical/physical call count와 total amplification에 포함한다.
최소 metric:
```text
http.client.auth.refresh.calls
http.client.auth.refresh.waiters
http.client.auth.refresh.failures
http.client.auth.replays
http.client.auth.replay.rejected
```
Raw token, client ID, tenant/user, scope의 unbounded 값과 credential generation은 metric tag에
넣지 않는다. Invalid request를 401마다 무한 재전송하지 않는다.
### 17.6 Engine hidden retry 금지
Apache:
- automatic retry strategy disabled;
- automatic redirect disabled;
- automatic auth challenge replay가 credential policy를 우회하지 않음;
- stale connection retry semantics를 검증;
- protocol upgrade/resend 관측.
JDK/reactive provider를 포함해 사후 문서화/count만으로는 충분하지 않다. Protocol event가 새
request를 요구하면 provider는 현재 attempt를 종료하고 transmission/processing evidence를 kernel에
반환한다. Kernel이 `RESTART_CONFIRMED_NOT_PROCESSED` 등 exact disposition을 만들고 새
`AttemptAuthorizationLease`, deadline/body/protected-root budget/quota/bulkhead/CB/span gate를 모두
통과한 뒤에만 다음 request를 시작한다.
Engine autonomous retry/auth/redirect/protocol resend가 이 pre-resend authorization 경계로 제어되지
않으면 반드시 disable한다. Disable할 수 없거나 callback이 실제 wire start보다 늦으면 해당
provider/profile은 release-eligible이 아니다. 보이지 않는 resend를 사후 span/count로 보정하지
않는다.
### 17.7 Hedging
R2 default는 disabled.
허용 조건:
- safe read;
- duplicate load 허용;
- distinct endpoint/connection;
- shared total deadline;
- retry/hedge combined amplification budget;
- loser active cancellation;
- provider quota 반영;
- attempt별 span;
- no mutation.
Hedging은 retry와 같은 config boolean이 아니라 R3 candidate card다.
## 18. Circuit breaker semantics
### 18.1 Default unit
Default circuit breaker는 `resilienceGroupId`별 physical attempt를 집계한다.
```text
retry loop
-> physical bulkhead
-> circuit permission lease
-> one physical attempt
-> exactly-once record/ignore/release
```
`destinationId` 하나에 모든 operation을 무조건 합치면 cheap health read와 expensive mutation이
서로 circuit을 오염시킨다. 반대로 operation마다 breaker를 만들면 state와 metric cardinality가
폭증한다. Committed bounded resilience group을 사용한다.
Circuit permission은 boolean이 아니라 `CircuitPermissionLease`로 관리한다.
```text
ACQUIRED
-> local preflight failure: RELEASED exactly once
-> immediately before provider ownership: STARTING (CAS)
STARTING
-> provider accepted handle: STARTED
-> synchronous start throw: tracker evidence로 RECORDED_SUCCESS/RECORDED_ERROR/RELEASED exactly once
STARTED
-> recordable success: RECORDED_SUCCESS exactly once
-> recordable failure: RECORDED_ERROR exactly once
-> ignored outcome: RELEASED exactly once
```
`engine.start()` 뒤에 started flag를 쓰는 순서를 금지한다. 그러면 synchronous send/failure가 flag
보다 먼저 발생해 permission을 잘못 release할 수 있다. `STARTING` 전이, transmission tracker와
provider ownership handoff를 하나의 protocol로 묶고 어느 경로든 atomic terminal state 하나만
허용한다.
Bulkhead/body/span/local preflight failure, cancellation, deadline, synchronous start failure,
half-open race도 finally 경로에서 permission을 회수한다. Resilience4j API의 ignore predicate가
permission을 어떻게 반환하는지 추측하지 않고 adapter wrapper test로 고정한다.
### 18.2 Record matrix
Default record:
- connect/DNS transient;
- response header/read timeout;
- selected 5xx;
- response truncated;
- provider overload;
- slow call threshold 초과.
Default ignore:
- application cancellation;
- shutdown;
- local admission/bulkhead/rate reject;
- caller/codec/programming defect;
- response size/media/schema contract violation;
- expected domain 404/409/412;
- 4xx config/auth error;
- SSRF/TLS policy rejection;
- circuit-open rejection 자체.
Operation-specific status mapping 이후 breaker outcome을 결정한다. Raw exception class만으로
breaker를 기록하지 않는다.
### 18.3 Slow-call policy
Failure rate와 별도로 slow-call rate를 구성할 수 있다.
- slow threshold < operation total deadline;
- body mode별 threshold 분리;
- streaming 전체 duration을 일반 JSON call과 같은 group에 넣지 않음;
- callback CPU time 포함 여부 명시;
- slow success도 capacity risk로 기록 가능.
### 18.4 Half-open
- bounded concurrent probes;
- retry disabled, ordinaryRetryCount=0, protected physical ceiling=1;
- provider quota 존중;
- representative safe operation만 probe;
- mutation을 half-open probe로 사용 금지;
- shutdown 중 probe 금지.
Automatic transition scheduler를 켜면 no-binding/disabled 상태에서 thread가 생기지 않아야 한다.
### 18.5 Logical-call breaker
일부 조직은 사용자에게 보인 논리 호출 성공률을 breaker에 반영할 수 있다. 이 경우 별도 정책 ID:
```text
physical-attempt-breaker
logical-call-breaker
```
를 사용하고 meter/span/test도 분리한다. Wrapper order의 우연한 side effect로 선택하지 않는다.
### 18.6 State와 deployment
Resilience4j breaker state는 process-local이다.
- pod마다 state가 다를 수 있음;
- rolling restart에서 reset;
- cluster-wide exact breaker 아님;
- 이 특성이 R2 availability protection에는 허용됨;
- shared distributed breaker를 위해 Redis/DB adapter에 직접 의존하지 않음.
## 19. Admission, bulkhead와 outbound quota
### 19.1 두 단계 bound
1. Logical admission:
- in-flight logical calls + backoff waiters 전체를 제한;
- bounded queue 또는 immediate reject;
- parent deadline 포함.
2. Physical attempt bulkhead:
- 실제 pool/network attempt만 제한;
- backoff 중 permit 미보유;
- per resilience group/destination.
Connection pool만으로 logical retry storm을 막을 수 없고, logical semaphore 하나를 backoff 동안
보유하면 healthy work가 starvation될 수 있다. 두 목적을 분리한다.
### 19.2 Queue
Default:
- unbounded queue 금지;
- queue capacity 명시;
- FIFO/fairness policy 명시;
- acquire timeout은 remaining deadline 이하;
- queue full과 deadline expiry 구분;
- request body를 queue 전에 대용량 materialize하지 않음;
- cancelled waiter 즉시 제거.
### 19.3 Virtual threads
Virtual thread는 blocking 비용을 낮추지만 downstream capacity를 늘리지 않는다.
필요:
- max logical concurrent;
- max protected physical attempts와 root-call total attempts;
- pool capacity;
- response-body memory budget;
- streaming connection budget;
- credential refresh bound.
예시 capacity constraint:
```text
maxBufferedInFlight * maxBufferedResponseBytes
+ maxBufferedRequestBytes
+ decoder overhead
<= allocated HTTP heap budget
```
정확한 수치는 배포 workload로 산정하고 템플릿이 임의 숫자를 성능 보장으로 제시하지 않는다.
### 19.4 Local outbound quota
Provider API quota를 보호하기 위한 local token bucket/leaky bucket을 optional로 제공할 수 있다.
- destination/operation group key만 사용;
- user/tenant high-cardinality limiter 아님;
- request cost weight 지원;
- monotonic refill;
- bounded wait 또는 reject;
- `Retry-After` local result;
- pod 수 증가 시 aggregate quota가 증가함을 명시.
정확한 조직 전체 quota가 필요하면 API gateway/provider-side quota 또는 별도 distributed
coordination capability를 사용한다. HTTP adapter가 Redis에 직접 의존하지 않는다.
### 19.5 Bulkhead와 pool 관계
권장:
```text
attemptBulkhead.maxConcurrent <= usablePoolCapacity
```
HTTP/1.1에서는 active request당 대체로 connection 하나가 필요하다. HTTP/2에서는 connection
수와 concurrent stream 수를 별도 계산한다. Pool pending queue와 adapter queue를 둘 다 크게
두어 이중 queue를 만들지 않는다.
## 20. 정확한 실행 순서와 state machine
### 20.1 Default logical call
```text
1. resolve registered operation/destination and dependency DAG
2. validate application request, auth profile and stable operation-attempt identity
3. compute absolute effective deadline and finite root-call amplification budget
4. acquire logical admission
5. encode/freeze buffered body or prepare reopenable source
6. enter explicit attempt state machine; do not freeze a credential generation for the whole call
7. release logical admission
8. map kernel result to application result
```
Logical preflight validates the auth profile/reference only. Current credential generation is selected for
each attempt after backoff and before protected attempt resources are held. Network credential refresh uses the
separate nested dependency path. Final credential injection/signing happens only after final URI/header/body
bytes are fixed and immediately before engine ownership transfer.
### 20.2 Physical attempt loop
```text
nextDisposition = INITIAL_ATTEMPT
nextAuthorization = INITIAL
physicalAttemptOrdinal = 0
while a disposition can start another protected request:
check common cancellation/shutdown/deadline gate
require protected physical ceiling and root-call HTTP budget have capacity
if nextDisposition is not INITIAL_ATTEMPT:
evaluate pure eligibility against semantics/body/identity/evidence/reason capacity
if ineligible -> return mapped terminal result without consuming a reason token
if nextDisposition is RESTART_CONFIRMED_NOT_SENT or RETRY_SAFE_READ:
wait bounded backoff or valid Retry-After without holding attempt permit/connection
recheck common gate and pure eligibility
nextAuthorization = tryAuthorizeNextAttempt(nextDisposition) exactly once
if authorization failed -> return reason-budget rejection
if nextDisposition is REFRESH_CREDENTIAL_AND_REPLAY:
join/create bounded RefreshFlight using the authorization lease
validate refreshed generation scope and detach waiter exactly once
if nextDisposition is FOLLOW_DECLARED_REDIRECT:
resolve hop target and revalidate method/body/origin/DNS/credential policy
if nextDisposition is RESTART_CONFIRMED_NOT_PROCESSED:
revalidate exact H2 processing evidence and protocol-restart opt-in
recheck common gate; carry the same nextAuthorization lease forward
select current credential generation; perform any network refresh only through nested dependency
recheck common gate
open initial body handle; on ordinal > 0 open a fresh identical buffered/reopenable body
recheck common gate
acquire uncommitted local-quota reservation with absolute execution cutoff
recheck common gate
acquire physical-attempt bulkhead with absolute execution cutoff
recheck common gate
acquire circuit-breaker permission lease without blocking past cutoff
recheck common gate
finalize URI/header/body metadata; validate generation expiry/scope and inject/sign credential
recheck common gate immediately before engine ownership transfer
on any refresh/hop/protocol revalidation, body-open, quota, bulkhead, CB, credential-signing,
local preflight or common-gate exit after authorization but before physical reservation:
abort nextAuthorization exactly once if its state is AUTHORIZED
release every acquired body/CB/bulkhead/uncommitted-quota resource in reverse order
engine start count = 0; return the exact mapped local/cancellation/deadline result
physicalReservation = atomically tryReserve one protected slot + one root HTTP-request slot
if reservation failed:
abort nextAuthorization if AUTHORIZED; release CB/bulkhead/uncommitted quota/body in reverse order
engine start count = 0; return amplification-budget rejection
prepare CLIENT span context, ordinal and one AttemptTerminalCoordinator before provider callback is possible
coordinator winner = OPEN -> RESPONSE_WON | FAILURE_WON | CANCELLATION_WON
coordinator cleanup = UNCLAIMED -> RUNNING -> DONE
atomically race pre-start cancellation gate against CB ACQUIRED -> STARTING handoff
if cancellation won before STARTING:
release uncommitted physicalReservation and quota reservation
abort nextAuthorization if AUTHORIZED; release CB/bulkhead/body; engine start count = 0
return RETURN_CANCELLED(reason) because transmission is exact NOT_SENT
if STARTING won:
atomically transfer cancellation token/handle + transmission tracker to provider
bind nextAuthorization to the ordinal when it is an authorization lease; record INITIAL marker otherwise
commit physical/root reservation and increment physicalAttemptOrdinal
commit local-quota token; it is consumed and never refunded
start one CLIENT span and invoke cancel-aware engine start with remaining phase budgets
if cancellation was already signaled, provider observes pre-cancelled token before first write
provider marks STARTED or reports synchronous start failure through the same tracker
all provider response/body/failure callbacks and cancellation submit events to the same coordinator
on response headers while winner is OPEN:
classify status/header/framing and choose bounded success/error decoder before body consumer
if exact VALID_HEADERS_ONLY outcome is authoritative:
tryWin RESPONSE_WON immediately with immutable header semantic evidence
run bounded drain/discard only as coordinator-owned cleanup; it cannot change the winner
else:
register exactly one coordinator-owned bounded body consumer
if required body/decode/semantic validation completes:
finalize ResponseIntegrity and ResponseSemanticClass; tryWin RESPONSE_WON
if body/framing/decode fails before an authoritative outcome:
preserve NONE_OR_INVALID + failure evidence; tryWin FAILURE_WON
on provider transport failure:
preserve absent/partial response, processing and transmission evidence; tryWin FAILURE_WON
on caller/deadline/shutdown cancellation:
preserve exact cancellation outcome and current evidence; tryWin CANCELLATION_WON
after one winner is visible:
coordinator claims exactly one cleanup finalizer
losing callbacks/body tasks relinquish body and cleanup ownership and observe the cleanup cancel token
preserve winner semantic class, processing evidence and monotonic transmission evidence
close/cancel exact resources
complete STARTING/STARTED circuit permission once from tracker + mapped outcome
release bulkhead; release only uncommitted quota or close committed handle without refund
finish span before any between-attempt wait
mark coordinator cleanup DONE; resolve exhaustive AttemptDisposition from the immutable winner evidence
if disposition is RETURN_COMPLETED/RETURN_DECLARED_REJECTION -> return mapped outcome
if disposition is RETURN_CANCELLED -> return cancellation reason
if disposition is RETURN_INDETERMINATE -> return receipt
if disposition is RETURN_PERMANENT_FAILURE -> return mapped permanent failure
if disposition is RECONCILE_SAME_OPERATION:
enter §16.7 bounded reconciliation child state; acquire no protected-attempt lease here
each child wire request obtains its own NestedHttpAuthorizationLease and root token
if child state returns terminal/receipt -> return it
if child state returns REPLAY_SAME_INTENT -> set nextDisposition; continue
if disposition is REPLAY_SAME_INTENT
or REFRESH_CREDENTIAL_AND_REPLAY
or FOLLOW_DECLARED_REDIRECT
or RESTART_CONFIRMED_NOT_PROCESSED:
require previous response/body, breaker permission, bulkhead and quota handle already closed
preserve exact identity/evidence required by disposition
set nextDisposition; set nextAuthorization = NONE; continue
evaluate ordinary pure eligibility without acquiring a token
if ineligible -> return mapped rejection/failure
set nextDisposition; set nextAuthorization = NONE; continue
```
`INITIAL_ATTEMPT`는 retry disposition이 아니며 ordinary reason token을 소비하지 않는다. 다만
실제 initial engine handoff도 protected/root shared physical token을 하나 소비한다. 후속 state는
`AttemptAuthorizationLease`를 정확히 하나 carrying하며 같은 disposition에서 다시 acquire하지
않는다.
`SINGLE_USE_SOURCE`는 ordinal 0에서만 열 수 있다.
모든 wait API는 복사된 상대 timeout이 아니라 같은 absolute `executionCutoff`와 cancellation
token을 받는다. Wait가 끝난 뒤 gate를 다시 확인하지 않고 다음 resource/network side effect를
시작하지 않는다. Gate failure면 body handle, circuit permission, bulkhead와 quota reservation handle을 획득
역순으로 exactly-once 정리하고 아직 bind되지 않은 `AttemptAuthorizationLease``ABORTED`로 닫는다.
Uncommitted quota만 반환하고 committed provider-quota token은 어떤 결과에서도 환불하지 않는다.
Credential refresh는 previous protected attempt의 resource를 모두 반납한 뒤에만 기다린다.
Refresh waiter cancellation은 shared refresh ownership과 분리하고, refresh 성공 직후에도 cutoff가
지났으면 새 protected request를 시작하지 않는다. Token endpoint call은 별도 logical call이며
그 failure를 protected destination breaker에 기록하지 않는다.
Engine 내부도 pool/stream lease, DNS, connect, TLS와 request-write phase 사이에서 remaining
budget/cancellation을 다시 확인한다. 특히 pool/DNS wait가 cutoff 전에 시작됐다는 이유로
cutoff 뒤 새 connection, TLS handshake나 request transmission을 시작하지 않는다.
Engine handoff에는 raw mutable counter가 아니라 parent-scoped `NestedHttpAuthorizationBroker`
함께 전달한다. Cold connection에서 HTTP CONNECT가 필요하거나 TLS validation이 named HTTP
OCSP/CRL lookup을 요구하면 provider는 각 wire request 전에 broker에서 exact child lease를 얻고,
별도 child cap + shared root token + cancellation/start handoff를 통과해야 한다. Authorization
failure는 origin request를 보내지 않고 해당 proxy/TLS failure로 종료한다. Provider/JVM이 broker
밖에서 implicit CONNECT 또는 revocation HTTP request를 만들 수 있는 profile은 금지한다.
### 20.3 Mermaid sequence
```mermaid
sequenceDiagram
participant U as Use case
participant A as Upstream adapter
participant K as HTTP kernel
participant R as Retry loop
participant C as Circuit breaker
participant B as Attempt bulkhead
participant E as Engine/pool
participant D as Dependency
U->>A: feature-specific request + budget
A->>K: registered operation
K->>K: validate, deadline, logical admission
loop bounded physical attempts
K->>R: next attempt decision
R->>B: acquire
B->>C: permission lease
C->>E: execute with remaining budget
E->>D: physical HTTP request
D-->>E: response/failure
E-->>C: typed attempt result
C-->>B: exact-once record/ignore/release
C-->>R: attempt outcome
end
R-->>K: completed/rejected/indeterminate
K-->>A: kernel result
A-->>U: application result
```
### 20.4 Resource-release order
Attempt 종료:
1. stop body producer/consumer;
2. close response/entity stream;
3. cancel request execution if incomplete;
4. mark connection reusable 또는 discard according to framing/cancel evidence;
5. release pool lease;
6. release uncommitted local-quota reservation; already committed wire-attempt token은 환불하지 않음;
7. derive final mapped attempt outcome;
8. complete/release `CircuitPermissionLease` exactly once;
9. release attempt bulkhead;
10. finish attempt observation;
11. resolve exhaustive attempt disposition;
12. finally release logical admission.
Retry decision 전에 prior response와 connection lifecycle을 정리한다.
### 20.5 Failure during cleanup
- cleanup failure가 primary failure를 덮지 않음;
- suppressed diagnostic은 secret-safe class/code만;
- incomplete entity는 connection discard;
- double-close idempotent;
- normal cleanup은 caller deadline reserve로 bounded;
- caller deadline을 넘으면 quarantine + bounded orphan reaper;
- engine 미시작 lease는 `releasePermission`, started lease는 mapped terminal outcome으로 exactly-once
완료;
- resource leak metric/alert;
- unknown cleanup state에서 pool reuse 금지.
## 21. Transport engine와 provider design
### 21.1 Engine SPI
Spring `RestClient` 자체를 provider로 부르지 않는다. 이는 synchronous API/codec facade이고 실제
network semantics는 request factory/engine이 결정한다.
개념적 SPI:
```java
interface HttpTransportEngine extends AutoCloseable {
HttpAttemptHandle start(HttpAttemptRequest request, HttpAttemptObserver observer);
HttpEngineDescriptor descriptor();
}
```
`HttpAttemptHandle`:
```text
awaitHeaders(deadline)
response()
cancel(reason)
transmissionEvidence()
completion()
```
`HttpAttemptResponse`:
```text
status
validated bounded header view
protocol
remote address evidence
wire body stream
trailers completion
close/discard
```
Engine type은 adapter 내부에만 존재한다.
### 21.2 Provider matrix
| Provider ID | 용도 | 초기 level | R2 제약 |
| --- | --- | --- | --- |
| `apache-hc5-classic` | imperative JSON/H1/stream callback | R2 candidate/default | hard cancel, pool, DNS, TLS, close evidence 통과 |
| `apache-hc5-async` | stronger cancellation, HTTP/2, async streaming | R2 candidate | codec bridge와 callback lifecycle evidence |
| `jdk-httpclient` | dependency-minimal H1/H2 | R1 | explicit pool lease/capacity/DNS evidence 부족 |
| `reactor-netty` | reactive pipeline/streaming | R1 candidate | reactive contract와 cancellation/backpressure suite 분리 |
| `http3-quic` | future HTTP/3 | R0 | 별도 R3 topology/security card |
Provider ID가 classpath detection 결과로 바뀌지 않는다.
### 21.3 초기 reference engine
초기 구현은 `apache-hc5-classic` + Spring `RestClient`를 reference candidate로 선택한다.
이유:
- 현재 imperative/virtual-thread template과 정렬;
- Spring message converter와 HTTP Service Interface 활용 가능;
- pool total/per-route/lease timeout;
- custom DNS resolver;
- TLS/mTLS/proxy;
- idle/expired eviction;
- engine lifecycle;
- request hard cancellation을 검증할 seam.
단, classic provider가 total deadline 때 실제 I/O와 connection을 확실히 취소하지 못하면
`httpclient-static-buffered``HRES-STATIC-HARD-CANCEL` evidence를 통과하지 못하며 R2로
표기하지 않는다. 이 경우
`apache-hc5-async`를 reference provider로 승격한다. 문서 선택이 evidence를 대신하지 않는다.
### 21.4 Exact engine selection
Spring Boot `ClientHttpRequestFactoryBuilder.detect()`는 classpath에 따라 HttpComponents, Jetty,
Reactor, JDK, Simple 순으로 선택할 수 있다. Template R2는 classpath 변화가 runtime engine을
바꾸게 두지 않는다.
```text
provider=apache-hc5-classic
-> exact HttpComponents builder
provider=jdk-httpclient
-> exact JDK builder
```
Selected provider dependency가 없으면 startup failure다. 다른 SDK가 transitive로 들어와도
provider가 바뀌지 않는다.
### 21.5 Boot-configured `RestClient.Builder`
Spring Boot 4는 preconfigured prototype `RestClient.Builder`에 message converters,
appropriate request factory와 observation customization을 제공한다.
구현 원칙:
- injected prototype builder를 destination마다 clone;
- exact request factory/engine을 명시적으로 교체;
- Boot observation registry/customizer를 보존;
- destination-specific base URI와 status/codec policy 적용;
- mutable builder를 destination 간 재사용하지 않음;
- `RestClient.create()`/raw builder로 auto-configuration을 우회하지 않음;
- configuration test가 expected interceptor/observation/codec set을 snapshot.
현재 모든 `RestClient.Builder` bean을 차단하는 `OutboundHttpTimeoutEnforcer`는 이 구조와
양립하지 않는다. 이를 제거하고 architecture/build gate와 registered factory 검증으로 대체한다.
### 21.6 Apache client hardening
Reference configuration은 최소 다음을 명시한다.
- `PoolingHttpClientConnectionManager`;
- `maxConnTotal`, `maxConnPerRoute`;
- connection lease timeout;
- connect timeout;
- response header timeout;
- socket/response idle semantics;
- connection TTL;
- validate-after-inactivity;
- idle/expired eviction;
- DNS resolver;
- TLS socket strategy;
- proxy route planner;
- user-token/connection state policy;
- automatic retry disabled;
- automatic redirect disabled;
- cookie management disabled;
- automatic content compression disabled unless bounded layer가 소유;
- default credentials/auth caching disabled unless named profile owns;
- hard cancellation policy;
- `Expect: 100-continue` operation-specific;
- finite max redirects even though default disabled;
- close ownership.
Apache default는 안전 계약이 아니다. 예를 들어 connection request timeout과 redirect default가
library release에서 바뀌거나 매우 클 수 있으므로 모두 typed setting으로 고정한다.
### 21.7 JDK provider limits
JDK Java 21 provider를 유지할 경우:
- `HttpClient` handle을 destination runtime이 보유하고 close;
- explicit executor와 lifecycle;
- redirect `NEVER`;
- version fixed;
- `ProxySelector` fixed/none;
- `CookieHandler` none;
- `Authenticator` none unless profile;
- SSL context/parameters explicit;
- request timeout과 outer total deadline distinction;
- `sendAsync` future cancellation evidence;
- `jdk.httpclient.*` implementation property 사용 여부를 descriptor에 표시;
- implementation property를 per-destination pool guarantee로 과장하지 않음.
JVM-wide property는 여러 destination의 isolation을 제공하지 않는다.
### 21.8 Reactive provider
`reactor-netty` 또는 WebClient provider는 다음 조건에서만 활성화한다.
- `spring-webflux`/Reactor 타입은 adapter 내부;
- application port가 synchronous이면 blocking bridge의 cancellation/context evidence;
- event-loop에서 `.block()` 금지;
- connection provider와 loop resource lifecycle;
- max connections/pending acquire/idle/lifetime;
- response release on cancel/error;
- context propagation;
- backpressure;
- H2 stream capacity;
- 별도 readiness card.
Classpath에 WebFlux가 있다는 이유만으로 default를 바꾸지 않는다.
## 22. Connection pool와 capacity
### 22.1 Pool isolation key
기본은 destination별 pool이다. 다음이 모두 같을 때만 explicit pool group으로 공유할 수 있다.
- scheme/authority;
- proxy route;
- TLS trust;
- mTLS client identity;
- DNS/address policy;
- protocol policy;
- credential connection-affinity requirement;
- lifecycle/rotation generation.
Bearer token만 다른 요청은 같은 TLS pool을 공유할 수 있지만 mTLS identity가 다르면 절대 공유하지
않는다. HTTP/2 origin coalescing도 default disabled다.
### 22.2 Required settings
```text
maxConnectionsTotal
maxConnectionsPerRoute
maxPendingAcquires
poolAcquireTimeout
connectionTtl
idleTimeout
validateAfterInactivity
defaultKeepAliveCap
evictionInterval
gracefulCloseTimeout
```
모든 값:
- finite;
- positive/zero semantics 명시;
- cross-field validation;
- selected protocol과 consistency;
- inactive destination에서는 무시가 아니라 dead-setting detection.
### 22.3 HTTP/1.1 capacity
대체로 한 active request가 connection 하나를 점유한다.
```text
usableConcurrentAttempts
<= min(maxConnectionsPerRoute, attemptBulkheadMax)
```
Streaming download는 callback 전체 동안 lease를 보유한다. 일반 JSON pool과 장시간 streaming
pool을 분리하지 않으면 작은 호출이 starvation될 수 있다.
### 22.4 HTTP/2 capacity
다음 축이 별도다.
```text
connections
max concurrent streams per connection
locally configured stream cap
server SETTINGS limit
pending stream acquires
```
`connections * streams`를 무조건 usable capacity로 계산하지 않는다. Flow control, large body,
server SETTINGS, GOAWAY와 head-of-line at application layer를 고려한다.
### 22.5 Pool acquire
- adapter logical queue 이후 attempt permit을 얻고 pool lease 요청;
- acquire timeout = min(profile cap, remaining deadline);
- pending acquire count bound;
- timeout이면 `POOL_ACQUIRE_TIMEOUT`;
- cancelled waiter 제거;
- mutation request는 아직 전송되지 않았으므로 `NOT_SENT`;
- same logical call에서 즉시 다시 pool queue에 들어가는 retry는 default 금지;
- saturation metric/alert.
### 22.6 Lifetime, idle와 DNS
권장 관계:
```text
connectionTtl <= approvedDnsPinLifetime
idleTimeout <= upstream/load-balancer idle timeout safety margin
```
DNS TTL이 짧아도 이미 열린 keep-alive connection은 자동으로 새 IP로 이동하지 않는다.
Connection TTL과 generation drain이 DNS rollout policy에 포함되어야 한다.
Lifetime jitter를 사용해 모든 pod/connection이 동시에 reconnect하지 않도록 할 수 있다. Jitter
range도 bounded/deterministic test 대상이다.
### 22.7 Keep-Alive
Server `Keep-Alive` hint를 무제한 신뢰하지 않는다.
```text
effectiveKeepAlive = min(serverHintIfValid, clientKeepAliveCap, connectionRemainingTtl)
```
Invalid/huge hint는 cap하고 low-cardinality metric을 남긴다.
### 22.8 Validation과 stale connection
- inactivity 후 validate;
- stale connection first-use failure;
- engine automatic retry disabled 상태의 동작;
- safe read의 retry decision은 kernel 소유;
- mutation은 stale connection이라는 추측만으로 blind retry하지 않음;
- repeated stale failures는 pool/DNS/load-balancer runbook 신호.
### 22.9 Resource/memory budget
Per destination capacity input:
```text
max logical calls
max protected physical attempts/root-call total attempts
max buffered request/response/error bytes
max streaming calls
max pending acquires
max auth refresh waiters
connection buffers/TLS overhead
```
Template는 arbitrary high default를 주지 않는다. Deployment overlay가 workload/SLO/downstream
quota에서 산정하고 readiness validator가 machine/container bounds와 모순을 검사한다.
### 22.10 Pool observability
필수:
```text
leased
available
pending
max
acquire duration
acquire timeout/reject
created/closed/expired/idle-evicted
reuse
cancel-discard
generation
```
Pool route나 remote IP를 unbounded tag로 쓰지 않는다. Destination/pool-profile ID만 tag한다.
## 23. DNS, address selection와 service discovery
### 23.1 DNS policy profile
```text
FIXED_PUBLIC
FIXED_PRIVATE
KUBERNETES_SERVICE
SERVICE_MESH_LOOPBACK
EGRESS_PROXY_ENFORCED
DYNAMIC_PUBLIC_FETCH (separate capability)
```
각 profile은 allowed/forbidden address, resolver, cache, stale policy, connection TTL과 readiness
evidence가 다르다.
### 23.2 Resolve-validate-connect binding
SSRF 방어에서 다음 TOCTOU를 금지한다.
```text
validate hostname resolution A
then engine independently resolves B
then connect B
```
필수 흐름:
```text
canonical hostname
-> designated resolver
-> all A/AAAA answers
-> canonical binary address classification
-> policy filter
-> only approved addresses handed to connection operator
-> TLS SNI/hostname verification uses original hostname
```
Engine이 filter 뒤 다시 resolve하면 해당 provider는 address-pinning card를 통과하지 못한다.
### 23.3 Address classification
Profile에 따라 최소 다음을 명시적으로 분류한다.
- unspecified;
- loopback;
- link-local;
- private;
- carrier-grade NAT;
- multicast;
- documentation/benchmark;
- IPv4-mapped IPv6;
- configured NAT64 well-known/network-specific prefix 안의 IPv4-embedded IPv6;
- 6to4, Teredo와 IANA special-purpose IPv6 transition address;
- IPv6 unique-local/link-local;
- cloud metadata ranges;
- organization internal CIDR;
- exact approved public/private ranges.
String prefix/regex가 아니라 parsed binary address와 prefix math를 사용한다. Configured
translation prefix에서는 embedded IPv4를 추출해 동일 IPv4 CIDR/metadata/private 정책을 다시
적용한다. Dynamic public fetch에서 translation prefix를 신뢰성 있게 확정하지 못하면 NAT64를
fail-closed한다.
`localhost`, decimal/octal/hex-like IPv4 ambiguity, shortened IPv4, trailing dot, mixed-case IDN,
IPv6 zone ID, DNS CNAME chain을 security test에 포함한다.
### 23.4 Mixed answer policy
Public profile에서 하나의 hostname이 public과 forbidden address를 함께 반환하면:
- default: 전체 resolution reject;
- 일부 safe address만 선택하는 mode는 DNS poisoning/partial outage 의미를 명시한 별도 profile;
- alert와 sanitized evidence;
- raw tenant/request hostname tag 금지.
### 23.5 TTL와 cache
Java name resolution은 positive/negative/stale cache를 가질 수 있고 일부 default가
implementation-specific다. R2는 JVM default를 암묵적으로 사용하지 않는다.
정의:
- positive TTL min/max;
- negative TTL;
- stale-if-DNS-error 허용 여부와 max stale;
- CNAME chain expiry;
- refresh jitter;
- resolution timeout;
- max concurrent resolutions;
- cache entry bound;
- config/DNS change generation.
Fixed internal service에서 previously validated stale address를 잠깐 쓰는 availability policy는
가능하지만 public/dynamic egress에서 stale을 허용하면 address ownership 변화 위험이 있다.
### 23.6 DNS timeout
Platform resolver call이 interruption/deadline을 보장하지 않으면 별도 bounded resolver executor,
resolver library 또는 proxy/service-discovery provider가 필요하다.
- executor queue bound;
- resolution deadline;
- abandoned task bound;
- resolver shutdown;
- DNS storm coalescing;
- negative cache;
- no platform thread leak.
Timeout wrapper만 반환하고 blocking resolver task를 계속 누적시키지 않는다.
### 23.7 Address selection
- approved A/AAAA 전체를 순환/정책에 따라 사용;
- one bad address가 전체 deadline을 소모하지 않도록 per-address connect cap;
- IPv4/IPv6 preference와 Happy Eyeballs 지원 여부;
- address failover도 physical connect attempt로 관측;
- mutation request 전 connect failure는 `NOT_SENT`;
- connection establishment 뒤 resend는 operation retry policy 소유.
### 23.8 Kubernetes/service discovery
Kubernetes Service profile:
- expected private CIDR;
- service FQDN;
- headless/ClusterIP 구분;
- readiness/pod churn과 connection TTL;
- DNS TTL;
- multi-address load spread;
- zone/locality awareness optional;
- mesh sidecar interception 여부.
Short service name/search suffix에 의존하지 않고 canonical name을 사용한다.
### 23.9 Service mesh
Sidecar가 traffic을 loopback으로 redirect하면 application이 본 peer address만으로 origin
security를 증명할 수 없다.
Descriptor:
```text
egress_enforcement=SERVICE_MESH
mesh_identity_policy_revision
direct_egress_blocked=true
proxy_tls_mode
```
Mesh policy evidence와 application destination registry를 둘 다 요구한다. Mesh가 있으니
application URI/credential/redirect 검증을 제거하지 않는다.
## 24. SSRF, redirect와 proxy security
### 24.1 Threat boundary
공격 입력:
- path/query value;
- upstream redirect;
- pagination link;
- configured DNS;
- compromised external response;
- proxy environment;
- IDN/encoding ambiguity;
- user-controlled webhook/fetch URL;
- poisoned service discovery.
보호 대상:
- cloud metadata;
- loopback/admin endpoints;
- cluster control plane;
- internal service;
- Unix/file/local schemes;
- credentials in cross-origin redirect;
- proxy credentials;
- network topology.
### 24.2 Fixed destination baseline
- exact scheme/host/port;
- relative route only;
- no userinfo;
- no fragment;
- no raw Host override;
- resolved IP policy;
- network policy/security group;
- redirect disabled;
- system proxy disabled;
- no direct fallback from proxy.
Application validation과 network egress policy를 함께 사용한다.
### 24.3 Redirect manual state machine
이 절은 future redirect card의 실행 계약이다. 현재 canonical card set에는 redirect-follow가
없으므로 fixed-destination R2에서는 engine과 kernel redirect를 모두 disable하고, follow mode를
설정하면 startup에서 거절한다.
Engine auto redirect를 끄고 kernel이 처리한다.
Hop마다:
1. 3xx가 operation에 허용되는지 확인;
2. `Location` size/syntax/relative resolution;
3. hop/loop bound;
4. method/body rewrite rule;
5. body replayability;
6. scheme downgrade 금지;
7. target origin allowlist;
8. fresh DNS/address validation;
9. cross-origin credential/header stripping;
10. remaining deadline/retry amplification budget;
11. new physical attempt span.
301/302가 POST를 GET으로 바꾸는 engine default에 business mutation을 맡기지 않는다.
307/308이 method를 보존해도 one-shot body는 replay하지 않는다.
Declared 3xx는 response/connection, CB permission, bulkhead와 quota handle을 먼저 닫은 뒤 §16의
`FOLLOW_DECLARED_REDIRECT`를 만든다. 다음 loop에서 redirect-hop
`AttemptAuthorizationLease`를 한 번 획득하고 protected/root physical ceiling, fresh DNS/SSRF,
credential/body와 deadline gate를 다시 통과한다. Redirect hop도 새 ordinal/CLIENT span과
`http.request.resend_count`를 가진다. Current card set은 이 disposition을 생성하지 않고 startup과
call에서 fail-closed한다.
### 24.4 Credential stripping
Origin이 달라지면 default 제거:
```text
Authorization
Cookie
API key header
Idempotency-Key
signature headers
client correlation containing protected data
```
Same-origin이라도 operation redirect contract가 없는 credential replay는 금지한다.
### 24.5 Proxy
Proxy profile:
- exact proxy scheme/host/port;
- proxy TLS 여부;
- proxy auth secret ref;
- CONNECT allowed target;
- local DNS 또는 remote DNS;
- target SNI/hostname verification;
- no `HTTP_PROXY`/`HTTPS_PROXY`/`NO_PROXY` ambient inheritance unless explicit;
- direct fallback disabled;
- proxy readiness;
- proxy audit/egress policy revision.
Proxy가 remote DNS를 수행하면 application의 A/AAAA 검증을 R2 evidence로 주장하지 않는다.
`EGRESS_PROXY_ENFORCED` DNS profile을 선택하면 `httpclient-egress-proxy` card가 proxy-side
address filtering을 증명해야 한다.
Proxy 자체도 숨은 transport 설정이 아니라 first-class named dependency다. Proxy
host/DNS/address/TLS/auth generation은 exact profile tuple과 readiness evidence를 가지며 parent
origin profile이 그 fingerprint를 참조한다. Proxy dependency cycle, ambient fallback과 direct
fallback을 startup에서 거절하고 shared proxy client/pool은 owner/reference count로 닫는다.
Raw-capture contract test는 `Proxy-Authorization`이 proxy hop에만 존재하고 CONNECT tunnel 내부
origin request, redirect와 telemetry에 절대 나타나지 않음을 검증한다.
새 tunnel에 HTTP CONNECT wire request가 필요하면 provider는 parent broker에
`tryAuthorizeNestedHttpRequest(PROXY_CONNECT, proxyProfileFingerprint)`를 호출한다. Lease는
`maxProxyConnectRequestsPerRootCall` token을 한 번 소비하고 CONNECT write 직전 shared root HTTP
token과 함께 bind한다. Proxy DNS/TCP/TLS만 실패했거나 기존 tunnel을 재사용해 CONNECT를 보내지
않으면 CONNECT lease를 만들지 않는다. Child lease/root reservation/cancellation race를 통제할 수
없는 engine proxy mode와 ambient proxy는 release-eligible이 아니다.
### 24.6 Dynamic public fetch
별도 high-risk capability다.
- no credentials;
- GET/HEAD only;
- public addresses only;
- redirect exact revalidation;
- content type/size/compression cap;
- malware/content scan;
- no cookies;
- no auth challenge;
- separate pool/quota;
- egress proxy recommended;
- audit;
- downloaded content를 trusted data로 바로 사용하지 않음.
Target authority 자체가 attacker-controlled이므로 이 card는 standard HTTP auto-instrumentation을
억제한다. Default telemetry는 fixed `destination_id=untrusted-fetch`, bounded operation/outcome을
사용하는 INTERNAL span/project metric뿐이며 `server.address`, `url.full`, `url.path/query`
`network.peer.address`를 SDK에 넣지 않는다. Target-level telemetry가 반드시 필요하면 별도
confidential pipeline, retention/access policy와 cardinality cap을 exact profile evidence로
승격해야 하며 없으면 fail-closed한다.
일반 outbound HTTP card 선택만으로 dynamic fetch가 활성화되지 않는다.
## 25. TLS, mTLS와 certificate lifecycle
### 25.1 TLS profiles
```text
public-system-trust
private-ca
mutual-tls
service-mesh-plaintext-to-sidecar
local-test
```
Profile ID는 destination에 고정되며 caller가 선택하지 않는다.
### 25.2 Required TLS controls
- HTTPS required in prod except approved profile;
- endpoint identification/hostname verification;
- original hostname SNI;
- supported TLS protocol allowlist;
- algorithm/cipher constraints;
- trust source;
- client key source if mTLS;
- certificate validity;
- handshake timeout;
- ALPN policy;
- session resumption policy;
- trust/key material version;
- secret-safe errors.
Trust-all manager, accept-all hostname verifier, expired cert ignore는 hard startup/test failure다.
### 25.3 Spring SSL Bundles
Spring Boot named `SslBundle`을 trust/key material source로 재사용한다.
- destination config에는 bundle ID만;
- secret literal/password를 repository YAML에 넣지 않음;
- exact bundle existence/type validation;
- engine-specific SSL context/socket strategy 생성;
- bundle ID low-cardinality metadata;
- key/trust fingerprint raw value log 금지.
Spring Boot의 bundle file reload가 모든 client consumer를 자동 재구성한다는 보장은 없다.
공식 문서는 reload-compatible component를 제한적으로 열거한다. HTTP client는 별도 generation
rotation을 구현하고 검증한다.
### 25.4 Rotation과 emergency revocation
정상 변경과 compromise/revocation을 같은 drain 정책으로 처리하지 않는다.
`NORMAL_ROLLOVER`:
```text
detect/receive new material version
-> validate chain/key match/expiry/hostname policy
-> build new engine + pool generation
-> optional safe probe
-> atomic registry swap
-> new calls use new generation
-> old in-flight calls drain
-> bounded timeout
-> cancel/close old pool
```
`EMERGENCY_REVOKE`:
```text
verified compromise/revocation signal
-> atomically block old-generation admission
-> invalidate matching token cache, TLS sessions and pool generation
-> cancel in-flight calls when the registered emergency policy requires it
-> forbid old-generation fallback and rollback
-> close old resources with bounded quarantine/reaper
-> remain NOT_READY until a validated non-revoked generation is published
```
Emergency event source/authenticity, affected generation/scope, in-flight cancellation policy와 audit
receipt를 registry/runbook가 소유한다. Availability를 위해 revoked credential/trust anchor를 계속
사용하지 않는다. In-place mutable SSLContext가 기존 connection을 새 certificate로 바꾼다고
가정하지 않는다.
### 25.5 mTLS
- client certificate/key pair validation;
- expected key alias;
- permitted subject/SAN policy if required;
- distinct identity => distinct pool;
- private key file permission;
- secret rotation;
- server request가 client cert를 실제 요구하는 integration test;
- expired/not-yet-valid/wrong CA/wrong key;
- dual-certificate overlap rollout;
- certificate expiry alert.
### 25.6 Revocation과 certificate-directed egress
OCSP/CRL은 environment policy에 따라 optional이지만 mode를 명시한다.
- off/soft-fail/hard-fail;
- stapling support;
- cache/concurrency/response count와 byte cap;
- privacy;
- outage failure mode.
R2 baseline은 certificate AIA/CRLDP URI, implicit OCSP responder discovery와 LDAP/FTP retrieval을
자동으로 따라가지 않도록 비활성화한다. 인증서가 지시한 URI는 신뢰된 outbound destination이
아니며 JVM default network timeout은 capability total deadline이 아니다.
Network revocation lookup을 활성화하려면 responder를 first-class named revocation dependency로
등록한다.
- exact HTTPS scheme/host/port allowlist와 pinned DNS/SSRF policy;
- redirect, cookie, ambient proxy와 origin credential 전파 금지;
- responder TLS/auth가 필요하면 별도 exact profile;
- handshake parent cutoff 안의 finite phase deadline;
- bounded response bytes, certificate/CRL count, cache TTL와 concurrent lookups;
- no direct fallback, no certificate-provided dynamic host;
- startup에서 JVM-global PKI/security property effective value 검증;
- lifecycle/readiness/evidence fingerprint에 responder profile 포함.
Cache miss로 named HTTP OCSP/CRL wire request가 필요하면 TLS/provider integration은 parent broker에
`tryAuthorizeNestedHttpRequest(REVOCATION, responderProfileFingerprint)`를 호출한다. 각 request는
`maxRevocationHttpRequestsPerRootCall` token과 shared root HTTP token을 write 직전에 bind하며,
pre-bind deadline/cancel/validation failure에서는 network side effect 없이 lease를 abort한다. Valid
cache hit/stapled evidence에는 token을 쓰지 않는다. Trust manager/JVM이 이 hook 밖에서 responder를
호출할 수 있으면 network revocation mode를 활성화하지 않는다.
HTTP 200은 revocation success evidence가 아니다. OCSP는 response signature와 authorized responder
chain, exact CertID/issuer/serial, status `GOOD|REVOKED|UNKNOWN`, `producedAt`/`thisUpdate`/
`nextUpdate`, configured max-age/clock skew와 nonce/replay policy를 검증한다. `UNKNOWN`, stale,
wrong-responder와 replayed old `GOOD`을 success로 승격하지 않는다. CRL은 issuer/signature,
distribution-point와 issuing-distribution-point scope, base/delta CRL number, `thisUpdate`/
`nextUpdate`, indirect-CRL support policy와 freshness를 검증한다.
Internal cache/single-flight key는 raw telemetry가 아닌 다음 bounded identity로 구성한다.
```text
revocationProfileFingerprint
+ responderProfileFingerprint
+ issuerNameHash/issuerKeyHash
+ certificateSerialOrCrlScope
+ base/delta revision
```
악성 certificate가 loopback, metadata, private CIDR 또는 oversized CRL/AIA URI를 가리켜도 network
side effect가 0인지 검증한다. Soft-fail은 policy가 허용한 responder outage만 의미하며 SSRF reject,
malformed/revoked evidence를 soft success로 바꾸지 않는다. “JVM default”라고만 두고 revocation
보장을 주장하지 않는다.
### 25.7 TLS failure semantics
- hostname/trust/expired/revoked => permanent security/config failure, retry 금지;
- handshake timeout/reset => replay-safe operation에서 bounded retry candidate;
- repeated handshake failure가 breaker를 열어도 security alert는 별도;
- certificate 내용/subject 전체 log 금지;
- peer cert hash도 metric tag 금지.
## 26. Authentication와 secret lifecycle
### 26.1 Auth profile
```text
none
api-key-header
static-bearer
oauth2-client-credentials
mutual-tls
request-signature
legacy-basic
```
Operation은 하나의 profile 또는 declared composition을 참조한다.
현재 canonical card set에서 `none`, conditional `api-key-header`/`static-bearer`,
`oauth2-client-credentials`, `mutual-tls`만 각각 정해진 card/evidence로 활성화할 수 있다.
`request-signature``legacy-basic`은 future card가 생길 때까지 vocabulary reservation일 뿐이며
설정하면 startup/readiness가 실패한다.
### 26.2 Secret reference
Config:
```text
secretRef
version/generation
headerName if allowlisted
scope/audience
rotation overlap
refresh skew
```
금지:
- repository literal;
- env registry의 example secret;
- `toString()`에 material;
- exception/log/span/metric;
- query parameter token;
- application command에 raw secret.
### 26.3 API key/static bearer
- fixed allowlisted header;
- CR/LF/length validation;
- `Authorization` scheme fixed;
- attempt 직전에 inject;
- redirect 전에 strip/re-evaluate;
- generation rotation with overlap if provider permits;
- health probe가 token을 노출하지 않음.
### 26.4 OAuth2 client credentials
Spring Security OAuth2 client integration을 사용할 수 있지만 다음은 HTTP capability가 검증한다.
- registration/profile ID;
- token endpoint destination;
- client authentication method;
- scope/audience allowlist;
- finite token call deadline;
- token response size/media/JSON bounds;
- child token-endpoint auth-purpose/mode exact profile fingerprint와 acyclic dependency DAG;
- exact token cache/single-flight key;
- expiry skew + refresh jitter;
- single-flight refresh;
- refresh waiter bound;
- failure/backoff;
- secret rotation;
- one replay on stale-token 401;
- no recursive use of protected destination client to fetch its own token.
Token cache와 single-flight는 정확히 같은 key를 사용한다.
```text
TokenCacheKey(
childProfileFingerprint,
registrationOrAuthProfileId,
credentialGeneration,
normalizedCaseSensitiveScopeSet,
audienceOrResource,
tenantOrDelegationScope
)
```
Scope는 provider contract에 따라 case를 바꾸지 않고 정렬/deduplicate하며 audience/resource와
opaque tenant/delegation scope도 typed canonicalization을 거친다. 어느 축 하나라도 다르면 token,
refresh future와 failure backoff를 공유하지 않는다. Key 원문은 telemetry에 넣지 않으며 credential
normal rotation/emergency revoke 시 matching entries와 waiter를 정확히 invalidate한다.
Token endpoint 자체도 first-class named outbound dependency이며 separate pool/resilience
group을 사용한다. 일반 origin auth와 별도의 purpose/mode를 exact tuple에 넣는다.
```text
AuthPurpose = ORIGIN | OAUTH_TOKEN_ENDPOINT | PROXY_HOP | REVOCATION_RESPONDER
TokenEndpointAuthMode =
CLIENT_SECRET_BASIC
| CLIENT_SECRET_POST
| PRIVATE_KEY_JWT
| MTLS_CLIENT_AUTH
| NONE_WHEN_PROVIDER_EXPLICITLY_ALLOWS
```
OAuth token child는 `AuthPurpose=OAUTH_TOKEN_ENDPOINT`이고 exact client-auth mode, header/body
ownership, redaction, signing/mTLS와 secret generation scenario를 증명한다. OAuth의
`CLIENT_SECRET_BASIC`은 generic origin `legacy-basic` card와 다른 protocol-owned mode다. Child가
다시 `authMode=oauth2-client-credentials`를 선택하는 재귀만 금지하며 client authentication 자체를
`none`으로 숨기지 않는다.
Parent OAuth profile은 child compatibility-profile fingerprint, provider/version와 policy digest를
참조한다. Token endpoint가 자신 또는 보호 destination의 OAuth profile로 되돌아가는 cycle과
self-reference는 startup failure다.
각 token network attempt는 `maxNestedCredentialRequestsPerLogicalCall`과 shared root-call total
budget을 소비한다. Shared token client/cache/pool은 owner/reference count를 가지며 마지막 parent
binding이 제거되면 refresh waiter, cache와 resource를 닫는다. No OAuth binding이면 token endpoint
resource/evidence도 0개다. Child tuple maturity/scenario가 release-eligible이 아니면 parent OAuth
profile도 release-eligible이 될 수 없다.
### 26.5 Multi-tenant/on-behalf-of
Per-user token을 global static client profile로 넣지 않는다.
필요 시:
- application/security가 opaque delegated credential reference 제공;
- adapter credential exchange;
- raw inbound token pass-through 금지;
- bounded credential cache;
- tenant/user를 metric tag로 사용 금지;
- connection pool은 bearer token별로 생성하지 않음;
- mTLS tenant identity라면 bounded separate pool/card;
- revocation/logout semantics.
### 26.6 Request signing
SigV4/HTTP Message Signature 등 provider-specific signer:
- final method/URI/header/body digest 확정 후 attempt마다 sign;
- redirect 후 기존 signature 재사용 금지;
- clock skew/nonce policy;
- stable payload bytes;
- idempotency key는 attempts 간 유지;
- signing key secret ref;
- signed header canonicalization golden vector;
- proxy가 signed fields를 변형하는지 test.
### 26.7 Basic auth
Legacy opt-in:
- HTTPS only;
- fixed destination;
- no preemptive cross-origin forwarding;
- credential rotation;
- log redaction;
- 별도 security waiver/readiness evidence.
### 26.8 Auth failure
```text
local material missing/expired
token endpoint unavailable
401 invalid token
403 insufficient scope
mTLS rejection
signature clock skew/mismatch
```
을 분리한다. 모든 401/403을 dependency 4xx 하나로 축소하지 않는다.
## 27. Request body, serialization와 upload
### 27.1 Body modes
```text
NONE
BUFFERED_JSON
BUFFERED_BINARY
REOPENABLE_STREAM
SINGLE_USE_STREAM
MULTIPART_MANAGED
```
Mode는 operation catalog에 고정한다.
### 27.2 Buffered encoding
- adapter wire DTO만 serialize;
- canonical ObjectMapper/profile;
- max encoded bytes;
- bounded output stream으로 encode 중 limit;
- content length 계산;
- immutable byte snapshot;
- digest if required;
- retries마다 동일 bytes;
- heap budget;
- encoder exception은 internal/request contract failure, network retry 금지.
Object를 먼저 거대한 byte array로 만든 뒤 limit을 검사하지 않는다.
### 27.3 JSON constraints
- maximum nesting depth;
- maximum string/name/number length;
- maximum token/document length;
- duplicate key policy;
- numeric overflow;
- polymorphic typing disabled;
- unknown field policy per API version;
- null/absent distinction;
- date/time/locale;
- UTF-8 default;
- non-finite number policy;
- golden wire snapshots.
Inbound API ObjectMapper의 관대한 설정을 outbound wire contract에 우연히 공유하지 않는다.
### 27.4 Reopenable stream
개념:
```java
interface ReopenableBodySource {
BodyHandle open(AttemptContext context);
BodyIdentity identity();
}
```
`BodyHandle`:
- content type;
- known/unknown length;
- bounded readable source/callback;
- close;
- optional checksum;
- attempt generation.
`open()`마다 같은 semantic content를 제공해야 하며 identity/digest drift면 retry를 중단한다.
### 27.5 One-shot
- `maxProtectedPhysicalAttemptsPerLogicalCall=1`;
- redirect/auth challenge replay 금지;
- pool/acquire/DNS failure처럼 `NOT_SENT`가 확실한 경우에 새 open 가능 여부도 source contract가
명시해야 함;
- body write 시작 뒤 실패는 indeterminate mutation 가능;
- caller가 stream close를 소유하지 않도록 scoped callback 선호.
### 27.6 Request compression
Default disabled.
Opt-in:
- provider accepted encoding;
- minimum threshold;
- original/encoded size caps;
- CPU budget;
- deterministic/replayable bytes;
- signature/digest order;
- compressed size observability;
- CRIME/BREACH 같은 secret/reflection context 해당 여부.
### 27.7 `Expect: 100-continue`
큰 authenticated upload에서 operation opt-in.
- interim response timeout;
- proxy/server compatibility;
- 401/413를 body 전송 전에 받을 수 있음;
- 100을 받았다는 것이 mutation 미적용 보장은 아님;
- unsupported server fallback 여부;
- test matrix.
### 27.8 Multipart
Optional card:
- library-generated boundary;
- fixed part names;
- sanitized filename;
- per-part/aggregate size;
- part media type;
- header injection 방지;
- streaming/replayability;
- checksum;
- temporary spool policy;
- provider contract.
Arbitrary caller part/header map 금지.
### 27.9 Spooling
Memory limit을 넘는 replayable upload에 bounded disk spool을 선택할 수 있다.
- encrypted 또는 data classification에 맞는 persistent/temp storage;
- restrictive permission;
- quota;
- checksum;
- crash cleanup;
- symlink/path defense;
- no shared predictable filename;
- lifecycle/retention;
- fileserver/objectstorage adapter 직접 의존 금지.
Spooling은 별도 provider-internal resource이며 기본 활성화하지 않는다.
## 28. Response body, decoding와 download
### 28.1 Header/status first
Response body consumer 전에:
1. status read;
2. header aggregate validation;
3. status mapping;
4. expected body presence;
5. content type/encoding/declared length;
6. success/error decoder selection;
7. body bound 설치;
8. consumer 호출.
Streaming `exchange()` callback이 이 순서를 직접 구현한다.
### 28.2 Wire와 decoded bound
```text
maxWireBytes
maxDecodedBytes
maxExpansionRatio
```
를 별도로 둔다.
`Content-Length`:
- fast reject hint;
- missing 가능;
- 거짓/압축 표현 가능;
- actual counting을 대체하지 않음.
Transparent engine decompression을 꺼서 counting layer가 compressed/decoded 경계를 소유하거나,
provider가 두 값을 확실히 관측하는 별도 구현을 제공한다.
### 28.3 Content encoding
Default accepted:
```text
identity
gzip (explicit operation/profile)
```
Unknown/multiple encoding:
- operation allowlist;
- decode chain depth;
- decoded cap;
- CPU/time budget;
- malformed/truncated handling;
- connection discard.
Brotli/Zstd native/runtime dependency는 별도 optional card다.
### 28.4 Buffered response
- bounded byte load;
- media/charset validation;
- decode;
- required semantic field validation;
- no raw body retention;
- generic type support through typed decoder, not caller `Class<T>`;
- success with empty/null body contract;
- response close in finally.
### 28.5 Streaming callback
개념:
```java
interface BoundedResponseConsumer<R> {
R consume(SafeResponseMetadata metadata, BoundedBody body, CancellationView cancellation);
}
```
불변식:
- body callback scope 밖 탈출 금지;
- limit은 streaming에도 적용;
- idle/total deadline;
- early return/exception에서 close/cancel;
- `readAllBytes()`를 streaming sample로 제시하지 않음;
- callback이 application/domain `InputStream`을 반환하지 않음;
- partial output publication은 consumer workflow가 staging/commit으로 처리;
- callback result가 body bytes에 비례해 무한히 커지지 않도록 별도 contract.
### 28.6 Partial/truncated response
- Content-Length 미충족;
- chunk terminator 없음;
- connection reset;
- checksum mismatch;
- decompressor EOF;
- HTTP/2 reset;
`RESPONSE_TRUNCATED`/`CHECKSUM_MISMATCH`다. 이미 consumer가 partial side effect를 만들었다면
adapter/use case가 abort/cleanup할 수 있어야 한다.
### 28.7 Range/resume
Optional:
- stable ETag/version required;
- `Range` + `If-Match`;
- 206 required;
- exact `Content-Range`;
- total size consistency;
- checksum;
- 200 fallback은 새 target으로 restart, 기존 partial에 append 금지;
- 416 mapping;
- provider change detection;
- same deadline/retry budget.
### 28.8 Error response
- separate small cap;
- status received 사실 보존;
- operation-specific error codec;
- raw provider message default 미노출;
- problem type/code allowlist;
- error decode failure가 status를 잃게 하지 않음;
- connection reuse를 위해 cap 안에서 consume하거나 discard/close.
### 28.9 Memory budget
Buffered mode의 limit은 개별 body만이 아니라 concurrent aggregate와 연결된다.
```text
maxConcurrentBufferedAttempts
* (request cap + response cap + error cap + codec overhead)
```
Startup validator가 configured concurrency와 container/JVM budget의 명백한 모순을 거부한다.
정확한 heap overhead는 load/soak evidence로 조정한다.
## 29. Protocol, media와 API compatibility
### 29.1 Success status is operation-specific
다음은 모두 가능한 정상 계약이다.
- create: 201;
- accepted async: 202 + status reference;
- delete: 204;
- conditional read: 200/304;
- range: 206;
- empty lookup: 404 mapped absence.
`is2xxSuccessful()` 하나로 완료를 정의하지 않는다.
### 29.2 Media negotiation
Operation이 고정:
- `Accept`;
- request `Content-Type`;
- response accepted type/parameters;
- charset;
- content encoding;
- API vendor media version.
Unexpected HTML error page를 JSON으로 decode하다 connect failure로 분류하지 않는다.
### 29.3 API version
Header/query/path version strategy를 operation catalog가 소유한다. Spring Boot/Framework client API
version support를 사용할 수 있지만:
- server-side version config 자동 공유를 가정하지 않음;
- version이 operation revision과 연결;
- N/N-1 rolling compatibility;
- sunset/deprecation header monitoring;
- version 값 caller override 금지.
### 29.4 Tolerant reader, strict semantic validation
- unknown additive field는 configured tolerant read 가능;
- required business field missing은 failure;
- enum unknown은 explicit `UNKNOWN`/failure policy;
- numeric unit/currency/version 검증;
- null/absent/default;
- provider timestamp/clock;
- schema validation과 business mapping 분리.
Wire DTO를 domain entity로 직접 deserialize하지 않는다.
### 29.5 Contract tests
가능한 evidence:
- provider OpenAPI schema;
- captured sanitized golden fixtures;
- consumer-driven contract;
- provider sandbox;
- backward/forward fixture matrix;
- unknown field/enum;
- deprecation header.
Mock fixture가 provider 실제 behavior와 같다는 보장은 없으므로 sandbox/nightly evidence를
구분한다.
### 29.6 Pagination
Generic kernel이 자동으로 모든 `next` link를 따라가지 않는다.
Upstream adapter가:
- bounded max pages/items/bytes;
- total deadline;
- cursor loop detection;
- known route reconstruction;
- per-page retry;
- partial-result policy;
- consistency/snapshot token;
을 operation 의미에 맞게 소유한다.
### 29.7 Conditional request
ETag/If-Match/If-None-Match:
- opaque validator;
- weak/strong semantics;
- operation-specific storage;
- 304/412 mapping;
- credential/tenant scope;
- cache interaction;
- raw ETag metric tag 금지.
Mutation retry safety를 위해 `If-Match`를 사용할 수 있지만 idempotency-key와 동일하지 않다.
### 29.8 HTTP caching
Default no transparent client cache.
필요하면:
- RFC caching semantics;
- auth/private response;
- `Vary`;
- freshness/revalidation;
- storage/eviction;
- tenant isolation;
- invalidation;
- cache observability;
를 별도 cache integration card로 설계한다. HTTP engine 내부 cache가 Redis/application cache
정책을 우회하지 않는다.
### 29.9 Informational responses와 trailers
- 100 Continue는 upload state machine;
- 103 Early Hints는 baseline에서 application outcome 아님;
- trailers는 declared allowlist와 size cap;
- checksum trailer가 있으면 full-body completion 전 success 금지;
- unsupported interim/trailer behavior는 provider matrix에 기록.
## 30. HTTP version policy
### 30.1 HTTP/1.1 baseline
초기 `apache-hc5-classic` R2 profile은 HTTP/1.1을 기본으로 한다.
- pipelining disabled;
- finite header limits;
- request/response framing strict;
- `Content-Length` + `Transfer-Encoding` ambiguity reject;
- connection-close delimited response는 truncation ambiguity를 명시;
- stale keep-alive test;
- proxy compatibility.
RFC 9112 framing violation과 smuggling 위험을 engine strict mode/test로 검증한다.
### 30.2 HTTP/2 opt-in
Provider/card requirements:
- ALPN negotiation;
- `H2_REQUIRED` 또는 fallback policy;
- stream concurrency/pending bounds;
- connection flow control;
- server SETTINGS;
- GOAWAY last-stream handling;
- `REFUSED_STREAM` retry safety;
- RST_STREAM;
- header list limit/HPACK abuse bound;
- server push disabled;
- origin coalescing disabled/verified;
- proxy CONNECT/H2 support;
- attempt observation.
GOAWAY/RST_STREAM을 무조건 safe retry로 보지 않는다. HTTP/2 card는 transmission evidence와
별도로 다음 processing evidence를 제공한다.
```text
ProcessingEvidence = CONFIRMED_NOT_PROCESSED | MAYBE_PROCESSED | UNKNOWN
```
`REFUSED_STREAM`과 수신한 GOAWAY의 last-stream-ID보다 큰 client stream만 RFC 9113/provider
contract가 정확히 뒷받침할 때 `CONFIRMED_NOT_PROCESSED`로 올릴 수 있다. 여러 GOAWAY를 받으면
last-stream-ID가 증가하지 않는지 검증하고 최신 evidence를 단조롭게 반영한다. 그 밖의
RST_STREAM/GOAWAY, connection loss와 provider가 stream ID를 노출하지 않는 경우는
`MAYBE_PROCESSED|UNKNOWN`이다. `CONFIRMED_NOT_PROCESSED`만 §16의 `RESTART_CONFIRMED_NOT_PROCESSED` 후보를 만들며,
protocol-restart `AttemptAuthorizationLease`와 body replayability, shared physical ceiling,
root-call budget, deadline, operation opt-in을 모두 통과한다. `MAYBE_PROCESSED|UNKNOWN`은 mutation
reattempt를 만들지 않는다. HTTP/2 provider가 이 evidence를 제공하지 못하면 mutation replay를
허용하지 않는다.
### 30.3 Downgrade
```text
H1_ONLY
NEGOTIATE_H2_H1
H2_REQUIRED
```
를 구분한다.
- `H2_REQUIRED`가 H1로 떨어지면 startup/readiness 또는 call failure;
- negotiated protocol metric;
- proxy/LB별 compatibility;
- silent downgrade 금지.
### 30.4 Connection coalescing
HTTP/2가 certificate와 DNS 조건상 여러 origin을 한 connection으로 합칠 수 있어도 default
금지한다.
- auth/tenant/mTLS isolation;
- DNS/address policy;
- destination metrics;
- circuit breaker;
- certificate SAN;
이 섞일 수 있기 때문이다. Opt-in은 별도 security/performance evidence가 필요하다.
### 30.5 HTTP/3
R2 baseline 제외, R3 candidate.
- QUIC/UDP egress;
- certificate/ALPN;
- connection migration;
- stream capacity;
- retry/token;
- load balancer;
- observability;
- 0-RTT replay.
Mutation과 credential-bearing request에 0-RTT를 허용하지 않는다. HTTP/3를 boolean 하나로
HTTP/2 profile에 추가하지 않는다.
## 31. Observability와 privacy
### 31.1 관측 단위
HTTP client는 logical call과 physical attempt를 분리한다.
| 단위 | 의미 | 기본 telemetry |
| --- | --- | --- |
| logical call | application port 호출 한 번 | application/internal span 또는 logical timer |
| physical attempt | 실제 wire request 한 번 | HTTP CLIENT span, 표준 HTTP client metric |
| admission wait | logical/physical bulkhead 진입 대기 | queue timer/gauge |
| pool lease | connection/stream capacity 대기 | pool acquire timer/gauge |
| retry delay | backoff와 `Retry-After` 대기 | retry delay timer |
| reconciliation | `INDETERMINATE` 후 상태 확인 | 별도 operation/span |
한 physical attempt를 두 개의 CLIENT span으로 감싸지 않는다. Spring/engine instrumentation이
CLIENT span을 만들면 adapter는 같은 attempt에 다른 CLIENT span을 추가하지 않는다. Logical call
span이 필요하면 kind를 `INTERNAL`로 두고 이름과 속성에서 attempt span과 구분한다.
예외는 attacker-controlled authority 때문에 §24.6이 표준 HTTP instrumentation 억제를 요구하는
`httpclient-untrusted-url-fetch`다. 이 card는 fixed bounded INTERNAL/project telemetry만 사용하며
confidential exact profile 없이는 CLIENT `server.address|url.full`을 만들지 않는다.
권장 span 구조:
```text
feature use-case span
└─ http.logical <destination>/<operation> // optional INTERNAL
├─ HTTP <method> physical attempt 0 // CLIENT
├─ retry wait
├─ HTTP <method> physical attempt 1 // CLIENT
└─ reconcile <destination>/<operation> // separate call when required
```
Manual retry, redirect, challenge replay가 새 wire request를 만들면 OTel HTTP semantic convention의
`http.request.resend_count`를 실제 재전송 횟수로 기록한다. Logical call ID나 idempotency key를
span attribute로 원문 기록하지 않는다.
### 31.2 Trace propagation 단일 소유자
Propagation은 application MDC가 아니라 OTel instrumentation이 단독 소유한다.
- `traceparent``tracestate`는 current Observation/Context에서 생성한다.
- 기존 MDC `trace_id`, `span_id` 문자열로 새 `traceparent`를 조립하지 않는다.
- sampling flag를 하드코딩하지 않는다.
- B3와 W3C를 동시에 보내지 않는다.
- destination별 propagation policy가 `deny`면 trace header도 보내지 않는다.
- untrusted public fetch에는 baggage와 tenant/correlation header를 보내지 않는다.
`docs/registries/headers.yaml``traceparent`/`tracestate` 선언은 “허용된 표준 header”를
뜻하며 custom writer 소유권을 뜻하지 않도록 registry 설명을 수정한다. 현재
`TraceContextPropagationInterceptor`는 구현 migration에서 제거한다.
### 31.3 Baggage와 correlation
기본 정책:
| Metadata | 내부 allowlisted destination | 외부 partner | untrusted URL |
| --- | --- | --- | --- |
| W3C trace context | opt-in/default allow | destination opt-in | deny |
| correlation ID | explicit allowlist | explicit contract일 때만 | deny |
| request ID | explicit allowlist | default deny | deny |
| tenant ID | destination + operation allowlist | default deny | deny |
| user principal | 금지 | 금지 | 금지 |
| auth credential | auth policy가 생성 | auth policy가 생성 | 금지 |
`docs/registries/mdc-keys.yaml``tenant_id.propagation=[http,...]`는 모든 HTTP 요청으로의
무조건 전파가 아니다. HTTP client policy가 destination/operation allowlist를 적용한다는 설명과
tenant leakage contract test를 registry migration에 함께 반영한다.
### 31.4 Metrics
표준 physical-attempt metric과 project logical-call metric을 분리한다.
| Metric | 단위 | 의미 |
| --- | --- | --- |
| `http.client.request.duration` | seconds | 표준 physical attempt duration |
| `dependency.client.requests` | seconds | 기존 registry 호환 logical call timer |
| proposed `http.client.logical.attempts` | count | logical call당 wire attempt 수 |
| proposed `http.client.admission.duration` | seconds | logical/physical admission wait |
| proposed `http.client.pool.acquire.duration` | seconds | lease wait |
| proposed `http.client.pool.connections` | connections | leased/idle/pending/max 상태 |
| proposed `http.client.retry.delay` | seconds | 실제 backoff/Retry-After |
| proposed `http.client.failures` | count | stable failure code/stage |
| proposed `http.client.body.size` | bytes | direction + wire/decoded class별 bounded distribution |
| proposed `http.client.generations` | count | active/draining client generation |
`proposed` metric은 먼저 `docs/registries/metrics.yaml`에 schema, tag cardinality, owner,
required test를 등록한 뒤 구현한다. 문서에 이름만 있고 meter가 없는 phantom metric을 만들지
않는다.
현재 `dependency.client.requests`는 다음 의미로 유지한다.
- 한 logical call당 정확히 한 번;
- retry가 성공해도 attempt 수만큼 중복 기록하지 않음;
- `dependency_name=<destination-id>`;
- `dependency_type=http`;
- `outcome=SUCCESS|FAILURE|CIRCUIT_OPEN|TIMEOUT|REJECTED`;
- 세부 failure stage/code는 별도 bounded metric 또는 log/span;
- duration에는 admission부터 body close/release까지 포함.
OTel HTTP metric과 compatibility metric이 같은 현상을 다른 단위로 세는 것을 dashboard에서
명시한다. 둘을 합산하지 않는다.
### 31.5 허용 tag와 금지 tag
허용되는 low-cardinality dimension:
- `destination_id`;
- `operation_id` 또는 bounded operation group;
- normalized method;
- status class, not arbitrary status text;
- stable outcome/failure code/stage;
- negotiated protocol;
- provider ID;
- `generation_role=active|draining|quarantined`
`rotation_outcome=success|failed|revoked` 같은 bounded enum;
- checked-in stable pool isolation ID/role; host, fingerprint, credential/TLS generation에서 동적
파생 금지;
- logical/physical scope.
표준 OTel metric은 semantic convention이 요구하는 `http.request.method`, registered
`server.address`, `server.port`, bounded `error.type/status`를 사용한다. `server.address`
runtime Host header나 user input이 아니라 validated fixed destination registry에서 나온 값이어야
한다. Project-specific metric은 destination/operation ID를 사용한다.
금지:
- raw URI, resolved IP, path variable, query;
- arbitrary host or redirect location;
- request/response header value;
- body, error body, exception message;
- token, API key, cookie, certificate subject 전체;
- tenant/user/customer/order/file ID;
- idempotency key, logical call ID;
- unbounded exception class/package.
Operation ID와 destination ID는 checked-in registry에 존재하는 값만 meter tag로 사용할 수 있다.
Unknown 값은 호출 전에 거절하므로 `unknown-<raw>` 같은 동적 tag를 만들지 않는다.
위 금지 목록은 metric tag와 application log에 대한 규칙이다. OTel CLIENT span은 standard가
요구하는 `url.full`, `server.address`, `server.port`를 다룰 수 있어 별도 sanitizer contract를
둔다.
- user-info는 URI validation 단계에서 금지;
- query value는 모두 제거하거나 allowlisted non-sensitive key만 값 없이 남김;
- path variable은 operation template에 따라 `REDACTED`로 치환;
- fixed scheme/registered authority는 보존;
- `url.template`이 selected instrumentation에서 지원되면 low-cardinality template을 추가;
- raw header capture는 off;
- untrusted URL fetch는 별도 privacy profile/egress trace policy;
- actual credential/TLS generation ID는 metric에 넣지 않고 sanitized descriptor/rotation audit에만
기록;
- `network.peer.address`는 trust-zone별 explicit opt-in과 secure telemetry pipeline이 있을 때만 span에
기록하며 metric/log tag와 external/untrusted profile에서는 금지;
- explicit caller cancellation은 dependency error로 세지 않고 OTel status를 unset으로 유지하며
bounded cancellation outcome만 logical telemetry에 기록.
Sanitizer는 exporter 후처리가 아니라 span attribute 생성 경계에 위치한다. Raw path/query/header와
unsanitized absolute URL을 OTel SDK, processor, sampler 또는 exporter에 한 번도 전달하지 않는다.
In-memory SDK/exporter test는 forbidden fixture 값이 생성된 attribute/event/link 전체에 0회 존재함을
검증한다.
Semantic convention 준수와 privacy가 충돌하는 selected instrumentation/version이면 조용히 raw
URL을 내보내지 않는다. Sanitized absolute URL contract를 구현하거나 해당 provider/card를
release-eligible로 승격하지 않고 deviation을 descriptor에 명시한다.
### 31.6 Resilience4j metric ownership
현재 adapter가 설치하는 global `MeterFilter.DENY`는 제거 대상이다. 한 capability가 application
전체의 `resilience4j.*` meter를 차단하면 Redis, messaging 등 다른 capability의 관측 계약을
깨뜨릴 수 있다.
두 선택지 중 하나를 composition root에서 명시한다.
1. registry가 허용한 Resilience4j meter를 bounded name/tag로 등록한다.
2. native binder를 사용하지 않고 HTTP adapter가 semantic logical/physical metric만 직접
기록한다.
초기 구현은 2를 권장한다. Circuit breaker instance name에는
`<destination-id>/<policy-id>`만 사용하고 operation/cardinality를 무제한 확장하지 않는다.
### 31.7 Log policy
한 logical call의 최종 failure는 한 번만 structured log로 남긴다. 개별 attempt는 기본적으로
span/metric이고, debug sampling이나 security audit 사유가 있을 때만 log한다.
필수 필드:
```text
event=http_client_call_completed
destination_id
operation_id
outcome
failure_code?
failure_stage?
attempt_count
duration_ms
http_status_class?
protocol?
policy_revision
```
금지:
- `Throwable#getMessage()` 직접 출력;
- URL/query/header/body dump;
- Authorization/cookie/idempotency key;
- redirect location 원문;
- resolved IP 원문을 일반 application log에 기록;
- TLS certificate 원문;
- OAuth token endpoint response.
예외는 stable class category와 sanitized code로 변환한다. Stack trace는 unexpected internal
defect에만 보안 필터를 거쳐 제한적으로 남기며, partner response body를 exception message에
포함하지 않는다.
### 31.8 Sampling
- failure와 `INDETERMINATE` span은 tail-sampling 후보;
- 성공 request의 high-volume span은 deployment sampling policy 적용;
- credential/PII가 들어갈 수 있는 event/body는 sampling 여부와 무관하게 기록 금지;
- attempt 수와 retry delay metric은 trace sampling과 무관하게 집계;
- debug wire logging은 production에서 불허;
- provider library의 header/body logger도 startup validation으로 비활성 확인.
### 31.9 Dashboard와 alert
Universal 임계값을 설계 문서에 하드코딩하지 않는다. Destination별 checked-in SLO가 다음
signal과 연결되어야 한다.
- logical success/availability;
- physical attempt amplification;
- timeout stage distribution;
- p50/p95/p99 logical/attempt latency;
- admission/pool wait와 saturation;
- circuit state/half-open result;
- DNS/TLS/auth failure;
- decoded-size/truncation reject;
- `INDETERMINATE` mutation count와 reconciliation age;
- readiness state와 client generation drain;
- retry budget exhaustion.
Alert는 traffic이 없는 상태와 100% 성공을 구분하고, optional dependency 장애를 application
liveness failure로 바꾸지 않는다.
## 32. Health, readiness와 SLO
### 32.1 Liveness
Liveness는 외부 destination을 호출하지 않는다.
- engine thread/executor 자체 deadlock을 외부 probe로 고치지 않는다;
- partner outage 때문에 pod를 반복 재시작하지 않는다;
- pool saturation도 liveness failure가 아니라 dependency/resource alert다;
- process가 자체 health endpoint에 응답 가능한지와 fatal internal state만 본다.
### 32.2 Startup validation
Startup 단계에서 network business call 없이 다음을 검증한다.
- binding이 존재하는 destination/provider/card가 registry에 존재;
- operation catalog의 destination과 binding이 일치;
- URI scheme/host/port와 SSRF policy;
- timeout/pool/body/retry cross-field invariant;
- auth/TLS/proxy/DNS profile reference;
- secret reference 해석 가능성과 최소 metadata;
- ACTIVE에서 derived selected card와 exact compatibility profile이 각 registry상 모두
`release-eligible`;
- OTel propagation owner가 하나;
- hidden redirect/retry/cookie가 disabled;
- no binding이면 resource/bean 생성이 없음.
TLS key material parsing이나 local trust-store load는 startup에 포함할 수 있다. 실제 remote
handshake는 readiness/evidence probe다.
### 32.3 Readiness impact
Destination마다 다음 impact 중 하나를 선언한다.
```text
REQUIRED_FOR_ALL_TRAFFIC
REQUIRED_FOR_CAPABILITY
OPTIONAL
```
의미:
| Impact | Remote failure 시 |
| --- | --- |
| `REQUIRED_FOR_ALL_TRAFFIC` | 충분한 debounce와 rollout 보호 후 global readiness에 반영 가능 |
| `REQUIRED_FOR_CAPABILITY` | 해당 use-case routing만 unavailable/degraded, global readiness는 정책에 따름 |
| `OPTIONAL` | global readiness 유지, descriptor/alert는 degraded |
`REQUIRED_FOR_ALL_TRAFFIC`은 아주 드물게 사용한다. External dependency 한 곳의 장애가 모든
pod를 동시에 NotReady로 만들어 트래픽 재분배와 재시작 폭주를 유발하지 않도록 최소 failure
window, success recovery window, stale-result TTL, rollout grace를 둔다.
### 32.4 Probe operation
Readiness probe는 business mutation을 호출하지 않는다.
허용:
- documented health endpoint;
- bounded `HEAD`/`GET` metadata endpoint;
- TLS/auth handshake까지 포함하는 provider-specific safe probe;
- service mesh/passive telemetry와 조합한 cached result.
금지:
- order/payment/notification 생성;
- unbounded payload download;
- normal retry budget을 소모하는 aggressive probe;
- circuit breaker의 normal call 통계를 왜곡하는 probe;
- pod마다 동기화된 고주기 polling.
Probe는 별도 operation ID, bulkhead, rate, breaker를 사용한다. Jitter와 single-flight를 적용하고
마지막 성공/실패 시각, stale age, failure class를 descriptor로 공개한다.
### 32.5 Capability descriptor state
```text
DISABLED
STARTING
READY
DEGRADED
NOT_READY
DRAINING
CLOSED
```
Descriptor는 최소 다음을 포함한다.
```text
capability=http-client
destination_id
binding/provider_id
selected_cards
compatibility_profile_ids
provider_maturity
card_maturities
compatibility_profile_maturities
effective_protocol
policy_revision
active_generation
dependency_profile_fingerprints
readiness_impact
state
last_probe_result/age
```
Secret, URI user-info, raw host for dynamic targets, IP, credential generation material은 포함하지
않는다.
### 32.6 SLO와 timeout budget
Timeout 값은 “connect 1초가 흔하다” 같은 template 상수로 정하지 않는다.
Destination별:
```text
inbound/use-case budget
> application processing reserve
+ http logical-call total deadline
+ response/compensation reserve
```
를 checked-in SLO/profile로 검토한다. Retry p99 amplification과 pool queue까지 포함해 capacity를
계산한다. Required dependency의 alert threshold는 해당 dependency SLO와 error budget에
연결한다.
## 33. Configuration design
### 33.1 Canonical activation shape
상위 capability platform과 같은 canonical prefix를 사용한다.
```yaml
ca-skeleton:
capabilities:
http-client:
expected-state: ACTIVE
bindings:
partner-catalog: apache-hc5-classic
providers:
http-client:
apache-hc5-classic:
destinations:
partner-catalog:
base-uri: ${PARTNER_CATALOG_BASE_URI}
readiness-impact: REQUIRED_FOR_CAPABILITY
protocol: H1_ONLY
operation-catalog: partner-catalog-v1
timeout:
total: 2s
cleanup-reserve: 100ms
minimum-attempt-budget: 200ms
pool-acquire: 100ms
dns: 250ms
connect: 300ms
tls-handshake: 500ms
request-write: 500ms
response-headers: 1s
response-idle: 500ms
orphan-reaper:
max-workers: 2
max-orphans: 8
max-queued-cleanups: 8
cleanup-timeout: 10s
pool:
max-total: 64
max-per-route: 32
max-pending-acquires: 64
connection-max-lifetime: 5m
idle-eviction: 30s
validate-after-inactivity: 5s
admission:
max-logical-in-flight: 96
max-physical-in-flight: 48
max-queued-logical-calls: 32
amplification:
max-protected-physical-attempts-per-logical-call: 2
max-nested-credential-requests-per-logical-call: 0
max-reconciliation-requests-per-logical-call: 0
max-proxy-connect-requests-per-root-call: 0
max-revocation-http-requests-per-root-call: 0
max-total-http-request-attempts-per-root-call: 2
retry:
ordinary-max-retries: 1
initial-backoff: 50ms
maximum-backoff: 250ms
jitter-ratio: 0.30
honor-retry-after: true
maximum-retry-after: 1s
retry-budget-ratio: 0.05
circuit-breaker:
policy: partner-read-v1
dns:
policy: fixed-internal-v1
egress-policy: partner-catalog-fixed-v1
tls:
ssl-bundle: partner-catalog-client
require-https: true
authentication:
type: none
proxy:
mode: DIRECT_ONLY
bounds:
request-header-bytes: 16KiB
response-header-bytes: 32KiB
buffered-request-bytes: 1MiB
buffered-response-wire-bytes: 2MiB
buffered-response-decoded-bytes: 4MiB
```
숫자는 schema 예시이며 production universal default가 아니다. 실제 값은 destination SLO,
provider limit, pod memory/CPU, replica 수, upstream quota를 근거로 승인한다. Secret 값은 직접
YAML에 쓰지 않는다. 이 예제는 static buffered/safe-read baseline만 표현한다. OAuth2를 선택하는
예제는 `httpclient-oauth2-client-credentials` maturity가 release-eligible이고 파생 selected set에
포함된 뒤에만 유효하다. 현재 모든 card/profile이 `not-implemented`이므로 위 ACTIVE 예제는
target configuration shape일 뿐이며 그대로는 startup/readiness를 통과하지 않는다.
### 33.2 Activation SSOT
- `capabilities.http-client.bindings`에 destination이 없으면 disabled다.
- provider destination 정의가 존재하는 것만으로 client를 만들지 않는다.
- 별도 `enabled` boolean을 두지 않는다.
- binding은 정확히 하나의 provider를 선택한다.
- runtime classpath가 provider를 자동 선택하지 않는다.
- binding 대상 provider/destination이 없으면 startup failure다.
- destination binding이 하나라도 있으면 resolver가 `httpclient-static-buffered` card를
자동 요구한다.
- binding이 0개면 derived selected card도 0개이고 descriptor는 `DISABLED`; R2를 표시하지 않는다.
- `expected-state=ACTIVE`인데 binding이 0개면 startup/readiness failure다.
- `expected-state=DISABLED`인데 binding/provider resource/derived card가 하나라도 있으면
startup/readiness failure다.
- `DISABLED` 성공은 `DISABLED_VERIFIED`이지 R2 HTTP 성공이 아니다.
Selected card는 사람이 여러 위치에서 중복 입력하지 않고 deterministic resolver가 계산한다.
```text
derivedSelectedCards =
baselineCards(required by active bindings)
union operationCatalog.requiredCards
union providerMode.requiredCards
```
Provider-mode derivation 예:
| Effective mode | Required card |
| --- | --- |
| any active fixed destination | `httpclient-static-buffered` |
| bounded binary body | `httpclient-static-buffered` + binary conditional scenarios |
| API key/static bearer | `httpclient-static-buffered` + exact auth conditional scenarios |
| custom server trust | `httpclient-static-buffered` + exact TLS conditional scenarios |
| idempotent/keyed mutation operation | `httpclient-idempotent-mutation` |
| non-retryable mutation operation | `httpclient-non-retryable-mutation` |
| response stream callback | `httpclient-streaming-download` |
| reopenable/one-shot upload | `httpclient-streaming-upload` |
| client certificate | `httpclient-mtls` |
| OAuth2 client credentials | `httpclient-oauth2-client-credentials` |
| required egress proxy | `httpclient-egress-proxy` |
| H2 negotiated/required | `httpclient-http2` |
| dynamic public fetch | `httpclient-untrusted-url-fetch` |
| redirect-follow, request-signature 또는 HTTP Basic | canonical card 없음 -> startup/readiness failure |
Resolver는 card ID나 mode 축별 합집합만 검사하지 않는다. Active operation마다 전체 tuple을
만든다.
```text
EffectiveHttpProfileTuple(
providerId,
resolvedProviderArtifactVersion,
jdkMajor,
protocol,
requestBodyMode,
responseBodyMode,
requestCodecMediaMode,
responseCodecMediaEncodingMode,
tlsMode,
authPurpose,
authMode,
proxyMode,
redirectMode,
dnsAddressMode,
egressTrustZone,
resilienceMode,
operationSemantics,
operationPolicyRevision,
dependencyProfileFingerprints,
effectiveBehaviorDigest
)
```
```text
for each active operation:
tuple = resolveExactEffectiveTuple(binding, provider, operation)
compatibility = profileCompatibilityRegistry.exactMatch(tuple)
require compatibility.requiredCards == cardsRequiredByRules(tuple)
requiredScenarioSet(tuple) =
union(compatibility.requiredCards.baseScenarioIds)
union compatibility.requiredScenarioIds
union compatibility.interactionScenarioIds
require resolvedScenarioIds(tuple) == requiredScenarioSet(tuple)
derivedSelectedCards = union(compatibility.requiredCards)
```
`exactMatch`는 wildcard, 축별 union, “각 축에서 하나씩 지원됨”을 허용하지 않는다. 예를 들어
API-key + buffered를 증명한 card와 auth-none + streaming을 증명한 card가 각각 있어도
API-key + streaming 전체 tuple entry와 interaction scenario가 없으면 실패한다. 동일 destination의
여러 operation은 각자 tuple을 통과하고 selected card는 그 결과의 union이다.
`effectiveBehaviorDigest`는 secret/raw endpoint를 제외한 validated immutable settings와 operation
descriptor의 canonical serialization을 SHA-256한 값이다. Timeout/pool/bounds/compression,
DNS/egress, codec/media, retry/resilience처럼 tuple의 readable dimension 밖에서 behavior를 바꾸는
필드도 digest를 바꾼다. OAuth token endpoint, proxy와 named revocation responder 같은 child
outbound dependency의 exact compatibility-profile fingerprint도 정렬된
`dependencyProfileFingerprints`와 digest 입력에 포함한다. Behavior schema revision이나 필드가
추가되면 canonical serializer와 compatibility entry를 함께 갱신하며 unknown field는 실패한다.
Dependency DAG cycle/self-reference와 child maturity gap은 resource 생성 전에 실패한다.
Release assertion registry는 expected selected card와 compatibility-profile ID를 assertion으로
둘 수 있으나 selector가 아니다. Assertion과 derived set이 byte-for-byte 다르면 실패한다.
Destination provider definition에
`readiness-cards`를 반복하지 않는다.
### 33.3 Typed settings
Spring binding model과 validated immutable runtime model을 분리한다.
```text
@ConfigurationProperties(
prefix = "ca-skeleton.capabilities.http-client",
ignoreUnknownFields = false)
HttpClientCapabilitySelectionProperties
ExpectedCapabilityState expectedState
Map<DestinationId, ProviderId> bindings
@ConfigurationProperties(
prefix = "ca-skeleton.providers.http-client",
ignoreUnknownFields = false)
HttpClientProviderProperties
Map<ProviderId, HttpProviderDefinitionProperties> providers
HttpDestinationSettingsFactory
binding + provider properties + operation catalog + card/compatibility registries
-> ValidatedHttpDestinationSettings
```
Runtime settings에는 raw mutable map을 남기지 않는다. `URI`, `Duration`, byte-size, enum,
validated ID, sealed auth/proxy/DNS/TLS policy로 변환한 뒤 client를 생성한다.
### 33.4 Cross-field validation
최소 startup failure 조건:
1. unknown destination/provider/card/operation catalog;
2. duplicate normalized ID;
3. binding과 provider destination 불일치;
4. absolute/relative URI invariant 위반;
5. production profile의 plain HTTP;
6. URI user-info, fragment, unsafe port;
7. timeout이 zero/negative/infinite, `cleanup-reserve >= total`, 또는 execution cutoff보다 큰
phase cap;
8. pool per-route가 total보다 큼;
9. pending acquire/admission queue가 unbounded;
10. response decoded cap이 hard process cap보다 큼;
11. compression enabled인데 wire/decoded cap 또는 ratio cap 없음;
12. ordinary retry가 1회 이상인데 retry budget/backoff upper bound 없음;
13. operation catalog보다 느슨한 config retry/redirect/header/body policy;
14. mutation semantics cross-field invariant 위반:
- `KEYED_MUTATION`: idempotency key/fingerprint/replay window/reconciliation 없음;
- `IDEMPOTENT_MUTATION`: authoritative same-intent semantics 또는 indeterminate fallback 없음;
- `NON_RETRYABLE_MUTATION`: protected physical ceiling=1/no auth·redirect replay/transmission
evidence-to-receipt 없음;
- non-retryable `NOT_SENT` restart opt-in인데 protected physical ceiling>2, body non-reopenable,
exact evidence/restart-policy/budget/scenario 없음;
15. streaming card인데 callback close/cancel evidence 없음;
16. mTLS card인데 client key/trust bundle 없음;
17. OAuth2 card인데 registration/ref와 token-call isolation 없음;
18. proxy required인데 direct fallback 허용;
19. HTTP/2 card인데 provider/proxy/TLS profile가 미지원;
20. multiple auth subtype 또는 auth type과 nested fields 불일치;
21. TLS hostname verification disabled;
22. provider hidden redirect/retry/auth/protocol resend/cookie가 enabled 또는 pre-start kernel
authorization을 우회;
23. derived selected card 또는 exact compatibility profile maturity가 `release-eligible`이 아님;
24. same canonical + legacy key 동시 사용;
25. secret literal처럼 보이는 credential 값;
26. orphan reaper worker/queue/count/timeout이 finite positive가 아니거나 physical capacity보다
큰 orphan을 허용;
27. `minimumAttemptBudget + cleanupReserve > totalDeadline`;
28. active operation의 full effective profile tuple이 compatibility registry에 exact match되지
않거나 required-card set이 requirement rules와 다르거나
`card base compatibility required compatibility interaction` scenario set이 완전
일치하지 않음;
29. redirect-follow, request-signature 또는 HTTP Basic처럼 canonical card가 없는 mode가 활성화됨;
30. protected/nested-credential/reconciliation/proxy/revocation/root-call HTTP amplification
상한이 finite가 아니거나 child HTTP request를 root budget에 포함하지 않음, 또는 exact profile의
cold OAuth/401/proxy/revocation `minimumRequiredRootHttpAttempts`보다 root ceiling이 작음;
31. OAuth token endpoint가 exact `OAUTH_TOKEN_ENDPOINT` purpose/client-auth mode child tuple이
아니거나 OAuth 재귀/cycle/self-reference, child maturity/fingerprint/owner lifecycle 누락;
32. proxy가 exact named child profile이 아니거나 ambient/direct fallback, proxy credential tunnel
leakage policy 누락;
33. network revocation lookup이 named responder 없이 certificate-directed URI/JVM implicit discovery를
활성화하거나 SSRF/deadline/size/concurrency/effective-property 검증 누락;
34. emergency revocation profile이 revoked-generation admission/fallback을 허용하거나 replacement 전
`NOT_READY`를 보장하지 않음.
### 33.5 Operation policy와 configuration의 관계
Operation safety는 code-reviewed catalog가 상한이다.
Configuration이 가능한 것:
- ordinary retry와 protected/root physical-attempt 상한 감소;
- total/phase timeout 감소;
- body/header cap 감소;
- allowed status/media type 축소;
- HTTP/2를 H1로 축소;
- optional propagation 제거;
- readiness impact를 더 보수적으로 변경.
Configuration이 불가능한 것:
- non-idempotent operation을 retry-safe로 승격;
- one-shot body를 replayable로 선언;
- absolute dynamic URL 허용;
- new header/credential propagation 추가;
- redirect host 확대;
- response cap 확대해 hard limit 우회;
- card가 증명하지 않은 protocol/auth/body mode 활성화.
확장이 필요하면 catalog와 readiness evidence를 같이 변경한다.
HTTP/2 축소는 operation/provider가 `NEGOTIATE_H2_H1`을 허용할 때만 가능하다.
`H2_REQUIRED`를 H1로 낮추는 configuration은 startup failure다.
### 33.6 Environment key
Flattened environment key는 canonical properties에서 기계적으로 파생한다. Destination ID를
환경 변수 key에 직접 넣어 동적 key 폭증을 만들기보다 environment-specific checked-in YAML과
secret reference를 사용한다. 꼭 필요한 scalar override만 `verifyEnvKeys` registry에 등록한다.
Base URI, proxy endpoint, SSL bundle/secret reference 변경은 운영 영향이 있으므로:
- old/new sanitized descriptor diff;
- rollout strategy;
- readiness probe;
- rollback generation;
을 요구한다.
### 33.7 Legacy migration
현재 `app.outbound.http.*`는 migration-only alias다.
1. legacy만 있으면 deprecation warning과 함께 immutable legacy settings로 변환;
2. canonical과 legacy가 동시에 있으면 값이 같아도 startup failure;
3. legacy global settings는 한 destination 외에는 사용할 수 없음;
4. retry/total-deadline 등 보장하지 못하는 legacy field를 canonical 보장으로 과장하지 않음;
5. 한 release window 뒤 alias 제거;
6. 제거 전 configuration migration test와 release note 제공.
## 34. Composition, activation과 lifecycle
### 34.1 Composition root
`app-bootstrap`만 다음을 수행한다.
1. canonical binding resolve;
2. selected destination settings validation;
3. provider factory 선택;
4. credential/TLS/DNS/proxy collaborator 주입;
5. client generation 생성;
6. feature-specific application port adapter wiring;
7. descriptor/health/lifecycle 등록.
HTTP adapter가 component scan만으로 모든 provider와 client를 자가 활성화하지 않는다.
Application은 adapter type, `RestClient`, Apache type을 알지 못한다.
### 34.2 Zero-binding contract
Binding이 없으면 다음이 모두 0개여야 한다.
- engine client와 connection manager;
- pool evictor/reaper;
- DNS resolver thread/cache;
- scheduler/executor/virtual-thread owner;
- TLS/secret file watcher;
- OAuth token refresh/cache;
- readiness probe;
- circuit breaker/retry registry entry;
- HTTP capability health contributor;
- generic default HTTP bean;
- background task.
Classpath presence만으로 `RestClient`, provider connection manager 또는 health probe가 생성되면
composition contract failure다.
### 34.3 Start order
```text
bind + validate settings
-> load local TLS/auth metadata
-> create engine generation
-> verify effective engine options
-> register bounded telemetry
-> run selected safe startup/readiness evidence
-> publish destination adapters
-> allow ingress readiness
```
Optional destination가 unavailable이면 policy에 따라 `DEGRADED`로 시작할 수 있다. Required
destination의 initial probe failure 처리에는 rollout grace와 cached state가 적용된다.
### 34.4 Drain order
현재 shutdown guard처럼 “outbound를 제일 먼저 닫는” 방식은 in-flight inbound request를 깨뜨린다.
종료 coordination은 상대적 순서를 명시한다.
```text
ACTIVE
-> DRAIN_REQUESTED
stop accepting new ingress
stop scheduled/background producers
already accepted request token may still start outbound calls
-> INGRESS_DRAINED_OR_GRACE_EXPIRED
-> OUTBOUND_ADMISSION_CLOSED
reject all new logical calls
wait active logical calls/streams
-> CANCEL_REMAINING
cancel request/response handles
-> CLOSE_RESOURCES
close pool/client/executor/resolver/watchers
-> CLOSED
```
Spring `SmartLifecycle` phase 숫자를 이 문서에서 임의로 고정하지 않는다. Bootstrap의 ingress/
background/outbound drain coordinator가 위 partial order를 contract test로 증명한 뒤 숫자를
배정한다. 현재 `MAX_VALUE` guard는 이 순서를 증명하지 못하므로 교체 대상이다.
### 34.5 Accepted-request lease
Drain 중 허용 대상을 thread name/MDC로 추정하지 않는다. Ingress가 request lease/token을 발급하고
그 request의 application call chain에 명시적으로 전달한다.
- drain 전에 발급된 lease는 grace 안에서 outbound 시작 가능;
- background job은 별도 producer lease;
- grace 이후 모든 lease 만료;
- child async task가 lease lifetime을 무한 연장하지 못함;
- active lease/call/stream 수가 drain metric에 나타남.
### 34.6 Generation swap
다음 변경은 in-place mutation이 아니라 immutable generation 교체를 기본으로 한다.
- certificate/trust material;
- credential/token client configuration;
- base endpoint/proxy;
- DNS policy;
- pool/protocol settings;
- operation policy revision.
절차:
```text
load new material
-> build generation N+1
-> local validation + safe probe
-> atomic new-call routing swap
-> generation N drain
-> cancel on generation deadline
-> close N resources
```
`NORMAL_ROLLOVER`에서만 new generation 검증 실패 시 아직 유효하고 non-revoked인 old
generation을 유지하고 alert할 수 있다. `EMERGENCY_REVOKE`는 별도 transition이다.
```text
ACTIVE(old)
-> REVOKE_REQUESTED
-> old admission blocked + matching sessions/tokens/pool invalidated
-> optional policy-driven in-flight cancellation
-> NOT_READY until validated replacement
-> ACTIVE(new) or CLOSED
```
Revoked generation으로의 fallback/rollback과 old-new overlap은 금지한다. Close가 지연되면 기존
quarantine/reaper 계약으로 추적하되 new admission을 열지 않는다. “reload supported”를 Spring SSL
bundle 존재만으로 가정하지 않는다. 실제 selected engine의 live-reload 증거가 없으면 항상
generation swap을 사용한다.
### 34.7 Runtime reconfiguration
Arbitrary hot reload는 R2 baseline이 아니다. 지원할 변경마다:
- source authenticity;
- version monotonicity;
- full validation;
- generation atomicity;
- rollback;
- audit;
- concurrent call behavior;
를 증명한다. 그렇지 않으면 deployment rollout로 변경한다.
### 34.8 Engine resource ownership
Provider factory는 다음 close handle을 반환한다.
```text
HttpEngineGeneration implements AutoCloseable
engine
connection manager
executor/scheduler
DNS resolver/cache
credential/token collaborator
TLS material handle
active call/stream registry
```
공유 가능한 executor도 소유자와 reference counting/close order가 명확할 때만 공유한다.
JDK `HttpClient` handle을 버리고 GC에 lifecycle을 맡기지 않는다.
## 35. Security threat model
### 35.1 위협과 통제
| Threat | Preventive control | Detective/CI evidence |
| --- | --- | --- |
| User-controlled SSRF | fixed destination + relative route, scheme/host/port/CIDR policy | URI property tests, internal IP deny tests |
| DNS rebinding | resolve-validate-connect binding, every answer validation, TTL policy | scripted DNS rebind test |
| Redirect credential leak | redirect default deny, hop-by-hop state machine, cross-origin credential strip | 30x chain test |
| Proxy bypass/credential leak | named exact proxy profile, no ambient/direct fallback, hop-only auth | proxy-down/CONNECT raw-capture test |
| Header injection | typed header values, CR/LF/NUL reject, forbidden header ownership | raw request capture |
| Request smuggling | strict framing, no conflicting length/transfer encoding, engine hardening | raw byte server tests |
| Response splitting | strict header parser and count/byte limits | malformed response tests |
| Decompression bomb | wire + decoded + expansion ratio + time bound | compressed bomb test |
| Oversized/truncated body | bounded reader/stream, declared length validation, EOF state | lying length/chunk truncation test |
| TLS downgrade/MITM | HTTPS required, hostname verification, trust policy, protocol floor | wrong host/untrusted CA/old TLS tests |
| Client-key leakage | secret reference, non-exportable/file permission policy, redacted telemetry | secret scan and log capture |
| OAuth/API-key leakage | auth owner creates header, no raw caller credential, redirect stripping | capture server and log tests |
| Duplicate mutation | typed idempotency contract, same key/digest, unknown outcome reconciliation | lost-response replay test |
| Retry/auth/redirect amplification | reason budgets + protected/nested/root shared physical ceilings, `Retry-After` cap | combined resend property/concurrent outage test |
| Pool/queue exhaustion | finite admission/pending/pool bounds, active cancellation | saturation/resource test |
| Slowloris response | response-header and idle/body deadline | drip-feed test |
| Slow upload sink | write progress/deadline and cancellable producer | no-read server test |
| Unsafe deserialization | per-operation codec, media type/schema/size/depth limits | malicious payload corpus |
| Cross-tenant metadata leak | destination/operation propagation allowlist | tenant leakage matrix |
| Cookie/session bleed | cookie store disabled baseline, destination isolation | sequential identity test |
| Metric cardinality attack | registry IDs/bounded generation role only, no raw URI/host/status text | cardinality test |
| Telemetry pre-export leak | sanitize at attribute construction, peer address trust-zone gate | SDK/processor/exporter forbidden-value test |
| Log injection/secret leak | structured sanitized fields, no throwable message/body | hostile header/body log test |
| Dependency compromise | lock/checksum/SBOM/vulnerability/license/KEV gate | supply-chain CI |
| Stale certificate/secret | generation metadata, expiry alert, normal rotation drill | rotation test/runbook |
| Revoked/compromised material fallback | emergency admission block, session/token/pool invalidate, NOT_READY | emergency revoke/no-fallback test |
| Certificate-directed SSRF | automatic AIA/CRLDP/implicit OCSP off or named responder egress | malicious certificate zero-side-effect test |
| HTTP/2 coalescing leak | coalescing disabled/verified, auth/pool isolation | multi-origin H2 test |
| 0-RTT replay | HTTP/3 excluded; mutations/credentials never 0-RTT | provider policy test |
### 35.2 Trust zones
Destination registry가 trust zone을 선언한다.
```text
INTERNAL_SERVICE
TRUSTED_PARTNER
PUBLIC_FIXED_ORIGIN
UNTRUSTED_FETCH
```
Zone은 기본 policy bundle을 고르지만 operation catalog보다 권한을 넓히지 않는다.
`UNTRUSTED_FETCH`는 baseline provider의 mode가 아니라 별도 readiness card/capability다.
### 35.3 Dynamic URL fetch 분리
Image/PDF preview처럼 user-provided URL이 정말 필요하면 별도 adapter port로 둔다.
- public IP만 허용하는 dedicated resolver/egress proxy;
- redirect hop마다 재검증;
- credential/cookie/trace/baggage zero;
- port/scheme allowlist;
- network policy로 metadata/control-plane/private CIDR 차단;
- content type sniffing과 decoded cap;
- sandbox/antivirus/timeout;
- audit와 abuse rate limit.
초기 card `httpclient-untrusted-url-fetch``not-implemented`다. Fixed-destination client에 boolean
하나로 열 수 없다.
### 35.4 Security defaults
- TLS/hostname verification on;
- redirect off;
- cookies off;
- engine automatic retry off;
- raw absolute URL off;
- arbitrary caller header off;
- proxy direct fallback off when proxy selected;
- wire/body logging off;
- trust-all/hostname-ignore API absent;
- unbounded buffer/queue absent;
- hidden auth challenge replay off unless carded;
- certificate pinning은 일반 default가 아니라 운영 가능한 rotation design이 있을 때만 opt-in.
### 35.5 Network policy와 application policy
Application SSRF 방어만으로 충분하지 않다. Deployment에는:
- destination/proxy egress allowlist;
- cloud metadata/control-plane deny;
- DNS egress 제한;
- service account 최소 권한;
- proxy access log와 alert;
- secret volume permission;
을 적용한다. 반대로 network policy만 믿고 raw URL을 application에서 허용하지 않는다. 두 층이
독립적으로 실패를 막는다.
## 36. Test strategy
### 36.1 Test pyramid
| Layer | 목적 | 외부 자원 |
| --- | --- | --- |
| pure unit/property | policy, parser, deadline, classification | 없음 |
| engine contract | 실제 provider wire behavior | loopback fake/raw server |
| fault integration | TCP/DNS/TLS/proxy/resource fault | pinned local containers |
| composition | binding/zero-resource/wiring/lifecycle | Spring context |
| compatibility | upstream schema/protocol fixtures | recorded/generated fixtures, no real partner |
| load/soak | pool, leak, retry amplification | isolated CI/nightly |
실제 인터넷 partner endpoint를 CI에서 호출하지 않는다. Fake server/container는 loopback 또는
CI private network에만 둔다.
### 36.2 Pure unit/property tests
최소:
- destination/operation ID normalization;
- relative path encoding과 dot-segment/double-encoding;
- query multi-value/order/null policy;
- URI scheme/host/port/CIDR validation;
- IPv4/IPv6 mapped/obfuscated, NAT64/6to4/Teredo/special-purpose address classification;
- header CR/LF/NUL, count, byte limit;
- forbidden header ownership;
- media type and charset selection;
- status success/error mapping;
- `Retry-After` delta/date parsing, past/overflow/cap;
- retry decision predicate truth table;
- body replayability/idempotency/reconciliation matrix;
- `operationSemantics × transmissionEvidence × processingEvidence × cancellationOutcome ×
responseIntegrity × responseSemanticClass × bodyReplayability` exhaustive disposition matrix;
- monotonic remaining budget and phase cap;
- jitter range with deterministic random source;
- CB record/ignore matrix;
- decoded/wire byte and ratio accounting;
- failure taxonomy exhaustive mapping;
- config cross-field validation;
- immutable `AllowedChildEdge` exact-match와 wrong-kind/wrong-child/unknown-fingerprint/cross-root lease
replay가 child/root token 소비와 wire side effect 0으로 거절되는 property;
- full effective profile tuple exact matching, card-set equality와 interaction scenario derivation;
- profile별 `minimumRequiredRootHttpAttempts`와 protected/token/reconcile/proxy/revocation cap
cross-field 계산;
- log/metric sanitizer/cardinality.
Deadline test는 fake monotonic clock와 deterministic scheduler로 정확히 검증하고 wall clock에
의존하지 않는다.
Profile resolver property test는 각 축이 개별 card에서 지원되더라도 full tuple entry가 없으면
항상 거절한다. Exact entry가 있을 때만 그 entry의 required card set과
base/conditional/interaction scenario set을 반환하며, 등록되지 않은 cartesian product를
생성하지 않는다. Validated behavior field 하나를 바꾸면 canonical behavior digest가 바뀌고,
새 exact compatibility entry/evidence 없이는 실패함을 mutation/property test로 검증한다.
### 36.3 HTTP semantic contract
`MockWebServer` 같은 programmable loopback server와 필요한 경우 raw socket fixture로:
- all declared success status;
- 3xx default reject;
- every 4xx/5xx mapping;
- 204/HEAD no-body behavior;
- error-body drain/close cap;
- duplicate headers/trailers;
- interim 100/103;
- chunked/fixed/close-delimited framing;
- malformed status/header/framing;
- connection reuse after success/error/partial close;
- keep-alive expiry/stale connection;
- media type/charset mismatch;
- gzip/other declared encoding;
- pagination/ETag/conditional request;
- provider hidden retry/redirect/cookie disabled.
401 stale-credential challenge, 429/503 retry control, declared 3xx와 reconciliation signal은 contract가
요구한 header/body validation 뒤 공통 cleanup/disposition resolver를 반드시 통과하며
completed/domain outcome으로 조기 반환되지 않는지 검증한다. Deterministic barrier로
`VALID_HEADERS_ONLY`는 header 검증 직후 cancellation보다 먼저 response CAS를 이길 수 있고 body
drain failure가 결과를 덮지 않으며, body-required outcome은 decode/semantic 완료 전 response CAS를
이기지 못함을 고정한다. Cancellation/failure/response-body callback 경합에서 winner와 cleanup
finalizer가 각각 정확히 하나이고 loser가 body/connection을 다시 닫지 않는지도 검증한다.
`RestClient.exchange()` 경로에는 status handler가 자동 적용된다고 가정하지 않고 callback이 status를
먼저 분기하는 contract test를 둔다.
### 36.4 Deadline와 cancellation
Server fault:
- accept하지 않음;
- connect 후 TLS bytes 정지;
- request body를 읽지 않음;
- response header를 보내지 않음;
- body byte를 천천히 drip;
- retry response 후 긴 `Retry-After`;
- pool slot을 점유한 채 정지;
- streaming callback이 정지/예외/조기 반환.
모든 경우:
1. logical call이 total deadline upper bound + 작은 scheduler tolerance 안에 반환;
2. execution cutoff에서 active engine request/stream cancel/close를 시작;
3. cooperative/normal cleanup은 caller deadline `D` 안에 끝나고 connection/permit를 정확히 한
번 회수 또는 폐기;
4. uncooperative task는 caller를 붙잡지 않고 `D`에 quarantine되며 reusable pool로 한 번도
반환되지 않음;
5. quarantined physical-attempt permit는 실제 task 종료까지 유지되고 orphan count가 새
admission capacity에 반영됨;
6. bounded orphan registry/queue/reaper가 `orphanCleanupTimeout` 안에 정리하고, 초과 시
generation을 `DEGRADED/NOT_READY`로 전환해 새 호출을 거부;
7. retry/backoff/새 network side effect가 execution cutoff 뒤 시작되지 않음;
8. 정상 경로에는 background task/thread가 남지 않고 quarantine 경로에는 registry가 추적하지
않는 task/thread가 남지 않음;
9. mutation은 transmission phase에 따라 `INDETERMINATE`를 보존.
Fake monotonic clock으로 `executionCutoff = D - cleanupReserve`와 caller return upper bound를
분리 검증한다. Parent deadline이 이미 지난 경우 synchronous cleanup budget 0과 즉시 quarantine
경로를 검증한다. Thread interrupt, caller cancellation, shutdown cancellation도 별도 테스트한다.
Deterministic latch로 backoff, credential/body open, local quota, physical bulkhead, circuit
permission, pool lease와 DNS wait 각각의 직후 deadline/cancellation을 발생시킨다. 각 경계에서
후속 resource/network side effect count가 0이고 body/quota/permit lease가 역순으로 회수되는지
검증한다. Engine은 pool/DNS wait 뒤 cutoff가 지나면 connect/TLS/write를 시작하지 않아야 한다.
### 36.5 Pool와 concurrency
- max-total/max-per-route 초과 연결 없음;
- pending acquire/queue finite;
- queue timeout은 `REJECTED_BEFORE_SEND`;
- logical bulkhead와 physical bulkhead 독립;
- virtual thread 수가 pool capacity를 우회하지 않음;
- canceled waiter가 queue에서 제거;
- response close 누락 방지;
- half-open probe가 bounded;
- two destinations/policies 간 pool isolation;
- old generation drain 중 new generation 정상;
- 반복 10k+ 호출 후 connection/thread/file-descriptor/heap 안정.
Soak의 exact 호출 수와 시간은 CI budget에 맞추되 leak assertion과 before/after resource delta를
artifact로 남긴다.
### 36.6 Retry, breaker와 mutation
Table-driven scenario:
| Scenario | Expected |
| --- | --- |
| safe GET, connect-before-send failure | bounded retry |
| safe GET, 503 + valid `Retry-After` | capped wait 후 retry |
| safe GET, 429 beyond deadline | no retry, stable rate-limit failure |
| one-shot upload failure | no retry |
| non-retryable mutation attempt 0 | retry token 없이 한 번 실행 |
| non-retryable mutation, exact `NOT_SENT`, explicit reopenable policy | pre-send restart budget을 소비해 최대 한 번 추가 실행 |
| non-retryable mutation, `MAYBE_SENT` 이상 | `INDETERMINATE`, auth/redirect 포함 blind replay 없음 |
| non-keyed POST, response loss | `INDETERMINATE`, no blind retry |
| keyed POST, response loss | same key/digest로 inspect/reconcile |
| keyed POST payload mismatch | local reject |
| 400/401/403/404 | breaker ignore, default no retry |
| 500/502/503/504 configured | physical attempt breaker record |
| codec/oversize/programmer failure | breaker ignore |
| CB open | no engine/pool acquisition |
| half-open | exact configured permits |
| retry + redirect + auth replay 조합 | 사유별 counter와 protected physical ceiling을 넘지 않음 |
| token refresh 자체 retry + protected replay | nested credential cap과 root-call total ceiling을 넘지 않음 |
| second stale-token 401 | terminal, no second refresh/replay |
| caller/deadline/shutdown cancel + exact `NOT_SENT` | typed `RETURN_CANCELLED(reason)` |
| mutation cancel + `MAYBE_SENT+|UNKNOWN` | `RETURN_INDETERMINATE`, not cancelled/permanent |
| declared redirect future card | `FOLLOW_DECLARED_REDIRECT`, new hop lease/ordinal/common ceilings |
| H2 exact not processed + opted-in replayable body | `RESTART_CONFIRMED_NOT_PROCESSED` |
| H2 maybe/unknown processed | no mutation reattempt |
README가 아니라 test에서 `retry -> CB(physical attempt)` 실행 순서와 exact attempt/breaker count를
검증한다. Initial/retry/pre-send restart/same-intent/auth/redirect/protocol resend 각각이 동일
`physicalAttemptOrdinal`과 protected/root shared token을 정확히 한 번 소비하는 property test를
둔다. Engine hidden resend가 pre-start gate를 우회하면 provider qualification이 실패해야 한다.
Pure eligibility 호출은 모든 counter가 불변이고, 각 후속 disposition에서
`AttemptAuthorizationLease`가 정확히 한 번 생성·bind/abort되며 auth replay token을 두 번 소비하지
않는지 검증한다. Final protected/root reservation CAS failure는 engine start/ordinal increment 0,
reverse cleanup과 authorization abort를 보장한다.
Authorization 직후 refresh/hop/protocol revalidation, body open, quota reject, bulkhead/CB acquire,
credential signing, local preflight와 각 common-gate failure를 하나씩 주입한다. 모든 pre-bind exit에서
`AttemptAuthorizationLease.abort` exactly once, engine/root commit/ordinal increment 0, 획득 resource
역순 정리를 검증한다. Reconciliation은 poll wire request마다 정확히 하나의
`NestedHttpAuthorizationLease(RECONCILIATION)`를 acquire/bind 또는 abort하고, retry를 포함한 실제
request count가 reconciliation child cap과 shared root ceiling을 넘지 않는지 검증한다.
`CircuitPermissionLease` contract는 다음 failure injection마다 terminal callback이 정확히 한
번인지 검증한다.
- `ACQUIRED` 뒤 body open/span creation/local preflight failure;
- `ACQUIRED -> STARTING` handoff 직전/직후 cancellation;
- engine synchronous start failure와 asynchronous failure, callback-before-return race;
- success, recordable failure, ignored outcome;
- execution cutoff/deadline/caller cancellation/shutdown;
- half-open concurrent permission race;
- response/connection cleanup failure;
- double completion 시도.
Local preflight에서 engine ownership이 없거나 ignored outcome이면 `releasePermission`,
`STARTING|STARTED`의 recordable success/failure면 tracker evidence에 따라 각각
`onSuccess`/`onError`만 호출한다. Permit leak, synchronous start race, half-open slot leak와 double
record는 모두 failure다.
Transmission tracker는 first possible request write 직전 `NOT_SENT -> MAYBE_SENT` callback과
단조 전이를 raw fixture로 검증한다. 각 전이 지점에서 cancel/valid response를 동시에 release해
terminal CAS winner가 하나뿐이고 mutation `MAYBE_SENT+|UNKNOWN`은 항상 `INDETERMINATE`, exact
`NOT_SENT`만 cancelled/pre-send restart가 되는지 검증한다. Handoff 뒤 confidence unknown에서
later authoritative response headers/completion만 progress를 refine하고 response 없이
`NOT_SENT|SENT`로 downgrade/guess하지 않는 property를 포함한다.
Mutation disposition은 네 operation semantics 각각에 대해
`NOT_SENT|MAYBE_SENT|SENT|RESPONSE_STARTED|RESPONSE_COMPLETE|UNKNOWN`, processing evidence와
cancellation winner 전부를 `VALID_COMPLETE|VALID_HEADERS_ONLY|NONE_OR_INVALID` 및 모든
`ResponseSemanticClass`와 교차한다. Same-intent
replay/reconciliation에서는 operation attempt ID, idempotency key, fingerprint, operation ID,
tenant/credential scope가 바뀌지 않음을 검증한다.
Attempt-number property test는 ordinal 0에서 reason token/replayability를 요구하지 않지만
protected/root token을 한 번 소비하고, ordinal>0에서는 disposition/body/reason authorization까지
요구함을 고정한다. Committed local provider-quota token은 success/failure/cancel 모두 환불하지 않고
uncommitted reservation만 release하는지도 검증한다. `SINGLE_USE_SOURCE`와
non-retryable default protected physical ceiling=1, confirmed-`NOT_SENT` pre-send restart opt-in ceiling=2,
restart/retry budget/metric 분리, `MAYBE_SENT`부터 receipt 반환을 각각 검증한다.
현재 결함을 닫는 회귀 test도 포함한다.
- 서로 다른 `OutboundRetryPolicy` instance를 resilience/client에 전달해도 silent retry disable이
재발하지 않으며 새 aggregate API는 그런 wiring 자체를 표현할 수 없음;
- response wire/decoded cap 초과는 `RESPONSE_TOO_LARGE` 계열, no-retry, breaker-ignore,
status-preserving, logical failure observation exactly once;
- oversized 4xx/5xx가 connect failure로 바뀌거나 반복 다운로드되지 않음.
### 36.7 Streaming과 body
- buffered path는 decoded hard cap 전 allocation을 제한;
- upload stream은 reopenable/one-shot을 구분;
- callback scope 밖 stream access 실패;
- callback return/throw/cancel 모두 response close;
- consumer가 일부만 읽고 반환해도 drain-or-discard policy;
- lying `Content-Length`;
- truncated fixed/chunked/gzip;
- compression bomb와 ratio limit;
- slow decompression/decoder deadline;
- multipart part/count/header/total limit;
- spooled temp file quota/permission/cleanup;
- range resume validator mismatch;
- error response가 success reader에 전달되지 않음.
현재 test의 `readAllBytes()`는 production streaming proof로 인정하지 않는다.
### 36.8 DNS와 SSRF
두 수준으로 검증한다.
1. scripted resolver unit test: A/AAAA/mixed/empty/timeout/rebind/TTL;
2. pinned CoreDNS/dnsmasq-like container: resolver integration, cache expiry, address rotation.
Cases:
- loopback, link-local, private, multicast, unspecified, IPv4-mapped IPv6;
- NAT64 well-known/custom prefix의 embedded private/metadata IPv4, 6to4, Teredo와 special-purpose;
- public + private mixed answer;
- validation 뒤 다른 address로 connect하지 않음;
- redirect hop 재해석/재검증;
- DNS timeout도 total deadline 포함;
- Kubernetes short name/search-domain ambiguity;
- proxy remote-DNS mode에서 local resolver bypass 정책.
### 36.9 TLS와 mTLS
Test가 매번 ephemeral CA/server/client certificate를 생성한다.
- trusted/wrong/untrusted/expired/not-yet-valid cert;
- hostname/SAN mismatch;
- TLS protocol/cipher floor;
- server requests client cert: present/missing/wrong;
- trust/key material malformed;
- OCSP/revocation profile behavior와 effective JVM PKI property assertion;
- loopback/metadata/private/oversized AIA·CRLDP 악성 certificate에서 automatic network side effect 0;
- named revocation responder의 SSRF/redirect/body/deadline/cache/concurrency bound;
- OCSP authorized signature/CertID/status/time/nonce와 CRL issuer/signature/scope/base+delta/freshness;
- 각 named HTTP OCSP/CRL request가 revocation child cap과 parent root HTTP token을 소비;
- revocation child authorization/root reservation 실패 시 responder network side effect 0, cache hit과
valid stapled evidence에서는 child/root token 소비 0;
- stale/wrong-responder/replayed-good/UNKNOWN OCSP와 wrong-scope/stale CRL reject, exact cache-key isolation;
- handshake timeout;
- `NORMAL_ROLLOVER` N -> N+1, atomic swap, old connection drain;
- normal rollover의 new certificate invalid이면 non-revoked old generation 유지;
- `EMERGENCY_REVOKE` old admission 즉시 차단, session/token/pool invalidate, no fallback,
replacement 전 NOT_READY와 policy-driven in-flight cancellation;
- secret/cert가 logs/JUnit/artifact에 없음.
### 36.10 Proxy
Pinned proxy fixture로:
- HTTP CONNECT success/failure/auth;
- proxy DNS와 local DNS policy;
- `NO_PROXY` precedence를 사용하지 않는 explicit bypass list;
- proxy unavailable 시 direct fallback 금지;
- proxy redirect/credential stripping;
- TLS tunnel hostname verification;
- pool isolation by proxy route와 parent profile fingerprint linkage;
- proxy dependency cycle/unknown maturity/ambient or direct fallback startup reject;
- `Proxy-Authorization`이 CONNECT hop에만 존재하고 tunnel origin/telemetry에는 0회인 raw capture;
- 각 HTTP CONNECT가 proxy child cap과 parent root HTTP token을 소비;
- CONNECT child authorization/root reservation 실패 시 CONNECT/origin request write 0, existing tunnel
재사용 시 CONNECT child/root token 소비 0;
- shared proxy owner/reference count와 shutdown/drain.
### 36.11 HTTP/2
`httpclient-http2` card 선택 시:
- ALPN H2 success;
- `H2_REQUIRED` fallback reject;
- stream concurrency/pending bound;
- SETTINGS reduction;
- GOAWAY/RST_STREAM/REFUSED_STREAM별 `CONFIRMED_NOT_PROCESSED|MAYBE_PROCESSED|UNKNOWN`과
exact disposition/protocol-restart lease/counter;
- flow-control stall/deadline;
- header list/HPACK abuse;
- server push disabled;
- connection coalescing disabled/verified;
- proxy CONNECT compatibility;
- negotiated protocol metric.
HTTP/1-only baseline test 성공이 HTTP/2 readiness를 의미하지 않는다.
### 36.12 Authentication
- API key/static bearer exact destination/header ownership와 conditional scenario derivation;
- OAuth token cache single-flight;
- token expiry skew/refresh failure;
- token endpoint 자체 exact auth-purpose/client-auth mode tuple, header/body/signature redaction,
timeout/pool/retry isolation과 acyclic dependency DAG;
- child profile fingerprint/maturity가 parent OAuth evidence에 귀속;
- nested credential cap과 root-call total amplification ceiling;
- exact cache/single-flight key 각 축의 collision/isolation과 normal/emergency invalidation;
- current generation attempt별 선택, waiter cancel detach와 shared refresh ownership;
- concurrent root callers의 creator budget lease, immutable flight deadline, owner detach, joiner
no-double-charge와 zero-waiter cancel;
- one 401 refresh replay upper bound, prior response/CB/bulkhead release와 second 401 terminal;
- replay-safe operation만 auth replay;
- redirect/cross-origin credential stripping;
- multi-tenant token cache scope;
- request-signature/HTTP Basic mode는 current card set에서 startup reject;
- future request-signature card가 추가될 때 canonicalization/body digest/replay suite;
- no secret in exception/log/span/metric/JUnit report.
### 36.13 Observability contract
In-memory OTel exporter와 meter registry로:
- logical call 1개, physical span N개;
- `http.request.resend_count`;
- parent context/`tracestate` 보존;
- sampling flag 강제 변경 없음;
- destination propagation deny;
- no duplicate CLIENT span;
- compatibility timer logical once;
- failure stage/code;
- metric tag allowlist/cardinality bound와 generation role enum만 사용;
- attribute 생성 시점부터 raw URL/query/header/body/token/tenant/idempotency key가 SDK/processor/
exporter 전체에 0회;
- `network.peer.address` trust-zone opt-in과 external/untrusted profile deny;
- untrusted fetch에서 auto HTTP instrumentation/`server.address`/`url.full`이 0회이고 fixed bounded
project telemetry만 생성;
- global MeterFilter side effect 없음;
- canceled/indeterminate span status.
### 36.14 Composition와 lifecycle
Spring context matrix:
- zero binding -> zero resource/bean/health side effect;
- one/two destination exact qualified adapter;
- unknown/duplicate/conflicting canonical+legacy fail;
- provider definition only -> disabled;
- derived selected card 또는 exact compatibility profile이 `release-eligible`이 아님 -> ACTIVE
fail;
- binary/API-key/static-bearer/custom-trust mode의 exact profile tuple 또는 conditional/interaction
scenario 누락 -> resource 생성 전 fail;
- individually supported axes를 섞은 미등록 조합(예: API-key + streaming)이 compatibility entry
없이 들어오면 resource 생성 전 fail;
- redirect-follow/request-signature/HTTP Basic처럼 canonical card 없는 mode -> resource 생성 전
fail;
- startup order;
- ingress drain before outbound admission close;
- accepted request lease allowed during grace;
- background new call reject;
- grace expiry cancel;
- pool/executor/resolver/watcher close once;
- normal generation swap과 emergency revoke/no-fallback/NOT_READY;
- OAuth token/proxy/revocation child dependency DAG, owner/reference count와 zero-binding close;
- advertised cold OAuth/401/proxy/revocation path보다 root cap이 작으면 startup/qualification failure;
- `QUALIFICATION_ONLY`는 candidate exact tuple을 composition하지만 `ACTIVE_READY`/release assertion을
절대 만들지 못함;
- context restart no resource leak.
### 36.15 Compatibility fixtures
Destination contract fixture는:
- request method/path/query/header/media/schema;
- success/error schema;
- tolerant optional field behavior;
- enum unknown policy;
- pagination/ETag/version;
- recorded sanitized examples;
를 검증한다. Consumer-driven contract 도구를 쓰더라도 secret/PII가 fixture에 들어가지 않고
provider verification 결과를 release artifact로 연결한다.
### 36.16 Fault-tool 경계
Toxiproxy는 TCP latency/reset/bandwidth/toxic 검증에만 사용한다. 다음을 대신하지 않는다.
- DNS rebinding;
- malformed HTTP framing;
- TLS certificate semantics;
- HTTP/2 stream/GOAWAY;
- application idempotency.
각 fault에 맞는 fixture를 사용해 한 도구가 모든 보장을 증명한다고 과장하지 않는다.
## 37. Readiness cards와 CI design
### 37.1 Maturity registry와 selection resolver
Canonical maturity registry:
```text
src/config/httpclient/readiness-cards.yaml
```
Registry는 card 구현 성숙도와 card 자체의 base evidence만 소유한다. 지원 profile 조합을 축별
목록으로 소유하지 않는다.
```text
id
maturity: not-implemented | implemented-candidate | release-eligible
base-scenario-ids
required-gradle-task
required-services/images
required-runbooks
owner
```
Canonical exact profile compatibility registry:
```text
src/config/httpclient/profile-compatibility.yaml
```
각 entry:
```text
profile-id
maturity: not-implemented | implemented-candidate | release-eligible
tuple:
provider-id
resolved-provider-artifact-version
jdk-major
protocol
request-body-mode
response-body-mode
request-codec-media-mode
response-codec-media-encoding-mode
tls-mode
auth-purpose
auth-mode
proxy-mode
redirect-mode
dns-address-mode
egress-trust-zone
resilience-mode
operation-semantics
operation-policy-revision
dependency-profile-fingerprints
effective-behavior-digest
required-cards
required-scenario-ids
interaction-scenario-ids
owner
```
Tuple dimension은 모두 필수이며 wildcard/version range/omission을 허용하지 않는다.
`required-cards`는 §33.2 requirement rules와 byte-for-byte 일치해야 한다.
`interaction-scenario-ids`는 streaming + API-key, proxy + mTLS처럼 개별 축 test의 합으로
증명할 수 없는 조합을 검증한다. 새로운 조합은 이 registry entry, interaction test와 evidence
fingerprint를 함께 추가해야 한다.
Compatibility profile maturity:
| Maturity | 의미 |
| --- | --- |
| `not-implemented` | planned tuple/coverage gap; ACTIVE 선택 불가 |
| `implemented-candidate` | exact tuple code와 모든 scenario가 있으나 release review/runbook 승인 전 |
| `release-eligible` | exact tuple/card composition, interaction evidence와 운영 자산이 승인됨 |
Card와 compatibility profile maturity는 서로 대체하지 않는다.
```text
effectiveReadiness =
all required cards release-eligible
AND exact compatibility profile release-eligible
AND exact scenario set passes
```
새 compatibility entry는 항상 `not-implemented`로 시작하고 evidence/review 없이 바로
`release-eligible`로 만들 수 없다. 초기 설계 직후 모든 profile entry도 `not-implemented`다.
Canonical release assertion registry:
```text
src/config/httpclient/release-profile-assertions.yaml
```
각 entry는 release `profile-id`, 실제 deployment configuration resource/digest,
`expected-state`, `expected-selected-cards`, `expected-compatibility-profile-ids`만 가진다.
Binding/provider/card·profile maturity나 tuple 내용을 복제하지 않는다. CI는 그 configuration을 실제
Spring property binding과 같은 resolver로 읽고 derived card/tuple set을 계산한 뒤 assertion과
대조한다.
`selected`는 registry state가 아니다. §33.2 resolver가 active bindings와 operation마다 exact
effective profile tuple을 만들고 compatibility entry의 required cards를 검증해 derived selected
set을 계산한다. Checked-in release assertion registry는 expected
state/card/compatibility-profile ID assertion만 소유한다. CLI flag, classpath, Docker
availability로 set을 바꾸지 않는다.
Maturity 의미:
| Maturity | 의미 |
| --- | --- |
| `not-implemented` | 설계/gap만 존재; release 선택 불가 |
| `implemented-candidate` | 코드와 evidence가 있으나 production release 승인 전 |
| `release-eligible` | exact supported profile evidence, review, runbook가 모두 승인됨 |
Derived selected card 또는 exact compatibility profile이 `release-eligible`이 아니면 ACTIVE
startup/readiness/release가 실패한다.
Candidate 승격 deadlock을 피하기 위해 test harness에만 `QUALIFICATION_ONLY` mode를 둔다.
Production과 동일한 property binding, dependency DAG, tuple resolver, behavior digest와 resource
composition을 사용하되 마지막 maturity predicate만 exact `implemented-candidate` profile을 허용한다.
이 mode는 isolated qualification task에서만 선택할 수 있고 runtime `ACTIVE_READY`, release
assertion 성공, ingress readiness 또는 production descriptor를 절대 만들지 않는다. 결과는
candidate evidence/review 입력일 뿐이며 registry가 실제 `release-eligible`로 승인되기 전에는
production 선택이 계속 실패한다.
### 37.2 Canonical card set
| Card ID | 범위 | 이번 설계 직후 실제 maturity |
| --- | --- | --- |
| `httpclient-static-buffered` | fixed destination, relative URI, H1, bounded JSON/bodiless minimum; registry-declared bounded binary/API-key/static-bearer/custom-trust conditional modes; server TLS, pool, deadline/hard cancel, safe-read retry/physical CB | `not-implemented`; first promotion target |
| `httpclient-idempotent-mutation` | declared idempotent/keyed mutation, same-identity replay, unknown outcome reconciliation | `not-implemented` |
| `httpclient-non-retryable-mutation` | one-shot mutation, transmission evidence, no blind retry, indeterminate receipt/reconciliation handoff | `not-implemented` |
| `httpclient-streaming-download` | bounded scoped streaming response | `not-implemented` |
| `httpclient-streaming-upload` | reopenable/one-shot upload, write cancellation | `not-implemented` |
| `httpclient-mtls` | client certificate, trust/key rotation | `not-implemented` |
| `httpclient-oauth2-client-credentials` | isolated token client/cache/refresh | `not-implemented` |
| `httpclient-egress-proxy` | explicit CONNECT/proxy DNS/no direct fallback | `not-implemented` |
| `httpclient-http2` | H2 negotiation, stream capacity, GOAWAY/flow control | `not-implemented` |
| `httpclient-untrusted-url-fetch` | dedicated public fetch security boundary | `not-implemented` |
Minimum target compatibility profile ID는 예를 들어
`httpclient-static-h1-json-auth-none-direct-safe-read.v1`처럼 bounded/stable하게 둔다. ID가
tuple 내용을 대신하지 않으며 registry의 exact tuple과 digest가 authority다. 이번 설계 직후 이
profile maturity도 `not-implemented`다.
현재 R1 skeleton test를 baseline card implementation으로 승격하지 않는다. Redirect-follow,
request-signature, HTTP Basic, HTTP caching, WebSocket, SSE, HTTP/3는 이 card set에 몰래
포함하지 않으며 활성화하면 실패한다.
Minimum R2는 §4.2의 `httpclient-static-buffered` JSON/bodiless + auth-none profile을 뜻한다.
Optional card나 conditional mode를 선택하지 않았다는 이유로 minimum R2를 실패시키지 않지만,
선택하지 않은 기능을 R2라고 부르지 않는다.
### 37.3 Expected deployment state
Release profile은 정확히 하나를 선언한다.
```text
DISABLED
ACTIVE
```
Truth table:
| Expected | Binding/derived card/resource | Result |
| --- | --- | --- |
| `DISABLED` | 모두 0 | `DISABLED_VERIFIED`, HTTP R2 label 없음 |
| `DISABLED` | 하나라도 존재 | failure |
| `ACTIVE` | binding 0 | failure |
| `ACTIVE` | binding >= 1, derived cards + exact compatibility profiles release-eligible/pass | selected profile ready |
| `ACTIVE` | card/profile/evidence/consumer 하나라도 누락 | failure |
Environment에서 key가 사라져 ACTIVE가 우연히 DISABLED green으로 바뀌지 않는다. Release gate는
task exit code와 함께 expected state, resolved selected card set, exact compatibility profile
set, runtime descriptor state가 모두 일치하는지 검사한다.
### 37.4 Evidence identity
Evidence key:
```text
compatibility-profile-id
× required-card-set
× provider-id
× resolved-provider-artifact-version
× JDK-version
× protocol
× request/response-body-mode
× request/response-codec-media-encoding-mode
× TLS/auth-purpose/auth/proxy/redirect mode
× DNS-address/egress-trust-zone/resilience mode
× operation-semantics
× operation/policy revision
× sorted child dependency profile fingerprints
× effective-behavior-digest
× evidence-scenario-id
```
Unique selected profile fingerprint는 위 effective values와 다음 SHA-256을 포함한다.
- card registry;
- exact profile compatibility registry;
- release profile assertion registry와 referenced deployment configuration;
- operation catalog;
- dependency lockfiles;
- dependency verification metadata;
- test-image manifest;
- Gradle wrapper/settings;
- selected provider descriptor.
다른 provider/version/profile의 evidence를 재사용하지 않는다.
```text
claimedReadyProfileTuples == resolvedEffectiveProfileTuples
```
가 byte-for-byte 일치해야 한다. 사람이 입력하는 `evidence-revision` 문자열만으로 귀속하지 않는다.
### 37.5 Evidence categories와 executable results
```text
HSM = semantic
HSEC = security
HRES = deadline/resource
HLIF = lifecycle/composition
HOBS = observability/privacy
HCMP = compatibility/protocol
```
각 card는 여섯 category에 하나 이상의 anchor scenario를 가지지만 anchor 하나가 category 전체를
증명하지 않는다. Card registry의 base scenario와 exact profile compatibility entry의
required/interaction scenario가 합쳐져 §36의 해당 tuple 필수 시나리오를 열거한다.
```text
requiredScenarioSet(tuple) =
union(for card in compatibility.requiredCards: card.baseScenarioIds)
union compatibility.requiredScenarioIds
union compatibility.interactionScenarioIds
claimedEvidenceScenarioIds(tuple) == requiredScenarioSet(tuple)
executedScenarioIds(task) contains-all claimedEvidenceScenarioIds(tuple)
```
Missing/duplicate/unknown/다른 tuple로 claim한 scenario는 실패한다. Task가 무관한 추가 regression
test를 실행하는 것은 허용하지만 그 test를 profile evidence로 자동 claim하지 않는다. Executed
전체 test도 pass/no-skip 조건을 만족해야 하며, extra 실행이 missing evidence를 대체하지 못한다.
JUnit source tag 존재만 보지 않는다. Resolved evidence tuple마다 JUnit XML과 task result에서:
```text
tests > 0
failures = 0
errors = 0
skipped = 0
aborted = 0
disabled = 0
```
를 확인한다. Duplicate scenario ID, unknown tag, empty filter, stale fingerprint, missing XML은
failure다.
`not-implemented` card/profile은 planned scenario ID를 가질 수 있지만 coverage gap을
`NOT_SELECTED_GAP`으로 보고할 뿐 readiness task를 통과하지 않는다.
`implemented-candidate`와 `release-eligible` card/profile은 exact required scenario set이 실제
성공해야 한다.
### 37.6 Minimum anchor matrix
| Card | HSM | HSEC | HRES | HLIF | HOBS | HCMP |
| --- | --- | --- | --- | --- | --- | --- |
| `httpclient-static-buffered` | `HSM-STATIC-STATUS-RETRY-CB` | `HSEC-STATIC-SSRF-TLS` | `HRES-STATIC-HARD-CANCEL` | `HLIF-STATIC-ZERO-DRAIN` | `HOBS-STATIC-SPANS-PRIVACY` | `HCMP-STATIC-H1-JSON` |
| `httpclient-idempotent-mutation` | `HSM-MUTATION-UNKNOWN` | `HSEC-MUTATION-KEY` | `HRES-MUTATION-BUDGET` | `HLIF-MUTATION-RECONCILE` | `HOBS-MUTATION-REDACT` | `HCMP-MUTATION-CONTRACT` |
| `httpclient-non-retryable-mutation` | `HSM-NONRETRY-UNKNOWN` | `HSEC-NONRETRY-REDACT` | `HRES-NONRETRY-CANCEL` | `HLIF-NONRETRY-RECEIPT` | `HOBS-NONRETRY-REDACT` | `HCMP-NONRETRY-CONTRACT` |
| `httpclient-streaming-download` | `HSM-DOWNLOAD-STATUS` | `HSEC-DOWNLOAD-BOMB` | `HRES-DOWNLOAD-CANCEL` | `HLIF-DOWNLOAD-DRAIN` | `HOBS-DOWNLOAD-BOUNDS` | `HCMP-DOWNLOAD-RANGE` |
| `httpclient-streaming-upload` | `HSM-UPLOAD-REPLAY` | `HSEC-UPLOAD-MULTIPART` | `HRES-UPLOAD-WRITE` | `HLIF-UPLOAD-CLEANUP` | `HOBS-UPLOAD-REDACT` | `HCMP-UPLOAD-100` |
| `httpclient-mtls` | `HSM-MTLS-HANDSHAKE` | `HSEC-MTLS-VERIFY` | `HRES-MTLS-TIMEOUT` | `HLIF-MTLS-ROTATE` | `HOBS-MTLS-EXPIRY` | `HCMP-MTLS-VERSION` |
| `httpclient-oauth2-client-credentials` | `HSM-OAUTH-REFRESH` | `HSEC-OAUTH-LEAK` | `HRES-OAUTH-BUDGET` | `HLIF-OAUTH-CLOSE` | `HOBS-OAUTH-REDACT` | `HCMP-OAUTH-ERROR` |
| `httpclient-egress-proxy` | `HSM-PROXY-CONNECT` | `HSEC-PROXY-NOFALLBACK` | `HRES-PROXY-TIMEOUT` | `HLIF-PROXY-CLOSE` | `HOBS-PROXY-BOUNDED` | `HCMP-PROXY-TLS` |
| `httpclient-http2` | `HSM-H2-GOAWAY` | `HSEC-H2-COALESCE` | `HRES-H2-FLOW` | `HLIF-H2-DRAIN` | `HOBS-H2-PROTOCOL` | `HCMP-H2-ALPN` |
| `httpclient-untrusted-url-fetch` | `HSM-FETCH-REDIRECT` | `HSEC-FETCH-REBIND` | `HRES-FETCH-BOUND` | `HLIF-FETCH-ISOLATE` | `HOBS-FETCH-PRIVATE` | `HCMP-FETCH-CONTENT` |
`httpclient-static-buffered`의 required scenario에는 anchor 외에도 status-first error, response
wire/decoded bound, server TLS/hostname, DNS binding, pool exhaustion, cleanup reserve/orphan
quarantine, circuit permission exact-once, retry amplification, shutdown, OTel sanitizer가 포함된다.
Bounded binary/API-key/static-bearer/custom-trust mode를 선택하면 해당 codec/size,
credential ownership·rotation·redaction 또는 trust/rotation scenario를 exact tuple에 추가한다.
Streaming/proxy/mTLS 등 다른 card와 결합되면 compatibility entry가 interaction scenario를
추가한다. Tuple이 활성인데 exact entry나 conditional/interaction scenario가 빠지면 card task는
실패한다.
### 37.7 Exact proposed Gradle tasks
Common evidence:
```text
:adapter:outbound:httpclient:test
:adapter:outbound:httpclient:httpSemanticContractTest
:adapter:outbound:httpclient:httpSecurityTest
:adapter:outbound:httpclient:httpTlsTest
:adapter:outbound:httpclient:httpResilienceTest
:adapter:outbound:httpclient:httpDeadlineAndResourceTest
:adapter:outbound:httpclient:httpObservabilityTest
:adapter:outbound:httpclient:httpFaultTest
:adapter:outbound:httpclient:httpCompatibilityTest
:app-bootstrap:httpClientCompositionTest
verifyHttpClientProfileCompatibility
httpClientConsumerContractTest
verifyHttpClientOperationalAssets
```
`verifyHttpClientProfileCompatibility`는 production resolver와 같은 code path로 모든 checked-in
release configuration을 resolve한다. Missing/duplicate tuple, wildcard/range/omitted dimension,
card requirement drift, unknown/missing interaction scenario, 미등록 cross-card 조합, tuple과
JUnit evidence fingerprint 불일치, required/claimed evidence set 불일치를 실패시킨다. 축별
cartesian product를 자동 허용하지 않는다.
```text
httpClientProductionReadiness.dependsOn(verifyHttpClientProfileCompatibility)
```
Card readiness:
```text
httpClientStaticBufferedReadiness
httpClientIdempotentMutationReadiness
httpClientNonRetryableMutationReadiness
httpClientStreamingDownloadReadiness
httpClientStreamingUploadReadiness
httpClientMtlsReadiness
httpClientOauth2ClientCredentialsReadiness
httpClientEgressProxyReadiness
httpClientHttp2Readiness
httpClientUntrustedUrlFetchReadiness
httpClientProductionReadiness
httpClientAllImplementedCandidates
```
이 task는 future public build contract이며 현재 존재한다고 주장하지 않는다. Card task mapping은
registry에 byte-for-byte 기록하고 configuration time에 생성/검증한다. Unknown task, duplicate
mapping, empty tag expression, dependency cycle은 test 실행 전 실패한다.
`httpClientProductionReadiness`:
1. card/compatibility/release-assertion registry schema, maturity와 IDs;
2. expected state와 binding resolve;
3. derived selected card/compatibility-profile set와 release profile assertion;
4. selected card와 exact compatibility profile이 모두 release-eligible;
5. every effective full profile tuple exact compatibility match + card-set equality +
base/required/interaction scenario set equality;
6. selected profile fingerprint별 card task;
7. ACTIVE consumer/operation contract 또는 DISABLED explicit N/A;
8. bootstrap composition/zero-resource contract;
9. operational assets;
10. architecture/config/public path/env gates;
11. supply-chain gates;
12. JUnit no-skip/zero-test assertion;
13. sanitized descriptor/evidence artifact;
을 aggregate한다.
`httpClientAllImplementedCandidates`는 card와 compatibility profile 중
`implemented-candidate`/`release-eligible` 전체 exact tuple을 nightly 실행한다.
`not-implemented`는 `NOT_SELECTED_GAP`으로 보고한다.
### 37.8 Consumer와 operational-asset gate
`httpClientConsumerContractTest`:
- ACTIVE binding마다 feature-specific port adapter가 하나;
- adapter operation ID와 catalog가 일치;
- request/response mapper/compatibility fixture;
- generic invoker를 controller/use case가 직접 사용하지 않음;
- sample shape이면 planned registry edge와 sample-local adapter;
- production shape이면 HTTP leaf의 direct `application-core` dependency;
- raw `repoUrl`/absolute URL이 application call에 없음.
`verifyHttpClientOperationalAssets`:
- selected card의 runbook path 존재;
- owner/escalation/rollback/recovery verification section;
- non-placeholder SLO/dashboard/alert registry link;
- provider upgrade and reconciliation runbook;
- descriptor/card/catalog revision link.
DISABLED일 때만 consumer/SLO가 explicit N/A일 수 있다. Provider candidate nightly는 fixture
consumer를 사용하되 deployment ACTIVE evidence로 가장하지 않는다.
### 37.9 Exact supply-chain gates
```text
verifyDependencyLocks
verifyDependencyVerificationMetadataCoverage
verifyHttpClientRuntimeClasspathIsolation
verifyHttpClientTestImageManifest
generateHttpClientRuntimeSbom
generateHttpClientTestSbom
httpClientVulnerabilityLicenseKevGate
```
`verifyHttpClientRuntimeClasspathIsolation`은:
- test server/Testcontainers/Toxiproxy/CA fixture가 production runtime에 없음;
- Apache/JDK/provider dependency가 selected provider policy와 일치;
- Jackson/Boot RestClient runtime이 module-isolated JSON test에 존재;
- forbidden duplicate major/engine leakage report;
를 검증한다.
Image manifest gate는 required service key, exact `tag@sha256`, placeholder/`latest` 금지, pull한
image digest 일치를 검증한다. Runtime/test SBOM을 분리하고 scan 결과는 commit SHA와 SBOM digest에
귀속한다.
### 37.10 CI service matrix와 no-skip
| Evidence | Fixture |
| --- | --- |
| HTTP semantics | MockWebServer + raw byte server |
| TCP fault | Toxiproxy |
| DNS | pinned authoritative DNS container/scripted resolver |
| TLS/mTLS | ephemeral CA + TLS endpoint |
| Proxy | pinned HTTP CONNECT proxy |
| HTTP/2 | ALPN/H2-capable test server |
| Observability | in-memory OTel exporter |
| Resource | process/JFR/OS counters where available |
Container image SSOT:
```text
src/gradle/httpclient-test-images.properties
```
Selected card service가 없으면 release/readiness는 실패한다. `assumeTrue`, Docker unavailable,
missing image를 skip-success로 바꾸지 않는다. Local developer task만 explicit `NOT_RUN`을 보고할
수 있고 release evidence가 아니다. Expected test가 0개이거나 skipped/aborted/disabled가 하나라도
있으면 selected evidence는 실패한다.
### 37.11 Repository workflow integration
현재 `.github/workflows/ci-quality-gates.yml`의 `release-gate`는
`quality-gates`, `sample-off`, `gate-matrix-lint`만 집계한다. 구현 change에서 exact job을 추가한다.
```yaml
httpclient-production-readiness:
runs-on: ubuntu-latest
steps:
# checkout + exact Java + Gradle cache setup
- name: Verify resolved HTTP client readiness from a clean runner
working-directory: src
run: >-
./gradlew httpClientProductionReadiness
--rerun-tasks --no-build-cache --no-daemon --stacktrace
httpclient-supply-chain:
uses: ./.github/workflows/_dependency-vulnerability-reusable.yml
with:
capability: httpclient
```
`release-gate`:
```yaml
needs:
- quality-gates
- sample-off
- gate-matrix-lint
- httpclient-production-readiness
- httpclient-supply-chain
env:
HTTPCLIENT_RESULT: ${{ needs.httpclient-production-readiness.result }}
HTTPCLIENT_SUPPLY_CHAIN_RESULT: ${{ needs.httpclient-supply-chain.result }}
```
Success loop에도 두 result를 포함한다. Job만 추가하고 `needs`/env/loop 중 하나라도 빠지면 blocking
아니다.
별도 `dependency-vulnerability.yml` job은 다른 workflow의 `needs`로 직접 연결할 수 없다. 현재
install/Trivy/KEV/license logic을 pinned reusable workflow로 추출하고 기존 workflow와
`ci-quality-gates.yml`이 같은 implementation을 호출한다. Reusable workflow가 반환하는 commit
SHA/SBOM/scan digest가 현재 checkout과 다르면 실패한다.
같은 change에서:
- `.github/ci-gate-matrix.yml`에 `httpclient-production-readiness`,
`httpclient-supply-chain` 두 blocking gate 추가;
- 현재 19개 기준의 `EXPECTED_GATE_COUNT`를 21로 갱신;
- `.github/scripts/verify-gate-matrix.sh`가 blocking job set,
`release-gate.needs`, result env, shell success loop의 집합 동등성을 검증;
- workflow contract test가 unknown/missing/extra job을 실패;
하도록 한다.
### 37.12 Fresh-runner와 scheduled qualification
PR/release job의 root task가 required fixture를 Testcontainers/local server로 직접 provision한다.
이전 matrix artifact가 있어도 성공을 신뢰하지 않고 clean checkout에서
`--rerun-tasks --no-build-cache`로 selected evidence를 재실행한다. Fixture/image/secret reference가
없으면 실패한다.
별도 `.github/workflows/httpclient-production-readiness.yml`:
- `schedule`;
- `workflow_dispatch`;
- release-candidate trigger;
- `QUALIFICATION_ONLY`로 resolve한 all `implemented-candidate` profile matrix와 production
resolver로 resolve한 `release-eligible` matrix;
- selected minimum/next approved provider version;
- soak/fault/compatibility;
- final clean runner `httpClientAllImplementedCandidates`;
를 실행한다. Nightly candidate failure는 해당 card/compatibility profile maturity 승격
blocker이며 이미 별도로 검증된 selected release profile을 다른 not-implemented card 때문에
허위 실패로 바꾸지 않는다.
### 37.13 Sanitized artifact
- expected state와 `DISABLED_VERIFIED|ACTIVE_READY`;
- derived selected/maturity/gap card와 compatibility-profile set;
- exact compatibility profile IDs, full tuple와 fingerprints;
- provider/JDK/protocol/body/codec/media/encoding/TLS/auth-purpose/auth/proxy/redirect/DNS/egress/resilience/
operation-semantics matrix와 effective-behavior digest;
- required base/conditional/interaction scenario IDs;
- scenario별 JUnit counts;
- exact Gradle command;
- fault/resource timeline;
- readiness descriptor;
- operational asset validation;
- runtime/test SBOM digest;
- vulnerability/license/KEV result;
- card/compatibility/release-assertion registry, deployment config, catalog, lock와 image-manifest
SHA-256;
- commit SHA.
Raw URL/IP/header/body/certificate/key/token/tenant/idempotency key는 artifact에서 제거한다. Standard
OTel span sanitizer evidence는 raw span이 아니라 pass/fail과 bounded fixture IDs만 담는다.
### 37.14 Common repository gates
HTTP root task는 다음 existing/future exact task에 의존한다.
```text
verifyCleanArchitectureDependencies
verifyEnvKeys
verifyPublicPathSnapshot
verifyConfigurationPropertiesProcessor
verifyDependencyLocks
verifyDependencyVerificationMetadataCoverage
verifyHttpClientProfileCompatibility
verifyHttpClientRuntimeClasspathIsolation
verifyHttpClientTestImageManifest
generateHttpClientRuntimeSbom
generateHttpClientTestSbom
verifyHttpClientOperationalAssets
```
`httpClientVulnerabilityLicenseKevGate`는 generated SBOM과 exact image에 대한 workflow result를
release aggregator가 요구한다. 현재 존재하지 않는 task/job은 구현 전까지 gap이며 이 설계
문서만으로 CI가 보장한다고 주장하지 않는다.
## 38. Gradle, dependency와 supply chain
### 38.1 Production dependency ownership
초기 `apache-hc5-classic` provider 후보:
```text
implementation project(':application-core') // production fork port implementation이 있을 때만
implementation project(':shared-contract')
implementation project(':adapter:outbound:support')
implementation 'org.springframework.boot:spring-boot-autoconfigure'
implementation 'org.springframework.boot:spring-boot-restclient'
implementation 'org.springframework.boot:spring-boot-jackson' // JSON card/codec를 실제 소유할 때
implementation 'org.springframework:spring-web'
implementation 'org.apache.httpcomponents.client5:httpclient5'
implementation 'io.micrometer:micrometer-observation' // 직접 API 사용 시
implementation 'io.micrometer:micrometer-core' // 직접 meter 소유 시
implementation 'io.github.resilience4j:resilience4j-retry'
implementation 'io.github.resilience4j:resilience4j-circuitbreaker'
implementation 'org.slf4j:slf4j-api'
annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
```
Spring Boot 4의 `spring-boot-restclient`가 preconfigured builder/HTTP client integration을,
`spring-boot-jackson`이 Jackson 3 runtime/auto-configuration을 명시적으로 소유한다. Inbound web
starter나 test classpath에서 우연히 들어오는 converter/codec에 의존하지 않는다.
실제 compile/runtime API 사용을 확인해 필요 없는 dependency는 제거한다. Broad
`spring-boot-starter-*`, Resilience4j Spring starter/AOP, WebFlux starter를 convenience로 넣지
않는다.
두 consumer shape:
- production fork가 HTTP leaf 안에 `application-core` port adapter를 둘 때만 HTTP leaf에 direct
`application-core` dependency 추가;
- template sample은 HTTP leaf에 sample dependency를 추가하지 않고
`sample-portfolio`가 HTTP leaf에 의존하도록 registry/build file을 변경.
JSON을 baseline에서 지원한다면 `spring-boot-restclient`, Boot Jackson 3와 selected
`JacksonJsonHttpMessageConverter`가 HTTP leaf isolated runtime test에서 실제 resolve되어야 한다.
### 38.2 Version policy
- Spring Boot BOM이 관리하는 버전은 version 없이 선언하되 실제 managed version을 CI에서 출력/
검증;
- Boot BOM이 Apache HC5를 관리하는지 구현 시 확인하고 미관리면 version catalog/constraint의
단일 SSOT 사용;
- 현재 module build의 Resilience4j `2.2.0` 세 번 직접 표기는 version catalog/provider
platform/constraint로 이동;
- 모든 configuration lockfile 갱신;
- dependency verification checksum 갱신;
- lock diff와 transitive dependency review.
BOM 사용이 exact selected provider의 compatibility 증거를 대신하지 않는다.
### 38.3 API leakage
Apache, Spring HTTP, Resilience4j, Micrometer type은 모두 adapter implementation detail이다.
- application/domain port signature에 노출 금지;
- `api` dependency로 export하지 않음;
- adapter public package 최소화;
- engine/provider는 internal package;
- feature adapter만 application port 구현;
- 예외적으로 §10.5의 framework-neutral registered-operation adapter-consumer SPI만 public;
- application/inbound가 이 SPI를 import하지 못하도록 ArchUnit;
- ArchUnit/public-path snapshot으로 leakage 검출.
### 38.4 Optional provider isolation
HTTP/2/async/reactive/cloud-auth provider가 추가되면 dependency set을 core leaf에 모두 넣지 않는다.
선택지:
1. 같은 leaf의 isolated source set + runtime factory;
2. registry 변경을 동반한 별도 provider leaf;
3. app-bootstrap-only composition dependency.
Provider가 서로 다른 Netty/Jackson/Apache major version을 강제하면 별도 leaf가 우선이다.
Classpath에 provider가 둘 존재해도 자동 fallback하지 않는다.
### 38.5 Test dependencies
Test-only 후보:
```text
testImplementation MockWebServer
testImplementation Testcontainers core/JUnit integration
testImplementation Toxiproxy module or pinned proxy fixture
testImplementation property-based test library already standardized by repository
testImplementation OTel/Micrometer test exporter APIs actually used
```
Runtime artifact에 test server, Docker client, CA generator, proxy implementation을 포함하지
않는다. Groovy/Spock을 유지할지 JUnit을 사용할지는 repository test convention과 실행 task
분리를 기준으로 결정하며 둘을 이유 없이 중복 도입하지 않는다.
Module-isolation test는 inbound web/app-bootstrap의 transitive classpath 없이 HTTP leaf runtime만
구성해 다음을 검증한다.
- `spring-boot-restclient` auto-configuration과 prototype builder;
- exact Apache request factory;
- Boot observation/customizer preservation;
- Jackson 3 JSON converter와 Java time behavior;
- no Jackson 2 converter ambiguity;
- selected TLS bundle integration;
- missing direct runtime dependency가 startup failure로 드러남.
### 38.6 Supply-chain gate
- dependency locks;
- checksum/signature verification;
- SBOM with runtime/test distinction;
- CVE + CISA KEV equivalent policy;
- license allow/deny;
- transitive logging/codec/network library review;
- container image digest/SBOM;
- no repository from unapproved URL;
- no dynamic version/range/SNAPSHOT;
- dependency update compatibility suite.
HTTP parser/TLS/compression/HTTP2 vulnerability는 high-risk로 분류하고 emergency update 때도 semantic
contract와 readiness cards를 다시 실행한다.
### 38.7 Current dependency findings
현재 증거:
- `spring-web`, `spring-boot-autoconfigure`, Micrometer core, SLF4J는 직접 선언;
- Resilience4j retry/circuitbreaker/micrometer가 각각 `2.2.0`으로 직접 pin;
- production engine은 JDK `HttpClient`;
- Apache HC5 production dependency와 pool manager가 없음;
- HTTP leaf runtime에는 `spring-boot-restclient`가 없고 preconfigured Boot
`RestClient.Builder` 소유권이 없음;
- HTTP leaf runtime에는 baseline JSON을 보장할 direct Boot Jackson 3 dependency가 없음;
- HTTP leaf는 `application-core`에 직접 의존하지 않고 production semantic port를 구현하지 않음;
- test dependency는 Spock만 직접 선언;
- Resilience4j transitive runtime module이 lockfile에 존재해도 명시적 production contract를
의미하지 않음.
이번 문서는 `build.gradle`을 바꾸지 않는다. 위 항목은 implementation plan의 dependency diff와
lock refresh 대상이다.
## 39. Implementation와 migration sequence
### 39.1 Phase 0 — Characterization and truth-in-labeling
작업:
- 현재 public path/bean/config/test characterization;
- stream 4xx/5xx success bug 재현;
- retry/CB actual order test;
- `globalCallTimeout` non-cancellation test;
- JDK request-factory `readTimeout`이 logical total/byte-idle timeout을 증명하지 않는
characterization;
- retry policy object-identity mismatch의 silent retry-disable 재현;
- response-size violation이 connect failure/no-log/retry candidate로 오분류되는 경로 재현;
- sample `RepoStatsPortClient` fixture와 raw `repoUrl` 전달 경계 기록;
- manual trace header test;
- current descriptor/README를 R0/R1로 정직하게 표시.
Acceptance:
- 현재 결함이 failing characterization 또는 explicit gap test로 재현;
- 기존 passing test의 증명 범위가 문서화;
- production-ready/R2 표현 없음.
Rollback:
- 문서/characterization만 제거 가능하나 보장 과장은 되살리지 않음.
### 39.2 Phase 1 — Semantic ports, catalog와 disabled composition
작업:
- production fork는 `application-core` feature-specific port와 HTTP leaf adapter, template sample은
sample-local port/adapter와 bounded adapter-consumer SPI 중 한 shape를 명시적으로 선택;
- sample shape의 `sample-portfolio -> adapter-outbound-httpclient` registry/build edge와
`RepositoryCoordinates` typed input;
- adapter wire DTO/mapper;
- destination/operation/card registry;
- `expected-state`와 binding에서 exact effective profile tuples/card sets/interaction scenarios를
계산하고 maturity/compatibility registry와 release profile assertion을 대조;
- new card/profile maturity starts `not-implemented` and explicit promotion evidence;
- canonical typed settings;
- zero-binding composition;
- legacy configuration conflict validation.
Acceptance:
- application에는 HTTP/Spring/adapter type 없음;
- arbitrary URL/header API 없음;
- unmatched full profile tuple/card-composition/interaction-scenario gap은 resource 생성 전 fail;
- card-base profile-required interaction required set과 claimed evidence set equality;
- no binding zero resources;
- architecture/config binding tests 통과.
### 39.3 Phase 2 — Apache HC5 baseline engine와 pool
작업:
- injected Boot `RestClient.Builder` 또는 equivalent fully configured builder;
- HTTP leaf가 `spring-boot-restclient`, baseline JSON이면 `spring-boot-jackson`, selected Apache
provider dependency를 직접 소유;
- Apache connection manager;
- finite pool/acquire/lifetime/idle settings;
- hidden retry/redirect/cookie disable;
- effective option startup assertion;
- close handle/lifecycle;
- inbound web/app-bootstrap transitive classpath 없이 Boot RestClient customization과 Jackson 3
converter를 검증하는 module-isolation test.
Acceptance:
- H1 static buffered semantic/pool contract;
- no resource leak;
- selected engine exact;
- JDK fallback 없음.
Counterargument:
Apache classic blocking cancellation이 DNS/TLS/write/body 단계의 hard total deadline을 증명하지
못하면 이 phase를 R2로 승인하지 않는다. 같은 Engine SPI의 Apache async provider를 reference로
승격한다.
### 39.4 Phase 3 — Deadline, cancellation와 resilience
작업:
- monotonic deadline;
- positive cleanup reserve와 execution cutoff;
- phase caps;
- cancellable engine handle;
- quarantine, bounded orphan registry/reaper와 generation degrade;
- logical/physical admission;
- explicit retry loop;
- physical-attempt CB와 exact-once `CircuitPermissionLease`;
- retry budget/`Retry-After`;
- failure taxonomy.
Acceptance:
- every blocked phase deadline/cancel test;
- every wait 직후 gate recheck와 cutoff 뒤 zero new resource/network side effect;
- ordinary/restart/auth/redirect/replay counter와 protected/root physical ceiling의 exact count;
- `NOT_SENT -> MAYBE_SENT` linearization과 terminal-result/cancel CAS race;
- exact attempt/CB count와 `ACQUIRED -> STARTING -> STARTED/terminal` lease handoff;
- ordinal 0은 ordinary retry token/replayability 없이 실행되지만 shared protected/root token은
소비하고, ordinal>0은 disposition/body/reason budget까지 검사;
- non-retryable default one attempt, explicit `NOT_SENT`-only pre-send restart with separate budget,
`MAYBE_SENT` receipt;
- no retry after deadline;
- caller return `<= D + tolerance`, normal cleanup `<= D`;
- quarantine resource no-reuse, permit hold until task termination, bounded reaper/degrade;
- pool/permit cleanup exactly once.
### 39.5 Phase 4 — Fixed egress, DNS, SSRF와 server TLS baseline
작업:
- fixed destination resolve-validate-connect;
- kernel/engine redirect disabled;
- direct-only, auth-none profile; proxy/mTLS/OAuth/dynamic fetch 설정은 fail-closed;
- public-system/private-CA server TLS와 SSL bundle generation swap;
- automatic certificate-directed AIA/CRLDP/implicit OCSP egress disabled;
- propagation allowlist.
Acceptance:
- fixed-origin SSRF/DNS/NAT64 security matrix;
- server trust/hostname/normal rotation와 emergency revoke;
- proxy/mTLS/OAuth/redirect를 켜면 startup failure;
- no secret/tenant leak.
### 39.6 Phase 5 — Status-first bounded buffered body baseline
작업:
- response integrity/semantic-class status-first 분기;
- wire/decoded/ratio cap;
- bounded buffered JSON/bodiless codec;
- error body original-status preservation;
- partial/truncated semantics;
- streaming/upload mode 설정은 fail-closed.
Acceptance:
- minimum static buffered card만 승격;
- compression bomb/truncation/slow body;
- response/body cleanup;
- streaming/upload card는 아직 `not-implemented`.
### 39.7 Phase 6 — Observability, health와 readiness
작업:
- OTel single propagation owner;
- logical/physical telemetry;
- registry metric/header/MDC migrations;
- descriptor/readiness;
- selected-card CI/root task;
- runbooks.
Acceptance:
- no duplicate CLIENT span/manual header;
- cardinality/privacy test;
- selected card no-skip gate;
- zero-binding descriptor/resource contract.
### 39.8 Phase 7 — Consumer migration and legacy removal
작업:
- actual feature adapters/consumers;
- old generic `OutboundHttpClient` caller 제거;
- `TraceContextPropagationInterceptor` 제거;
- `globalCallTimeout` enforcer 제거;
- legacy `app.outbound.http.*` alias 제거;
- README/runbook/public snapshot update.
Acceptance:
- no old bean/config/property/reference;
- every consumer uses feature port and registered operation;
- full architecture/config/public-path/CI checks.
### 39.9 Phase 8 — Optional cards
각 card를 별도 change로:
- idempotent mutation;
- non-retryable mutation;
- streaming download;
- streaming upload;
- mTLS;
- OAuth2 client credentials;
- egress proxy;
- HTTP/2;
- future untrusted fetch.
Card끼리 묶어 한 번에 R2를 선언하지 않는다. 각 card의 evidence matrix와 rollback을 독립적으로
완성한다.
### 39.10 Rollout
1. shadow descriptor와 metrics만 활성;
2. one non-critical fixed destination;
3. canary pod/traffic;
4. ordinary retry disabled, protected physical ceiling 1 baseline;
5. pool/deadline/cancellation 관찰;
6. safe operation retry 점진 활성;
7. required destination 전환;
8. legacy 제거.
자동 provider fallback/downgrade는 rollback이 아니다. Rollback은 checked-in binding/config를
이전 generation/provider revision으로 되돌리는 명시적 배포다.
## 40. Completion and R2 criteria
HTTP capability를 “운영에서 바로 사용 가능” 또는 R2라고 부르려면 selected scope에서 모두
충족해야 한다.
1. Application use case는 feature-specific port만 의존한다.
2. Domain/application에 Spring/HTTP/engine/adapter type이 없다.
3. 모든 destination/operation/provider/card/exact profile과 OAuth token/proxy/revocation child
dependency DAG가 checked-in registry에 있고 cycle/self-reference가 없다.
4. Arbitrary absolute URL과 raw credential/header API가 baseline에 없다.
5. Operation catalog가 response integrity/semantic class, status/media/body,
retry/replay/idempotency/reconciliation 계약을 가진다.
6. Canonical binding만 activation하며 no binding은 zero resource다.
7. Exact provider가 선택되고 shared pre-start gate를 통과하지 않는 hidden
retry/redirect/auth/protocol resend/cookie가 disabled다.
8. Total deadline이 admission부터 body까지 monotonic하게 적용되고 positive cleanup reserve가
execution cutoff와 caller-visible deadline을 분리한다. 모든 wait가 absolute cutoff를 받고
wait 직후와 engine network phase 직전에 gate를 재검사한다.
9. Deadline/cancel 시 normal cleanup은 `D` 안에 끝나며, 끝나지 않은 task/resource는 caller
반환을 지연하지 않고 quarantine되어 bounded reaper가 처리하고 재사용되지 않는다.
10. Initial/retry/pre-send restart/auth/redirect/same-intent/protocol restart와 nested
token/reconciliation/proxy-CONNECT/revocation HTTP request가 reason/child counter, protected
ceiling과 root-call total HTTP 상한을 함께 지키고, 모든 child wire start가 exactly-once
`NestedHttpAuthorizationLease`/root-token handoff를 거친다. Broker는 parent exact
`AllowedChildEdge`만 승인하고 bind된 child profile/operation/authority와 root가 일치하며 required
cold path가 그 안에서 실행 가능하다.
11. Ordinal 0은 ordinary reason token/replayability 없이 실행 가능하고, 후속 protected request는 pure
eligibility 뒤 exactly-once `AttemptAuthorizationLease`를 사용한다. 모든 pre-bind exit가 이를
abort하고 engine/root commit 0을 보장한다. `NOT_SENT -> MAYBE_SENT`
first-write linearization, confidence refinement, typed cancellation disposition와
terminal-result/cancel/handoff CAS가 test로 고정된다.
12. Mutation lost response와 `MAYBE_SENT+|UNKNOWN` cancellation은 `INDETERMINATE`와 bounded
reconciliation을 보존한다.
13. Stale-token 401은 prior resource release, exact cache key, immutable-deadline/creator-budget
single-flight, one auth lease/replay, same identity와 second-401 terminal 계약을 모두 지킨다.
14. Circuit breaker record/ignore와 physical/logical 단위,
`ACQUIRED -> STARTING -> STARTED/terminal` permission lease exact-once 완료가 test로 고정된다.
15. Admission, pool pending, connection/stream, body/temp, credential waiter와 orphan quota가 유한하다.
16. Response status/header/framing과 bounded decoder를 body consumer 전에 분기하고 body/decode
뒤 response integrity와 semantic control outcome을 독립적으로 finalize한다. Header-authoritative
outcome은 header 검증 직후, body-required outcome은 body/semantic 검증 뒤 terminal CAS를 시도하며
단일 `AttemptTerminalCoordinator`가 winner와 cleanup owner를 각각 하나만 만든다. Success, 401,
retry-control, redirect, reconciliation, partial response와 cancellation 모두 공통 cleanup과 exhaustive
disposition resolver를 지나며 CAS loser가 정상 결과로 반환되지 않는다.
17. Wire/decoded/ratio/header/body limit과 truncation semantics가 있다.
18. DNS resolve-validate-connect, NAT64/transition address, SSRF/CIDR, redirect와 named proxy policy가
증명된다.
19. TLS hostname/trust, certificate-directed revocation egress와 OCSP/CRL signature/identity/
freshness semantics, normal rotation와 emergency revoke/no-fallback/NOT_READY가 증명된다.
20. mTLS/OAuth/API-key credential lifecycle, exact OAuth token-client auth child tuple와
cache/single-flight isolation key, shared owner/close가 증명된다.
21. OTel이 propagation을 단독 소유하고 duplicate CLIENT span이 없으며 sanitized attribute만 생성해
raw URL/query/header가 SDK/processor/exporter에 한 번도 들어가지 않는다.
22. Metric/log에는 raw URL이 없고 metric generation/pool tag는 checked-in bounded role만
사용한다. 모든 telemetry에 secret/body/tenant/idempotency key가 없고 peer address는 trust-zone
policy를 따른다. Untrusted fetch는 attacker-controlled authority를 표준 HTTP telemetry에 넣지 않는다.
23. Liveness는 remote dependency와 분리되고 readiness impact/probe가 명시된다.
24. Ingress drain 뒤 outbound close/cancel/resource close와 normal/emergency generation lifecycle이
증명된다.
25. ACTIVE binding에서 derived selected card와 exact parent/child compatibility profile이 모두
`release-eligible`이고, required/claimed evidence scenario set이 완전 일치하며 executed set이 이를
포함하고 evidence matrix가 0 skip으로 통과한다. `QUALIFICATION_ONLY` 결과는 ACTIVE_READY나
release assertion으로 사용할 수 없다. DISABLED는 zero-resource를 증명하며 R2로 표시하지 않는다.
26. Architecture/env/config/public-path/dependency/supply-chain gate가 통과한다.
27. Container/test dependency가 digest/lock/checksum으로 고정된다.
28. Required runbook와 dashboard/alert/SLO link가 존재한다.
29. Current provider/version/card/parent-child compatibility profile/policy revision이 sanitized
descriptor에 나온다.
30. ACTIVE profile에는 actual feature consumer가 최소 하나 contract를 통과한다. DISABLED profile은
consumer를 명시적 N/A로 검증한다.
31. Full selected release workflow가 fresh runner에서 root readiness task로 재실행된다.
32. Known gaps는 `not-implemented` card/profile로 fail-closed되고 R2 범위에 포함되지 않는다. Card
registry와 compatibility registry가 각각 card/profile maturity의 유일한 SSOT이며 selection은
binding/catalog/provider의 exact tuple과 child dependency fingerprints에서만 파생된다.
33. LLM Wiki capture와 independent review evidence가 완료된다.
현재 구현은 위 조건을 충족하지 않는다. 이 문서 완료는 구현 R2 완료가 아니다.
## 41. Required runbooks
구현과 함께 최소 다음 runbook을 제공한다.
1. destination onboarding/offboarding;
2. operation catalog와 compatibility 변경;
3. timeout/deadline budget 조정;
4. pool saturation/pending acquire;
5. retry storm/retry budget exhaustion;
6. circuit open/half-open;
7. DNS failure/rebinding/TTL/address rotation;
8. TLS handshake/certificate expiry/trust rotation;
9. mTLS client certificate rotation;
10. OAuth/API key/secret rotation;
11. proxy outage/auth/direct-fallback verification;
12. streaming leak/oversize/decompression bomb;
13. `INDETERMINATE` mutation reconciliation;
14. readiness probe outage/false negative;
15. client generation stuck draining;
16. provider upgrade/rollback;
17. selected readiness card evidence failure;
18. security incident and credential/metadata leakage;
19. HTTP/2 GOAWAY/flow-control/downgrade;
20. untrusted URL fetch incident if that future card is ever selected;
21. combined retry/redirect/auth/reconciliation amplification ceiling exhaustion;
22. OCSP/CRL/AIA responder outage와 certificate-directed egress rejection;
23. credential/key/trust compromise emergency revoke, no-fallback와 NOT_READY recovery;
24. OAuth token/proxy/revocation child profile qualification and ownership leak.
각 runbook:
- symptom/alert;
- safe diagnostic query;
- secret-safe evidence;
- immediate containment;
- retry/restart 금지 조건;
- rollback;
- reconciliation/data impact;
- owner/escalation;
- recovery verification;
을 포함한다.
## 42. Primary references
이 설계는 2026-07-27 기준으로 다음 primary source를 참고했다.
- [Spring Framework REST Clients](https://docs.spring.io/spring-framework/reference/integration/rest-clients.html):
`RestClient`, `exchange()`와 status-handler 경계.
- [Spring Framework `RestClient` Javadoc](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/client/RestClient.html):
current API contract.
- [Spring Boot 4 REST Client](https://docs.spring.io/spring-boot/4.0/reference/io/rest-client.html):
preconfigured `RestClient.Builder`, HTTP Service, SSL integration.
- [Spring Boot `HttpClientSettings`](https://docs.spring.io/spring-boot/4.0/api/java/org/springframework/boot/http/client/HttpClientSettings.html):
Boot HTTP client settings surface.
- [Spring Boot `ClientHttpRequestFactoryBuilder`](https://docs.spring.io/spring-boot/4.0/api/java/org/springframework/boot/http/client/ClientHttpRequestFactoryBuilder.html):
request-factory selection/customization.
- [Java 21 `HttpClient`](https://docs.oracle.com/en/java/javase/21/docs/api/java.net.http/java/net/http/HttpClient.html):
JDK client lifecycle, executor, redirect, protocol surface.
- [Java 21 `HttpRequest.Builder`](https://docs.oracle.com/en/java/javase/21/docs/api/java.net.http/java/net/http/HttpRequest.Builder.html):
request timeout/header/method surface.
- [Java networking properties](https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/net/doc-files/net-properties.html):
JVM networking/DNS properties and scope.
- [Java 21 PKI Programmer's Guide](https://docs.oracle.com/en/java/javase/21/security/java-pki-programmers-guide.html):
certification path, CRLDP/AIA/OCSP network retrieval와 security properties.
- [Apache HttpComponents 5 pooling manager builder](https://hc.apache.org/httpcomponents-client-5.6.x/current/httpclient5/apidocs/org/apache/hc/client5/http/impl/io/PoolingHttpClientConnectionManagerBuilder.html):
pool construction and connection-manager options.
- [Apache HttpComponents 5 request configuration](https://hc.apache.org/httpcomponents-client-5.6.x/current/httpclient5/apidocs/org/apache/hc/client5/http/config/RequestConfig.Builder.html):
request/connect/connection-request policy surface.
- [RFC 9110 — HTTP Semantics](https://www.rfc-editor.org/rfc/rfc9110.html):
methods, status, representation, retry-related HTTP semantics.
- [RFC 9112 — HTTP/1.1](https://www.rfc-editor.org/rfc/rfc9112.html):
framing and HTTP/1.1 message parsing.
- [RFC 9113 — HTTP/2](https://www.rfc-editor.org/rfc/rfc9113.html):
stream errors, `REFUSED_STREAM`, GOAWAY와 last-stream-ID semantics.
- [RFC 5280 — PKIX Certificate and CRL Profile](https://www.rfc-editor.org/rfc/rfc5280.html):
certificate AIA/CRL distribution point, CRL scope/signature/freshness semantics.
- [RFC 6960 — OCSP](https://www.rfc-editor.org/rfc/rfc6960.html):
authorized responder, CertID, response signature/status/time와 replay semantics.
- [RFC 6749 — OAuth 2.0](https://www.rfc-editor.org/rfc/rfc6749.html):
client authentication, token endpoint와 scope semantics.
- [RFC 6052 — IPv6 Addressing of IPv4/IPv6 Translators](https://www.rfc-editor.org/rfc/rfc6052.html):
NAT64 IPv4-embedded IPv6 prefix/translation format.
- [RFC 6890 — Special-Purpose Address Registries](https://www.rfc-editor.org/rfc/rfc6890.html):
IPv4/IPv6 special-purpose address classification context.
- [RFC 6585 — Additional HTTP Status Codes](https://www.rfc-editor.org/rfc/rfc6585.html):
429 and `Retry-After` context.
- [OWASP SSRF Prevention Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Server_Side_Request_Forgery_Prevention_Cheat_Sheet.html):
application/network-layer SSRF controls.
- [OpenTelemetry HTTP spans](https://opentelemetry.io/docs/specs/semconv/http/http-spans/):
client span and resend semantic convention.
- [OpenTelemetry HTTP metrics](https://opentelemetry.io/docs/specs/semconv/http/http-metrics/):
HTTP client metric semantic convention.
- [OpenTelemetry metrics concepts](https://opentelemetry.io/docs/concepts/signals/metrics/):
attribute-set aggregation과 cardinality context.
- [W3C Trace Context](https://www.w3.org/TR/trace-context/):
`traceparent`/`tracestate` format and propagation.
- [Spring Boot SSL](https://docs.spring.io/spring-boot/reference/features/ssl.html):
SSL bundle and documented reload integration boundary.
- [Spring Security OAuth2 Client](https://docs.spring.io/spring-security/reference/servlet/oauth2/client/index.html):
client registration/provider/authorized-client model.
- [Resilience4j Retry](https://resilience4j.readme.io/docs/retry):
retry configuration and result/exception predicates.
- [Resilience4j CircuitBreaker](https://resilience4j.readme.io/docs/circuitbreaker):
circuit breaker state/configuration model.
- [Idempotency-Key Internet-Draft 07](https://datatracker.ietf.org/doc/draft-ietf-httpapi-idempotency-key-header/07/):
HTTP idempotency-key 설계 참고. 2026-04-18에 만료된 Internet-Draft이며 표준으로 간주하지
않는다. 실제 partner contract가 우선한다.
Primary source가 지원하는 API가 실제 Spring Boot BOM/selected provider version에서 동일한지는
구현 시 dependency lock과 compatibility test로 다시 확인한다. 문서 링크는 executable evidence를
대신하지 않는다.