Skip to content

Commit

Permalink
Initial support for per-request 100 continue.
Browse files Browse the repository at this point in the history
Signed-off-by: Santiago Pericas-Geertsen <[email protected]>
  • Loading branch information
spericas committed Feb 11, 2025
1 parent 2be95bd commit c4e9962
Show file tree
Hide file tree
Showing 9 changed files with 177 additions and 11 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2022, 2023 Oracle and/or its affiliates.
* Copyright (c) 2022, 2025 Oracle and/or its affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -388,6 +388,16 @@ default <T> ClientResponseTyped<T> outputStream(OutputStreamHandler outputStream
*/
T readContinueTimeout(Duration readContinueTimeout);

/**
* Whether Expect 100-Continue header is sent to verify server availability before sending an entity.
* Can be used to override the setting inherited from {@link HttpClientConfig#sendExpectContinue()}
* on a per-request basis.
*
* @param sendExpectContinue value to override behavior for a single request
* @return whether Expect 100-Continue header should be sent on streamed transfers
*/
T sendExpectContinue(boolean sendExpectContinue);

/**
* Handle output stream.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2023, 2024 Oracle and/or its affiliates.
* Copyright (c) 2023, 2025 Oracle and/or its affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -87,18 +87,30 @@ public abstract class ClientRequestBase<T extends ClientRequest<T>, R extends Ht
private Proxy proxy;
private boolean keepAlive;
private ClientConnection connection;
private Boolean sendExpectContinue;

protected ClientRequestBase(HttpClientConfig clientConfig,
WebClientCookieManager cookieManager,
String protocolId,
Method method,
ClientUri clientUri,
Map<String, String> properties) {
this(clientConfig, cookieManager, protocolId, method, clientUri, null, properties);
}

protected ClientRequestBase(HttpClientConfig clientConfig,
WebClientCookieManager cookieManager,
String protocolId,
Method method,
ClientUri clientUri,
Boolean sendExpectContinue,
Map<String, String> properties) {
this.clientConfig = clientConfig;
this.cookieManager = cookieManager;
this.protocolId = protocolId;
this.method = method;
this.clientUri = clientUri;
this.sendExpectContinue = sendExpectContinue;
this.properties = new HashMap<>(properties);

this.headers = clientConfig.defaultRequestHeaders();
Expand Down Expand Up @@ -277,6 +289,12 @@ public R outputStream(OutputStreamHandler outputStreamConsumer) {
return doOutputStream(outputStreamConsumer);
}

@Override
public T sendExpectContinue(boolean sendExpectContinue) {
this.sendExpectContinue = sendExpectContinue;
return identity();
}

/**
* Append additional headers before sending the request.
*/
Expand Down Expand Up @@ -364,6 +382,11 @@ public boolean skipUriEncoding() {
return skipUriEncoding;
}

@Override
public Optional<Boolean> sendExpectContinue() {
return Optional.ofNullable(sendExpectContinue);
}

protected abstract R doSubmit(Object entity);

protected abstract R doOutputStream(OutputStreamHandler outputStreamHandler);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2023 Oracle and/or its affiliates.
* Copyright (c) 2025 Oracle and/or its affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -112,4 +112,12 @@ public interface FullClientRequest<T extends ClientRequest<T>> extends ClientReq
* @return whether to skip encoding
*/
boolean skipUriEncoding();

/**
* Whether Expect 100-Continue header is sent to verify server availability before sending
* an entity. Overrides the setting from {@link HttpClientConfig#sendExpectContinue()}.
*
* @return Expect 100-Continue value if set
*/
Optional<Boolean> sendExpectContinue();
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2023, 2024 Oracle and/or its affiliates.
* Copyright (c) 2023, 2025 Oracle and/or its affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -51,7 +51,22 @@ public class HttpClientRequest extends ClientRequestBase<HttpClientRequest, Http
List<LoomClient.ProtocolSpi> tcpProtocols,
List<String> tcpProtocolIds,
LruCache<LoomClient.EndpointKey, HttpClientSpi> clientSpiCache) {
super(clientConfig, webClient.cookieManager(), "any", method, clientUri, clientConfig.properties());
this(webClient, clientConfig, method, clientUri, protocolsToClients, protocols, tcpProtocols,
tcpProtocolIds, null, clientSpiCache);
}

HttpClientRequest(WebClient webClient,
WebClientConfig clientConfig,
Method method,
ClientUri clientUri,
Map<String, LoomClient.ProtocolSpi> protocolsToClients,
List<LoomClient.ProtocolSpi> protocols,
List<LoomClient.ProtocolSpi> tcpProtocols,
List<String> tcpProtocolIds,
Boolean send100Continue,
LruCache<LoomClient.EndpointKey, HttpClientSpi> clientSpiCache) {
super(clientConfig, webClient.cookieManager(), "any", method, clientUri,
send100Continue, clientConfig.properties());
this.webClient = webClient;
this.clients = protocolsToClients;
this.protocols = protocols;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2023 Oracle and/or its affiliates.
* Copyright (c) 2023, 2025 Oracle and/or its affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -114,6 +114,11 @@ public FakeHttpClientRequest readContinueTimeout(Duration readContinueTimeout) {
return this;
}

@Override
public FakeHttpClientRequest sendExpectContinue(boolean sendExpectContinue) {
return null;
}

@Override
public FakeHttpClientRequest tls(Tls tls) {
return this;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2023, 2024 Oracle and/or its affiliates.
* Copyright (c) 2023, 2025 Oracle and/or its affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -354,7 +354,10 @@ private void writeContent(BufferData buffer) throws IOException {
}

private void sendPrologueAndHeader() {
boolean expects100Continue = clientConfig.sendExpectContinue() && !noData;
// setting for expect 100 header, can be overridden for each request
boolean expects100Continue = !noData;
Boolean override100Continue = originalRequest.sendExpectContinue().orElse(null);
expects100Continue &= (override100Continue != null) ? override100Continue : clientConfig.sendExpectContinue();
if (expects100Continue) {
headers.add(HeaderValues.EXPECT_100);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2022, 2023 Oracle and/or its affiliates.
* Copyright (c) 2022, 2025 Oracle and/or its affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -74,6 +74,7 @@ public ClientRequest<?> clientRequest(FullClientRequest<?> clientRequest, Client
Http1ClientRequest request = new Http1ClientRequestImpl(this,
clientRequest.method(),
clientUri,
clientRequest.sendExpectContinue().orElse(null),
clientRequest.properties());

clientRequest.connection().ifPresent(request::connection);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2022, 2024 Oracle and/or its affiliates.
* Copyright (c) 2022, 2025 Oracle and/or its affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -43,10 +43,20 @@ class Http1ClientRequestImpl extends ClientRequestBase<Http1ClientRequest, Http1
Method method,
ClientUri clientUri,
Map<String, String> properties) {
this(http1Client, method, clientUri, null, properties);
}

Http1ClientRequestImpl(Http1ClientImpl http1Client,
Method method,
ClientUri clientUri,
Boolean sendExpectContinue,
Map<String, String> properties) {
super(http1Client.clientConfig(),
http1Client.webClient().cookieManager(),
Http1Client.PROTOCOL_ID,
method, clientUri,
method,
clientUri,
sendExpectContinue,
properties);
this.http1Client = http1Client;
}
Expand All @@ -59,6 +69,7 @@ class Http1ClientRequestImpl extends ClientRequestBase<Http1ClientRequest, Http1
this(request.http1Client,
method,
clientUri,
null,
properties);

followRedirects(request.followRedirects());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/*
* Copyright (c) 2025 Oracle and/or its affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package io.helidon.webclient.tests;

import io.helidon.http.HeaderValues;
import io.helidon.http.Status;
import io.helidon.webclient.api.HttpClientResponse;
import io.helidon.webclient.api.WebClient;
import io.helidon.webclient.http1.Http1Client;
import io.helidon.webserver.http.HttpRouting;
import io.helidon.webserver.testing.junit5.ServerTest;
import io.helidon.webserver.testing.junit5.SetUpRoute;

import org.junit.jupiter.api.Test;

import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;

@ServerTest
class Send100ContinueTest {
private static final byte[] DATA = new byte[] { 1, 2, 3 };

private final WebClient webClient;
private final Http1Client http1Client;

Send100ContinueTest(WebClient webClient, Http1Client http1Client) {
this.webClient = webClient;
this.http1Client = http1Client;
}

@SetUpRoute
static void routing(HttpRouting.Builder router) {
router.post("/100Continue", (req, res) -> {
res.status(req.headers().contains(HeaderValues.EXPECT_100) ?
Status.OK_200 : Status.BAD_REQUEST_400).send();
})
.post("/no100Continue", (req, res) -> {
res.status(req.headers().contains(HeaderValues.EXPECT_100) ?
Status.BAD_REQUEST_400 : Status.OK_200).send();
});
}

@Test
public void test100ContinueDefaultWeb() {
try (HttpClientResponse response = webClient.post("/100Continue")
.outputStream(os -> { os.write(DATA); os.close(); })) {
assertThat(response.status(), is(Status.OK_200));
}
}

@Test
public void no100ContinueWeb() {
try (HttpClientResponse response = webClient.post("/no100Continue")
.sendExpectContinue(false) // turns off 100 continue
.outputStream(os -> { os.write(DATA); os.close(); })) {
assertThat(response.status(), is(Status.OK_200));
}
}

@Test
public void test100ContinueDefaultHttp1() {
try (HttpClientResponse response = http1Client.post("/100Continue")
.outputStream(os -> { os.write(DATA); os.close(); })) {
assertThat(response.status(), is(Status.OK_200));
}
}

@Test
public void no100ContinueHttp1() {
try (HttpClientResponse response = http1Client.post("/no100Continue")
.sendExpectContinue(false) // turns off 100 continue
.outputStream(os -> { os.write(DATA); os.close(); })) {
assertThat(response.status(), is(Status.OK_200));
}
}
}

0 comments on commit c4e9962

Please sign in to comment.