Skip to content

Commit 97fd931

Browse files
HTTPCLIENT-1482: added ExpectContinueTrigger, extended RequestConfig, and modified RequestExpectContinue so that Expect: 100-continue is emitted only when the underlying connection has already processed at least one request; default behaviour (ALWAYS) preserved, API remains binary-compatible. (#683)
1 parent b55f106 commit 97fd931

File tree

4 files changed

+189
-7
lines changed

4 files changed

+189
-7
lines changed
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/*
2+
* ====================================================================
3+
* Licensed to the Apache Software Foundation (ASF) under one
4+
* or more contributor license agreements. See the NOTICE file
5+
* distributed with this work for additional information
6+
* regarding copyright ownership. The ASF licenses this file
7+
* to you under the Apache License, Version 2.0 (the
8+
* "License"); you may not use this file except in compliance
9+
* with the License. You may obtain a copy of the License at
10+
*
11+
* http://www.apache.org/licenses/LICENSE-2.0
12+
*
13+
* Unless required by applicable law or agreed to in writing,
14+
* software distributed under the License is distributed on an
15+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16+
* KIND, either express or implied. See the License for the
17+
* specific language governing permissions and limitations
18+
* under the License.
19+
* ====================================================================
20+
*
21+
* This software consists of voluntary contributions made by many
22+
* individuals on behalf of the Apache Software Foundation. For more
23+
* information on the Apache Software Foundation, please see
24+
* <http://www.apache.org/>.
25+
*
26+
*/
27+
package org.apache.hc.client5.http.config;
28+
29+
/**
30+
* Enumeration of strategies that govern automatic inclusion of the
31+
* {@code Expect: 100-continue} request header when
32+
* {@link org.apache.hc.client5.http.config.RequestConfig#isExpectContinueEnabled()
33+
* expect-continue support} is enabled.
34+
*
35+
* @since 5.6
36+
*/
37+
public enum ExpectContinueTrigger {
38+
39+
/**
40+
* Always add {@code Expect: 100-continue} to every entity-enclosing request.
41+
*/
42+
ALWAYS,
43+
44+
/**
45+
* Add {@code Expect: 100-continue} <em>only</em> when the underlying
46+
* connection has already processed at least one request (that is, when the
47+
* socket has been taken from the connection pool and may be stale).
48+
*/
49+
IF_REUSED
50+
}

httpclient5/src/main/java/org/apache/hc/client5/http/config/RequestConfig.java

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import org.apache.hc.core5.annotation.Contract;
3535
import org.apache.hc.core5.annotation.ThreadingBehavior;
3636
import org.apache.hc.core5.http.HttpHost;
37+
import org.apache.hc.core5.util.Args;
3738
import org.apache.hc.core5.util.TimeValue;
3839
import org.apache.hc.core5.util.Timeout;
3940

@@ -66,12 +67,15 @@ public class RequestConfig implements Cloneable {
6667
private final boolean protocolUpgradeEnabled;
6768
private final Path unixDomainSocket;
6869

70+
private final ExpectContinueTrigger expectContinueTrigger;
71+
6972
/**
7073
* Intended for CDI compatibility
7174
*/
7275
protected RequestConfig() {
7376
this(false, null, null, false, false, 0, false, null, null,
74-
DEFAULT_CONNECTION_REQUEST_TIMEOUT, null, null, DEFAULT_CONN_KEEP_ALIVE, false, false, false, null);
77+
DEFAULT_CONNECTION_REQUEST_TIMEOUT, null, null, DEFAULT_CONN_KEEP_ALIVE, false, false, false, null,
78+
ExpectContinueTrigger.ALWAYS);
7579
}
7680

7781
RequestConfig(
@@ -91,7 +95,8 @@ protected RequestConfig() {
9195
final boolean contentCompressionEnabled,
9296
final boolean hardCancellationEnabled,
9397
final boolean protocolUpgradeEnabled,
94-
final Path unixDomainSocket) {
98+
final Path unixDomainSocket,
99+
final ExpectContinueTrigger expectContinueTrigger) {
95100
super();
96101
this.expectContinueEnabled = expectContinueEnabled;
97102
this.proxy = proxy;
@@ -110,6 +115,7 @@ protected RequestConfig() {
110115
this.hardCancellationEnabled = hardCancellationEnabled;
111116
this.protocolUpgradeEnabled = protocolUpgradeEnabled;
112117
this.unixDomainSocket = unixDomainSocket;
118+
this.expectContinueTrigger = expectContinueTrigger;
113119
}
114120

115121
/**
@@ -238,6 +244,10 @@ public Path getUnixDomainSocket() {
238244
return unixDomainSocket;
239245
}
240246

247+
public ExpectContinueTrigger getExpectContinueTrigger() {
248+
return expectContinueTrigger;
249+
}
250+
241251
@Override
242252
protected RequestConfig clone() throws CloneNotSupportedException {
243253
return (RequestConfig) super.clone();
@@ -312,6 +322,7 @@ public static class Builder {
312322
private boolean hardCancellationEnabled;
313323
private boolean protocolUpgradeEnabled;
314324
private Path unixDomainSocket;
325+
private ExpectContinueTrigger expectContinueTrigger;
315326

316327
Builder() {
317328
super();
@@ -322,6 +333,7 @@ public static class Builder {
322333
this.contentCompressionEnabled = true;
323334
this.hardCancellationEnabled = true;
324335
this.protocolUpgradeEnabled = true;
336+
this.expectContinueTrigger = ExpectContinueTrigger.ALWAYS;
325337
}
326338

327339
/**
@@ -668,6 +680,20 @@ public Builder setUnixDomainSocket(final Path unixDomainSocket) {
668680
return this;
669681
}
670682

683+
/**
684+
* Defines under which circumstances the client should add the
685+
* {@code Expect: 100-continue} header to entity-enclosing requests.
686+
*
687+
* @param trigger expectation-continue trigger strategy
688+
* @return this builder
689+
* @see ExpectContinueTrigger
690+
* @since 5.6
691+
*/
692+
public Builder setExpectContinueTrigger(final ExpectContinueTrigger trigger) {
693+
this.expectContinueTrigger = Args.notNull(trigger, "ExpectContinueTrigger");
694+
return this;
695+
}
696+
671697
public RequestConfig build() {
672698
return new RequestConfig(
673699
expectContinueEnabled,
@@ -686,7 +712,8 @@ public RequestConfig build() {
686712
contentCompressionEnabled,
687713
hardCancellationEnabled,
688714
protocolUpgradeEnabled,
689-
unixDomainSocket);
715+
unixDomainSocket,
716+
expectContinueTrigger);
690717
}
691718

692719
}

httpclient5/src/main/java/org/apache/hc/client5/http/protocol/RequestExpectContinue.java

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,11 @@
2929

3030
import java.io.IOException;
3131

32+
import org.apache.hc.client5.http.config.ExpectContinueTrigger;
3233
import org.apache.hc.client5.http.config.RequestConfig;
3334
import org.apache.hc.core5.annotation.Contract;
3435
import org.apache.hc.core5.annotation.ThreadingBehavior;
36+
import org.apache.hc.core5.http.EndpointDetails;
3537
import org.apache.hc.core5.http.EntityDetails;
3638
import org.apache.hc.core5.http.HeaderElements;
3739
import org.apache.hc.core5.http.HttpException;
@@ -41,6 +43,7 @@
4143
import org.apache.hc.core5.http.HttpVersion;
4244
import org.apache.hc.core5.http.ProtocolVersion;
4345
import org.apache.hc.core5.http.protocol.HttpContext;
46+
import org.apache.hc.core5.http.protocol.HttpCoreContext;
4447
import org.apache.hc.core5.util.Args;
4548

4649
/**
@@ -68,13 +71,20 @@ public void process(final HttpRequest request, final EntityDetails entity, final
6871
if (!request.containsHeader(HttpHeaders.EXPECT)) {
6972
final HttpClientContext clientContext = HttpClientContext.cast(context);
7073
final ProtocolVersion version = request.getVersion() != null ? request.getVersion() : clientContext.getProtocolVersion();
74+
final RequestConfig config = clientContext.getRequestConfigOrDefault();
75+
if (!config.isExpectContinueEnabled()) {
76+
return;
77+
}
78+
if (config.getExpectContinueTrigger() == ExpectContinueTrigger.IF_REUSED) {
79+
final EndpointDetails details = HttpCoreContext.cast(context).getEndpointDetails();
80+
if (details != null && details.getRequestCount() == 0) {
81+
return;
82+
}
83+
}
7184
// Do not send the expect header if request body is known to be empty
7285
if (entity != null
7386
&& entity.getContentLength() != 0 && !version.lessEquals(HttpVersion.HTTP_1_0)) {
74-
final RequestConfig config = clientContext.getRequestConfigOrDefault();
75-
if (config.isExpectContinueEnabled()) {
76-
request.addHeader(HttpHeaders.EXPECT, HeaderElements.CONTINUE);
77-
}
87+
request.addHeader(HttpHeaders.EXPECT, HeaderElements.CONTINUE);
7888
}
7989
}
8090
}

httpclient5/src/test/java/org/apache/hc/client5/http/protocol/TestRequestExpectContinue.java

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,16 +27,21 @@
2727

2828
package org.apache.hc.client5.http.protocol;
2929

30+
import java.net.InetSocketAddress;
3031
import java.nio.charset.StandardCharsets;
3132

33+
import org.apache.hc.client5.http.config.ExpectContinueTrigger;
3234
import org.apache.hc.client5.http.config.RequestConfig;
3335
import org.apache.hc.core5.http.ClassicHttpRequest;
3436
import org.apache.hc.core5.http.Header;
3537
import org.apache.hc.core5.http.HeaderElements;
38+
import org.apache.hc.core5.http.HttpConnectionMetrics;
3639
import org.apache.hc.core5.http.HttpHeaders;
3740
import org.apache.hc.core5.http.HttpVersion;
41+
import org.apache.hc.core5.http.impl.BasicEndpointDetails;
3842
import org.apache.hc.core5.http.io.entity.StringEntity;
3943
import org.apache.hc.core5.http.message.BasicClassicHttpRequest;
44+
import org.apache.hc.core5.util.Timeout;
4045
import org.junit.jupiter.api.Assertions;
4146
import org.junit.jupiter.api.Test;
4247

@@ -119,4 +124,94 @@ void testRequestExpectContinueIgnoreNonenclosingRequests() throws Exception {
119124
Assertions.assertEquals(0, request.getHeaders().length);
120125
}
121126

127+
128+
@Test
129+
void testRequestExpectContinueIfReused() throws Exception {
130+
final HttpClientContext context = HttpClientContext.create();
131+
final RequestConfig config = RequestConfig.custom()
132+
.setExpectContinueEnabled(true)
133+
.setExpectContinueTrigger(ExpectContinueTrigger.IF_REUSED)
134+
.build();
135+
context.setRequestConfig(config);
136+
137+
final HttpConnectionMetrics metrics = new HttpConnectionMetrics() {
138+
@Override
139+
public long getRequestCount() {
140+
return 1;
141+
}
142+
143+
@Override
144+
public long getResponseCount() {
145+
return 0;
146+
}
147+
148+
@Override
149+
public long getSentBytesCount() {
150+
return 0;
151+
}
152+
153+
@Override
154+
public long getReceivedBytesCount() {
155+
return 0;
156+
}
157+
};
158+
159+
final BasicEndpointDetails reused = new BasicEndpointDetails(
160+
new InetSocketAddress("localhost", 0),
161+
new InetSocketAddress("localhost", 80),
162+
metrics,
163+
Timeout.ofSeconds(30));
164+
context.setEndpointDetails(reused);
165+
166+
final ClassicHttpRequest request = new BasicClassicHttpRequest("POST", "/");
167+
request.setEntity(new StringEntity("data", StandardCharsets.US_ASCII));
168+
169+
new RequestExpectContinue().process(request, request.getEntity(), context);
170+
171+
final Header header = request.getFirstHeader(HttpHeaders.EXPECT);
172+
Assertions.assertNotNull(header);
173+
Assertions.assertEquals(HeaderElements.CONTINUE, header.getValue());
174+
}
175+
176+
@Test
177+
void testNoExpectContinueFreshConnectionWithIfReused() throws Exception {
178+
final HttpClientContext context = HttpClientContext.create();
179+
final RequestConfig cfg = RequestConfig.custom()
180+
.setExpectContinueEnabled(true)
181+
.setExpectContinueTrigger(ExpectContinueTrigger.IF_REUSED)
182+
.build();
183+
context.setRequestConfig(cfg);
184+
185+
// fresh endpoint: requestCount == 0
186+
context.setEndpointDetails(new BasicEndpointDetails(
187+
new InetSocketAddress("localhost", 0),
188+
new InetSocketAddress("localhost", 80),
189+
null,
190+
Timeout.ofSeconds(30)));
191+
192+
final ClassicHttpRequest req = new BasicClassicHttpRequest("POST", "/");
193+
req.setEntity(new StringEntity("data", StandardCharsets.US_ASCII));
194+
195+
new RequestExpectContinue().process(req, req.getEntity(), context);
196+
197+
Assertions.assertNull(req.getFirstHeader(HttpHeaders.EXPECT));
198+
}
199+
200+
@Test
201+
void testHeaderAlreadyPresentIsNotDuplicated() throws Exception {
202+
final HttpClientContext context = HttpClientContext.create();
203+
final RequestConfig cfg = RequestConfig.custom()
204+
.setExpectContinueEnabled(true)
205+
.build(); // default trigger = ALWAYS
206+
context.setRequestConfig(cfg);
207+
208+
final ClassicHttpRequest req = new BasicClassicHttpRequest("POST", "/");
209+
req.setEntity(new StringEntity("data", StandardCharsets.US_ASCII));
210+
req.addHeader(HttpHeaders.EXPECT, HeaderElements.CONTINUE); // pre-existing
211+
212+
new RequestExpectContinue().process(req, req.getEntity(), context);
213+
214+
final Header[] headers = req.getHeaders(HttpHeaders.EXPECT);
215+
Assertions.assertEquals(1, headers.length); // no duplicates
216+
}
122217
}

0 commit comments

Comments
 (0)