-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Support Async Gzip Decoding #165
Conversation
examples/async.rs
Outdated
let work = client.get("https://hyper.rs").unwrap().send().map(|res| { | ||
println!("{}", res.status()); | ||
}); | ||
let work = client.get("http://localhost:8080/range/4096?duration=5&chunk_size=300") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've just been using the samples to test. I'll go revert them at some point.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Idea that might make testing this as you go easier.
Not sure how easy it is to set up the test stub server (usage here) to chunk and stutter the packets back to behave like more like a scenario you'd want to test handling using async, but if its not too difficult I think it would make debugging and testing as you go easier than using thee example. At least that was my experience when I initially added gzip decoding. Also means you won't have to do that work at the end 😄
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @echochamber! That's a great idea. Debugging has been tricky, since there are a few permutations of this code that behave differently. I need to get some proper tests in there.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I could see adding something like chunk_size: 300
as part of the server!
macro, to change from using write_all
on the whole response to looping and trying to write that number of bytes at a time.
Usage of the macro could look like this:
let server = server! {
request: expected_req,
response: bytes_to_respond_with,
chunk_size: 300
};
What does duration
do? Is that a timeout between chunks?
Implementation in the support/server.rs
would need these things:
- add
chunk_size: Option<usize>
to theTxn
struct - Update the [write portion[(https://github.com/seanmonstar/reqwest/blob/master/tests/support/server.rs#L57-L64) to loop on writing
if let Some(size) = self.chunk_size
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, I'll work on the server and revert these examples 👍
What does duration do? Is that a timeout between chunks?
Yeh this is currently just pointing at a local httpbin (the hosted one is behind nginx so it won't actually chunk responses). From memory it's a delay between the chunks. It'd be nice to be able to tell the server when to send the next chunk so we can force WouldBlock
imperatively, but don't want to complicate it too much.
src/async_impl/decoder.rs
Outdated
/// A lazily constructed response decoder. | ||
pub struct LazyDecoder<TBodyStream>(LazyDecoderInner<TBodyStream>); | ||
|
||
enum LazyDecoderInner<TBodyStream> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually... Looking at this now I think this could all go away by just converting the gzip encoder into inner. So it doesn't matter if the body stream is already in a decoder.
The current tests picked up a nice little bug in my changes where a small chunk was being dropped rather than retained to be errored on later. I've fixed that up. If you're happy with the general approach @seanmonstar then I'll go add a bunch of test cases. It'd be nice to make a test server that can programmatically delay chunks so we can test |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Awesome! Thanks for getting this started!
I'd probably go about it this way:
- Create a
Decoder
inasync_impl/decoder.rs
. This would implementStream
, and have an inner enum of whether it is plain text or gzip (or someday brotli or whatever). I haven't read the libflate source, so I don't know the answer to this: is it always possible to decode a chunk, no matter the size? If not, then the gzip variant would need a buffer, and try to decode and yield aChunk
when decoding is successful. - I'd leave the
ReadableBody
thing insrc/response.rs
, as a way of doing synchronous reading.
examples/async.rs
Outdated
let work = client.get("https://hyper.rs").unwrap().send().map(|res| { | ||
println!("{}", res.status()); | ||
}); | ||
let work = client.get("http://localhost:8080/range/4096?duration=5&chunk_size=300") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I could see adding something like chunk_size: 300
as part of the server!
macro, to change from using write_all
on the whole response to looping and trying to write that number of bytes at a time.
Usage of the macro could look like this:
let server = server! {
request: expected_req,
response: bytes_to_respond_with,
chunk_size: 300
};
What does duration
do? Is that a timeout between chunks?
Implementation in the support/server.rs
would need these things:
- add
chunk_size: Option<usize>
to theTxn
struct - Update the [write portion[(https://github.com/seanmonstar/reqwest/blob/master/tests/support/server.rs#L57-L64) to loop on writing
if let Some(size) = self.chunk_size
.
examples/simple.rs
Outdated
@@ -16,7 +16,7 @@ fn run() -> Result<()> { | |||
|
|||
println!("GET https://www.rust-lang.org"); | |||
|
|||
let mut res = reqwest::get("https://www.rust-lang.org/en-US/")?; | |||
let mut res = reqwest::get("http://localhost:8080/range/4096?duration=5&chunk_size=300")?; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just leaving a reminder so we don't forget to switch this back.
src/async_impl/response.rs
Outdated
|
||
|
||
/// A Response to a submitted `Request`. | ||
pub struct Response { | ||
status: StatusCode, | ||
headers: Headers, | ||
url: Url, | ||
body: Body, | ||
body: Decoder<Body>, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it probably makes sense that the body is a Decoder
instead, like you've done here. I'd probably remove the generic, and just expose Decoder
(at least, publicly. If that wraps something that is generic internally, that's fine.)
src/async_impl/response.rs
Outdated
@@ -45,17 +51,13 @@ impl Response { | |||
&mut self.headers | |||
} | |||
|
|||
/// Get a mutable reference to the `Body` of this `Response`. | |||
#[inline] | |||
pub fn body_mut(&mut self) -> &mut Body { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd leave this in, but returning &mut Decoder
.
src/async_impl/response.rs
Outdated
@@ -95,6 +97,25 @@ impl Response { | |||
} | |||
} | |||
|
|||
impl Read for Response { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd rather not impl Read
for the async Response
, I think. Maybe it does make sense, but probably a separate issue.
Thanks for the feedback @seanmonstar. As we discussed on gitter, So I'm not really sure how to go about refactoring the decoder to work on chunks as you described yet @seanmonstar but don't want this to stall so I'll work on the test server and get a decent suite of tests up in the meantime. |
@KodrAus Could we change decoder to implement a |
@echochamber I did that initially but came to the conclusion that:
So I ended up with this implementation that can read synchronously without blocking unless a chunk is unavailable. Note that for the second point I didn't think about wrapping the asynchronous body in a |
I looked at all the compression libraries, and We can then wrap the |
Thanks @ishitatsuyuki 👍 I think the best way forward is to wrap the As far as I know, we're using If we really don't want the async Does anyone have any other thoughts before I go and make those changes? @seanmonstar @echochamber @ishitatsuyuki? |
No problems, sounds good to me.
You are correct. Kind of anyway. Flate2 works on windows, it just requires the users to install MSVC. |
No, BufReader is an additional layer of nonsense. As Chunk is already buffered, we should wrap it to satisfy the |
I'm not sure I understand you @ishitatsuyuki. I've arrived at
I think maybe we should sketch out a plan of what we expect the sync and async responses to look like, so we're all on the same page. I think (straw-man names):
Do you have other ideas of how this should work @ishitatsuyuki? One other idea is that we have totally separate |
This is what I think:
|
Ok I've made some changes:
The diff on the last commit is really noisy so it's probably worth looking at it from the top again. |
Travis says no. Looks like a random Travis fail? |
It just seems that it's hitting the per user build limit and being queued. |
src/async_impl/decoder.rs
Outdated
DecoderInnerFuture::PendingGzip(ref mut body) => { | ||
match body.read(&mut peek) { | ||
Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => return Ok(Async::NotReady), | ||
read @ _ => read |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This condition doesn't look cool to me. Can you explain? Is it same as x => x
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@ishitatsuyuki That's right. I refactored it into this shape but should simplify it.
src/async_impl/decoder.rs
Outdated
The following types directly support the gzip compression case: | ||
|
||
- `DecoderInnerFuture` is a non-blocking constructor for a `Decoder` in case the body needs to be read | ||
- `Peeked` is a wrapper around a peeked byte that prevents an EOF on read |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure I understand. Anything asynchronous should never block and returning WouldBlock instead. What do you mean by "EOF on read" here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is because libflate
does a read_exact
for 2 bytes, which will return an eof error if there aren't 2 bytes available.
So we've got a wrapper that reads a byte and checks whether the response is actually empty.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So... from the docs of read_exact:
If any other read error is encountered then this function immediately returns. The contents of buf are unspecified in this case.
If this function returns an error, it is unspecified how many bytes it has read, but it will never read more than would be necessary to completely fill the buffer.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is not that libflate returns EOF, it's that even if the body were empty, the read_exact
will return an UnexpectedEof
error. Users should be able to expect that an empty body just returns Ok(0)
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah my mistake, thanks @seanmonstar
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you mean that special casing is the best way? It's not gzip in that case then.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You could imagine a less-stellar server setting a header universally, but then it turns out the file it is serving is empty.
Of course, in this case, since the bytes are coming in a Stream
of Chunks
, we can detect EOF before ever using the libflate encoder: if poll
returns Ok(Async::Ready(None))
, that's the EOF. No need to ask libflate to decode then :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah yes we could do that... I could just simplify the gzip future I've already got 👍
If you're looking a way to provide
See also what |
src/async_impl/response.rs
Outdated
} | ||
} | ||
|
||
pub struct DecodedBody { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So, my opinion may seem odd, and if so, that's OK. If it seems too absurd, let me know.
But, here's an API that I think would make sense to expose:
impl Response {
pub fn body_mut(&mut self) -> &mut Decoder {
// ...
}
}
pub struct Decoder {
inner: DecoderImplWhateverNameNotSuperImportant,
}
impl Stream for Decoder {
type Item = Chunk;
type Error = io::Error;
}
I realize that libflate needs an io::Read
, but I'm not certain yet about exposing those traits to the public.
The inner decoder impl can provide a Read
for libflate to use, which does like you've done, just polling a chunk out when needed. When the libflate decoder returns some decoded bytes, that can be emitted as a new Chunk
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've just got a few questions (sorry I don't usually do this kind of programming):
- Would you expect the synchronous response to consume the
Decoder: Stream
, or theDecoderImplWhateverNameNotSuperImportant: Read
? I guess the inner decoder could be bothRead
andStream
, so the plain text decoding just means passing on chunks - If we're going to emit chunks for the gzip decoder, then we'll need to read them into some buffer. I'm not sure how big that should be or where it should come from
So I'm not sure whether this should change the way I've done the Decoder
completely or if it's just a matter of the async Response
using DecoderImplWhateverNameNotSuperImportant: Stream
and the sync Response
using DecoderImplWhateverNameNotSuperImportant: Read
.
Additional one. As both |
tests/support/server.rs
Outdated
@@ -55,10 +56,16 @@ pub fn spawn(txns: Vec<Txn>) -> Server { | |||
} | |||
|
|||
if let Some(dur) = txn.write_timeout { | |||
let headers_end = ::std::str::from_utf8(&reply).unwrap().find("\r\n\r\n").unwrap() + 4; | |||
let headers_end = unsafe { ::std::str::from_utf8_unchecked(&reply) }.find("\r\n\r\n").unwrap() + 4; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why are you creating a potential root of UB here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because gzipped content is not valid UTF8 and we'll always hit the sequence before the invalid content. Again, this is a test server so it's not so important that this is unsafe. If something unexpected happens then the test will fail.
But the road to ruin is paved with good intentions so I think it's reasonable to call this out and do it in an alternative way without the unsafe.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The fact that it is UB doesn't change, and this code may break any time. Change the type signature instead.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I changed this to just operate over the [u8]
instead of turning it into a str
for the sake of convenience.
@@ -23,6 +23,7 @@ pub struct Txn { | |||
pub read_timeout: Option<Duration>, | |||
pub response_timeout: Option<Duration>, | |||
pub write_timeout: Option<Duration>, | |||
pub chunk_size: Option<usize>, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is basically simply slowing down tests, as you're just stressing hyper grouping delayed response in that case. To divide it into chunks, apply the real chunked encoding. See https://en.wikipedia.org/wiki/Chunked_transfer_encoding for details.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's the point. It's blocking the response to force WouldBlock
. It doesn't necessarily need to be chunked encoding because that isn't what it's testing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Huh? You don't need to split them into chunks, as it's just the difference of 3 WouldBlock vs 1.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When it blocks is important because the internal state of the decoder will change over time. That is, blocking before any bytes are received is a different code path to blocking after bytes are received.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You don't get any data before the response is complete. You get a chunk of the whole response after you send everything.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Additionally, you will get interesting results if you sent it with chunk encoding of size 1. It should break because read_exact is not transactional (see above).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I checked this out and you're right, the response isn't received until all chunks are received. I was hoping I wouldn't have to do actual chunked encoding here but looks like I will. I'll worry about this one later though, I haven't quite finished the decoder side yet.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm, you should get a stream of chunks as they are received, not at the end of the response, that's the point of the streaming interface. That is to say, even with a body that has a Content-Length: 15
, if a read in hyper receives 5 bytes, it will yield a Chunk
of those 5 bytes, and register to read more (still expecting 10 bytes).
However, it is also true that if the body is broken up by the chunked encoding, hyper will yield Chunk
s each time it reaches the end of an encoding "chunk". So, if it received 5\r\nabcde\r\n1\r\nf\r\n
, it would yield a Chunk
of 5, and then another poll would yield a Chunk
of 1.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I must've been doing something else wrong here before, because changing this back to emit the body in chunks without explicit chunked encoding is giving us individual chunks and hitting the right WouldBlock
s...
src/async_impl/decoder.rs
Outdated
let chunk_start = *pos; | ||
let len = cmp::min(buf.len(), chunk.len() - chunk_start); | ||
let chunk_end = chunk_start + len; | ||
buf[..len].copy_from_slice(&chunk[chunk_start..chunk_end]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using [u8]
's Read implementation may be easier.
src/async_impl/decoder.rs
Outdated
|
||
fn into_inner(self) -> TStream { | ||
match self.state { | ||
ReadState::Ready(_, _) => panic!("attempted to take used reader"), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We might want to rethink these error conditions. They should effectively be unreachable, but the invariants are spread a bit thin.
Maybe it would be better to return Option
or Result
here and expect
it in the synchronous response? Or just silently ignore the first chunk of there is one, but that seems worse to me.
I've pushed a few changes but this isn't ready for another review yet. I'll send a ping when I think it is. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any reason to do so? I think this is completely nonsense.
What in particular are you referring to? Also, calling people's work "complete nonsense" is kind of hostile, could you dial it back? Pointing out specific flaws will be much more useful for everyone. |
I think the 'complete nonsense' @ishitatsuyuki is referring to is the fact that we're decompressing into an intermediate buffer, not the fact that I haven't implemented it properly yet. To be honest, I'm also not 100% sure why we don't want the response body to be |
I intended to comment on the latest commit, but for some reason the mobile UI turned it into a review. I don't think using a stream fits here. HTTP response is just a stream of bytes, and I think there's no better solution than Read/AsyncRead. Anyway, do you have a particular reason to make it a Stream? There's more overhead, it is less intuitive, and API compatibility is not a problem. |
@ishitatsuyuki I basically agree with you. Maybe @seanmonstar can shed some more light on why this needs to be But I think it's also worth considering that this is an unstable API. I don't think we need to get it perfect right away. For the stable API there'll be an impact on memory usage for compressed responses but that could be improved through major changes to this async API in non-breaking version bumps (I'm assuming the So personally I'm happy for this to move forward in any shape that @seanmonstar is happy with. |
I cannot understand why you're moving to use Stream where AsyncRead would be definitely be better. I saw too much changes that it would require substantial amount of work to revert to AsyncRead. Can you elaborate on why you're switching to Chunks? |
Yeh I get that it doesn't seem ideal, but I don't really want to push back on the design right now. I'd rather focus on having a robust implementation that isn't going to cause regressions for all the current synchronous @ishitatsuyuki Your reviews have been great, you've caught a lot of errors in this PR. So I think any more bug-related feedback you have would help drive this forward more than the merits of |
There's a |
@seanmonstar other than the |
Looks like the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good work! I didn't inspect the implementation closely because I believe with buffering it's mostly safe. I reviewed the tests carefully instead.
examples/simple.rs
Outdated
@@ -1,4 +1,7 @@ | |||
//! `cargo run --example simple` | |||
|
|||
#![allow(unknown_lints, unused_doc_comment)] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is a leftover. Upgrade error-chain, although it's still in rc.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is only a dev-dependency
so I have no objection to using an rc build.
examples/async.rs
Outdated
@@ -13,4 +13,4 @@ fn main() { | |||
}); | |||
|
|||
core.run(work).unwrap(); | |||
} | |||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Adjust your editor settings.
tests/async.rs
Outdated
\r\n\ | ||
", | ||
chunk_size: chunk_size, | ||
write_timeout: Duration::from_secs(1), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I recommend a smaller delay here. 5~10 ms is enough for machines today, as the operation happens at microseconds.
Also, I would recommend an extreme case with chunked encoding of 1 byte each line. That stress tests the buffer implementation to provide a correct implementation of read_exact.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So this is interesting. libflate
does not like reading 1 byte. I think I'll need to bring back the Peeked
type to cater for this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've added a little 2 byte buf reader just to make sure that would keep libflate
happy. Looks like it does. I'll simplify it a bit later on.
tests/gzip.rs
Outdated
@@ -34,14 +38,16 @@ fn test_gzip_response() { | |||
Accept-Encoding: gzip\r\n\ | |||
\r\n\ | |||
", | |||
chunk_size: chunk_size, | |||
write_timeout: Duration::from_secs(1), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Smaller delay here too.
tests/support/server.rs
Outdated
// - a `chunk_size` has been supplied | ||
// | ||
// the server won't read headers so if the response doesn't specify `Transfer-Encoding` | ||
// then it'll misbehave and send an invalid response. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I recommend a debug assertion if it's easy to implement.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good idea 👍 I can search the response bytes for a Transfer-Encoding
header and assert it's present.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think the server needs to do it's own chunked encoding, that can be part of the text passed to the macro, so that the exact text expected is always visible in the test cases.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Excellent work!
@@ -23,6 +23,7 @@ pub struct Txn { | |||
pub read_timeout: Option<Duration>, | |||
pub response_timeout: Option<Duration>, | |||
pub write_timeout: Option<Duration>, | |||
pub chunk_size: Option<usize>, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm, you should get a stream of chunks as they are received, not at the end of the response, that's the point of the streaming interface. That is to say, even with a body that has a Content-Length: 15
, if a read in hyper receives 5 bytes, it will yield a Chunk
of those 5 bytes, and register to read more (still expecting 10 bytes).
However, it is also true that if the body is broken up by the chunked encoding, hyper will yield Chunk
s each time it reaches the end of an encoding "chunk". So, if it received 5\r\nabcde\r\n1\r\nf\r\n
, it would yield a Chunk
of 5, and then another poll would yield a Chunk
of 1.
tests/support/server.rs
Outdated
// - a `chunk_size` has been supplied | ||
// | ||
// the server won't read headers so if the response doesn't specify `Transfer-Encoding` | ||
// then it'll misbehave and send an invalid response. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think the server needs to do it's own chunked encoding, that can be part of the text passed to the macro, so that the exact text expected is always visible in the test cases.
- `Pending` is a non-blocking constructor for a `Decoder` in case the body needs to be checked for EOF | ||
- `Peeked` is a buffer that keeps a few bytes available so `libflate`s `read_exact` calls won't fail | ||
*/ | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The work in the module is phenomenal! It was quite easy to read and follow what was happening, thanks!
One comment is that the style throughout the rest of the crate is to prefer generics of just a single letter. As for my personal opinion, I find seeing argument types that are a word makes me immediately assume it's some concrete type, like TStream
, instead of T
, where I know that is a generic.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you! I've updated the generics to match the rest of the crate. I'll rebase this on master now.
de1e2ec
to
e2fa972
Compare
src/response.rs
Outdated
@@ -183,11 +182,11 @@ impl Response { | |||
/// ``` | |||
#[inline] | |||
pub fn error_for_status(self) -> ::Result<Self> { | |||
let Response { body, inner, _thread_handle } = self; | |||
let Response { inner, body, _thread_handle } = self; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Curious reordering :)
src/async_impl/response.rs
Outdated
/// | ||
/// This function will replace the body on the response with an empty one. | ||
#[inline] | ||
pub fn body(&mut self) -> Decoder { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I actually would have body() -> &Decoder
, to match the http
crate...
Taking a Decoder
can be done with mem::swap
and body_mut()
, right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's a bit unintuitive to have to use mem::replace
, but I guess at some point if this uses http::Response
then we'll have into_parts
.
I've updated the async sample to show how you can move the body out.
18149b0
to
2cb70c8
Compare
Thanks for this spectacular work! |
For #161
I figured I'd open this up for visibility, and because it's easier to talk about code in PRs than issues or gitter. It's an attempt to support decoding in the async implementation, but there may be better approaches. This does introduce a bit of indirection and complexity.