From 9dd559e1aed49203d29a341af3729630a0359c7d Mon Sep 17 00:00:00 2001 From: Santiago Pericas-Geertsen Date: Tue, 11 Feb 2025 09:24:45 -0500 Subject: [PATCH] Initial support for per-request 100 continue. Signed-off-by: Santiago Pericas-Geertsen --- .../helidon/webclient/api/ClientRequest.java | 12 +++- .../webclient/api/ClientRequestBase.java | 25 ++++++- .../webclient/api/FullClientRequest.java | 11 ++- .../webclient/api/HttpClientRequest.java | 19 ++++- .../helidon/webclient/api/HttpClientTest.java | 7 +- .../http1/Http1CallOutputStreamChain.java | 10 ++- .../webclient/http1/Http1ClientImpl.java | 5 +- .../http1/Http1ClientRequestImpl.java | 8 ++- .../webclient/tests/Send100ContinueTest.java | 70 +++++++++++++++++++ 9 files changed, 155 insertions(+), 12 deletions(-) create mode 100644 webclient/tests/http1/src/test/java/io/helidon/webclient/tests/Send100ContinueTest.java 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..0b1aa1cc357 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 any settings 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..3e2456852d0 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,13 @@ 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. Inherited from {@link ClientRequest#sendExpectContinue(boolean)} but + * optionally overridden for each request. + * + * @return sendExpectContinue 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..19c9ab5867a 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,13 @@ 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; + if (originalRequest.sendExpectContinue().isPresent()) { + expects100Continue &= originalRequest.sendExpectContinue().get(); // from request + } else { + expects100Continue &= clientConfig.sendExpectContinue(); // from client config + } 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..57c8e2d29ed 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. @@ -52,7 +52,7 @@ public Http1ClientRequest method(Method method) { clientConfig.baseFragment().ifPresent(clientUri::fragment); clientConfig.baseQuery().ifPresent(clientUri.writeableQuery()::from); - return new Http1ClientRequestImpl(this, method, clientUri, clientConfig.properties()); + return new Http1ClientRequestImpl(this, method, clientUri, null, clientConfig.properties()); } @Override @@ -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..2097a4f2afa 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. @@ -42,11 +42,14 @@ class Http1ClientRequestImpl extends ClientRequestBase properties) { super(http1Client.clientConfig(), http1Client.webClient().cookieManager(), Http1Client.PROTOCOL_ID, - method, clientUri, + method, + clientUri, + sendExpectContinue, properties); this.http1Client = http1Client; } @@ -59,6 +62,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 test100ContinueDefault() { + try (HttpClientResponse response = webClient.post("/100Continue") + .outputStream(os -> { os.write(DATA); os.close(); })) { + assertThat(response.status(), is(Status.OK_200)); + } + } + + @Test + public void no100Continue() { + 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)); + } + } +}