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:
co-authored by
Claude Opus 5
parent
5f10b791d3
commit
0cd959a494
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user