What is the problem your feature solves, or the need it fulfills?
ProxyHttp::response_body_filter receives end_of_stream: bool as an
input and can rewrite the chunk bytes, but it has no way to declare the
response finished. In proxy_h1.rs and proxy_h2.rs, the task pipeline
maps HttpTask::Body(data, end) through the filter and re-emits
HttpTask::Body(data, end) with the original end flag.
So a filter that decides mid-stream that a response must stop has only
two options, and both are wrong on the wire:
- Return
Err(...). The session is torn down: the client sees a
truncated chunked body or an H2 reset, not a well-formed end of
response, and the downstream connection is lost.
- Replace the current chunk with a final message and swallow every
subsequent chunk (*body = None). The client eventually sees a clean
end, but the upstream transfer runs to completion, which can be
arbitrarily long and, for metered upstreams, costly.
Who this is for: any proxy that enforces policy on streaming responses
and needs to end one mid-stream with a final well-formed message rather
than a reset. Content-policy and DLP filters that must stop a response on
a match, quota and metering enforcement that cuts at a limit, and timeout
policies that end a stalled stream with an explanatory final chunk (for
example a terminating SSE event that says why) all hit this today.
Describe the solution you'd like
A method on Session (name entirely up to you):
/// After the current `response_body_filter` call returns, treat the
/// filtered chunk as the last chunk of the response: finish the
/// downstream body encoding cleanly and stop reading from upstream.
pub fn finish_response_body_after_current_chunk(&mut self) -> Result<()>;
How it would work:
- The proxy loop checks the flag after each
response_body_filter
return. When set, the current task is forwarded as
HttpTask::Body(data, true) (or Body followed by Done), so the
downstream write path runs its normal end-of-body logic: final chunk
plus 0\r\n\r\n terminator on H1 chunked, END_STREAM on H2. The
downstream connection stays reusable.
- The upstream read loop stops. On H1 the upstream connection is closed
(mid-body, it is not reusable anyway); on H2 the stream is reset. For
metered upstreams this is the point: the transfer stops at the cut.
- Fixed Content-Length responses cannot be ended early without violating
the framing, so the method returns an error when the downstream
response is neither chunked nor H2 (the response header has already
been sent by filter time, so eligibility is known at the call). The
filter author chooses how to handle that; nothing changes silently.
- Expected trailers are simply not sent; the response ended at the body.
- Calling it when
end_of_stream is already true is a no-op.
Describe alternatives you've considered
- Changing the filter signature to
end_of_stream: &mut bool. More
direct, but it breaks every existing ProxyHttp implementation. The
session-method shape is purely additive; the tradeoff is one extra flag
check in the response loop, which seems the better exchange.
- Returning
Err from the filter (what we do today). Works as an
interim: the client stops receiving and the upstream stops generating.
But the client sees a protocol error instead of a well-formed response
end, and the downstream connection is lost. Acceptable as a fallback,
hostile as the permanent behavior.
- Swallowing all subsequent chunks after substituting a final
message. Clean for the client eventually, but the proxy keeps
consuming the upstream to completion: unbounded time and, on metered
upstreams, unbounded cost.
- Solving it outside pingora-proxy. Not possible: the
end flag is
decided before the filter runs, and the upstream read loop is not
reachable from filter code.
Additional context
- Verified against pingora-proxy 0.8.1: the filter call sites in
proxy_h1.rs (the HttpTask::Body and HttpTask::UpgradedBody arms)
and proxy_h2.rs re-emit the task with the original end flag; the
filter can mutate data only.
HttpTask::Done ("Signal that the response is already finished")
already exists in pingora-core, which suggests the pipeline has the
vocabulary for this; what is missing is the filter's ability to say it.
- We searched existing issues and pull requests for prior art on forcing
end-of-stream from the body filter and found none.
- Happy to implement this behind whatever API shape you prefer, with
tests for the H1 chunked, H2, and Content-Length-refusal paths, if the
design direction is acceptable.
What is the problem your feature solves, or the need it fulfills?
ProxyHttp::response_body_filterreceivesend_of_stream: boolas aninput and can rewrite the chunk bytes, but it has no way to declare the
response finished. In
proxy_h1.rsandproxy_h2.rs, the task pipelinemaps
HttpTask::Body(data, end)through the filter and re-emitsHttpTask::Body(data, end)with the originalendflag.So a filter that decides mid-stream that a response must stop has only
two options, and both are wrong on the wire:
Err(...). The session is torn down: the client sees atruncated chunked body or an H2 reset, not a well-formed end of
response, and the downstream connection is lost.
subsequent chunk (
*body = None). The client eventually sees a cleanend, but the upstream transfer runs to completion, which can be
arbitrarily long and, for metered upstreams, costly.
Who this is for: any proxy that enforces policy on streaming responses
and needs to end one mid-stream with a final well-formed message rather
than a reset. Content-policy and DLP filters that must stop a response on
a match, quota and metering enforcement that cuts at a limit, and timeout
policies that end a stalled stream with an explanatory final chunk (for
example a terminating SSE event that says why) all hit this today.
Describe the solution you'd like
A method on
Session(name entirely up to you):How it would work:
response_body_filterreturn. When set, the current task is forwarded as
HttpTask::Body(data, true)(orBodyfollowed byDone), so thedownstream write path runs its normal end-of-body logic: final chunk
plus
0\r\n\r\nterminator on H1 chunked,END_STREAMon H2. Thedownstream connection stays reusable.
(mid-body, it is not reusable anyway); on H2 the stream is reset. For
metered upstreams this is the point: the transfer stops at the cut.
the framing, so the method returns an error when the downstream
response is neither chunked nor H2 (the response header has already
been sent by filter time, so eligibility is known at the call). The
filter author chooses how to handle that; nothing changes silently.
end_of_streamis already true is a no-op.Describe alternatives you've considered
end_of_stream: &mut bool. Moredirect, but it breaks every existing
ProxyHttpimplementation. Thesession-method shape is purely additive; the tradeoff is one extra flag
check in the response loop, which seems the better exchange.
Errfrom the filter (what we do today). Works as aninterim: the client stops receiving and the upstream stops generating.
But the client sees a protocol error instead of a well-formed response
end, and the downstream connection is lost. Acceptable as a fallback,
hostile as the permanent behavior.
message. Clean for the client eventually, but the proxy keeps
consuming the upstream to completion: unbounded time and, on metered
upstreams, unbounded cost.
endflag isdecided before the filter runs, and the upstream read loop is not
reachable from filter code.
Additional context
proxy_h1.rs(theHttpTask::BodyandHttpTask::UpgradedBodyarms)and
proxy_h2.rsre-emit the task with the original end flag; thefilter can mutate
dataonly.HttpTask::Done("Signal that the response is already finished")already exists in
pingora-core, which suggests the pipeline has thevocabulary for this; what is missing is the filter's ability to say it.
end-of-stream from the body filter and found none.
tests for the H1 chunked, H2, and Content-Length-refusal paths, if the
design direction is acceptable.