Skip to content

Commit 3325054

Browse files
4.x: Fix problem where throwing an Error would close connection but send keep-alive (#9014) (#9016)
* Fix wrong keep alive when error happens. * Error handlers now correctly handle Throwable and not just Exception. The HTTP/1.1 connection now correctly sends Connection: close header on throwables that cause the connection to be closed. Co-authored-by: Tomas Langer <[email protected]>
1 parent a7790f0 commit 3325054

File tree

3 files changed

+96
-5
lines changed

3 files changed

+96
-5
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
/*
2+
* Copyright (c) 2024 Oracle and/or its affiliates.
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 io.helidon.webserver.tests;
18+
19+
import io.helidon.http.Header;
20+
import io.helidon.http.HeaderName;
21+
import io.helidon.http.HeaderNames;
22+
import io.helidon.http.HeaderValues;
23+
import io.helidon.webclient.api.ClientResponseTyped;
24+
import io.helidon.webclient.http1.Http1Client;
25+
import io.helidon.webserver.http.ErrorHandler;
26+
import io.helidon.webserver.http.HttpRouting;
27+
import io.helidon.webserver.http.ServerRequest;
28+
import io.helidon.webserver.http.ServerResponse;
29+
import io.helidon.webserver.testing.junit5.DirectClient;
30+
import io.helidon.webserver.testing.junit5.RoutingTest;
31+
import io.helidon.webserver.testing.junit5.SetUpRoute;
32+
33+
import org.junit.jupiter.api.Test;
34+
35+
import static io.helidon.common.testing.http.junit5.HttpHeaderMatcher.hasHeader;
36+
import static org.hamcrest.CoreMatchers.is;
37+
import static org.hamcrest.MatcherAssert.assertThat;
38+
39+
@RoutingTest
40+
class ErrorHandlingThrowableTest {
41+
private static final HeaderName CONTROL_HEADER = HeaderNames.create("X-HELIDON-JUNIT");
42+
private static final Header THROW = HeaderValues.create(CONTROL_HEADER, "throw");
43+
44+
private final Http1Client client;
45+
46+
ErrorHandlingThrowableTest(DirectClient client) {
47+
this.client = client;
48+
}
49+
50+
@SetUpRoute
51+
static void routing(HttpRouting.Builder builder) {
52+
builder.error(SomeError.class, new SomeErrorHandler())
53+
.get("/", ErrorHandlingThrowableTest::handler);
54+
}
55+
56+
@Test
57+
void testOk() {
58+
String response = client.get()
59+
.requestEntity(String.class);
60+
assertThat(response, is("Done"));
61+
}
62+
63+
@Test
64+
void testSomeError() {
65+
ClientResponseTyped<String> response = client.get()
66+
.header(THROW)
67+
.request(String.class);
68+
assertThat(response.headers(), hasHeader(HeaderValues.CONNECTION_CLOSE));
69+
assertThat(response.entity(), is("Handled"));
70+
}
71+
72+
private static void handler(ServerRequest req, ServerResponse res) throws Exception {
73+
if (req.headers().contains(THROW)) {
74+
throw new SomeError();
75+
}
76+
res.send("Done");
77+
}
78+
79+
private static class SomeErrorHandler implements ErrorHandler<SomeError> {
80+
@Override
81+
public void handle(ServerRequest req, ServerResponse res, SomeError throwable) {
82+
res.header(HeaderValues.CONNECTION_CLOSE);
83+
res.send("Handled");
84+
}
85+
}
86+
87+
private static class SomeError extends Error {
88+
}
89+
}

webserver/webserver/src/main/java/io/helidon/webserver/http/ErrorHandlers.java

+3-1
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ public String toString() {
6767
* @param response HTTP server response
6868
* @param task task to execute
6969
*/
70+
@SuppressWarnings({"unchecked", "rawtypes"})
7071
public void runWithErrorHandling(ConnectionContext ctx,
7172
RoutingRequest request,
7273
RoutingResponse response,
@@ -116,7 +117,7 @@ public void runWithErrorHandling(ConnectionContext ctx,
116117
}
117118
} catch (RuntimeException e) {
118119
handleError(ctx, request, response, e);
119-
} catch (Exception e) {
120+
} catch (Throwable e) {
120121
if (e.getCause() instanceof SocketException se) {
121122
throw new UncheckedIOException(se);
122123
}
@@ -176,6 +177,7 @@ private void handleRequestException(ConnectionContext ctx,
176177
response.commit();
177178
}
178179

180+
@SuppressWarnings("unchecked")
179181
private void handleError(ConnectionContext ctx, RoutingRequest request, RoutingResponse response, Throwable e) {
180182
errorHandler(e.getClass())
181183
.ifPresentOrElse(it -> handleError(ctx, request, response, e, (ErrorHandler<Throwable>) it),

webserver/webserver/src/main/java/io/helidon/webserver/http1/Http1Connection.java

+4-4
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,6 @@ public void handle(Semaphore requestSemaphore) throws InterruptedException {
215215
.cause(e)
216216
.type(EventType.BAD_REQUEST)
217217
.status(e.status())
218-
.setKeepAlive(e.keepAlive())
219218
.build());
220219
} catch (RequestException e) {
221220
handleRequestException(e);
@@ -465,9 +464,10 @@ private void handleRequestException(RequestException e) {
465464

466465
BufferData buffer = BufferData.growing(128);
467466
ServerResponseHeaders headers = response.headers();
468-
if (!e.keepAlive()) {
469-
headers.set(HeaderValues.CONNECTION_CLOSE);
470-
}
467+
468+
// we are escaping the connection loop, the connection will be closed
469+
headers.set(HeaderValues.CONNECTION_CLOSE);
470+
471471
byte[] message = response.entity().orElse(BufferData.EMPTY_BYTES);
472472
headers.set(HeaderValues.create(HeaderNames.CONTENT_LENGTH, String.valueOf(message.length)));
473473

0 commit comments

Comments
 (0)