diff --git a/webclient/api/src/main/java/io/helidon/webclient/api/ClientRequest.java b/webclient/api/src/main/java/io/helidon/webclient/api/ClientRequest.java index 49bf7cbae8a..6a0e52803da 100644 --- a/webclient/api/src/main/java/io/helidon/webclient/api/ClientRequest.java +++ b/webclient/api/src/main/java/io/helidon/webclient/api/ClientRequest.java @@ -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. @@ -388,6 +388,16 @@ default ClientResponseTyped 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. */ diff --git a/webclient/api/src/main/java/io/helidon/webclient/api/ClientRequestBase.java b/webclient/api/src/main/java/io/helidon/webclient/api/ClientRequestBase.java index d1cec37df6e..ee467cdd90a 100644 --- a/webclient/api/src/main/java/io/helidon/webclient/api/ClientRequestBase.java +++ b/webclient/api/src/main/java/io/helidon/webclient/api/ClientRequestBase.java @@ -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. @@ -87,6 +87,7 @@ public abstract class ClientRequestBase, R extends Ht private Proxy proxy; private boolean keepAlive; private ClientConnection connection; + private Boolean sendExpectContinue; protected ClientRequestBase(HttpClientConfig clientConfig, WebClientCookieManager cookieManager, @@ -94,11 +95,22 @@ protected ClientRequestBase(HttpClientConfig clientConfig, Method method, ClientUri clientUri, Map properties) { + this(clientConfig, cookieManager, protocolId, method, clientUri, null, properties); + } + + protected ClientRequestBase(HttpClientConfig clientConfig, + WebClientCookieManager cookieManager, + String protocolId, + Method method, + ClientUri clientUri, + Boolean sendExpectContinue, + Map 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(); @@ -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. */ @@ -364,6 +382,11 @@ public boolean skipUriEncoding() { return skipUriEncoding; } + @Override + public Optional sendExpectContinue() { + return Optional.ofNullable(sendExpectContinue); + } + protected abstract R doSubmit(Object entity); protected abstract R doOutputStream(OutputStreamHandler outputStreamHandler); diff --git a/webclient/api/src/main/java/io/helidon/webclient/api/FullClientRequest.java b/webclient/api/src/main/java/io/helidon/webclient/api/FullClientRequest.java index 806ea34318f..d0170e1ca8e 100644 --- a/webclient/api/src/main/java/io/helidon/webclient/api/FullClientRequest.java +++ b/webclient/api/src/main/java/io/helidon/webclient/api/FullClientRequest.java @@ -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. @@ -112,4 +112,12 @@ public interface FullClientRequest> 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 sendExpectContinue(); } diff --git a/webclient/api/src/main/java/io/helidon/webclient/api/HttpClientRequest.java b/webclient/api/src/main/java/io/helidon/webclient/api/HttpClientRequest.java index 20618f33c9d..92ad73d91f8 100644 --- a/webclient/api/src/main/java/io/helidon/webclient/api/HttpClientRequest.java +++ b/webclient/api/src/main/java/io/helidon/webclient/api/HttpClientRequest.java @@ -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. @@ -51,7 +51,22 @@ public class HttpClientRequest extends ClientRequestBase tcpProtocols, List tcpProtocolIds, LruCache 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 protocolsToClients, + List protocols, + List tcpProtocols, + List tcpProtocolIds, + Boolean send100Continue, + LruCache clientSpiCache) { + super(clientConfig, webClient.cookieManager(), "any", method, clientUri, + send100Continue, clientConfig.properties()); this.webClient = webClient; this.clients = protocolsToClients; this.protocols = protocols; diff --git a/webclient/api/src/test/java/io/helidon/webclient/api/HttpClientTest.java b/webclient/api/src/test/java/io/helidon/webclient/api/HttpClientTest.java index 0f0e20461f6..e3ea72af258 100644 --- a/webclient/api/src/test/java/io/helidon/webclient/api/HttpClientTest.java +++ b/webclient/api/src/test/java/io/helidon/webclient/api/HttpClientTest.java @@ -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. @@ -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; diff --git a/webclient/http1/src/main/java/io/helidon/webclient/http1/Http1CallOutputStreamChain.java b/webclient/http1/src/main/java/io/helidon/webclient/http1/Http1CallOutputStreamChain.java index 1532f9d4777..e410621bf65 100644 --- a/webclient/http1/src/main/java/io/helidon/webclient/http1/Http1CallOutputStreamChain.java +++ b/webclient/http1/src/main/java/io/helidon/webclient/http1/Http1CallOutputStreamChain.java @@ -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. @@ -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); } diff --git a/webclient/http1/src/main/java/io/helidon/webclient/http1/Http1ClientImpl.java b/webclient/http1/src/main/java/io/helidon/webclient/http1/Http1ClientImpl.java index f06c4300837..548ee3f0334 100644 --- a/webclient/http1/src/main/java/io/helidon/webclient/http1/Http1ClientImpl.java +++ b/webclient/http1/src/main/java/io/helidon/webclient/http1/Http1ClientImpl.java @@ -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. @@ -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); diff --git a/webclient/http1/src/main/java/io/helidon/webclient/http1/Http1ClientRequestImpl.java b/webclient/http1/src/main/java/io/helidon/webclient/http1/Http1ClientRequestImpl.java index 22260adcaed..50103c3bb12 100644 --- a/webclient/http1/src/main/java/io/helidon/webclient/http1/Http1ClientRequestImpl.java +++ b/webclient/http1/src/main/java/io/helidon/webclient/http1/Http1ClientRequestImpl.java @@ -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. @@ -43,10 +43,20 @@ class Http1ClientRequestImpl extends ClientRequestBase properties) { + this(http1Client, method, clientUri, null, properties); + } + + Http1ClientRequestImpl(Http1ClientImpl http1Client, + Method method, + ClientUri clientUri, + Boolean sendExpectContinue, + Map properties) { super(http1Client.clientConfig(), http1Client.webClient().cookieManager(), Http1Client.PROTOCOL_ID, - method, clientUri, + method, + clientUri, + sendExpectContinue, properties); this.http1Client = http1Client; } @@ -59,6 +69,7 @@ class Http1ClientRequestImpl extends ClientRequestBase { + 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)); + } + } +}