Task 81 — Kestrel tuning, framework rate limiter, compression, output cache
Phase: 5
Initiative: 45
Status: done
Owner: hercules-coder
Started: 2026-08-16
Completed: 2026-08-16
Slug: kestrel-rate-limiter-compression
Goal
WebAPI использует дефолтные Kestrel limits, custom RateLimitMiddleware (только /api/chat, без eviction idle IPs → unbounded memory), нет response compression, нет output cache. Дубль app.MapCache() (Program.cs:636-637). Для production API под нагрузкой это bottleneck.
Acceptance criteria
Sub-tasks
Hercules.WebApi/Program.cs—builder.WebHost.ConfigureKestrel(o => { o.Limits.MaxConcurrentConnections = 1000; o.Limits.MaxConcurrentUpgradedConnections = 100; o.Limits.MaxRequestBodySize = 4_194_304; o.Limits.KeepAliveTimeout = TimeSpan.FromMinutes(2); o.Limits.RequestHeadersTimeout = TimeSpan.FromSeconds(30); }).- Оставить custom
RequestBodyLimitMiddlewareкак defense-in-depth но с real stream-length check (оборачиваетRequest.BodyвLimitedStream, отсекает chunked uploads при превышении лимита). builder.Services.AddRateLimiter(o => { o.AddFixedWindowLimiter("chat", ...); o.AddConcurrencyLimiter("expensive", ...); })+app.UseRateLimiter(). Применить/api/chat→ fixed window 30/min per-IP; expensive endpoints (reflection, eval, slo) → concurrency limiter 3 concurrent.- Убрать custom
RateLimitMiddleware(Auth/RateLimitMiddleware.cs) — заменить на framework rate limiter. СохранитьX-RateLimit-*headers черезOnRejectedcallback. builder.Services.AddResponseCompression(o => { o.EnableForHttps = true; o.Providers.Add<BrotliCompressionProvider>(); o.Providers.Add<GzipCompressionProvider>(); o.MimeTypes = ["application/json", "text/plain", "text/event-stream"]; })+app.UseResponseCompression().builder.Services.AddOutputCache(o => { o.AddBasePolicy(b => b.Expire(TimeSpan.FromSeconds(30))); o.AddPolicy("Skills", b => b.Expire(TimeSpan.FromMinutes(5)).Tag("skills")); })+app.UseOutputCache(). Применить кGET /api/skills,GET /api/config(read-only endpoints).Program.cs:636-637— убрать дубльapp.MapCache();.- CORS:
Program.cs:463-465— заменитьAllowAnyOrigin()default на whitelist из configAllowedCorsOrigins; если empty → разрешить толькоlocalhost(dev), не*. appsettings.json— добавитьKestrel.MaxConcurrentConnections,RateLimiting.ChatPerMinute,RateLimiting.ExpensiveConcurrency,Cors.AllowAnyOrigin(default false).- Unit/Integration-тест: 31st request to
/api/chatв минуту → 429 +X-RateLimit-Resetheader. - Integration-тест:
GET /api/skillsсAccept-Encoding: br→ responseContent-Encoding: br. - Integration-тест: 2nd
GET /api/skillswithin 5 min →X-Output-Cache: HIT. dotnet build+dotnet testpass.
Scope / Likely files
src/agent/Hercules.WebApi/Program.cs, src/agent/Hercules.WebApi/Auth/RateLimitMiddleware.cs (remove), src/agent/Hercules.WebApi/Controllers/* (add output cache attributes), src/agent/appsettings.json
Dependencies
- блокирует / опирается на: task_005 — interfaces
- блокирует / опирается на: task_056 — rate-limits-quotas
Risks / Rollback
Kestrel MaxConcurrentConnections=1000 может reject traffic под пиком; configurable. Output cache может serve stale data; короткий TTL (5 min) + invalidate on skill update. Rollback: вернуть custom middleware (но bugs останутся).
Links
- Backlog: ../backlog.md
- Roadmap (EN): ../../ROADMAP-EN.md
- Roadmap (RU): ../../ROADMAP-RU.md
Implementation notes (2026-08-16)
Behaviour delivered
- Kestrel tuning —
Program.csтеперь вызываетbuilder.WebHost.ConfigureKestrel(...)и поднимает production-grade лимиты (MaxConcurrentConnections=1000,MaxConcurrentUpgradedConnections=100,MaxRequestBodySize=4 MiB,KeepAliveTimeout=120s,RequestHeadersTimeout=30s). Все значения читаются изWebApiConfig.Kestrelи переопределяются черезappsettings.json. - Framework rate limiter —
AddRateLimiterс двумя политиками:chat—FixedWindowRateLimiter, partitioned per IP (с учётомX-Forwarded-For),PermitLimit=30/min,Window=60s,QueueLimit=0. Применён кPOST /api/chat.expensive—ConcurrencyLimiterс глобальнымPermitLimit=3для/api/reflect,/api/slos/*,/api/maintenance/{run,run-all,proposals/{id}/approve}и/api/skills/{id}/eval/{harness,baseline}.OnRejectedcallback проставляетRetry-After+X-RateLimit-Reset(секунды) и возвращает 429 + JSON{"error":"Rate limit exceeded..."}. Поддержка X-Forwarded-For включена вRateLimitPolicies.GetClientKey.
- Response compression —
AddResponseCompressionсBrotliCompressionProvider+GzipCompressionProvider,EnableForHttps=true, MIME-types:application/json,text/plain,text/event-stream,application/xml,text/html. Подключён какapp.UseResponseCompression()ПЕРВЫМ в pipeline. - Output cache —
AddOutputCacheс базовой политикой (30s) и двумя именованными:Skills(5 min,SetVaryByQuery("skillId", "includeDeprecated"), tagskills) иConfig(30s, tagconfig). Подключён какapp.UseOutputCache()послеUseRateLimiter, чтобы 401/429 не кэшировались. Применён кGET /api/skills,GET /api/skills/{id},GET /api/config. - CORS hardening — дефолтный fallback сменился с
AllowAnyOrigin()на whitelist localhost (http://localhost:{3000,4321,5000}+ их127.0.0.1аналоги). Для production по-прежнему указываетсяWebApi.AllowedCorsOrigins. ОпцияWebApi.AllowAnyOrigin=trueосталась для dev/edge-opt-in. - Custom
RateLimitMiddlewareудалён — заменён на framework rate limiter. ФайлAuth/RateLimitMiddleware.csостался как no-op stub с header-комментарием, указывающим на replacement. RequestBodyLimitMiddlewareусилен — defense-in-depth обёрткаLimitedStreamповерхRequest.Body. Помимо проверкиContentLength, поток реально лимитируется на чтение (InvalidDataExceptionпри превышении), что корректно работает с chunked uploads. Middleware ловитInvalidDataExceptionи конвертирует в 413 + JSON.- Duplicate
app.MapCache()убран — был вызван дважды подряд; оставлен один.
Components
src/agent/Hercules.WebApi/RateLimitPolicies.cs(new) — stable policy name constants +GetClientKeyhelper.src/agent/Hercules.WebApi/Config/WebApiConfig.cs— добавленыKestrelConfig,RateLimitingConfig, флагAllowAnyOrigin;MaxRequestBodyBytesсталlong(для согласования с Kestrel).src/agent/appsettings.json— новая секцияWebApiсKestrelиRateLimitingподсекциями.src/agent/Hercules.WebApi/Auth/RequestBodyLimitMiddleware.cs—LimitedStreamтеперьpublic(для тестов), throwsInvalidDataExceptionпри превышении.src/agent/Hercules.WebApi/Auth/RateLimitMiddleware.cs— no-op stub, заменён framework’ом.- 7 controllers: добавлен
.RequireRateLimiting(...)/.CacheOutput(...)где применимо.
Validation
dotnet build src/agent/Hercules.csproj— 0 errors, 0 new warningsdotnet build src/agent/Hercules.WebApi/Hercules.WebApi.csproj— 0 errors, 0 warningsdotnet build tests/Hercules.Agent.Tests/Hercules.Agent.Tests.csproj— 0 errors- New tests: 23/23 pass
RateLimitPoliciesTests(7) — policy names stable,GetClientKeyrespects X-Forwarded-ForLimitedStreamTests(6) — Read/ReadAsync happy path + over-limit throws, Write NotSupportedRequestBodyLimitMiddlewareTests(4) — ContentLength reject, pass-through, chunked 413, non-/api bypassKestrelRateLimitCachePipelineTests(6) — chat 429 after limit, Retry-After header, concurrency rejection, Brotli + Gzip content-encoding, output cache HIT (counter-based)
- Full test suite: 1863 passed, 8 failed — все 8 failures pre-existing environmental
(
OtelServiceTests×5,NumericValidatorTests×2,RedisTaskQueueTests×1 — нет Redis / Otel runtime в test host).