feat(httpclient): close the platform review's P0/P1/P2 findings

The review found one defect shape repeated across the platform: surfaces
that were declared, bound, and documented, but that nothing read. An
operator configuring fullUrlRecording, bodyLogging, retry.policy,
validatedDnsPinning, timeout.dns, or any of ten declared metric names got a
guarantee the code never delivered. Every such surface is now in exactly one
of three states -- wired for real, rejected at startup, or registered in a
test-enforced gap list with its reason. No silent no-ops remain.

P0:
- Activate the platform from bootstrap behind app.httpclient.enabled, with a
  single auto-configuration importing the nine child configurations.
- Give the platform a strict, repository-level ENV contract: 74 leaf fields
  derived from the settings record tree, unknown APP_HTTPCLIENT_* rejected.
- Route typed HTTP service clients through the call kernel via
  KernelHttpExchangeAdapter, so they stop bypassing platform policy.
- Pin dynamic-target DNS resolution to the socket for the life of a call,
  closing the resolve-then-connect TOCTOU / rebinding window.
- Actually transmit the idempotency key, and make retry eligibility depend on
  transmission rather than on merely holding one.
- Reject reactive authentication and reactive redirect at startup instead of
  declaring support that does not function.
- Fix the Reactor-only Stable contract row so the lane stops failing.
- Stop advertising HTTP/3 on a transport that negotiates HTTP/1.

P1 covers execution and retry accounting, redirect security (per-hop target
guarding, sensitive-header stripping, 303 body handling), runtime rotation
and transport resource ownership keyed by generation, dynamic-target
hardening (subdomain matching, global-unicast classification, strict CIDR
parsing), protocol intent, pool and timeout wiring, streaming and body
limits, observability parity, and OAuth single-flight refresh on a bounded
pool with a bounded wait.

P2 covers configuration and documentation drift, the Gradle check wiring for
the four hermetic lanes, and the CI gate matrix.

Two test-quality defects surfaced while closing these: the HTTP/2 stream
saturation test ran against cleartext HTTP/1.1 while asserting nothing about
the protocol, and an OAuth contention test slept on a latch that could fire
before the callers it meant to observe. Both now assert what their names
claim.

Verification run: :adapter:outbound:httpclient:check and :app-bootstrap:check
(checkstyle, spotless, spotbugs, and the four hermetic lanes),
verifyCleanArchitectureDependencies, verifyEnvKeys, verifyOneTypePerFile,
verifyDependencyLocks, the documentation and gate-matrix verifiers, and the
performance lane against a real TLS+ALPN HTTP/2 server.

Not executed, and tracked rather than claimed: Docker/Toxiproxy fault
injection, JMH, a real QUIC/HTTP3 server, a real Spring Framework 6.2
distribution (now a delegated-pending gate), live OAuth/TLS/proxy/DNS
integration, and a whole-repository check.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
DongHyeonka
2026-08-11 16:49:31 +09:00
co-authored by Claude Opus 5
parent 5f10b791d3
commit 0cd959a494
148 changed files with 26812 additions and 2368 deletions
+84 -39
View File
@@ -15,16 +15,13 @@ APP_MIGRATION_ON_STARTUP=true
APP_RATE_LIMIT_ENABLED=false
APP_RATE_LIMIT_CLIENT_IP_MODE=remote-addr-only
APP_RATE_LIMIT_PROVIDER=disabled
APP_RATE_LIMIT_REDIS_KEY_HMAC_SECRET=
APP_IDEMPOTENCY_TTL=24h
APP_IDEMPOTENCY_PROVIDER=jdbc
APP_IDEMPOTENCY_REDIS_KEY_HMAC_SECRET=
APP_IDEMPOTENCY_REDIS_NAMESPACE_ENVIRONMENT=local
APP_IDEMPOTENCY_PROCESSING_LEASE=30s
APP_IDEMPOTENCY_FAILURE_RETENTION=24h
APP_LEASE_PROVIDER=disabled
APP_LEASE_REDIS_KEY_HMAC_SECRET=
APP_LEASE_REDIS_NAMESPACE_ENVIRONMENT=local
APP_LEASE_REDIS_DRIFT_BUDGET=10ms
# ----- Async executor -----
@@ -34,33 +31,10 @@ APP_ASYNC_EXECUTOR_QUEUE_CAPACITY=200
# ----- Optional integration adapters (default: all disabled) -----
APP_CACHE_CANONICAL_DEFAULT_PROVIDER=disabled
# Sentinel primary revalidation cadence for canonically active Sentinel roles.
APP_REDIS_SENTINEL_DISCOVERY_REFRESH_PERIOD=30s
# Canonical role semantic readiness: refresh no more often than this interval.
APP_REDIS_SEMANTIC_PROBE_MINIMUM_INTERVAL=5s
# Fail closed when the last completed semantic observation is older than this bound.
APP_REDIS_SEMANTIC_PROBE_MAXIMUM_STALENESS=15s
APP_CACHE_REDIS_ENABLED=false
APP_CACHE_REDIS_CLIENT_MODE=managed
APP_CACHE_REDIS_HOST=localhost
APP_CACHE_REDIS_PORT=6379
APP_CACHE_REDIS_PASSWORD=
APP_CACHE_REDIS_KEY_HMAC_SECRET=
APP_CACHE_REDIS_COMMAND_TIMEOUT=2s
APP_CACHE_REDIS_MAXIMUM_QUEUED_COMMANDS=8
APP_CACHE_REDIS_MAXIMUM_IN_FLIGHT_BYTES=16777216
APP_CACHE_REDIS_NAMESPACE_ENVIRONMENT=local
APP_CACHE_REDIS_SEMANTIC_REGION=default
APP_CACHE_REDIS_MAXIMUM_VALUE_BYTES=1048576
APP_CACHE_REDIS_L1_ENABLED=false
APP_CACHE_REDIS_L1_MAXIMUM_ENTRIES=10000
APP_CACHE_REDIS_L1_MAXIMUM_WEIGHT_BYTES=67108864
APP_CACHE_REDIS_L1_MAXIMUM_ENTRY_WEIGHT_BYTES=1048576
APP_CACHE_REDIS_L1_TTL=30s
APP_CACHE_REDIS_L1_GENERATION_RECHECK_INTERVAL=5s
APP_CACHE_REDIS_L1_INVALIDATION_QUEUE_CAPACITY=1024
APP_CACHE_DEFAULT_TTL=300s
APP_CACHE_NEGATIVE_TTL=60s
# The single global Redis switch. False means no Redis settings, secrets, client, threads or
# health contributor exist. Role selectors (cache/session/idempotency/lease/rate-limit) choose
# which capabilities compose once Redis is on; none of them turns Redis on.
APP_REDIS_ENABLED=false
APP_MESSAGING_BROKER=
APP_MESSAGING_KAFKA_BROKERS=
APP_NOTIFICATION_SLACK_PROVIDER=
@@ -149,15 +123,6 @@ APP_SESSION_COOKIE_SAME_SITE=Lax
APP_SESSION_COOKIE_PATH=/
APP_SESSION_CSRF_COOKIE_NAME=XSRF-TOKEN
APP_SESSION_CSRF_HEADER_NAME=X-XSRF-TOKEN
APP_SESSION_REDIS_KEY_HMAC_SECRET=
APP_SESSION_REDIS_NAMESPACE_ENVIRONMENT=local
APP_SESSION_IDLE_TIMEOUT=30m
APP_SESSION_ABSOLUTE_LIFETIME=8h
APP_SESSION_TOUCH_INTERVAL=1m
APP_SESSION_TOMBSTONE_TTL=5m
APP_SESSION_MAXIMUM_ENVELOPE_BYTES=32768
APP_SESSION_MAXIMUM_ATTRIBUTES=64
APP_SESSION_MAXIMUM_SCALAR_BYTES=8192
# ----- CORS -----
APP_SECURITY_CORS_ENABLED=true
@@ -186,3 +151,83 @@ APP_DATASOURCE_POOL_MAX_LIFETIME=1800000
# ----- Management / Actuator -----
MANAGEMENT_SERVER_PORT=9001
# ----- Fileserver HTTP platform (app.fileserver-platform.*) -----
# Off by default. While false nothing below is bound: the platform auto-configuration binds this
# block itself and is not processed until the master switch is true.
APP_FILESERVER_PLATFORM_ENABLED=false
APP_FILESERVER_PLATFORM_INSTANCE_ID=local-node
APP_FILESERVER_PLATFORM_DEFAULT_NAMESPACE=default
# Storage root must be an absolute path on its own volume, never under a web or config root.
APP_FILESERVER_PLATFORM_STORAGE_ROOT=/var/lib/backend/files
APP_FILESERVER_PLATFORM_STORAGE_PUBLISH_MODE=atomic-move-preferred
APP_FILESERVER_PLATFORM_STORAGE_BUFFER_SIZE=128KB
APP_FILESERVER_PLATFORM_STORAGE_FORBIDDEN_ROOT_ANCESTORS=/app,/etc,/usr/share/nginx/html
# Shared with spring.servlet.multipart.* so the container and the policy cannot disagree.
APP_FILESERVER_PLATFORM_UPLOAD_MAX_FILE_SIZE=100MB
APP_FILESERVER_PLATFORM_UPLOAD_MAX_REQUEST_SIZE=110MB
APP_FILESERVER_PLATFORM_UPLOAD_INITIAL_RESERVATION=8MB
APP_FILESERVER_PLATFORM_UPLOAD_MAX_PARTS=16
APP_FILESERVER_PLATFORM_UPLOAD_TTL=1h
APP_FILESERVER_PLATFORM_UPLOAD_RESERVATION_TTL=24h
APP_FILESERVER_PLATFORM_UPLOAD_LEASE_DURATION=30s
APP_FILESERVER_PLATFORM_UPLOAD_REQUIRE_CONTENT_LENGTH=false
APP_FILESERVER_PLATFORM_DOWNLOAD_CACHE_CONTROL=private, no-store
APP_FILESERVER_PLATFORM_DOWNLOAD_INLINE_ALLOWED=false
APP_FILESERVER_PLATFORM_DOWNLOAD_MAX_RANGES=1
APP_FILESERVER_PLATFORM_DOWNLOAD_MAX_RANGE_BYTES=100MB
APP_FILESERVER_PLATFORM_DOWNLOAD_ZERO_COPY_ENABLED=true
APP_FILESERVER_PLATFORM_DOWNLOAD_ZERO_COPY_MINIMUM_BYTES=16MB
APP_FILESERVER_PLATFORM_TRANSFER_CORE_SIZE=8
APP_FILESERVER_PLATFORM_TRANSFER_MAX_SIZE=32
APP_FILESERVER_PLATFORM_TRANSFER_QUEUE_CAPACITY=64
APP_FILESERVER_PLATFORM_TRANSFER_AWAIT_SECONDS=300
# required | role-based | unenforced (unenforced is refused under a production profile).
APP_FILESERVER_PLATFORM_SECURITY_ACCESS_POLICY=required
APP_FILESERVER_PLATFORM_SECURITY_READ_ROLES=ROLE_FILE_READ
APP_FILESERVER_PLATFORM_SECURITY_WRITE_ROLES=ROLE_FILE_WRITE
APP_FILESERVER_PLATFORM_SECURITY_ADMIN_ROLES=ROLE_FILE_ADMIN
APP_FILESERVER_PLATFORM_VERIFICATION_TIMEOUT=5s
APP_FILESERVER_PLATFORM_VERIFICATION_REQUIRE_MEDIA_TYPE_VERDICT=false
APP_FILESERVER_PLATFORM_VERIFICATION_INLINE_SAFE_PROFILE=false
APP_FILESERVER_PLATFORM_QUOTA_INSTANCE_UPLOAD_PERMITS=16
APP_FILESERVER_PLATFORM_QUOTA_SCOPE_UPLOAD_PERMITS=4
APP_FILESERVER_PLATFORM_QUOTA_DIRECT_DOWNLOAD_PERMITS=64
APP_FILESERVER_PLATFORM_QUOTA_SOFT_HIGH_WATER=0.70
APP_FILESERVER_PLATFORM_QUOTA_HARD_HIGH_WATER=0.85
APP_FILESERVER_PLATFORM_ADMIN_ENABLED=false
APP_FILESERVER_PLATFORM_ADMIN_ORPHAN_MINIMUM_AGE=1h
APP_FILESERVER_PLATFORM_CLEANUP_ENABLED=false
APP_FILESERVER_PLATFORM_CLEANUP_INTERVAL=60s
APP_FILESERVER_PLATFORM_CLEANUP_MAX_ITEMS=100
APP_FILESERVER_PLATFORM_CLEANUP_MAX_BYTES=1GB
APP_FILESERVER_PLATFORM_CLEANUP_RETRY_BACKOFF=5m
APP_FILESERVER_PLATFORM_TUS_ENABLED=false
APP_FILESERVER_PLATFORM_HTTPBIS_DRAFT12_ENABLED=false
APP_FILESERVER_PLATFORM_NGINX_ENABLED=false
APP_FILESERVER_PLATFORM_NGINX_INTERNAL_PREFIX=/__files/
APP_FILESERVER_PLATFORM_NGINX_OBJECT_SUFFIX=.bin
APP_FILESERVER_PLATFORM_NGINX_MINIMUM_SIZE=16MB
APP_FILESERVER_PLATFORM_OBSERVABILITY_METRICS_ENABLED=true
# Secret. Required while metrics are enabled; an unkeyed digest of an enumerable id is reversible.
APP_FILESERVER_PLATFORM_OBSERVABILITY_FINGERPRINT_KEY=
# ----- HTTP Client platform (app.httpclient.*) -----
# The single switch for outbound HTTP. False means no HTTP client property is bound, and no
# transport provider, connection pool, TLS context, credential, thread or gateway is created.
# The per-client surface is indexed and per-deployment, so it is set directly in the environment
# rather than declared here; docs/httpclient/env-fields.yaml is its registry, and an
# APP_HTTPCLIENT_ variable that is not in that registry fails startup.
APP_HTTPCLIENT_ENABLED=false