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

Issue #12104 - HTTP/1.0 should produce a valid Connection header, never blank/null. #12105

Closed
wants to merge 9 commits into from

Conversation

joakime
Copy link
Contributor

@joakime joakime commented Jul 29, 2024

  • Enable ee9 ErrorHandlerTest again
  • Fix empty Connection: response header issue seen on HTTP/1.0

Fixes #12104

 * make it easier to test different scenarios
+ Fix empty `Connection:` response
  header issue seen on HTTP/1.0
@joakime joakime self-assigned this Jul 29, 2024
@joakime joakime requested a review from sbordet July 29, 2024 21:19
@joakime joakime added the Bug For general bugs on Jetty side label Jul 29, 2024
  header issue seen on HTTP/1.0
@joakime joakime marked this pull request as draft July 30, 2024 16:49
@joakime
Copy link
Contributor Author

joakime commented Jul 30, 2024

Setting this to draft mode, as there's still a problem with the response headers on HTTP/1.0 to address.

  header issue seen on HTTP/1.0
  when Servlet uses `Connection: keep-alive`
  against a request that wasn't persistent.
@joakime joakime requested a review from gregw July 31, 2024 11:45
@joakime
Copy link
Contributor Author

joakime commented Jul 31, 2024

@gregw this started out being a fix for HTTP/1.0 responses sometimes having an empty Connection: header.
But its growing in ways I'm uncomfortable with.
I'm going tools down on this PR until we can discuss it, in detail.
There's no rush for this PR either.

Comment on lines +631 to +633
boolean hasValue = StringUtil.isNotBlank(field.getValue());
boolean hasKeepAlive = field.contains(HttpHeaderValue.KEEP_ALIVE.asString());
boolean hasClose = field.contains(HttpHeaderValue.CLOSE.asString());
Copy link
Contributor

Choose a reason for hiding this comment

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

Whilst this style is easily readable, it contains a lot of redundant code/tests. If hasValue==false, then you can break straight away and avoid subsequent work.

If it hasKeepAlive is true, then in your code there is no need to lookup hasClose (but see below).

}
if (field.contains(HttpHeaderValue.CLOSE.asString()))
else if (hasClose)
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not sure we want the else here. If it came Connection: keep-alive, close then either that is a 400 or at least we should close. So perhaps invert the order of the tests and look for close, else look for keepAlive.

Copy link
Contributor Author

@joakime joakime Aug 1, 2024

Choose a reason for hiding this comment

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

I was reading the Hop-by-hop parts of the spec in light of other tests with have for this.

Looks like if we receive a comma delimited list, then it's a 400 Bad Request in normal operations.
BUT if we receive that when we are a proxy, then we have to act on all of the various Hop-by-hop headers (of which Connection is just one of them).

BUT we can create a response with multi-value Connection at all times (normal operation, and proxy operation), and we are only on the hook to act on a persistence state change for our hop.

I'm at a loss on the exact meaning of "our hop" in these cases.
Eg: If the request was keep-alive and response from the servlet is close, keep-alive vs keep-alive, close, what do we do at the prepareRequest / httpGenerator?

Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think we need to 400 for both a Keep-Alive and Close. The close is a must-not persist and the Keep-Alive is a may persist, so they combine easily to a must-not persist.

Comment on lines +1388 to +1389
boolean hasConnectionClose = responseHeaders.contains(HttpHeader.CONNECTION, HttpHeaderValue.CLOSE.asString());
boolean hasConnectionKeepAlive = responseHeaders.contains(HttpHeader.CONNECTION, HttpHeaderValue.KEEP_ALIVE.toString());
Copy link
Contributor

Choose a reason for hiding this comment

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

again, don't lookup values if we don't need them, especially in this case as it involves a full iteration to do each lookup.

So lookup only if needed and perhaps look for close first, so it can override keepalive.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The problem is that each of these could then be looked up multiple times instead.
So I took the case of the lookup occurring 4 times down to 2, which I felt was better.

Copy link
Contributor Author

@joakime joakime Aug 1, 2024

Choose a reason for hiding this comment

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

A single iteration through is best, I'll work on that after you have had a look at the ResponseHeadersTest.

In particular the new tests ...

  • testHTTP10ConnectionBehavior for when the client is using HTTP/1.0
  • testHTTP11ConnectionBehavior for when the client is using HTTP/1.1

Are you good with the set of expectations for those?
Each test case arguments have 3 parameters.

  1. The Connection request header as sent from the Client (null means no Connection header sent)
  2. The Connection response header as set by an application (Servlet in this case. null means no Connection header is set)
  3. The expected Connection response header as seen by the client. (null means no Connection header should be present received)

Once I get that set of tests sane, then I can look closer at the other tests that are now failing due to this PR.
Some of which are suspect (like a test client that uses HTTP/1.0 but also Connection: close, which is not typical, but we should accept it, and the new testcases I mentioned has variants of this scenario too)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If you take just the changes to the ResponseHeadersTest back to jetty-12.0.x HEAD and run them, you'll quickly see the various cases that currently return a Connection: (empty value) response header.

Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not saying avoid local variables with those looked up values (so we don't need to lookup multiple times), but I am saying delay doing the lookup until it is known that it is needed.

Comment on lines +650 to +652
String resultingValue = Stream.of(field.getValues())
.filter(s -> !HttpHeaderValue.KEEP_ALIVE.is(s))
.collect(Collectors.joining(", "));
Copy link
Contributor

Choose a reason for hiding this comment

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

I wonder if string operations here would be better than streams...
Also perhaps we could do this in the condition bodies above?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, I'm not a fan of the stream operation here, but it was here before I started this, so I left it.
I was going to look into adding a HttpField.removeValue(String) method as a possible replacement.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, so that. I agree a removeValue method would allow us to hide the details.

@joakime
Copy link
Contributor Author

joakime commented Aug 1, 2024

@gregw the branch jetty-12.0.x-null-connection-response-header has just the changes in ResponseHeadersTest.java (commit 11e8faf)

Build results -> https://jenkins.webtide.net/blue/organizations/jenkins/jetty.project/activity?branch=jetty-12.0.x-null-connection-response-header

Basically, this is the state of jetty-12.0.x HEAD at the moment.
I really need a first pass through the test cases / expectations, as I'm sure some of the expectations are not right.
But I'm 100% sure that sending Connection: (a blank value of "") is wrong.

testHTTP10ConnectionBehavior(String, String, String)

Num client-req servlet-resp expected-client-resp pass / fail
1 null null close ❌ (Connection value was null)
2 null close close
3 null keep-alive close ❌ (Connection value was "")
4 keep-alive null keep-alive
5 keep-alive close close
6 keep-alive keep-alive keep-alive
7 close null close ❌ (Connection value was null)
8 close close close
9 close keep-alive close ❌ (Connection value was "")
10 null close, keep-alive close
11 null keep-alive, close close
12 close close, keep-alive close
13 close keep-alive, close close
14 keep-alive close, keep-alive close, keep-alive ❌ (Connection value was "close")
15 keep-alive keep-alive, close keep-alive, close ❌ (Connection value was "close")

testHTTP11ConnectionBehavior(String, String, String)

Num client-req servlet-resp expected-client-resp pass / fail
1 null null null
2 null close close
3 null keep-alive null ❌ (Connection value was "keep-alive")
4 close null close
5 close close close
6 close keep-alive close ❌ (Connection value was "")
7 keep-alive null null
8 keep-alive close close
9 keep-alive keep-alive null ❌ (Connection value was "keep-alive")
10 null close, keep-alive close
11 null keep-alive, close close
12 close close, keep-alive close
13 close keep-alive, close close
14 keep-alive close, keep-alive close
15 keep-alive keep-alive, close close

@gregw
Copy link
Contributor

gregw commented Aug 2, 2024

@joakime I definitely agree that sending a blank connection is wrong, but I think I see a pretty simple fix for that.
I also see a bunch of other little cleanups that I'd like to do now that I've seen them. But ultimately, I don't think we need big changes.

Let me have a play with your branch with just the test changes and see if I can come up with a minimal fix... stand by...

@gregw
Copy link
Contributor

gregw commented Aug 2, 2024

Actually, I'm not sure why you have done this as eeX tests. I think this is a core issue??? I may move your test to core....

@joakime
Copy link
Contributor Author

joakime commented Aug 2, 2024

Actually, I'm not sure why you have done this as eeX tests. I think this is a core issue??? I may move your test to core....

It was originally discovered while testing ee8 issues reported from the community.
So I started there.

But now that I've been deep in it, I don't see this being a EE8/EE9/EE10 specific issue.

@joakime
Copy link
Contributor Author

joakime commented Aug 2, 2024

OH! And when I enabled the disabled test ErrorHandlerTest.java (in ee8/ee9) one of those tests also showed the problem.

@gregw
Copy link
Contributor

gregw commented Aug 2, 2024

See my fix on #12127. I moved your test to core and combined. I changed a few expected values. The fix is in the HttpGenerator and is a reworking of Connection handling that is a lot more explicit.

@joakime
Copy link
Contributor Author

joakime commented Aug 2, 2024

Closing in favor of alternate approach in #12127

@joakime joakime closed this Aug 2, 2024
@joakime joakime deleted the fix/12.0.x/ee9-errorhandlertest-restore branch August 2, 2024 12:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Bug For general bugs on Jetty side
Projects
No open projects
Status: ✅ Done
Development

Successfully merging this pull request may close these issues.

Error handling on ee9 / ee8 with HTTP/1.0 can result in an empty Connection: response header.
2 participants