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

Ember client drop head body #7369

Merged
merged 4 commits into from
May 3, 2024

Conversation

hamnis
Copy link
Contributor

@hamnis hamnis commented Jan 26, 2024

Adds a flag to ember-core Parser.Response.parse to discard the body

this is only enabled for HTTP/1.1 HEAD requests, but there may be other http methods which does not expect a body in the response.

Not sure if this is the best location for this code, but the reproducer now passes.

Only enabled if the request is a HEAD request.
This should only be an issue for HTTP/1.1 as http/2 body frames should
not be generated for HEAD.

Fixes http4s#7362
@hamnis hamnis force-pushed the ember-client-drop-head-body branch from a26d69e to 1c53199 Compare January 26, 2024 21:44
@hamnis
Copy link
Contributor Author

hamnis commented Feb 2, 2024

@danicheg @armanbilge please re-review this, @yanns confirmed that this fix works.

@danicheg
Copy link
Member

danicheg commented Feb 2, 2024

It's ready to go, as far as I'm concerned. 👍🏻 But @armanbilge requested themselves to make a review. So the ball's in their court.

Copy link
Member

@valencik valencik left a comment

Choose a reason for hiding this comment

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

This seems reasonable.
At first I was confused why we have a discardBody and a expectNoBody, but expectNoBody only considers the Status, not the Method.

@@ -373,6 +373,11 @@ private[ember] object Parser {
def parser[F[_]: Concurrent](maxHeaderSize: Int)(
buffer: Array[Byte],
read: Read[F],
): F[(Response[F], Drain[F])] = parser[F](maxHeaderSize, discardBody = false)(buffer, read)

def parser[F[_]: Concurrent](maxHeaderSize: Int, discardBody: Boolean)(
Copy link
Member

Choose a reason for hiding this comment

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

Is discardBody really the right name? AFAICT there is no discarding happening, it seems more similar to expectNoBody. Perhaps we can have requestExpectsNoBody and statusExpectsNoBody.

Copy link
Member

Choose a reason for hiding this comment

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

Sorry, I did some more reading.

Warning: A response to a HEAD method should not have a body. If it has one anyway, that body must be ignored: any representation headers that might describe the erroneous body are instead assumed to describe the response which a similar GET request would have received.

https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/HEAD

So it does seem like the body must be discarded if it's present. So now I'm not sure if it's appropriate to share a codepath with statusExpectsNoBody.

@hamnis hamnis force-pushed the ember-client-drop-head-body branch from d2fdc1d to 1bd393a Compare February 2, 2024 17:24
Comment on lines 394 to 395
assertEquals(body, "")
assertEquals(rest, "helloeverything after the body")
Copy link
Member

Choose a reason for hiding this comment

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

I'm pretty sure we should be asserting that both of these are empty. Otherwise we haven't really discarded the body, just left it on the socket.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

are you sure about that? then expectNoBody would also leave data on the socket.

Copy link
Member

Choose a reason for hiding this comment

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

Right, but I don't think that "discard body" and "expect no body" are the same situation at all. See #7369 (comment).

For HEAD requests the spec mandates that we discard the body, if it's present. Meanwhile, the spec for e.g. 204 No Content makes no such mandate: it's up to us to decide how to handle that situation.

@hamnis
Copy link
Contributor Author

hamnis commented Feb 16, 2024

@armanbilge can you please do another pass on this?

@yanns
Copy link
Contributor

yanns commented Mar 10, 2024

This issue is blocking migration from blaze to ember client. It'd be great to have the fix merged and released. Thanks all!

@yanns
Copy link
Contributor

yanns commented Apr 8, 2024

@armanbilge sorry for the ping, but would you have time to check this out?

@hamnis hamnis requested a review from a team April 30, 2024 15:23
@rossabaker
Copy link
Member

I need to get a release out this week. I don't typically work on Ember, but given the positive feedback and that the interface changes are private, I'm inclined to merge this someone objects tomorrow.

@@ -364,6 +364,39 @@ class ParsingSuite extends Http4sSuite {
}
}

test(
"Parser.Response.parser return everything if the body is discarded"
Copy link
Member

Choose a reason for hiding this comment

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

Now that we've changed the behaviour to discard the rest perhaps we should update this test name?

Suggested change
"Parser.Response.parser return everything if the body is discarded"
"Parser.Response.parser returns empty body and rest when the body is discarded"

@@ -406,7 +411,11 @@ private[ember] object Parser {
)

resp <-
if (headerP.chunked) {
if (discardBody) {
(baseResp -> none[Array[Byte]].pure[F]).pure[F]
Copy link
Member

Choose a reason for hiding this comment

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

Disclaimer, I haven't looked at the ember parser before.

What is this doing that is different from the line two below?

The return type is F[(Response[F], Drain[F])]. Presumably the thing on the right is what we are draining? Or is it the "drained bytes" 🤔 ?

My concern is whether or not

if (discardBody) {
  (baseResp -> none[Array[Byte]].pure[F]).pure[F]

is actually draining things from the socket or just saying "there wasn't anything on the socket".

@@ -364,6 +364,39 @@ class ParsingSuite extends Http4sSuite {
}
}

test(
"Parser.Response.parser return everything if the body is discarded"
Copy link
Member

Choose a reason for hiding this comment

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

What happens to the connection in this case? Discarding an ad hoc number of bytes and then continuing to parse makes me nervous about smuggling attacks.

Copy link
Member

Choose a reason for hiding this comment

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

This path is only for response parsing, right?

Copy link
Member

Choose a reason for hiding this comment

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

Is the expectation that the connection gets closed, even after a properly framed HEAD response?

Copy link
Member

@rossabaker rossabaker left a comment

Choose a reason for hiding this comment

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

As far as I can tell, Ember is closing the connection after a successful HEAD request, both on series/0.23 and on this branch. This doesn't make that problem worse and fixes the original problem. Furthermore, if it's too aggressively closing connections, it at least relieves us of worrying about the security implications of excess bytes in a response.

@rossabaker rossabaker merged commit b42d37b into http4s:series/0.23 May 3, 2024
17 checks passed
@hamnis hamnis deleted the ember-client-drop-head-body branch May 3, 2024 16:37
@danicheg
Copy link
Member

danicheg commented May 3, 2024

Amazing work, team! Additional gratitude to @rossabaker for going an extra mile on verifying this 🙇

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

Successfully merging this pull request may close these issues.

Ember client hangs when parsing response body from HEAD request
6 participants