You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
feat(middleware): add streaming v2 HTTP hooks and deprecate the unary request hook #3307
As a supervisor middleware author, I want one streaming HTTP hook that supports headers-only, whole-body, and incremental inspection, so I can handle small requests and large uploads with the same contract I use for responses.
Problem Statement
SupervisorMiddleware.EvaluateHttpRequest is unary. It gets one buffered HttpRequestEvaluation and returns one HttpRequestResult. Request and replacement bodies are capped at 4 MiB. A stage that only needs headers still goes through the buffered path.
The v1 response hook, HttpResponsePreReturn.Evaluate (#3073, #3074), is already a bidirectional stream with preflight, body units, trailers, and session end. That leaves request and response middleware on two different processing models.
Middleware authors can't look at request headers without whole-body buffering, and can't process bodies over the inspection limit incrementally. Operators either accept the buffering latency and size cap, bypass inspection where fail-open allows it, or run another proxy. Bypassing loses the inspection. Another proxy duplicates policy and credential-boundary work.
Scope change
This issue originally asked for a breaking replacement before 0.1.0, with no compatibility path. 0.1.x shipped with the unary hook, so PR #4359 takes a different route:
It adds v2 HTTP hooks next to v1 instead of replacing v1.
It covers responses too. EvaluateHttpResponseV2 replaces HttpResponsePreReturn.Evaluate on the same terms, so both directions now share one message set.
v1 hooks are deprecated for removal in 0.2.0. Removing them is a follow-up and not part of this issue.
RPCs.SupervisorMiddleware.EvaluateHttpRequestV2 (HTTP_REQUEST_V2 at PRE_CREDENTIALS) and SupervisorMiddleware.EvaluateHttpResponseV2 (HTTP_RESPONSE_V2 at PRE_RETURN). Both are bidirectional streams with the same event and result messages. The HttpRequestPreCredentials.Evaluate name from the original proposal didn't survive. The methods live on SupervisorMiddleware and the binding operation selects them.
Preflight. One stream per stage and HTTP message. The first event is a preflight with the head, context, config, permitted and removed body modes (each removed mode has a reason), limits, and declared body length. The stage returns continue_without_body, inspect (BUFFERED or STREAM), or reject, with optional header mutations. Every stage's preflight runs in chain order before any stage sees a body.
BUFFERED. One body up to the stage payload limit (max 4 MiB). The stage returns unchanged or a replacement, where empty bytes delete the body, plus late header and trailer mutations. No request bytes go upstream until the whole chain approves.
STREAM. Ordered input chunks (64 KiB max) and one input_end with trailers. The stage emits its own output with output_start, output_chunk, and finish. Input and output counts are independent, and there is no total size or time cap. 30 seconds of middleware stall fails the stage. 30 seconds without upload progress from the sandbox returns 408.
Commitment. Request output streams upstream during upload unless the route needs the full body: a BUFFERED stage, SigV4, body credential rewrite, forward proxy, JSON-RPC/MCP/GraphQL, HTTP/1.0, or Upgrade. In those cases the supervisor withholds up to 4 MiB and sends it with the head. Body-aware endpoints re-check each replaced body against policy. Credentials are injected after all middleware mutations. If a STREAM stage fails after the head went upstream, the supervisor closes the upstream connection and never replays.
Failure. Always fail-closed. on_error doesn't apply, and the gateway rejects fail_open on v2 entries. Explicit rejects get 403 middleware_denied. Errors, timeouts, bad results, and lifecycle violations get 403 middleware_failed.
Compatibility. Older gateways and supervisors refuse HTTP_REQUEST_V2/HTTP_RESPONSE_V2 at Describe, so there is no silent fallback to v1. A service uses one hook version, and a chain can't mix versions. The gateway rejects such policies, and the supervisor fails mixed chains closed with middleware_hook_versions_mixed.
Uninspectable traffic. Something the original proposal didn't cover. For tls: skip, h2c, unsupported tunnels, raw TCP, and SQL passthrough, v2 request middleware gets a body-less preflight and allows or refuses the connection. So v2 request middleware can now attach to tls: skip endpoints.
The protobuf contract documents preflight results, body modes, ordered results, replacements including empty bytes, trailers, session end, invalid transitions, and diagnostic limits, the same way for requests and responses.
Headers-only and STREAM requests can go past the old 4 MiB limit without whole-request buffering. BUFFERED stays bounded and sends nothing upstream before approval.
Chains keep policy order, transformations, explicit denial, and fail-closed behaviour without replay or lost input.
Partial forwarding, downstream errors, cancellation, disconnects, and policy reload have defined, tested outcomes.
Backpressure, the concurrency cap, per-result timeouts, the BUFFERED deadline, and cleanup bound resource use, including for slow or unknown-length uploads.
Tests cover bodyless and empty requests, Content-Length and chunked bodies, trailers, Expect: 100-continue, size-changing transformations, oversized input and replacements, invalid results, and failures before and after forwarding.
Tests keep body-aware policy checks after transformation, credential non-disclosure, protected-header validation, and correct upstream framing. OCSF events leave out bodies, credentials, query secrets, and free-form middleware reasons.
The e2e:middleware-http-v2 suite runs the content guard example with v2 request and response hooks in both modes. The v1 e2e still passes.
RFC 0009, the V2 HTTP Hooks docs page, the middleware config docs, and the relevant skills describe the new contract and the v1 deprecation. The docs include a v1-to-v2 migration guide and the gateway-first upgrade order.
Follow-up, out of scope here:
Remove EvaluateHttpRequest, HttpResponsePreReturn.Evaluate, and their request-only messages in 0.2.0, and list the removal in the 0.2.0 upgrade notes.
Alternatives Considered
Breaking replacement before 0.1.0. The original plan, overtaken by the 0.1.x release. A deprecation window gives running services a release to migrate.
Unary compatibility adapter on a shared runner (#2431's original proposal). More supervisor code to keep alive an API we plan to delete. Separate v1 engines plus a no-mixing rule are easier to remove in 0.2.0.
Wrap the buffered request in a stream. Changes the transport shape but keeps the buffering and size limits.
One universal inspection RPC for requests and responses. We kept two RPCs because requests and responses differ in metadata, mutation rights, policy checks, and commitment. They still share message types and most of the runtime.
This issue has had no activity for 14 days and is now marked stale. It may be closed in 7 days if there is no further activity. Comment or remove the state:stale label to keep it open.
changed the title [-]feat(middleware)!: replace unary HTTP request evaluation with a streaming hook[/-][+]feat(middleware): add streaming v2 HTTP hooks and deprecate the unary request hook[/+]on Oct 9, 2026
User Story
As a supervisor middleware author, I want one streaming HTTP hook that supports headers-only, whole-body, and incremental inspection, so I can handle small requests and large uploads with the same contract I use for responses.
Problem Statement
SupervisorMiddleware.EvaluateHttpRequestis unary. It gets one bufferedHttpRequestEvaluationand returns oneHttpRequestResult. Request and replacement bodies are capped at 4 MiB. A stage that only needs headers still goes through the buffered path.The v1 response hook,
HttpResponsePreReturn.Evaluate(#3073, #3074), is already a bidirectional stream with preflight, body units, trailers, and session end. That leaves request and response middleware on two different processing models.Parent: #2565.
Impact / Why This Matters
Middleware authors can't look at request headers without whole-body buffering, and can't process bodies over the inspection limit incrementally. Operators either accept the buffering latency and size cap, bypass inspection where fail-open allows it, or run another proxy. Bypassing loses the inspection. Another proxy duplicates policy and credential-boundary work.
Scope change
This issue originally asked for a breaking replacement before
0.1.0, with no compatibility path. 0.1.x shipped with the unary hook, so PR #4359 takes a different route:EvaluateHttpResponseV2replacesHttpResponsePreReturn.Evaluateon the same terms, so both directions now share one message set.Design (as implemented in #4359)
RPCs.
SupervisorMiddleware.EvaluateHttpRequestV2(HTTP_REQUEST_V2atPRE_CREDENTIALS) andSupervisorMiddleware.EvaluateHttpResponseV2(HTTP_RESPONSE_V2atPRE_RETURN). Both are bidirectional streams with the same event and result messages. TheHttpRequestPreCredentials.Evaluatename from the original proposal didn't survive. The methods live onSupervisorMiddlewareand the binding operation selects them.Preflight. One stream per stage and HTTP message. The first event is a preflight with the head, context,
config, permitted and removed body modes (each removed mode has a reason), limits, and declared body length. The stage returnscontinue_without_body,inspect(BUFFERED or STREAM), orreject, with optional header mutations. Every stage's preflight runs in chain order before any stage sees a body.BUFFERED. One body up to the stage payload limit (max 4 MiB). The stage returns
unchangedor areplacement, where empty bytes delete the body, plus late header and trailer mutations. No request bytes go upstream until the whole chain approves.STREAM. Ordered input chunks (64 KiB max) and one
input_endwith trailers. The stage emits its own output withoutput_start,output_chunk, andfinish. Input and output counts are independent, and there is no total size or time cap. 30 seconds of middleware stall fails the stage. 30 seconds without upload progress from the sandbox returns408.Commitment. Request output streams upstream during upload unless the route needs the full body: a BUFFERED stage, SigV4, body credential rewrite, forward proxy, JSON-RPC/MCP/GraphQL, HTTP/1.0, or
Upgrade. In those cases the supervisor withholds up to 4 MiB and sends it with the head. Body-aware endpoints re-check each replaced body against policy. Credentials are injected after all middleware mutations. If a STREAM stage fails after the head went upstream, the supervisor closes the upstream connection and never replays.Failure. Always fail-closed.
on_errordoesn't apply, and the gateway rejectsfail_openon v2 entries. Explicit rejects get403 middleware_denied. Errors, timeouts, bad results, and lifecycle violations get403 middleware_failed.Compatibility. Older gateways and supervisors refuse
HTTP_REQUEST_V2/HTTP_RESPONSE_V2atDescribe, so there is no silent fallback to v1. A service uses one hook version, and a chain can't mix versions. The gateway rejects such policies, and the supervisor fails mixed chains closed withmiddleware_hook_versions_mixed.Uninspectable traffic. Something the original proposal didn't cover. For
tls: skip, h2c, unsupported tunnels, raw TCP, and SQL passthrough, v2 request middleware gets a body-less preflight and allows or refuses the connection. So v2 request middleware can now attach totls: skipendpoints.Acceptance Criteria
Covered by #4359. Check these off when it merges:
Expect: 100-continue, size-changing transformations, oversized input and replacements, invalid results, and failures before and after forwarding.e2e:middleware-http-v2suite runs the content guard example with v2 request and response hooks in both modes. The v1 e2e still passes.Follow-up, out of scope here:
EvaluateHttpRequest,HttpResponsePreReturn.Evaluate, and their request-only messages in 0.2.0, and list the removal in the 0.2.0 upgrade notes.Alternatives Considered
Breaking replacement before 0.1.0. The original plan, overtaken by the 0.1.x release. A deprecation window gives running services a release to migrate.
Unary compatibility adapter on a shared runner (#2431's original proposal). More supervisor code to keep alive an API we plan to delete. Separate v1 engines plus a no-mixing rule are easier to remove in 0.2.0.
Wrap the buffered request in a stream. Changes the transport shape but keeps the buffering and size limits.
One universal inspection RPC for requests and responses. We kept two RPCs because requests and responses differ in metadata, mutation rights, policy checks, and commitment. They still share message types and most of the runtime.