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
Client support for receiving 1xx informational responses #2565
This is to discuss adding support for accessing 1xx informational responses on the client side (see #2426 for server side). Currently, hyper parses all 1xx responses and ignores them, as RFC 7230 says it must at least be able to receive and ignore them.
The Client (and client::conn) interfaces return an impl Future<Output = Result<Response>>, which only allows awaiting it for a single response. We can't really add new methods to allow also awaiting for informational responses, unless we stuck to always return a specialized type that is both a Future and has extra methods. However, using those together wouldn't be that clear to a user. You can't be sure if or how many informational responses would be received before the full response, so you would want to await "both" types.
Instead, we can use a callback format. We could define a type (hyper::client::ext::Informational? On1xx? names...), and the user would store that in the request extensions. We'd then check for that in the h1 proto code and if it exists, call the callback with the 1xx headers.
I much prefer the idea of adding methods on ResponseFuture, hooks make bookkeeping harder for the caller, and I don't think it is more confusing to have both those methods and the Future impl than say, using poll_data and poll_trailers on HttpBody.
In the curl project, there's right now 105 test cases disabled when built with hyper. 46 of them use 100-continue in one way or another and we really need to know when we get 1xx responses (or not) to make curl+hyper behave like the built-in HTTP code does. It could allow us to make significant progress in my task to make curl+hyper match the built-in HTTP code.
I've spent some time researching options (prior art of APIs), and considering the existing code, and I think I've settled on staged approach. I agree with you, @nox, we can add a method to ResponseFuture on the Rust side, and make it work right. However, it'd be less easy to access in the exposed C API, and will take more time to get right. So:
First, add support for a callback extension, which will be exposed in the C API. This is the least complicated to do on hyper's side, so we can do it quickly. Importantly, though, most of it can be reused or only slightly altered to support the next steps.
Because of the current client design, which separates the ResponseFuture from the separate client dispatcher task running in the background, we'd need some to add some sort of channel where the receiver is inside ResponseFuture, and the sender is in a request extension. Instead of storing the sender directly, we can reuse the callback to simply store a closure sending on the sender side of the channel.
Finally, the higher level ResponseFuture (returned by hyper::Client, not hyper::client::conn) would need to separate some of its stages into an internal enum, so that polling ResponseFuture::poll_informational() could advance the stages far enough that the task wouldn't be stuck since the dns/connect/write stage hadn't gotten far enough.
@bagder the piece you'd need is merged to master now. hyper's C API doesn't use timers at the moment, so you'd need to use a timer in the normal curl way, and check in the body poll callback whether a 100 Continue has been received (in your informational callback, you could set a flag and trigger the body waker to start polling again) or the timer has expired.
I would've liked the callback to take a return code so that we can return error easier.
If my code in the callback runs into a fatal error, such as out of memory, I need to return error and abort everything. Instead of returning error, now I need to pass that info back to the parent caller by storing a magic value in a struct field instead. Not your typical erroring out style code.
What's perhaps worse, is that it doesn't seem to work? At least not the way I thought it is supposed to work. Shouldn't the following callback code be able to extract the 100 response and its associated response headers?
When I run this in curl test 158, the header callback doesn't get invoked even once. I suppose I've done something wrong?
I noticed a difference in your test case, which is that it seems it's only sending the 100 Continue, and not sending a 200 OK afterwards, right? When I make a local server doing the same thing, I still see the capi/examples/upload.c callback trigger, but don't see it print about the final response, which makes sense.
I run the test server in curl's tests/ directory like this, and then it runs a stupid HTTP server foir IPv4 on port 8999. It loads test case based on the file name part in the URL. I put 158 there to make it load contents from test 158, which I have edited as above.
upload example
I then run the hyper capi "upload" test program against this test server URL.
I fire away the upload example to get to see the response codes and the headers:
$ ./upload upload.c localhost 8999 /here/158
connecting to port 8999 on localhost...
connected to localhost, now upload to /here/158
http handshake ...
preparing http request ...
sending ...
Response Status: 200
Server: moo
Content-Length: 0
Something very strange indeed... For instance, when I have a local server doing the same thing, I get this output:
$ ./upload ../README.md 127.0.0.1 3000 /up
connecting to port 3000 on 127.0.0.1...
connected to 127.0.0.1, now upload to /up
http handshake (hyper v0.14.11) ...
preparing http request ...
with expect-continue ...
sending ...
Informational (1xx): 100
HTTP/1.1 100 Continue swsclose
Silly-Header: yeeesss
Response Status: 200
Content-Length: 5
hello
-- Done! --
It could something weird inside hyper, but a possible idea popped into my head: has the rust library been re-compiled after pulling from git? Just for my sanity, I went ahead and added the hyper_version() output to the examples in #2623.
Ah, with that change, I still see the callback called, I see Informational (1xx): 100\n, but then no headers are printed. (I also notice the C compiler yelling at me about const pointers...) Let me see what's going on internally...
Activity
nox commentedon Jun 11, 2021
I much prefer the idea of adding methods on
ResponseFuture, hooks make bookkeeping harder for the caller, and I don't think it is more confusing to have both those methods and theFutureimpl than say, usingpoll_dataandpoll_trailersonHttpBody.bagder commentedon Jul 1, 2021
In the curl project, there's right now 105 test cases disabled when built with hyper. 46 of them use 100-continue in one way or another and we really need to know when we get 1xx responses (or not) to make curl+hyper behave like the built-in HTTP code does. It could allow us to make significant progress in my task to make curl+hyper match the built-in HTTP code.
seanmonstar commentedon Jul 2, 2021
Right-o, I got distracted with other work. I'll do some thinking and work on this in the next few days.
seanmonstar commentedon Jul 7, 2021
I've spent some time researching options (prior art of APIs), and considering the existing code, and I think I've settled on staged approach. I agree with you, @nox, we can add a method to
ResponseFutureon the Rust side, and make it work right. However, it'd be less easy to access in the exposed C API, and will take more time to get right. So:ResponseFuturefrom the separate client dispatcher task running in the background, we'd need some to add some sort of channel where the receiver is insideResponseFuture, and the sender is in a request extension. Instead of storing the sender directly, we can reuse the callback to simply store a closure sending on the sender side of the channel.ResponseFuture(returned byhyper::Client, nothyper::client::conn) would need to separate some of its stages into an internal enum, so that pollingResponseFuture::poll_informational()could advance the stages far enough that the task wouldn't be stuck since the dns/connect/write stage hadn't gotten far enough.The first step is my next work priority.
seanmonstar commentedon Jul 8, 2021
Step 1 PR is available at #2594.
seanmonstar commentedon Jul 12, 2021
@bagder the piece you'd need is merged to master now. hyper's C API doesn't use timers at the moment, so you'd need to use a timer in the normal curl way, and check in the body poll callback whether a 100 Continue has been received (in your informational callback, you could set a flag and trigger the body waker to start polling again) or the timer has expired.
bagder commentedon Aug 14, 2021
I would've liked the callback to take a return code so that we can return error easier.
If my code in the callback runs into a fatal error, such as out of memory, I need to return error and abort everything. Instead of returning error, now I need to pass that info back to the parent caller by storing a magic value in a struct field instead. Not your typical erroring out style code.
bagder commentedon Aug 14, 2021
What's perhaps worse, is that it doesn't seem to work? At least not the way I thought it is supposed to work. Shouldn't the following callback code be able to extract the 100 response and its associated response headers?
When I run this in curl test 158, the header callback doesn't get invoked even once. I suppose I've done something wrong?
https://github.com/curl/curl/blob/931a3e33e11c75feca367b90532d95d6f17985f7/lib/c-hyper.c#L726-L750
seanmonstar commentedon Aug 16, 2021
We could probably adjust it to be more like the foreach functions, returning a
HYPERE_ABORTED_BY_CALLBACKif you return a non-zero.It should... I added it's usage to the
upload.cexample, and checked it worked there. I'll dig a little into how you're using it.seanmonstar commentedon Aug 17, 2021
I noticed a difference in your test case, which is that it seems it's only sending the
100 Continue, and not sending a200 OKafterwards, right? When I make a local server doing the same thing, I still see thecapi/examples/upload.ccallback trigger, but don't see it print about the final response, which makes sense.bagder commentedon Aug 18, 2021
Yes, the test only responds with a 100 and then closes the connection. But there's something else going on here.
Here's what I do:
response
I now made the HTTP server respond this exact byte stream. I did it by editing my local curl test 158
(with CRLF newlines)
test server
I run the test server in curl's
tests/directory like this, and then it runs a stupid HTTP server foir IPv4 on port 8999. It loads test case based on the file name part in the URL. I put 158 there to make it load contents from test 158, which I have edited as above.upload example
I then run the hyper capi "upload" test program against this test server URL.
I fire away the upload example to get to see the response codes and the headers:
There's no Informational (1xx): output there...
seanmonstar commentedon Aug 18, 2021
Something very strange indeed... For instance, when I have a local server doing the same thing, I get this output:
It could something weird inside hyper, but a possible idea popped into my head: has the rust library been re-compiled after pulling from git? Just for my sanity, I went ahead and added the
hyper_version()output to the examples in #2623.bagder commentedon Aug 19, 2021
Spot on. A simple
makein the examples dir didn't rebuildupload...bagder commentedon Aug 19, 2021
I can reproduce my problem with this patch to upload.c:
seanmonstar commentedon Aug 19, 2021
Ah, with that change, I still see the callback called, I see
Informational (1xx): 100\n, but then no headers are printed. (I also notice the C compiler yelling at me about const pointers...) Let me see what's going on internally...seanmonstar commentedon Aug 19, 2021
Found the issue with the missing headers. I also took the opportunity to fix up that
constwarning.bagder commentedon Aug 19, 2021
Right, that's exactly the problem I saw.
bagder commentedon Aug 20, 2021
Now also confirmed with curl test 158 working fine in my end. 🎉
feat(ext): add `ext::on_informational()` callback extension (hyperium…
sync rama-http-core code with hyperium/hyper v1.6.0