Skip to content
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

Forward request trailers (h2) #3241

Open
wants to merge 18 commits into
base: master
Choose a base branch
from
Open

Forward request trailers (h2) #3241

wants to merge 18 commits into from

Conversation

i110
Copy link
Contributor

@i110 i110 commented May 25, 2023

If h2o receives trailing headers from the client after request body streaming is started, it's just discarded now. This PR let h2o forward those trailers after the last DATA frame.

Considerations:

  • Is it better not to merge trailers to headers even if it's non-streaming mode?
  • Any better ideas than adding a trailers argument to write_req callback like this PR currently does?
  • Now we can't use client->pool to copy the trailers because it's actually req->pool and bound to req's lifetime

@i110 i110 requested a review from kazuho May 25, 2023 01:32
Copy link
Member

@kazuho kazuho left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the PR. This is looking good.

  • Is it better not to merge trailers to headers even if it's non-streaming mode?

Yes.

  • Any better ideas than adding a trailers argument to write_req callback like this PR currently does?

I think we are already on the right track but please see my comment below.

  • Now we can't use client->pool to copy the trailers because it's actually req->pool and bound to req's lifetime

IIUC, this is a problem when response is closed before the trailers are sent by httpclient. If that happens, in on_body_on_close function of lib/core/proxy.c, we detach httpclient but keep it running (instead of calling cancel).

Assuming this understanding is correct, I think there are two ways to proceed:

  • i) In http2client, retain copy of trailers so that they can be sent at a later moment.
  • ii) Rename the cancel callback to close, and call it when the end of the response is observed. Rationale for renaming the callback is that from the viewpoint of the user of httpclient everything is going normal. All the request up to trailers have been passed to httpclient and entire response has been received. Therefore, we are doing a normal shutdown which has to be signaled explicitly.

That said, I think my preference goes to i, considering that having a mandatory close callback is cumbersome. Note that the requirement to retain the copy of trailers is specific to http2client. http1client and http3client can serialize trailers in conjunction with the last chunk being received.

@@ -292,7 +292,7 @@ struct st_h2o_httpclient_t {
* Function for writing request body. `proceed_req_cb` supplied through the `on_connect` callback will be called when the
* given data is sent to the server. Regarding the usage, refer to the doc-comment of `h2o_write_req_cb`.
*/
int (*write_req)(h2o_httpclient_t *client, h2o_iovec_t chunk, int is_end_stream);
int (*write_req)(h2o_httpclient_t *client, h2o_iovec_t chunk, h2o_headers_t *trailers, int is_end_stream);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Considering that trailers are to be provided only when is_end_stream is set, would it make sense to get rid of is_end_stream?

If trailers is NULL, it indicates that the chunk written is mid-stream. If trailers is non-NULL, that indicates that the last chunk is being provided. This trailers can be an empty list (and that would typically be the case).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 05aa1cc

@i110
Copy link
Contributor Author

i110 commented Aug 17, 2023

@kazuho I pushed some commits to support forwarding trailers in non-streaming mode, along with some header utility functions (h2o_copy_header and h2o_dispose_header). Could you take a look once again please?

Copy link
Member

@kazuho kazuho left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the changes, looking good.

I think all my comments are about style and consistency.

if (stream->state.req == STREAM_STATE_BODY) {
h2o_buffer_init(&stream->output.buf, &h2o_socket_buffer_prototype);
h2o_buffer_append(&stream->output.buf, body.base, body.len);
}
h2o_linklist_insert(&conn->output.sending_streams, &stream->output.sending_link);

/* trailers */
if (trailers != NULL) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (trailers != NULL) {
if (trailers != NULL && num_trailers != 0) {
assert(stream->output.proceed_req == NULL);

In do_write_req, we make sure that stream->output.trailers.entries becomes non-NULL only when the list is going to be non-empty. I do not how we use trailers, but it should be consistent at least. Addition of num_trailers != 0 as well as the change to the doc-comment builds on top of the requirements that we have in do_write_req.

Separately, am I correct to understand that the user is forbidden from providing trailers if it has opted in to use request streaming? If so, it might make sense to have an assert.

Copy link
Contributor Author

@i110 i110 Sep 4, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That makes sense, addressed in c61257b

Separately, am I correct to understand that the user is forbidden from providing trailers if it has opted in to use request streaming? If so, it might make sense to have an assert.

Thank you, this is a great point. Ideally we might be able to merge the passed trailers with coming request trailers, but this assertion should be sufficient for now. Addressed in 2fb2e36

include/h2o.h Outdated Show resolved Hide resolved
@@ -100,6 +100,7 @@ typedef h2o_httpclient_body_cb (*h2o_httpclient_head_cb)(h2o_httpclient_t *clien
typedef h2o_httpclient_head_cb (*h2o_httpclient_connect_cb)(h2o_httpclient_t *client, const char *errstr, h2o_iovec_t *method,
h2o_url_t *url, const h2o_header_t **headers, size_t *num_headers,
h2o_iovec_t *body, h2o_httpclient_proceed_req_cb *proceed_req_cb,
const h2o_header_t **trailers, size_t *num_trailers,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The way we pass the trailers seem to be diffeent between here and write_req. Is it intentional?

Unless there is a reason, it would make sense to use the same way, and based on our convention, that would be passing a list and the size; i.e.,

  • h2o_header_t * (as each element of the field list is h2o_header_t, the passed argument should be a pointer rather than a double pointer), and
  • size_tdenoting the size of the list

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in f3b3a39

@i110
Copy link
Contributor Author

i110 commented Nov 14, 2023

@kazuho It's been quite a while, but could you please give this a review? Since you last reviewed I've addressed issues and fixed the broken test, now CI passes!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants