Skip to content

Commit 6285c78

Browse files
committed
Merge pull request #1909 from Netflix/add_http_request_header_read_timeout_handler
Add a channel handler for HTTP request header read timeouts
1 parent 855774a commit 6285c78

File tree

5 files changed

+182
-0
lines changed

5 files changed

+182
-0
lines changed

zuul-core/src/main/java/com/netflix/zuul/netty/server/BaseServerStartup.java

+4
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,8 @@ protected void addChannelDependencies(
166166

167167
channelDeps.set(ZuulDependencyKeys.sessionCtxDecorator, sessionCtxDecorator);
168168
channelDeps.set(ZuulDependencyKeys.requestCompleteHandler, reqCompleteHandler);
169+
Counter httpRequestHeadersReadTimeoutCounter = registry.counter("server.http.request.headers.read.timeout");
170+
channelDeps.set(ZuulDependencyKeys.httpRequestHeadersReadTimeoutCounter, httpRequestHeadersReadTimeoutCounter);
169171
Counter httpRequestReadTimeoutCounter = registry.counter("server.http.request.read.timeout");
170172
channelDeps.set(ZuulDependencyKeys.httpRequestReadTimeoutCounter, httpRequestReadTimeoutCounter);
171173
channelDeps.set(ZuulDependencyKeys.filterLoader, filterLoader);
@@ -189,6 +191,8 @@ protected void addChannelDependencies(
189191

190192
channelDeps.set(ZuulDependencyKeys.sessionCtxDecorator, sessionCtxDecorator);
191193
channelDeps.set(ZuulDependencyKeys.requestCompleteHandler, reqCompleteHandler);
194+
Counter httpRequestHeadersReadTimeoutCounter = registry.counter("server.http.request.headers.read.timeout");
195+
channelDeps.set(ZuulDependencyKeys.httpRequestHeadersReadTimeoutCounter, httpRequestHeadersReadTimeoutCounter);
192196
Counter httpRequestReadTimeoutCounter = registry.counter("server.http.request.read.timeout");
193197
channelDeps.set(ZuulDependencyKeys.httpRequestReadTimeoutCounter, httpRequestReadTimeoutCounter);
194198
channelDeps.set(ZuulDependencyKeys.filterLoader, filterLoader);

zuul-core/src/main/java/com/netflix/zuul/netty/server/BaseZuulChannelInitializer.java

+15
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package com.netflix.zuul.netty.server;
1818

1919
import com.google.common.base.Preconditions;
20+
import com.netflix.config.CachedDynamicBooleanProperty;
2021
import com.netflix.config.CachedDynamicIntProperty;
2122
import com.netflix.netty.common.CloseOnIdleStateHandler;
2223
import com.netflix.netty.common.Http1ConnectionCloseHandler;
@@ -56,6 +57,7 @@
5657
import com.netflix.zuul.netty.insights.PassportStateHttpServerHandler;
5758
import com.netflix.zuul.netty.insights.ServerStateHandler;
5859
import com.netflix.zuul.netty.server.ssl.SslHandshakeInfoHandler;
60+
import com.netflix.zuul.netty.timeouts.HttpHeadersTimeoutHandler;
5961
import com.netflix.zuul.passport.PassportState;
6062
import io.netty.channel.Channel;
6163
import io.netty.channel.ChannelHandler;
@@ -88,6 +90,12 @@ public abstract class BaseZuulChannelInitializer extends ChannelInitializer<Chan
8890
public static final CachedDynamicIntProperty MAX_CHUNK_SIZE =
8991
new CachedDynamicIntProperty("server.http.decoder.maxChunkSize", 32768);
9092

93+
public static final CachedDynamicBooleanProperty HTTP_REQUEST_HEADERS_READ_TIMEOUT_ENABLED =
94+
new CachedDynamicBooleanProperty("server.http.request.headers.read.timeout.enabled", false);
95+
96+
public static final CachedDynamicIntProperty HTTP_REQUEST_HEADERS_READ_TIMEOUT =
97+
new CachedDynamicIntProperty("server.http.request.headers.read.timeout", 10000);
98+
9199
/**
92100
* The port that the server intends to listen on. Subclasses should NOT use this field, as it may not be set, and
93101
* may differ from the actual listening port. For example:
@@ -128,6 +136,7 @@ public abstract class BaseZuulChannelInitializer extends ChannelInitializer<Chan
128136
// protected final RequestRejectedChannelHandler requestRejectedChannelHandler;
129137
protected final SessionContextDecorator sessionContextDecorator;
130138
protected final RequestCompleteHandler requestCompleteHandler;
139+
protected final Counter httpRequestHeadersReadTimeoutCounter;
131140
protected final Counter httpRequestReadTimeoutCounter;
132141
protected final FilterLoader filterLoader;
133142
protected final FilterUsageNotifier filterUsageNotifier;
@@ -204,6 +213,7 @@ private BaseZuulChannelInitializer(
204213

205214
this.sessionContextDecorator = channelDependencies.get(ZuulDependencyKeys.sessionCtxDecorator);
206215
this.requestCompleteHandler = channelDependencies.get(ZuulDependencyKeys.requestCompleteHandler);
216+
this.httpRequestHeadersReadTimeoutCounter = channelDependencies.get(ZuulDependencyKeys.httpRequestHeadersReadTimeoutCounter);
207217
this.httpRequestReadTimeoutCounter = channelDependencies.get(ZuulDependencyKeys.httpRequestReadTimeoutCounter);
208218

209219
this.filterLoader = channelDependencies.get(ZuulDependencyKeys.filterLoader);
@@ -249,6 +259,11 @@ protected HttpServerCodec createHttpServerCodec() {
249259
}
250260

251261
protected void addHttpRelatedHandlers(ChannelPipeline pipeline) {
262+
pipeline.addLast(new HttpHeadersTimeoutHandler.InboundHandler(
263+
HTTP_REQUEST_HEADERS_READ_TIMEOUT_ENABLED::get,
264+
HTTP_REQUEST_HEADERS_READ_TIMEOUT::get,
265+
httpRequestHeadersReadTimeoutCounter
266+
));
252267
pipeline.addLast(new PassportStateHttpServerHandler.InboundHandler());
253268
pipeline.addLast(new PassportStateHttpServerHandler.OutboundHandler());
254269
if (httpRequestReadTimeout > -1) {

zuul-core/src/main/java/com/netflix/zuul/netty/server/ZuulDependencyKeys.java

+2
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ public class ZuulDependencyKeys {
4848
new ChannelConfigKey<>("sessionCtxDecorator");
4949
public static final ChannelConfigKey<RequestCompleteHandler> requestCompleteHandler =
5050
new ChannelConfigKey<>("requestCompleteHandler");
51+
public static final ChannelConfigKey<Counter> httpRequestHeadersReadTimeoutCounter =
52+
new ChannelConfigKey<>("httpRequestHeadersReadTimeoutCounter");
5153
public static final ChannelConfigKey<Counter> httpRequestReadTimeoutCounter =
5254
new ChannelConfigKey<>("httpRequestReadTimeoutCounter");
5355
public static final ChannelConfigKey<FilterLoader> filterLoader = new ChannelConfigKey<>("filterLoader");
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
/*
2+
* Copyright 2018 Netflix, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.netflix.zuul.netty.timeouts;
18+
19+
import java.util.concurrent.TimeUnit;
20+
import java.util.function.BooleanSupplier;
21+
import java.util.function.IntSupplier;
22+
23+
import org.slf4j.Logger;
24+
import org.slf4j.LoggerFactory;
25+
26+
import com.netflix.spectator.api.Counter;
27+
28+
import io.netty.channel.ChannelHandlerContext;
29+
import io.netty.channel.ChannelInboundHandlerAdapter;
30+
import io.netty.handler.codec.http.HttpMessage;
31+
import io.netty.handler.timeout.ReadTimeoutException;
32+
import io.netty.util.AttributeKey;
33+
import io.netty.util.concurrent.ScheduledFuture;
34+
35+
public class HttpHeadersTimeoutHandler {
36+
private static final Logger LOG = LoggerFactory.getLogger(HttpHeadersTimeoutHandler.class);
37+
38+
private static final AttributeKey<ScheduledFuture<Void>> HTTP_HEADERS_READ_TIMEOUT_FUTURE =
39+
AttributeKey.newInstance("httpHeadersReadTimeoutFuture");
40+
41+
public static class InboundHandler extends ChannelInboundHandlerAdapter {
42+
private final BooleanSupplier httpHeadersReadTimeoutEnabledSupplier;
43+
private final IntSupplier httpHeadersReadTimeoutSupplier;
44+
45+
private final Counter httpHeadersReadTimeoutCounter;
46+
47+
private boolean closed = false;
48+
49+
public InboundHandler(BooleanSupplier httpHeadersReadTimeoutEnabledSupplier, IntSupplier httpHeadersReadTimeoutSupplier, Counter httpHeadersReadTimeoutCounter) {
50+
this.httpHeadersReadTimeoutEnabledSupplier = httpHeadersReadTimeoutEnabledSupplier;
51+
this.httpHeadersReadTimeoutSupplier = httpHeadersReadTimeoutSupplier;
52+
this.httpHeadersReadTimeoutCounter = httpHeadersReadTimeoutCounter;
53+
}
54+
55+
@Override
56+
public void channelActive(ChannelHandlerContext ctx) throws Exception {
57+
try {
58+
if (!httpHeadersReadTimeoutEnabledSupplier.getAsBoolean())
59+
return;
60+
int timeout = httpHeadersReadTimeoutSupplier.getAsInt();
61+
ctx.channel().attr(HTTP_HEADERS_READ_TIMEOUT_FUTURE).set(
62+
ctx.executor().schedule(
63+
() -> {
64+
if (!closed) {
65+
ctx.fireExceptionCaught(ReadTimeoutException.INSTANCE);
66+
ctx.close();
67+
closed = true;
68+
httpHeadersReadTimeoutCounter.increment();
69+
LOG.debug("[{}] HTTP headers read timeout handler timed out", ctx.channel().id());
70+
}
71+
return null;
72+
},
73+
timeout,
74+
TimeUnit.MILLISECONDS
75+
)
76+
);
77+
LOG.debug("[{}] Adding HTTP headers read timeout handler: {}", ctx.channel().id(), timeout);
78+
} finally {
79+
super.channelActive(ctx);
80+
}
81+
}
82+
83+
@Override
84+
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
85+
try {
86+
if (msg instanceof HttpMessage) {
87+
ScheduledFuture<Void> future = ctx.channel().attr(HTTP_HEADERS_READ_TIMEOUT_FUTURE).get();
88+
if (future != null) {
89+
future.cancel(false);
90+
ctx.pipeline().remove(this);
91+
LOG.debug("[{}] Removing HTTP headers read timeout handler", ctx.channel().id());
92+
}
93+
}
94+
} finally {
95+
super.channelRead(ctx, msg);
96+
}
97+
}
98+
}
99+
}

zuul-integration-test/src/test/java/com/netflix/zuul/integration/IntegrationTest.java

+62
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
import java.io.InputStream;
5252
import java.net.HttpURLConnection;
5353
import java.net.ServerSocket;
54+
import java.net.Socket;
5455
import java.net.URL;
5556
import java.nio.charset.StandardCharsets;
5657
import java.time.Duration;
@@ -140,6 +141,10 @@ static void afterAll() {
140141

141142
@BeforeEach
142143
void beforeEachTest() {
144+
AbstractConfiguration config = ConfigurationManager.getConfigInstance();
145+
config.setProperty("server.http.request.headers.read.timeout.enabled", false);
146+
config.setProperty("server.http.request.headers.read.timeout", 10000);
147+
143148
this.pathSegment = randomPathSegment();
144149

145150
this.wireMock = wireMockExtension.getRuntimeInfo().getWireMock();
@@ -252,6 +257,63 @@ void httpGetFailsDueToOriginReadTimeout(
252257
verifyResponseHeaders(response);
253258
}
254259

260+
@ParameterizedTest
261+
@MethodSource("arguments")
262+
void httpGetHappyPathWithHeadersReadTimeout(
263+
final String description,
264+
final OkHttpClient okHttp,
265+
final boolean requestBodyBuffering,
266+
final boolean responseBodyBuffering)
267+
throws Exception {
268+
AbstractConfiguration config = ConfigurationManager.getConfigInstance();
269+
config.setProperty("server.http.request.headers.read.timeout.enabled", true);
270+
271+
wireMock.register(get(anyUrl()).willReturn(ok().withBody("hello world")));
272+
273+
Request request = setupRequestBuilder(requestBodyBuffering, responseBodyBuffering)
274+
.get()
275+
.build();
276+
Response response = okHttp.newCall(request).execute();
277+
assertThat(response.code()).isEqualTo(200);
278+
assertThat(response.body().string()).isEqualTo("hello world");
279+
verifyResponseHeaders(response);
280+
}
281+
282+
@ParameterizedTest
283+
@MethodSource("arguments")
284+
void httpPostHappyPathWithHeadersReadTimeout(
285+
final String description,
286+
final OkHttpClient okHttp,
287+
final boolean requestBodyBuffering,
288+
final boolean responseBodyBuffering)
289+
throws Exception {
290+
AbstractConfiguration config = ConfigurationManager.getConfigInstance();
291+
config.setProperty("server.http.request.headers.read.timeout.enabled", true);
292+
293+
wireMock.register(post(anyUrl()).willReturn(ok().withBody("Thank you next")));
294+
295+
Request request = setupRequestBuilder(requestBodyBuffering, responseBodyBuffering)
296+
.post(RequestBody.create("Simple POST request body".getBytes(StandardCharsets.UTF_8)))
297+
.build();
298+
Response response = okHttp.newCall(request).execute();
299+
assertThat(response.code()).isEqualTo(200);
300+
assertThat(response.body().string()).isEqualTo("Thank you next");
301+
verifyResponseHeaders(response);
302+
}
303+
304+
@Test
305+
void httpGetFailsDueToHeadersReadTimeout() throws Exception {
306+
AbstractConfiguration config = ConfigurationManager.getConfigInstance();
307+
config.setProperty("server.http.request.headers.read.timeout.enabled", true);
308+
config.setProperty("server.http.request.headers.read.timeout", 100);
309+
310+
Socket slowClient = new Socket("localhost", ZUUL_SERVER_PORT);
311+
Thread.sleep(500);
312+
// end of stream reached because zuul closed the connection
313+
assertThat(slowClient.getInputStream().read()).isEqualTo(-1);
314+
slowClient.close();
315+
}
316+
255317
@ParameterizedTest
256318
@MethodSource("arguments")
257319
void httpGetFailsDueToMalformedResponseChunk(

0 commit comments

Comments
 (0)