http: add request header timer#13341
Conversation
Add a new config field for an additional timeout that will cancel streams that take too long to send headers, and implement it in the HTTP Connection Manager. Signed-off-by: Alex Konradi <akonradi@google.com>
…er-timer Signed-off-by: Alex Konradi <akonradi@google.com>
alyssawilk
left a comment
There was a problem hiding this comment.
Very cool to see work towards more fine grained (and maybe adaptive) timeouts!
Starting out with a few questions since I want to make sure I know what we're aiming for both here and in the long run
| // when the request is initiated, and is disarmed when the last byte of the headers is sent | ||
| // upstream (i.e. all decoding filters have processed the headers). If not specified or set to 0, | ||
| // this timeout is disabled. | ||
| google.protobuf.Duration request_headers_timeout = 41 |
There was a problem hiding this comment.
Hm, this sounds like a different timeout than described in the issue. I think issue is about the client sending headers (not hogging resources with a trickle attack). Going through the filter chain seems to mix Envoy processing time with potentially malicious clients. Did we want the latter as well?
Also, do we think this is a fixed timer long term, or do we hope it will evolve into the ranged timeout in the linked isssue? If so I think we want to start with a different API and hide it until the range timeout is implemented.
I'm going to tag antonio for first pass here as I think he has a bit more context than I do.
There was a problem hiding this comment.
+1 I would not recommend making this involve filters at all. I think we want a timer that starts at stream init (1st header block), and ends after all blocks/continuations are complete?
Given ^ it might be easier to configure this at the codec/protocol level?
There was a problem hiding this comment.
Agreed, the upstreams bit was a mistake on my part. I think the existing implementation that disables the timer in onHeadersReceived does what we want.
| google.protobuf.Duration request_timeout = 28 | ||
| [(udpa.annotations.security).configure_for_untrusted_downstream = true]; | ||
|
|
||
| // The amount of time that Envoy will wait for the headers to be received. The timer is activated |
There was a problem hiding this comment.
nit: the request headers
api/envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto
Outdated
Show resolved
Hide resolved
Signed-off-by: Alex Konradi <akonradi@google.com>
…er-timer Signed-off-by: Alex Konradi <akonradi@google.com>
Signed-off-by: Alex Konradi <akonradi@google.com>
Signed-off-by: Alex Konradi <akonradi@google.com>
Call disableTimer() before setting deleting for testing Signed-off-by: Alex Konradi <akonradi@google.com>
…er-timer Signed-off-by: Alex Konradi <akonradi@google.com>
|
Windows failure looks to be unrelated. |
| // The amount of time that Envoy will wait for the request headers to be received. The timer is | ||
| // activated when the request is initiated, and is disarmed when the last byte of the headers has | ||
| // been received (i.e. all decoding filters have processed the headers). If not specified or set | ||
| // to 0, this timeout is disabled. |
There was a problem hiding this comment.
What happens if this duration is configured to a negative number? Does a proto validator to ensure that durations are >= 0 exist?
| [(udpa.annotations.security).configure_for_untrusted_downstream = true]; | ||
|
|
||
| // The amount of time that Envoy will wait for the request headers to be received. The timer is | ||
| // activated when the request is initiated, and is disarmed when the last byte of the headers has |
There was a problem hiding this comment.
"when the request is initiated" -> "when the first byte of the headers is received"
| EXPECT_CALL(*codec_, dispatch(_)).WillOnce(Invoke([&](Buffer::Instance&) -> Http::Status { | ||
| Event::MockTimer* request_header_timer = setUpTimer(); | ||
| EXPECT_CALL(*request_header_timer, enableTimer(request_headers_timeout_, _)); | ||
| EXPECT_CALL(*request_header_timer, disableTimer()); |
There was a problem hiding this comment.
What triggers this disable? the request not parsing and triggering an error reply?
There was a problem hiding this comment.
Yeah that was the actual issue. I made the test more restrictive to verify that the correct behavior is being checked.
| return Http::okStatus(); | ||
| })); | ||
|
|
||
| Buffer::OwnedImpl fake_input("1234"); |
There was a problem hiding this comment.
Should this input be a partial valid request to avoid potential for header parse error?
There was a problem hiding this comment.
Yep, this test is more realistic now.
|
|
||
| conn_manager_->newStream(response_encoder_); | ||
| EXPECT_CALL(filter_callbacks_.connection_.dispatcher_, setTrackedObject(_)).Times(2); | ||
| request_header_timer->invokeCallback(); |
There was a problem hiding this comment.
Invoking this callback from within dispatch doesn't seem like something that would normally happen. Please call this callback outside codec dispatch.
| new TestRequestHeaderMapImpl{{":authority", "host"}, {":path", "/"}, {":method", "GET"}}}; | ||
|
|
||
| // the second parameter 'false' leaves the stream open | ||
| decoder->decodeHeaders(std::move(headers), false); |
There was a problem hiding this comment.
There's just too much mocking happening on these "tests". I don't follow what you're testing here or how it relates to the call to conn_manager_->onData below.
There was a problem hiding this comment.
That's reasonable, because the tests were bad. PTAL at the cleaned-up version.
Signed-off-by: Alex Konradi <akonradi@google.com>
Signed-off-by: Alex Konradi <akonradi@google.com>
| Event::TimerPtr stream_idle_timer_; | ||
| // Per-stream request timeout. | ||
| // Per-stream request timeout. This timer is enabled when the stream is created and disabled | ||
| // when the downstream closes the connection. If triggered, it will close the stream. |
There was a problem hiding this comment.
I think there are some errors in this comment. This seems to be a stream timeout, but there are references to downstream closing the connection.
This is what was decided on in the PR and I forgot to change it back before now. Signed-off-by: Alex Konradi <akonradi@google.com>
Signed-off-by: Alex Konradi <akonradi@google.com>
…er-timer Signed-off-by: Alex Konradi <akonradi@google.com>
Signed-off-by: Alex Konradi <akonradi@google.com>
Signed-off-by: Alex Konradi <akonradi@google.com>
…er-timer Signed-off-by: Alex Konradi <akonradi@google.com>
Signed-off-by: Alex Konradi <akonradi@google.com>
|
Retrying Azure Pipelines: |
|
@alyssawilk did you want to take another pass at this? |
alyssawilk
left a comment
There was a problem hiding this comment.
Looks good overall! I've added a few comments but I'm going to throw it over to snow for the non-googler pass
|
|
||
| // The amount of time that Envoy will wait for the request headers to be received. The timer is | ||
| // activated when the first byte of the headers is received, and is disarmed when the last byte of | ||
| // the headers has been received (i.e. all decoding filters have processed the headers). If not |
There was a problem hiding this comment.
I think the info in parens is not accurate. Maybe just remove?
| // stream. | ||
| Event::TimerPtr request_header_timer_; | ||
| // Per-stream alive duration. This timer is enabled once when the stream is created and, if | ||
| // triggered, will close the stream. |
There was a problem hiding this comment.
optional, think it's worth mentioning which of these try to send a reply?
There was a problem hiding this comment.
I'm going to punt on this since whether or not a reply is sent for some of these is dependent on a runtime override.
| request_headers_timeout->set_seconds(1); | ||
| request_headers_timeout->set_nanos(0); | ||
| }); | ||
| setDownstreamProtocol(Http::CodecClient::Type::HTTP1); |
There was a problem hiding this comment.
I'd be inclined to return if the protocol isn't HTTP/1, so we don't run this twice (and the H2 test confusingly runs HTTP/1)
| // Track locally queued bytes, to make sure the outbound client queue doesn't back up. | ||
| uint64_t bytes_to_send = send_buffer.length(); | ||
| raw_connection->addBytesSentCallback([&](uint64_t bytes) { bytes_to_send -= bytes; }); | ||
| raw_connection->write(send_buffer, false); |
There was a problem hiding this comment.
I think you can replace most of this with a call to
sendRawHttpAndWaitForResponse()
There was a problem hiding this comment.
This test needs to do stuff to the connection while the dispatcher would otherwise be waiting, but I cribbed the use of RawConnectionDriver and that simplified things.
Signed-off-by: Alex Konradi <akonradi@google.com>
…er-timer Signed-off-by: Alex Konradi <akonradi@google.com>
|
@snowp can I get a review on this? |
|
Oops - snow is out today - I'll update the maintainer calendar and hopefully he can take a pass tomorrow. |
snowp
left a comment
There was a problem hiding this comment.
Thanks this seems good to me, just one test suggestion
| EXPECT_TRUE(connection_driver->closed()); | ||
| EXPECT_THAT(response, HasSubstr("408")); |
There was a problem hiding this comment.
Maybe check for the error text as well to make sure that we're hitting the timeout we think we are here? Or check stats
Signed-off-by: Alex Konradi <akonradi@google.com>
|
@envoyproxy/api-shepherds review needed for the new timer config |
mattklein123
left a comment
There was a problem hiding this comment.
API LGTM. Can you please update https://www.envoyproxy.io/docs/envoy/latest/faq/configuration/timeouts? Thank you.
/wait
Signed-off-by: Alex Konradi <akonradi@google.com>
Commit Message: Add request header timeout to HTTP connection manager
Additional Description:
Add a timer and config option to enforce a maximum allowed duration between when a downstream starts a new stream and when it finishes sending headers on that stream.
Risk Level: low - behavior is disabled unless the config option is specified
Testing: added tests
Docs Changes: autogenerated proto docs
Release Notes: documented new config field
Towards #11427