Skip to content

Commit a57812b

Browse files
authored
Update httpcore5 to 5.4.3 (#7078)
This update fixes an issue with 3xx early response handling with the request has 'Expect: 100-continue`. Fixes #7047 See also https://issues.apache.org/jira/browse/HTTPCORE-796
1 parent b1b02c2 commit a57812b

3 files changed

Lines changed: 178 additions & 1 deletion

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"type": "bugfix",
3+
"category": "Apache 5 HTTP Client",
4+
"contributor": "",
5+
"description": "Update `httpcore5` to `5.4.3` to fix an issue where early 3xx responses to requests with `Expect: 100-continue` does not cause the client to terminate the request. Fixes [#7047](https://github.com/aws/aws-sdk-java-v2/issues/7047)."
6+
}
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
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+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
package software.amazon.awssdk.http.apache5;
17+
18+
import static org.assertj.core.api.Assertions.assertThat;
19+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
20+
21+
import java.io.BufferedReader;
22+
import java.io.IOException;
23+
import java.io.InputStream;
24+
import java.io.InputStreamReader;
25+
import java.io.OutputStream;
26+
import java.net.ServerSocket;
27+
import java.net.Socket;
28+
import java.nio.charset.StandardCharsets;
29+
import java.util.ArrayList;
30+
import java.util.List;
31+
import java.util.concurrent.ExecutorService;
32+
import java.util.concurrent.Executors;
33+
import java.util.concurrent.TimeUnit;
34+
import org.junit.jupiter.api.AfterAll;
35+
import org.junit.jupiter.api.BeforeAll;
36+
import org.junit.jupiter.api.BeforeEach;
37+
import org.junit.jupiter.api.Test;
38+
import software.amazon.awssdk.http.ContentStreamProvider;
39+
import software.amazon.awssdk.http.ExecutableHttpRequest;
40+
import software.amazon.awssdk.http.HttpExecuteRequest;
41+
import software.amazon.awssdk.http.HttpExecuteResponse;
42+
import software.amazon.awssdk.http.SdkHttpClient;
43+
import software.amazon.awssdk.http.SdkHttpFullRequest;
44+
import software.amazon.awssdk.http.SdkHttpMethod;
45+
import software.amazon.awssdk.metrics.NoOpMetricCollector;
46+
47+
public class Apache5Expect100ContinueTest {
48+
private static TestServer server;
49+
private static ExecutorService exec;
50+
51+
@BeforeAll
52+
static void setup() throws IOException {
53+
server = new TestServer(0);
54+
exec = Executors.newSingleThreadExecutor();
55+
exec.submit(server::serve);
56+
}
57+
58+
@AfterAll
59+
static void teardown() throws IOException, InterruptedException {
60+
server.close();
61+
exec.shutdown();
62+
exec.awaitTermination(5, TimeUnit.SECONDS);
63+
}
64+
65+
@BeforeEach
66+
void methodSetup() {
67+
server.clientSockets.clear();
68+
}
69+
70+
@Test
71+
void execute_serverResponds3xx_closesConnection() {
72+
SdkHttpFullRequest request = SdkHttpFullRequest.builder()
73+
.method(SdkHttpMethod.PUT)
74+
.host("localhost")
75+
.protocol("http")
76+
.port(server.getPort())
77+
.encodedPath("/")
78+
.appendHeader("Expect", "100-continue")
79+
.appendHeader("Content-Length", Integer.toString(Integer.MAX_VALUE))
80+
.build();
81+
82+
ContentStreamProvider provider = ContentStreamProvider.fromInputStream(new EndlessInputStream());
83+
84+
HttpExecuteRequest executeRequest = HttpExecuteRequest.builder()
85+
.request(request)
86+
.contentStreamProvider(provider)
87+
.metricCollector(NoOpMetricCollector.create())
88+
.build();
89+
90+
try (SdkHttpClient client = Apache5HttpClient.create()) {
91+
ExecutableHttpRequest executableRequest = client.prepareRequest(executeRequest);
92+
HttpExecuteResponse executeResponse = executableRequest.call();
93+
assertThat(executeResponse.httpResponse().statusCode()).isEqualTo(301);
94+
95+
assertThatThrownBy(() -> server.clientSockets.get(0).getInputStream().read())
96+
.isInstanceOf(IOException.class)
97+
.hasMessage("Connection reset");
98+
} catch (IOException e) {
99+
throw new RuntimeException(e);
100+
}
101+
}
102+
103+
104+
/**
105+
* Test HTTP server: accepts a connection, reads all request headers, replies with a 301 permanent redirect; connections
106+
* are not closed by the server.
107+
*/
108+
public static class TestServer implements AutoCloseable {
109+
110+
private final ServerSocket serverSocket;
111+
private final List<Socket> clientSockets = new ArrayList<>();
112+
113+
public TestServer(int port) throws IOException {
114+
this.serverSocket = new ServerSocket(port);
115+
}
116+
117+
public int getPort() {
118+
return serverSocket.getLocalPort();
119+
}
120+
121+
public void serve() {
122+
while (!serverSocket.isClosed()) {
123+
try {
124+
Socket connection = serverSocket.accept();
125+
clientSockets.add(connection);
126+
handle(connection);
127+
} catch (IOException e) {
128+
if (serverSocket.isClosed()) {
129+
break; // closed during accept() — normal shutdown
130+
}
131+
}
132+
}
133+
}
134+
135+
private void handle(Socket connection) throws IOException {
136+
// Read the request line + headers (everything up to the blank line).
137+
// We don't decode the body; only headers are required here.
138+
BufferedReader in = new BufferedReader(
139+
new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8));
140+
141+
List<String> requestLines = new ArrayList<>();
142+
String line;
143+
while ((line = in.readLine()) != null && !line.isEmpty()) {
144+
requestLines.add(line);
145+
}
146+
147+
String response =
148+
"HTTP/1.1 301 Moved Permanently\r\n"
149+
+ "Location: https://example.com/\r\n"
150+
+ "Content-Length: 0\r\n"
151+
+ "Connection: close\r\n"
152+
+ "\r\n";
153+
154+
OutputStream out = connection.getOutputStream();
155+
out.write(response.getBytes(StandardCharsets.US_ASCII));
156+
out.flush();
157+
}
158+
159+
@Override
160+
public void close() throws IOException {
161+
serverSocket.close();
162+
}
163+
}
164+
165+
private static class EndlessInputStream extends InputStream {
166+
@Override
167+
public int read() {
168+
return 0xFF;
169+
}
170+
}
171+
}

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@
190190
<httpcomponents.httpclient.version>4.5.13</httpcomponents.httpclient.version>
191191
<httpcomponents.httpcore.version>4.4.16</httpcomponents.httpcore.version>
192192
<httpcomponents.client5.version>5.6.1</httpcomponents.client5.version>
193-
<httpcomponents.core5.version>5.4.2</httpcomponents.core5.version>
193+
<httpcomponents.core5.version>5.4.3</httpcomponents.core5.version>
194194
<!-- Reactive Streams version -->
195195
<reactive-streams.version>1.0.4</reactive-streams.version>
196196

0 commit comments

Comments
 (0)