Skip to content

Commit bddcd08

Browse files
committed
8304701: Request with timeout aborts later in-flight request on HTTP/1.1 cxn
Reviewed-by: dfuchs, michaelm
1 parent 91279fc commit bddcd08

File tree

2 files changed

+179
-1
lines changed

2 files changed

+179
-1
lines changed

src/java.net.http/share/classes/jdk/internal/net/http/MultiExchange.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ class MultiExchange<T> implements Cancelable {
101101
);
102102

103103
private final List<HeaderFilter> filters;
104-
ResponseTimerEvent responseTimerEvent;
104+
volatile ResponseTimerEvent responseTimerEvent;
105105
volatile boolean cancelled;
106106
AtomicReference<CancellationException> interrupted = new AtomicReference<>();
107107
final PushGroup<T> pushGroup;
@@ -231,6 +231,7 @@ public Optional<Duration> remainingConnectTimeout() {
231231
private void cancelTimer() {
232232
if (responseTimerEvent != null) {
233233
client.cancelTimer(responseTimerEvent);
234+
responseTimerEvent = null;
234235
}
235236
}
236237

@@ -457,6 +458,7 @@ private CompletableFuture<Response> responseAsyncImpl() {
457458
}
458459
return completedFuture(response);
459460
} else {
461+
cancelTimer();
460462
this.response =
461463
new HttpResponseImpl<>(currentreq, response, this.response, null, exch);
462464
Exchange<T> oldExch = exch;
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
/*
2+
* Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved.
3+
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4+
*
5+
* This code is free software; you can redistribute it and/or modify it
6+
* under the terms of the GNU General Public License version 2 only, as
7+
* published by the Free Software Foundation. Oracle designates this
8+
* particular file as subject to the "Classpath" exception as provided
9+
* by Oracle in the LICENSE file that accompanied this code.
10+
*
11+
* This code is distributed in the hope that it will be useful, but WITHOUT
12+
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13+
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
14+
* version 2 for more details (a copy is included in the LICENSE file that
15+
* accompanied this code).
16+
*
17+
* You should have received a copy of the GNU General Public License version
18+
* 2 along with this work; if not, write to the Free Software Foundation,
19+
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20+
*
21+
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22+
* or visit www.oracle.com if you need additional information or have any
23+
* questions.
24+
*/
25+
26+
/*
27+
* @test
28+
* @bug 8304701
29+
* @summary Verifies that for a redirected request, the given HttpClient
30+
* will clear and start a new response timer instead of throwing
31+
* an HttpTimeoutException during the redirected request.
32+
* @library /test/lib /test/jdk/java/net/httpclient/lib
33+
* @build jdk.test.lib.net.SimpleSSLContext
34+
* @run testng/othervm -Djdk.httpclient.HttpClient.log=errors,trace -Djdk.internal.httpclient.debug=false RedirectTimeoutTest
35+
*/
36+
37+
import jdk.httpclient.test.lib.common.HttpServerAdapters.HttpTestExchange;
38+
import jdk.httpclient.test.lib.common.HttpServerAdapters.HttpTestHandler;
39+
import jdk.httpclient.test.lib.common.HttpServerAdapters.HttpTestResponseHeaders;
40+
import jdk.httpclient.test.lib.common.HttpServerAdapters.HttpTestServer;
41+
import org.testng.TestException;
42+
import org.testng.annotations.AfterTest;
43+
import org.testng.annotations.BeforeTest;
44+
import org.testng.annotations.DataProvider;
45+
import org.testng.annotations.Test;
46+
47+
import java.io.IOException;
48+
import java.io.OutputStream;
49+
import java.io.PrintStream;
50+
import java.net.URI;
51+
import java.net.http.HttpClient;
52+
import java.net.http.HttpClient.Version;
53+
import java.net.http.HttpRequest;
54+
import java.net.http.HttpResponse;
55+
import java.net.http.HttpTimeoutException;
56+
import java.nio.charset.StandardCharsets;
57+
import java.time.Duration;
58+
import java.time.Instant;
59+
60+
import static java.net.http.HttpClient.Redirect.ALWAYS;
61+
import static java.net.http.HttpClient.Version.HTTP_1_1;
62+
import static java.net.http.HttpClient.Version.HTTP_2;
63+
import static jdk.test.lib.Utils.adjustTimeout;
64+
65+
public class RedirectTimeoutTest {
66+
67+
static HttpTestServer h1TestServer, h2TestServer;
68+
static URI h1Uri, h1RedirectUri, h2Uri, h2RedirectUri, h2WarmupUri, testRedirectURI;
69+
private static final long TIMEOUT_MILLIS = 3000L; // 3s
70+
private static final long SLEEP_TIME = 1500L; // 1.5s
71+
public static final int ITERATIONS = 4;
72+
private static final PrintStream out = System.out;
73+
74+
@BeforeTest
75+
public void setup() throws IOException {
76+
h1TestServer = HttpTestServer.create(HTTP_1_1);
77+
h2TestServer = HttpTestServer.create(HTTP_2);
78+
h1Uri = URI.create("http://" + h1TestServer.serverAuthority() + "/h1_test");
79+
h1RedirectUri = URI.create("http://" + h1TestServer.serverAuthority() + "/h1_redirect");
80+
h2Uri = URI.create("http://" + h2TestServer.serverAuthority() + "/h2_test");
81+
h2RedirectUri = URI.create("http://" + h2TestServer.serverAuthority() + "/h2_redirect");
82+
h2WarmupUri = URI.create("http://" + h2TestServer.serverAuthority() + "/h2_warmup");
83+
h1TestServer.addHandler(new GetHandler(), "/h1_test");
84+
h1TestServer.addHandler(new RedirectHandler(), "/h1_redirect");
85+
h2TestServer.addHandler(new GetHandler(), "/h2_test");
86+
h2TestServer.addHandler(new RedirectHandler(), "/h2_redirect");
87+
h2TestServer.addHandler(new Http2Warmup(), "/h2_warmup");
88+
h1TestServer.start();
89+
h2TestServer.start();
90+
}
91+
92+
@AfterTest
93+
public void teardown() {
94+
h1TestServer.stop();
95+
h2TestServer.stop();
96+
}
97+
98+
@DataProvider(name = "testData")
99+
public Object[][] testData() {
100+
return new Object[][] {
101+
{ HTTP_1_1, h1Uri, h1RedirectUri },
102+
{ HTTP_2, h2Uri, h2RedirectUri }
103+
};
104+
}
105+
106+
@Test(dataProvider = "testData")
107+
public void test(Version version, URI uri, URI redirectURI) throws InterruptedException {
108+
out.println("Testing for " + version);
109+
testRedirectURI = redirectURI;
110+
HttpClient.Builder clientBuilder = HttpClient.newBuilder().followRedirects(ALWAYS);
111+
HttpRequest request = HttpRequest.newBuilder().uri(uri)
112+
.GET()
113+
.version(version)
114+
.timeout(Duration.ofMillis(adjustTimeout(TIMEOUT_MILLIS)))
115+
.build();
116+
117+
try (HttpClient client = clientBuilder.build()) {
118+
if (version.equals(HTTP_2))
119+
client.send(HttpRequest.newBuilder(h2WarmupUri).HEAD().build(), HttpResponse.BodyHandlers.discarding());
120+
/*
121+
With TIMEOUT_MILLIS set to 1500ms and the server's RedirectHandler sleeping for 750ms before responding
122+
to each request, 4 iterations will take a guaranteed minimum time of 3000ms which will ensure that any
123+
uncancelled/uncleared timers will fire within the test window.
124+
*/
125+
for (int i = 0; i < ITERATIONS; i++) {
126+
out.println(Instant.now() + ": Client: Sending request #" + (i + 1));
127+
client.send(request, HttpResponse.BodyHandlers.ofString());
128+
out.println("Request complete");
129+
}
130+
} catch (IOException e) {
131+
if (e.getClass() == HttpTimeoutException.class) {
132+
e.printStackTrace(System.out);
133+
throw new TestException("Timeout from original HttpRequest expired on redirect when it should have been cancelled.");
134+
} else {
135+
throw new RuntimeException(e);
136+
}
137+
}
138+
}
139+
140+
public static class Http2Warmup implements HttpTestHandler {
141+
142+
@Override
143+
public void handle(HttpTestExchange t) throws IOException {
144+
t.sendResponseHeaders(200, 0);
145+
}
146+
}
147+
148+
public static class GetHandler implements HttpTestHandler {
149+
150+
@Override
151+
public void handle(HttpTestExchange exchange) throws IOException {
152+
out.println(Instant.now() + ": Server: Get Handler Called");
153+
HttpTestResponseHeaders responseHeaders = exchange.getResponseHeaders();
154+
responseHeaders.addHeader("Location", testRedirectURI.toString());
155+
exchange.sendResponseHeaders(302, 0);
156+
}
157+
}
158+
159+
public static class RedirectHandler implements HttpTestHandler {
160+
161+
@Override
162+
public void handle(HttpTestExchange exchange) throws IOException {
163+
out.println(Instant.now() + ": Server: Redirect Handler Called");
164+
byte[] data = "Test".getBytes(StandardCharsets.UTF_8);
165+
try {
166+
Thread.sleep(adjustTimeout(SLEEP_TIME));
167+
} catch (InterruptedException e) {
168+
throw new RuntimeException(e);
169+
}
170+
exchange.sendResponseHeaders(200, data.length);
171+
try (OutputStream os = exchange.getResponseBody()) {
172+
os.write(data);
173+
}
174+
}
175+
}
176+
}

0 commit comments

Comments
 (0)