-
Notifications
You must be signed in to change notification settings - Fork 624
Chunked stream responses #823
Comments
If you don't call the response body ( for example @honfika In future, we may want to add an option to read the response body bytes by bytes by adding a new method. The problem is that in that case the user will have to write the response on his own, because proxy won't be caching the body. So user would have to call a new write to stream method. Also, once read from stream is called, user will not be allowed to call
If someone could create a PR would be great. |
I think there are separate questions here.
The problem above occurs only on chunked responses, but a clearer way to express these concepts is to have both interfaces, at same time: a) a HttpMessageReceived callback, that contains all parsed headers and all bytes that are cached anyway. b) a HttpChunkReceived callback, that contains only the bytes received on the chunk, with no cache by the proxy. Both callbacks can have the same parameters, only the name denotes where this is a complete message or a simple chunk. |
Based on the discussion above, I just added two new event handlers as a prototype to develop branch, which if someone could implement in future would be great. There are two scenarios that we need to handle.
Note that Similarly, in case of fixed length body with public class BeforeBodyWriteEventArgs : ProxyEventArgsBase
{
internal BeforeBodyWriteEventArgs(SessionEventArgs session, byte[] bodyBytes, bool isChunked, bool isFinalChunk) : base(session.ClientConnection)
{
Session = session;
BodyBytes = bodyBytes;
IsChunked = isChunked;
IsFinalChunk = isFinalChunk;
}
/// <value>
/// The session arguments.
/// </value>
public SessionEventArgs Session { get; }
/// <summary>
/// Indicates whether body is written chunked stream.
/// If this is true, BeforeRequestBodySend or BeforeResponseBodySend will be called until IsLastChunk is true.
/// </summary>
public bool IsChunked { get; }
/// <summary>
/// Indicates if this is the last chunk from client or server stream, when request is chunked.
/// Override this property to true if there are more bytes to write.
/// </summary>
public bool IsFinalChunk { get; set; }
/// <summary>
/// The bytes about to be written. If IsChunked is true, this will be a chunk of the bytes to be written.
/// Override this property with custom bytes if needed, and adjust IsLastChunk accordingly.
/// </summary>
public byte[] BodyBytes { get; set; }
} |
I think this approach is relatively simple to implement and shouldn't break existing functionality. All we need to do is pass the new handlers all the way down to the write bytes call in http stream, and do the call back logic described above. |
Actually, the above suggested new handlers can also be used for fixed content length body to be read bytes by bytes. In that case, the handler will be called again and again each time we fill our read buffer, until content-length is reached. User can also set the bytes and the handler will be called again and again until content-length amount of bytes is reached. That would be helpful to keep the proxy memory footprint low, when the fixed content-length is large, say in tens of megabytes. |
I've also did some prep work so that both |
I'm suggesting to change the protocol a little, to make these new handlers
more akin to a filter than a batch processing. First I will comment inline,
and then the racionalle below.
On Thu, Apr 22, 2021 at 11:16 PM Jehonathan Thomas ***@***.***> wrote:
1.
*Read & Modify:* User wants to modify the body bytes. In that case,
user would set the byte[] BodyBytes { get; set; } property.
In that case, the user would set *or clear* the byte[] BodyBytes { get;
set; } property. (With null? With new byte[]? A method .ClearBytes() to
make this unambiguous?)
A cleared chunked fragment is not sent down or upstream if an intermediary
empty chunk message is impossible, or sent without any data if possible by
protocol.
1.
If IsChunked is false, user will have to set the full body bytes only
once.
If IsChunked is false, the user will have a chance to modify body bytes
only once.
1.
If IsChunked is true, user would set chunk body and mark IsFinalChunk
to true when it is the final chunk.
The user probably dont want to mess with IsFinalChunck, as this may cause
severe disruptions. Say, an intermediary chunk marked as final followed by
another chunk.
1.
In all cases, the handlers will be called again and again until
IsFinalChunk is false. We would need to ensure that any unread chunked
bytes are discarded from client or server stream by siphoning them out
before finalizing the request or response. Also, if body is a compressed
stream, that need to be handled properly, we need to decompress for the
handler call, and compress upon write chunk call.
It's guaranteed that every chunk sits in a compression boundary or frame?
If not, this may not be possible without a ton of bookkeeping, up to the
original situation where a chunked message is eternally cached until the
RAM or a timeout runs off.
In this situation, a RawBytes/DecodedBytes property may be necessary in
lieu of the BodyBytes property that can be impossible to offer.
1.
Note that IsChunked property is readonly, i.e user will not be allowed
to modify the Transfer-encoding header at this stage, because its
already send to client or server.
By the same reasoning, IsFinalChunk shouldn't be touched if the user
really *really* knows that he is doing, but I can foresee some
situations where the user need to mark some outbound chunk stream as
completed but keep collecting the inbound chunk stream.
So the protocol I suggest is this:
User wants to modify the body bytes. In that case, the user would set the
byte[] BodyBytes { get; set; } property in each callback, or set it to null
/ call .ClearBody() to avoid these bytes to be sent. IsChunked only
indicates if the original HTTP message is chunked or not, but this callback
is called on every body byte block received by the proxy, chunked or not.
The handlers will be called again and again until IsFinalChunk is *true*.
Also, if the body is a compressed stream, the RawBytes will contain the
original bytes, and ~BodyBytes~ DecodedBytes is filled with inflated data
on a best efforts basis, but may be null in case of errors. IsChunked
property is readonly, i.e user is not allowed to modify the
Transfer-encoding header at this stage, because it's already sent to client
or server.
André L F S Bacci
|
On Thu, Apr 22, 2021 at 11:51 PM Jehonathan Thomas ***@***.***> wrote:
Actually, the above suggested new handlers can also be used for fixed
content length body to be read bytes by bytes. In that case, the handler
will be called again and again each time we fill our read buffer, until
content-length is reached. User can also set the bytes and the handler will
be called again and again until content-length amount of bytes is reached.
That would be helpful to keep the proxy memory footprint low, when the
fixed content-length is large, say in tens of megabytes.
Yes, there are some situations where this would be *necessary*. Say, a
proxy capable of recompressing or decompressing any request that passes
there, to fix some upstream or downstream incompatibility. Some big files
appear in this situation and not all RAM in the world will solve this
problem if it's necessary to wait for the whole file. And timeouts.
|
To clarify some of the things you mentioned. This new handlers will NOT be called if the body was already read through Sending Empty Body 1. For chunked body, if you don't want to send any response body, this new handler will be called with original chunks read (inside private async Task OnResponseBodyWrite(object sender, BeforeBodyWriteEventArgs e)
{
//append original bytes to disk file
appendAndSaveOriginalBytes(e.BodyBytes);
e.BodyBytes = null;
} 2. For Fixed content length, if you don't want to send any response body, you would set the private async Task OnResponseBodyWrite(object sender, BeforeBodyWriteEventArgs e)
{
//append original bytes to disk file
appendAndSaveOriginalBytes(e.BodyBytes);
e.BodyBytes = null;
} Sending non-empty Body 1. For chunked body, http compression is done for the entire stream. So when this handler is called, it will be uncompressed
private async Task OnResponseBodyWrite(object sender, BeforeBodyWriteEventArgs e)
{
//append original bytes to disk file
if(e.BodyBytes!=null){ appendAndSaveOriginalBytes(e.BodyBytes); }
//set uncompressed new body bytes
e.BodyBytes = modifiedBytes;
if(modifiedBytes is LastChunk) { e.IsLastChunk = true; } else { e.IsLastChunk = false; }
}
private async Task OnResponseBodyWrite(object sender, BeforeBodyWriteEventArgs e)
{
//append original bytes to disk file
appendAndSaveOriginalBytes(e.BodyBytes);
if(all modified bytes are send) { e.BodyBytes = null; } //set to null
else { e.BodyBytes = modifiedBytes; }
} 2. For fixed content length, if you modify the body bytes, you need to specify the
private async Task OnResponseBodyWrite(object sender, BeforeBodyWriteEventArgs e)
{
//append original bytes to disk file
if(e.BodyBytes!=null){ appendAndSaveOriginalBytes(e.BodyBytes); }
//set uncompressed new body bytes
e.BodyBytes = modifiedBytes;
}
private async Task OnResponseBodyWrite(object sender, BeforeBodyWriteEventArgs e)
{
//append original bytes to disk file
appendAndSaveOriginalBytes(e.BodyBytes);
//set to null if nothing more to send
if(all modified bytes are send) { e.BodyBytes = null; } else { e.BodyBytes = modifiedBytes; }
} |
Sorry for deleting my responses multiple times. 😅 I think I was thinking out loud while typing. One more thing that we need to do apart from this new handler is for |
Did anyone implement this feature ? Can I get a sample ? |
Hi, I'm trying to read a long chunked response which never ends. So it's got blocked forever, with the proxy waiting for all chunks to assembly in one response body.
There is a way to inspect the chunked parts while delivering the responses in parallel?
If not, I suggest implementing a "Stream" flag or a "StreamSize" int, somewhere in
SessionEventArgs
, to make it possible to disable this caching mechanism, so that partial responses got delivered instantly.A never ending response also calls for a "NoCache" flag, too, to avoid a OutOfMemoryException in long running connections.
The text was updated successfully, but these errors were encountered: