HTTP session hooks: streaming request and response middleware
The PR replaces the v1 HTTP middleware hooks with two bidirectional streaming RPCs, so middleware can inspect bodies of any size. A separate unary hook lets middleware decide about connections OpenShell cannot parse as HTTP.
- Streaming API
HttpSessionMiddleware.EvaluateRequestandEvaluateResponse, one stream per HTTP message and stage. A preflight picks continue, reject, or inspect (BUFFERED or STREAM). - Opt in by capabilitySame
HTTP_REQUEST/HTTP_RESPONSEbindings. A service that requiresopenshell.supervisor-middleware.http-sessiongets the new RPCs. No new HTTP operations or versioned enums. - Uninspectable trafficA service that binds
UNINSPECTABLE_NETWORK/PRE_FORWARDanswersUninspectableNetworkMiddleware.Evaluate, a unary call that allows or deniestls: skip, h2c, and raw TCP connections without seeing their content. - Always fail-closed
on_error: fail_openis rejected for session hook services. The uninspectable hook also always fails closed. - v1 deprecated
EvaluateHttpRequestandHttpResponsePreReturnkeep working and are removed in 0.2.0, as is thefail_openbypass for uninspectable traffic.
In this deck
- Before and after
- Shape of the new API
- How a service selects the hook version
- Migrating a v1 service
- Next iterations
- Preflight and body eligibility rules
- Chain ordering and when the head commits
- STREAM stalls and deadlines
- Failure semantics per phase
- Code map for the diff
Same place in the proxy, different conversation with the service
The middleware still sits between network-policy admission and credential injection for requests, and before delivery for responses. What changes is how the supervisor talks to it, and which traffic it can see.
v to toggle
What changes
| v1 hooks | Session hooks | |
|---|---|---|
| Body size | ≤ 4 MiB, buffered | BUFFERED ≤ 4 MiB or unbounded STREAM |
| Request vs response | Two unrelated contracts | One lifecycle; an event/result pair per RPC |
| Who chooses body handling | Request: always full body. Response: mode at preflight | Service, at preflight, from modes OpenShell permits |
tls: skip, h2c, raw TCP | Not shown to middleware; on_error decides | Separate unary hook allows or refuses the connection |
| Failures | on_error decides | Always fail-closed |
| Version selection | Default | Required capability in Describe |
Two streaming RPCs for HTTP, one unary hook for the rest
Requests and responses each have their own event and result messages, with the same lifecycle. The first event is always a preflight, and its result decides which of the remaining events the service will see. Connections OpenShell cannot parse go to a separate unary hook.
proto/supervisor_middleware.proto
service SupervisorMiddleware { rpc Describe(...) returns (MiddlewareManifest); rpc ValidateConfig(...) returns (ValidateConfigResponse); rpc EvaluateHttpRequest(HttpRequestEvaluation) returns (HttpRequestResult); // deprecated rpc EvaluateWebSocketSession(...) // unchanged } service HttpSessionMiddleware { rpc EvaluateRequest(stream HttpRequestSessionEvent) returns (stream HttpRequestSessionResult); rpc EvaluateResponse(stream HttpResponseSessionEvent) returns (stream HttpResponseSessionResult); } service UninspectableNetworkMiddleware { rpc Evaluate(EvaluateUninspectableNetworkRequest) returns (EvaluateUninspectableNetworkResponse); } service HttpResponsePreReturn { ... } // deprecated
message HttpRequestSessionEvent { oneof event { preflight = 1; begin = 2; buffered_body = 3; input_chunk = 4; input_end = 5; session_end = 6; } }
message HttpRequestSessionResult { oneof result { preflight_result = 1; buffered_result = 2; output_start = 3; output_chunk = 4; finish = 5; reject = 6; } } // responses: block_delivery = 6
message HttpRequestSessionPreflight { HttpRequestPreflightHead request; // responses: response head phase, context, middleware_name, config repeated HttpBodyMode permitted_body_modes; // BUFFERED, STREAM repeated HttpBodyModeUnavailable unavailable_body_modes; // mode + reason HttpBodyLimits limits; // chunk, buffer, idle optional uint64 declared_body_bytes; }
Exchange explorer · pick what the service decides at preflight
A required capability selects the hook version
The bindings stay HTTP_REQUEST and HTTP_RESPONSE. Gateways and supervisors have rejected unmet required_capabilities at Describe since v0.1.0, so an older peer refuses a session hook service instead of calling it through the v1 RPCs.
Describe → MiddlewareManifest
bindings { operation: HTTP_REQUEST phase: PRE_CREDENTIALS }
bindings { operation: HTTP_RESPONSE phase: PRE_RETURN }
bindings { operation: WEBSOCKET_MESSAGE ... } // unaffected
bindings { operation: UNINSPECTABLE_NETWORK phase: PRE_FORWARD }
extension {
supported_capabilities: "openshell.supervisor-middleware.http-session"
required_capabilities: "openshell.supervisor-middleware.http-session"
}
Rust helper
extension: Some(http_session_middleware_metadata( MANIFEST_NAME, openshell_core::VERSION, )), // openshell_core::extension_protocol
- The capability applies to every HTTP binding of the service. To serve both versions during a rollout, run two registrations.
- A binding may set
max_payload_bytes: 0. It then gets no body modes and can only continue or reject. UNINSPECTABLE_NETWORK/PRE_FORWARDneeds no capability: peers that predate it reject the unknown operation/phase pair atDescribe.
What happens for each manifest and peer
HttpSessionMiddleware.No RPC, message, or enum value carries a version number. buf breaking against v0.1.2 reports no breaking change.
Moving a service from v1 to session hooks
Both hook versions run side by side until 0.2.0. The Supported Operations docs page, docs/extensibility/supervisor-middleware/operations.mdx, describes the session hooks for users.
Steps for a service owner
- Require the capabilityAdd
openshell.supervisor-middleware.http-sessionto bothsupported_capabilitiesandrequired_capabilities. Keep the bindings. - Implement the session RPCsServe
HttpSessionMiddlewareon the same endpoint, withEvaluateRequest/EvaluateResponsefor the bindings you declare; drop the v1 RPCs. - Make policy entries fail-closedChange
on_error: fail_opentofail_closed. The gateway rejectsfail_openfor session hook services. - Decide about uninspectable trafficA fail-closed entry refuses
tls: skip, h2c, and raw TCP connections, and the gateway rejects its overlap withtls: skipendpoints. To allow such connections, bindUNINSPECTABLE_NETWORK/PRE_FORWARDand implementUninspectableNetworkMiddleware.Evaluate. - Separate selectors from v1 entriesEndpoint selectors must not overlap v1 HTTP hook entries for the same direction, including built-in
openshell/regex. - Roll out gateway firstUpgrade the gateway, then deploy the service. Recreate sandboxes whose supervisors predate this PR.
v1 → session hooks
| v1 | Session hooks | |
|---|---|---|
EvaluateHttpRequest | → | inspect: buffered |
Decision DENY | → | reject |
HEADERS_ONLY | → | continue_without_body + header mutations |
WHOLE_BODY_BYTES | → | inspect: buffered |
STREAM_BYTES | → | inspect: stream |
block_delivery | → | block_delivery |
on_error: fail_open | → | not allowed |
New gateway policy checks
- Overlapping selectors that mix hook versions for one direction are rejected. If a mixed chain still occurs at runtime, traffic is blocked with
middleware_hook_versions_mixed. - Only middleware that binds
UNINSPECTABLE_NETWORK, or afail_openentry until 0.2.0, may selecttls: skipendpoints. - Stored policies with
fail_openstill load; supervisors run their HTTP traffic fail-closed once they reach the service.
deprecated.fail_open bypass for uninspectable traffic removed.