From dd114b6eb576a9438d5f531b7534bebf828f2787 Mon Sep 17 00:00:00 2001 From: Jonas Konrad Date: Fri, 8 Nov 2024 14:33:50 +0100 Subject: [PATCH] Implement netty client using the new micronaut-core RawHttpClient (#989) * Implement netty client using the new micronaut-core RawHttpClient This PR replaces most of the netty-based HTTP client that previously used the low-level micronaut-http-client ConnectionManager API with an implementation based on the new micronaut-http-client-core RawHttpClient introduced in micronaut-core 4.7.0. This offers some advantages: - Much simpler implementation. Once the legacy implementation is not needed anymore, all classes deprecated in this PR can be removed. - Possibility to work with non-netty RawHttpClient implementations, once those exist. In particular, this will allow using the JDK http client instead. - RawHttpClient runs normal micronaut-core ClientFilters, so we won't need the `OciNettyClientFilter` API anymore. While in theory the new implementation should be a drop-in replacement, it is possible that RawHttpClient differs slightly in behavior. For that reason, I've kept the old implementation around. It can be enabled by the `oci.netty.legacy-netty-client` config property, or the `io.micronaut.oraclecloud.httpclient.netty.legacy-netty-client` system property. This also means that MicronautHttpRequest and MicronautHttpResponse are actually mostly copied from NettyHttpRequest and NettyHttpResponse. Please consider that when reviewing, the actual changes are not that big. This PR works and is ready for review, but still needs a release of micronaut-core 4.7.0 (and of micronaut-serialization due to the unrelated https://github.com/micronaut-projects/micronaut-serialization/pull/943 ). * review * small changes * fix test * try fixing test * trigger build --------- Co-authored-by: Sergio del Amo --- .../httpclient/netty/BufferFutureHandler.java | 1 + .../netty/CustomTrustManagerFactory.java | 257 ------------- .../httpclient/netty/DecidedBodyHandler.java | 1 + .../httpclient/netty/DiscardingHandler.java | 1 + .../httpclient/netty/HeaderMap.java | 1 + .../netty/LimitedBufferingBodyHandler.java | 1 + .../netty/LimitedBufferingSubscriber.java | 103 +++++ .../netty/ManagedNettyHttpProvider.java | 17 +- .../httpclient/netty/MicronautHeaderMap.java | 125 +++++++ .../netty/MicronautHttpRequest.java | 351 ++++++++++++++++++ .../netty/MicronautHttpResponse.java | 152 ++++++++ .../httpclient/netty/NettyHttpClient.java | 36 +- .../netty/NettyHttpClientBuilder.java | 2 + .../httpclient/netty/NettyHttpRequest.java | 1 + .../httpclient/netty/NettyHttpResponse.java | 1 + .../netty/OciNettyConfiguration.java | 38 ++ .../netty/StreamReadingHandler.java | 1 + .../netty/StreamWritingHandler.java | 1 + .../netty/UndecidedBodyHandler.java | 1 + .../netty/BufferFutureHandlerTest.java | 1 + .../httpclient/netty/HeaderMapTest.java | 7 +- .../netty/LegacyNettyManagedTest.java | 24 ++ .../httpclient/netty/ManagedPropertyTest.java | 29 +- .../httpclient/netty/ManagedTest.java | 8 +- .../httpclient/netty/NettyHttpClientTest.java | 6 +- .../httpclient/netty/NettyUnmanagedTest.java | 2 +- .../netty/StreamReadingHandlerTest.java | 1 + .../netty/StreamWritingHandlerTest.java | 3 +- .../netty/UndecidedBodyHandlerTest.java | 1 + 29 files changed, 864 insertions(+), 309 deletions(-) delete mode 100644 oraclecloud-httpclient-netty/src/main/java/io/micronaut/oraclecloud/httpclient/netty/CustomTrustManagerFactory.java create mode 100644 oraclecloud-httpclient-netty/src/main/java/io/micronaut/oraclecloud/httpclient/netty/LimitedBufferingSubscriber.java create mode 100644 oraclecloud-httpclient-netty/src/main/java/io/micronaut/oraclecloud/httpclient/netty/MicronautHeaderMap.java create mode 100644 oraclecloud-httpclient-netty/src/main/java/io/micronaut/oraclecloud/httpclient/netty/MicronautHttpRequest.java create mode 100644 oraclecloud-httpclient-netty/src/main/java/io/micronaut/oraclecloud/httpclient/netty/MicronautHttpResponse.java create mode 100644 oraclecloud-httpclient-netty/src/main/java/io/micronaut/oraclecloud/httpclient/netty/OciNettyConfiguration.java create mode 100644 oraclecloud-httpclient-netty/src/test/java/io/micronaut/oraclecloud/httpclient/netty/LegacyNettyManagedTest.java diff --git a/oraclecloud-httpclient-netty/src/main/java/io/micronaut/oraclecloud/httpclient/netty/BufferFutureHandler.java b/oraclecloud-httpclient-netty/src/main/java/io/micronaut/oraclecloud/httpclient/netty/BufferFutureHandler.java index 740346e35..94172070f 100644 --- a/oraclecloud-httpclient-netty/src/main/java/io/micronaut/oraclecloud/httpclient/netty/BufferFutureHandler.java +++ b/oraclecloud-httpclient-netty/src/main/java/io/micronaut/oraclecloud/httpclient/netty/BufferFutureHandler.java @@ -27,6 +27,7 @@ * {@link io.netty.handler.codec.http.LastHttpContent} is received completes a {@link #future} with * the accumulated data. */ +@Deprecated final class BufferFutureHandler extends DecidedBodyHandler { final CompletableFuture future = new CompletableFuture<>(); private CompositeByteBuf buffer; diff --git a/oraclecloud-httpclient-netty/src/main/java/io/micronaut/oraclecloud/httpclient/netty/CustomTrustManagerFactory.java b/oraclecloud-httpclient-netty/src/main/java/io/micronaut/oraclecloud/httpclient/netty/CustomTrustManagerFactory.java deleted file mode 100644 index 8f6863db2..000000000 --- a/oraclecloud-httpclient-netty/src/main/java/io/micronaut/oraclecloud/httpclient/netty/CustomTrustManagerFactory.java +++ /dev/null @@ -1,257 +0,0 @@ -/* - * Copyright 2017-2022 original authors - * - * 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 - * - * https://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.micronaut.oraclecloud.httpclient.netty; - -import io.netty.handler.ssl.util.SimpleTrustManagerFactory; - -import javax.net.ssl.ExtendedSSLSession; -import javax.net.ssl.HostnameVerifier; -import javax.net.ssl.ManagerFactoryParameters; -import javax.net.ssl.SNIServerName; -import javax.net.ssl.SSLEngine; -import javax.net.ssl.SSLPeerUnverifiedException; -import javax.net.ssl.SSLSessionContext; -import javax.net.ssl.TrustManager; -import javax.net.ssl.TrustManagerFactory; -import javax.net.ssl.X509ExtendedTrustManager; -import java.net.Socket; -import java.security.KeyStore; -import java.security.Principal; -import java.security.cert.Certificate; -import java.security.cert.CertificateException; -import java.security.cert.X509Certificate; -import java.util.Arrays; -import java.util.List; - -/** - * Wrapper around {@link TrustManagerFactory} that falls back to a {@link HostnameVerifier} if the normal cert check - * fails. - */ -final class CustomTrustManagerFactory extends SimpleTrustManagerFactory { - private final TrustManagerFactory delegate; - private final HostnameVerifier fallback; - - CustomTrustManagerFactory(TrustManagerFactory delegate, HostnameVerifier fallback) { - this.delegate = delegate; - this.fallback = fallback; - } - - @Override - protected void engineInit(KeyStore keyStore) throws Exception { - delegate.init(keyStore); - } - - @Override - protected void engineInit(ManagerFactoryParameters managerFactoryParameters) throws Exception { - delegate.init(managerFactoryParameters); - } - - @Override - protected TrustManager[] engineGetTrustManagers() { - return Arrays.stream(delegate.getTrustManagers()) - .map(tm -> new CustomTrustManager((X509ExtendedTrustManager) tm, fallback)) - .toArray(TrustManager[]::new); - } - - private static final class CustomTrustManager extends X509ExtendedTrustManager { - private final X509ExtendedTrustManager delegate; - private final HostnameVerifier fallback; - - CustomTrustManager(X509ExtendedTrustManager delegate, HostnameVerifier fallback) { - this.delegate = delegate; - this.fallback = fallback; - } - - @Override - public void checkServerTrusted(X509Certificate[] chain, String authType, SSLEngine engine) throws CertificateException { - try { - delegate.checkServerTrusted(chain, authType, engine); - } catch (CertificateException e) { - // note: getPeerHost javadoc says the value cannot be trusted. However, this value is also used by - // OpenJDK X509TrustManagerImpl. Debugging also shows this value does not come from the handshake, it's - // the value we pass to sslContext.newHandler. So it should be fine. - String host = engine.getPeerHost(); - // wrap the SSLSession to return the right certificate chain. The chain is only set after - // checkServerTrusted succeeds, so we need to do extra work to provide it to the hostname verifier. - CustomSslSession session = new CustomSslSession((ExtendedSSLSession) engine.getHandshakeSession(), chain); - if (!fallback.verify(host, session)) { - throw e; - } - } - } - - @Override - public X509Certificate[] getAcceptedIssuers() { - return delegate.getAcceptedIssuers(); - } - - @Override - public void checkClientTrusted(X509Certificate[] chain, String authType) { - throw new UnsupportedOperationException(); - } - - @Override - public void checkServerTrusted(X509Certificate[] chain, String authType) { - throw new UnsupportedOperationException(); - } - - @Override - public void checkClientTrusted(X509Certificate[] chain, String authType, Socket socket) { - throw new UnsupportedOperationException(); - } - - @Override - public void checkServerTrusted(X509Certificate[] chain, String authType, Socket socket) { - throw new UnsupportedOperationException(); - } - - @Override - public void checkClientTrusted(X509Certificate[] chain, String authType, SSLEngine engine) { - throw new UnsupportedOperationException(); - } - } - - private static class CustomSslSession extends ExtendedSSLSession { - private final ExtendedSSLSession delegate; - private final X509Certificate[] peerCertificates; - - CustomSslSession(ExtendedSSLSession delegate, X509Certificate[] peerCertificates) { - this.delegate = delegate; - this.peerCertificates = peerCertificates; - } - - @Override - public String[] getLocalSupportedSignatureAlgorithms() { - return delegate.getLocalSupportedSignatureAlgorithms(); - } - - @Override - public String[] getPeerSupportedSignatureAlgorithms() { - return delegate.getPeerSupportedSignatureAlgorithms(); - } - - @Override - public List getRequestedServerNames() { - return delegate.getRequestedServerNames(); - } - - @Override - public byte[] getId() { - return delegate.getId(); - } - - @Override - public SSLSessionContext getSessionContext() { - return delegate.getSessionContext(); - } - - @Override - public long getCreationTime() { - return delegate.getCreationTime(); - } - - @Override - public long getLastAccessedTime() { - return delegate.getLastAccessedTime(); - } - - @Override - public void invalidate() { - delegate.invalidate(); - } - - @Override - public boolean isValid() { - return delegate.isValid(); - } - - @Override - public void putValue(String name, Object value) { - delegate.putValue(name, value); - } - - @Override - public Object getValue(String name) { - return delegate.getValue(name); - } - - @Override - public void removeValue(String name) { - delegate.removeValue(name); - } - - @Override - public String[] getValueNames() { - return delegate.getValueNames(); - } - - @Override - public Certificate[] getPeerCertificates() { - return peerCertificates; - } - - @Override - public Certificate[] getLocalCertificates() { - return delegate.getLocalCertificates(); - } - - @Override - public Principal getPeerPrincipal() throws SSLPeerUnverifiedException { - return delegate.getPeerPrincipal(); - } - - @Override - public Principal getLocalPrincipal() { - return delegate.getLocalPrincipal(); - } - - @Override - public String getCipherSuite() { - return delegate.getCipherSuite(); - } - - @Override - public String getProtocol() { - return delegate.getProtocol(); - } - - @Override - public String getPeerHost() { - return delegate.getPeerHost(); - } - - @Override - public int getPeerPort() { - return delegate.getPeerPort(); - } - - @Override - public int getPacketBufferSize() { - return delegate.getPacketBufferSize(); - } - - @Override - public int getApplicationBufferSize() { - return delegate.getApplicationBufferSize(); - } - - @Deprecated - @Override - public javax.security.cert.X509Certificate[] getPeerCertificateChain() { - throw new UnsupportedOperationException(); - } - } -} diff --git a/oraclecloud-httpclient-netty/src/main/java/io/micronaut/oraclecloud/httpclient/netty/DecidedBodyHandler.java b/oraclecloud-httpclient-netty/src/main/java/io/micronaut/oraclecloud/httpclient/netty/DecidedBodyHandler.java index 2d779e6c4..790b57fc9 100644 --- a/oraclecloud-httpclient-netty/src/main/java/io/micronaut/oraclecloud/httpclient/netty/DecidedBodyHandler.java +++ b/oraclecloud-httpclient-netty/src/main/java/io/micronaut/oraclecloud/httpclient/netty/DecidedBodyHandler.java @@ -32,6 +32,7 @@ * consume the body. First any data buffered by {@link UndecidedBodyHandler} is processed, then a {@link HandlerImpl} * is added to the pipeline to directly process the remaining chunks. */ +@Deprecated abstract class DecidedBodyHandler { private boolean done = false; private volatile ChannelHandlerContext context; diff --git a/oraclecloud-httpclient-netty/src/main/java/io/micronaut/oraclecloud/httpclient/netty/DiscardingHandler.java b/oraclecloud-httpclient-netty/src/main/java/io/micronaut/oraclecloud/httpclient/netty/DiscardingHandler.java index d28e8cba6..1536d4fb5 100644 --- a/oraclecloud-httpclient-netty/src/main/java/io/micronaut/oraclecloud/httpclient/netty/DiscardingHandler.java +++ b/oraclecloud-httpclient-netty/src/main/java/io/micronaut/oraclecloud/httpclient/netty/DiscardingHandler.java @@ -20,6 +20,7 @@ /** * Handler that discards incoming data. */ +@Deprecated final class DiscardingHandler extends DecidedBodyHandler { static final DiscardingHandler INSTANCE = new DiscardingHandler(); diff --git a/oraclecloud-httpclient-netty/src/main/java/io/micronaut/oraclecloud/httpclient/netty/HeaderMap.java b/oraclecloud-httpclient-netty/src/main/java/io/micronaut/oraclecloud/httpclient/netty/HeaderMap.java index 077b1cbf8..846f433e1 100644 --- a/oraclecloud-httpclient-netty/src/main/java/io/micronaut/oraclecloud/httpclient/netty/HeaderMap.java +++ b/oraclecloud-httpclient-netty/src/main/java/io/micronaut/oraclecloud/httpclient/netty/HeaderMap.java @@ -26,6 +26,7 @@ /** * {@link java.util.Map} wrapper around netty {@link HttpHeaders}. Read-only. */ +@Deprecated final class HeaderMap extends AbstractMap> { private final HttpHeaders headers; diff --git a/oraclecloud-httpclient-netty/src/main/java/io/micronaut/oraclecloud/httpclient/netty/LimitedBufferingBodyHandler.java b/oraclecloud-httpclient-netty/src/main/java/io/micronaut/oraclecloud/httpclient/netty/LimitedBufferingBodyHandler.java index eaa1e1e0b..7c5a239ba 100644 --- a/oraclecloud-httpclient-netty/src/main/java/io/micronaut/oraclecloud/httpclient/netty/LimitedBufferingBodyHandler.java +++ b/oraclecloud-httpclient-netty/src/main/java/io/micronaut/oraclecloud/httpclient/netty/LimitedBufferingBodyHandler.java @@ -28,6 +28,7 @@ * Handler that buffers some response bytes until a set limit. This way, when normal body reading fails, we can still * read a potentially short error message from this handler. */ +@Deprecated final class LimitedBufferingBodyHandler extends ChannelInboundHandlerAdapter { private final int maxBuffer; private CompositeByteBuf buffer; diff --git a/oraclecloud-httpclient-netty/src/main/java/io/micronaut/oraclecloud/httpclient/netty/LimitedBufferingSubscriber.java b/oraclecloud-httpclient-netty/src/main/java/io/micronaut/oraclecloud/httpclient/netty/LimitedBufferingSubscriber.java new file mode 100644 index 000000000..467fc34e9 --- /dev/null +++ b/oraclecloud-httpclient-netty/src/main/java/io/micronaut/oraclecloud/httpclient/netty/LimitedBufferingSubscriber.java @@ -0,0 +1,103 @@ +/* + * Copyright 2017-2024 original authors + * + * 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 + * + * https://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.micronaut.oraclecloud.httpclient.netty; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.io.buffer.ByteBuffer; +import io.micronaut.core.io.buffer.ReferenceCounted; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; + +import java.io.ByteArrayOutputStream; +import java.io.Closeable; +import java.io.IOException; +import java.util.concurrent.CompletableFuture; + +/** + * {@link Subscriber} implementation that buffers its input up to a certain number of bytes. + * + * @author Jonas Konrad + * @since 4.3 + */ +@Internal +final class LimitedBufferingSubscriber implements Subscriber>, Closeable { + final CompletableFuture future = new CompletableFuture<>(); + + private final int maxBuffer; + private final ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + + private boolean closed; + private Subscription subscription; + + LimitedBufferingSubscriber(int maxBuffer) { + this.maxBuffer = maxBuffer; + } + + @Override + public void onSubscribe(Subscription s) { + boolean closed; + synchronized (this) { + closed = this.closed; + if (!closed) { + this.subscription = s; + } + } + if (closed) { + s.cancel(); + } + } + + @Override + public void onNext(ByteBuffer byteBuffer) { + try { + byteBuffer.toInputStream().transferTo(buffer); + } catch (IOException e) { + future.completeExceptionally(e); + } + if (byteBuffer instanceof ReferenceCounted rc) { + rc.release(); + } + if (buffer.size() >= maxBuffer) { + future.completeExceptionally(new IOException("Request body was streamed and too large for opportunistic buffering")); + subscription.cancel(); + } else { + subscription.request(1); + } + } + + @Override + public void onError(Throwable t) { + future.completeExceptionally(t); + } + + @Override + public void onComplete() { + future.complete(buffer.toByteArray()); + } + + @Override + public void close() { + synchronized (this) { + if (!closed) { + if (subscription != null) { + subscription.cancel(); + subscription = null; + } + closed = true; + } + } + } +} diff --git a/oraclecloud-httpclient-netty/src/main/java/io/micronaut/oraclecloud/httpclient/netty/ManagedNettyHttpProvider.java b/oraclecloud-httpclient-netty/src/main/java/io/micronaut/oraclecloud/httpclient/netty/ManagedNettyHttpProvider.java index 55f9268fe..3606e9fd6 100644 --- a/oraclecloud-httpclient-netty/src/main/java/io/micronaut/oraclecloud/httpclient/netty/ManagedNettyHttpProvider.java +++ b/oraclecloud-httpclient-netty/src/main/java/io/micronaut/oraclecloud/httpclient/netty/ManagedNettyHttpProvider.java @@ -22,7 +22,8 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.Nullable; import io.micronaut.http.client.HttpClient; -import io.micronaut.http.client.HttpClientRegistry; +import io.micronaut.http.client.RawHttpClient; +import io.micronaut.http.client.RawHttpClientRegistry; import io.micronaut.json.JsonMapper; import io.micronaut.oraclecloud.serde.OciSdkMicronautSerializer; import io.micronaut.oraclecloud.serde.OciSerdeConfiguration; @@ -50,8 +51,8 @@ public class ManagedNettyHttpProvider implements HttpProvider { static final String SERVICE_ID = "oci"; - final HttpClientRegistry mnHttpClientRegistry; - final HttpClient mnHttpClient; + final RawHttpClientRegistry mnHttpClientRegistry; + final RawHttpClient mnHttpClient; final List> nettyClientFilters; /** @@ -60,17 +61,20 @@ public class ManagedNettyHttpProvider implements HttpProvider { @Nullable final ExecutorService ioExecutor; final JsonMapper jsonMapper; + final OciNettyConfiguration configuration; @Inject public ManagedNettyHttpProvider( - HttpClientRegistry mnHttpClientRegistry, + RawHttpClientRegistry mnHttpClientRegistry, @Named(TaskExecutors.BLOCKING) @Nullable ExecutorService ioExecutor, ObjectMapper jsonMapper, OciSerdeConfiguration ociSerdeConfiguration, OciSerializationConfiguration ociSerializationConfiguration, - @Nullable List> nettyClientFilters + @Nullable List> nettyClientFilters, + @Nullable OciNettyConfiguration configuration ) { this.mnHttpClientRegistry = mnHttpClientRegistry; + this.configuration = configuration == null ? new OciNettyConfiguration(false) : configuration; this.mnHttpClient = null; this.ioExecutor = ioExecutor; this.jsonMapper = jsonMapper.cloneWithConfiguration(ociSerdeConfiguration, ociSerializationConfiguration, null); @@ -84,10 +88,11 @@ public ManagedNettyHttpProvider( @Nullable List> nettyClientFilters ) { this.mnHttpClientRegistry = null; - this.mnHttpClient = mnHttpClient; + this.mnHttpClient = (RawHttpClient) mnHttpClient; this.ioExecutor = ioExecutor; this.jsonMapper = OciSdkMicronautSerializer.getDefaultObjectMapper(); this.nettyClientFilters = nettyClientFilters == null ? Collections.emptyList() : nettyClientFilters; + this.configuration = new OciNettyConfiguration(false); } @Override diff --git a/oraclecloud-httpclient-netty/src/main/java/io/micronaut/oraclecloud/httpclient/netty/MicronautHeaderMap.java b/oraclecloud-httpclient-netty/src/main/java/io/micronaut/oraclecloud/httpclient/netty/MicronautHeaderMap.java new file mode 100644 index 000000000..e6b8a8daf --- /dev/null +++ b/oraclecloud-httpclient-netty/src/main/java/io/micronaut/oraclecloud/httpclient/netty/MicronautHeaderMap.java @@ -0,0 +1,125 @@ +/* + * Copyright 2017-2022 original authors + * + * 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 + * + * https://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.micronaut.oraclecloud.httpclient.netty; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.http.HttpHeaders; +import io.micronaut.http.MutableHttpHeaders; + +import java.util.AbstractMap; +import java.util.AbstractSet; +import java.util.Iterator; +import java.util.List; +import java.util.Set; + +/** + * {@link java.util.Map} wrapper around micronaut {@link HttpHeaders}. + */ +@Internal +final class MicronautHeaderMap extends AbstractMap> { + private final HttpHeaders headers; + + MicronautHeaderMap(HttpHeaders headers) { + this.headers = headers; + } + + @Override + public Set>> entrySet() { + return new AbstractSet>>() { + @Override + public Iterator>> iterator() { + return new HeaderIterator(headers); + } + + @Override + public int size() { + return headers.names().size(); + } + }; + } + + @Override + public List get(Object key) { + if (!(key instanceof String)) { + return null; + } + List found = headers.getAll((String) key); + return found.isEmpty() ? null : found; + } + + @Override + public List remove(Object key) { + if (!(key instanceof String s)) { + return null; + } + List items = headers.getAll(s); + ((MutableHttpHeaders) headers).remove(s); + return items.isEmpty() ? null : items; // follow remove() contract + } + + @Override + public boolean containsKey(Object key) { + return key instanceof String s && headers.contains(s); + } + + @Override + public Set keySet() { + return new KeySet(); + } + + private final class KeySet extends AbstractSet { + @Override + public boolean contains(Object o) { + return containsKey(o); + } + + @Override + public boolean remove(Object o) { + return MicronautHeaderMap.this.remove(o) != null; + } + + @Override + public Iterator iterator() { + return headers.names().iterator(); + } + + @Override + public int size() { + return headers.names().size(); + } + } + + private static final class HeaderIterator implements Iterator>> { + final HttpHeaders headers; + final Iterator keyItr; + + HeaderIterator(HttpHeaders headers) { + this.headers = headers; + keyItr = headers.names().iterator(); + } + + @Override + public boolean hasNext() { + return keyItr.hasNext(); + } + + @Override + public Entry> next() { + String key = keyItr.next(); + return new SimpleEntry<>(key, headers.getAll(key)); + } + } +} diff --git a/oraclecloud-httpclient-netty/src/main/java/io/micronaut/oraclecloud/httpclient/netty/MicronautHttpRequest.java b/oraclecloud-httpclient-netty/src/main/java/io/micronaut/oraclecloud/httpclient/netty/MicronautHttpRequest.java new file mode 100644 index 000000000..3216d3409 --- /dev/null +++ b/oraclecloud-httpclient-netty/src/main/java/io/micronaut/oraclecloud/httpclient/netty/MicronautHttpRequest.java @@ -0,0 +1,351 @@ +/* + * Copyright 2017-2022 original authors + * + * 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 + * + * https://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.micronaut.oraclecloud.httpclient.netty; + +import com.oracle.bmc.http.client.HttpRequest; +import com.oracle.bmc.http.client.HttpResponse; +import com.oracle.bmc.http.client.Method; +import com.oracle.bmc.http.client.RequestInterceptor; +import io.micronaut.buffer.netty.NettyByteBufferFactory; +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.http.HttpHeaders; +import io.micronaut.http.HttpMethod; +import io.micronaut.http.MutableHttpRequest; +import io.micronaut.http.body.AvailableByteBody; +import io.micronaut.http.body.ByteBody; +import io.micronaut.http.body.CloseableByteBody; +import io.micronaut.http.body.stream.InputStreamByteBody; +import io.micronaut.http.netty.body.AvailableNettyByteBody; +import io.netty.buffer.ByteBufUtil; +import io.netty.handler.codec.http.HttpHeaderNames; +import io.netty.handler.codec.http.HttpHeaderValues; +import reactor.core.publisher.Mono; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.nio.CharBuffer; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.OptionalLong; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.Executor; + +import static io.micronaut.oraclecloud.httpclient.netty.NettyClientProperties.CLASS_AND_METHOD_KEY_NAME; + +@Internal +final class MicronautHttpRequest implements HttpRequest { + + private static final long UNKNOWN_CONTENT_LENGTH = -1; + + private final NettyHttpClient client; + + private final Map attributes; + + private final MutableHttpRequest mnRequest; + + private final StringBuilder uri; + private final StringBuilder query; + + private Executor offloadExecutor; + private Thread blockHint; + + private boolean expectContinue; + private Object returningBody; + @Nullable + private CloseableByteBody byteBody; + + public MicronautHttpRequest(NettyHttpClient nettyHttpClient, Method method) { + client = nettyHttpClient; + this.uri = new StringBuilder(client.baseUri.toString()); + attributes = new HashMap<>(); + StackWalker.StackFrame frame = StackWalker.getInstance().walk(s -> s.filter(stackFrame -> stackFrame.getClassName().contains("com.oracle.bmc") && !stackFrame.getClassName().contains("com.oracle.bmc.http.internal")).toList()).stream().findFirst().orElse(null); + attributes.put(CLASS_AND_METHOD_KEY_NAME, frame == null ? "N/A" : Arrays.stream(frame.getClassName().split("\\.")).reduce((first, second) -> second).orElse("N/A") + "." + frame.getMethodName()); + query = new StringBuilder(); + mnRequest = io.micronaut.http.HttpRequest.create(switch (method) { + case GET -> HttpMethod.GET; + case HEAD -> HttpMethod.HEAD; + case DELETE -> HttpMethod.DELETE; + case POST -> HttpMethod.POST; + case PUT -> HttpMethod.PUT; + case PATCH -> HttpMethod.PATCH; + }, ""); + } + + private MicronautHttpRequest(MicronautHttpRequest from) { + this.client = from.client; + this.attributes = new HashMap<>(from.attributes); + this.mnRequest = copyRequest(from.mnRequest); + this.uri = new StringBuilder(from.uri); + this.query = new StringBuilder(from.query); + this.offloadExecutor = from.offloadExecutor; + this.blockHint = from.blockHint; + this.expectContinue = from.expectContinue; + + this.returningBody = from.returningBody; + this.byteBody = from.byteBody == null ? null : from.byteBody.split(ByteBody.SplitBackpressureMode.FASTEST); // todo + } + + private static MutableHttpRequest copyRequest(io.micronaut.http.HttpRequest original) { + MutableHttpRequest req = io.micronaut.http.HttpRequest.create(original.getMethod(), original.getUri().toString()); + for (Map.Entry> entry : original.getHeaders()) { + for (String value : entry.getValue()) { + req.getHeaders().add(entry.getKey(), value); + } + } + return req; + } + + @Override + public Method method() { + return switch (mnRequest.getMethod()) { + case GET -> Method.GET; + case HEAD -> Method.HEAD; + case POST -> Method.POST; + case PUT -> Method.PUT; + case DELETE -> Method.DELETE; + case PATCH -> Method.PATCH; + default -> throw new UnsupportedOperationException("Unsupported method: " + mnRequest.getMethodName()); + }; + } + + @Override + public HttpRequest body(Object body) { + if (byteBody != null) { + byteBody.close(); + } + + if (body instanceof String) { + byteBody = new AvailableNettyByteBody(ByteBufUtil.encodeString(client.alloc(), CharBuffer.wrap((CharSequence) body), StandardCharsets.UTF_8)); + returningBody = body; + } else if (body instanceof InputStream) { + body((InputStream) body, UNKNOWN_CONTENT_LENGTH); + } else if (body == null) { + byteBody = AvailableNettyByteBody.empty(); + returningBody = ""; + } else { + // todo: would be better to write directly to ByteBuf here, but RequestSignerImpl does not yet support + // anything but String + String json; + try { + json = client.jsonMapper.writeValueAsString(body); + } catch (IOException e) { + throw new IllegalArgumentException("Unable to process JSON body", e); + } + byteBody = new AvailableNettyByteBody(ByteBufUtil.encodeString(client.alloc(), CharBuffer.wrap(json), StandardCharsets.UTF_8)); + returningBody = json; + } + return this; + } + + @Override + public HttpRequest body(InputStream body, long contentLength) { + byteBody = InputStreamByteBody.create( + body, + contentLength == UNKNOWN_CONTENT_LENGTH ? OptionalLong.empty() : OptionalLong.of(contentLength), + client.blockingIoExecutor, + NettyByteBufferFactory.DEFAULT + ); + returningBody = body; + return this; + } + + @Override + public Object body() { + return returningBody; + } + + @Override + public HttpRequest appendPathPart(String encodedPathPart) { + boolean hasSlashLeft = uri.charAt(uri.length() - 1) == '/'; + boolean hasSlashRight = encodedPathPart.startsWith("/"); + if (hasSlashLeft) { + if (hasSlashRight) { + uri.append(encodedPathPart, 1, encodedPathPart.length()); + } else { + uri.append(encodedPathPart); + } + } else { + if (hasSlashRight) { + uri.append(encodedPathPart); + } else { + uri.append('/').append(encodedPathPart); + } + } + return this; + } + + @Override + public HttpRequest query(String name, String value) { + if (!query.isEmpty()) { + query.append('&'); + } + query.append(name).append('=').append(value); + return this; + } + + private String buildUri() { + int length = uri.length(); + if (!query.isEmpty()) { + uri.append('?').append(query); + } + String built = uri.toString(); + uri.setLength(length); // remove query again + return built; + } + + @Override + public URI uri() { + return URI.create(buildUri()); + } + + @Override + public HttpRequest header(String name, String value) { + mnRequest.header(name, value); + if (HttpHeaderNames.EXPECT.contentEqualsIgnoreCase(name)) { + expectContinue = HttpHeaderValues.CONTINUE.contentEqualsIgnoreCase(value); + } + return this; + } + + @Override + public Map> headers() { + return new MicronautHeaderMap(mnRequest.getHeaders()); + } + + @Override + public Object attribute(String name) { + return attributes.get(name); + } + + @Override + public HttpRequest removeAttribute(String name) { + attributes.remove(name); + return this; + } + + @Override + public HttpRequest attribute(String name, Object value) { + attributes.put(name, value); + return this; + } + + @Override + public HttpRequest offloadExecutor(Executor offloadExecutor) { + this.offloadExecutor = offloadExecutor; + // this is technically not what this setter is for, but offloadExecutor() is only called in + // callSync and always at top level, i.e. in the thread that will actually block. + this.blockHint = Thread.currentThread(); + return this; + } + + @Override + public HttpRequest copy() { + return new MicronautHttpRequest(this); + } + + @Override + public void discard() { + if (byteBody != null) { + byteBody.close(); + } + } + + @Override + public CompletionStage execute() { + // jersey client buffers even when BUFFER_REQUEST is off, if the content length is not explicitly set. + if (byteBody != null && !(byteBody instanceof AvailableByteBody) && (client.buffered || byteBody.expectedLength().isEmpty()) && !expectContinue) { + + // asynchronously buffer the body, then run execute() again + return byteBody.buffer() + .thenCompose(v -> { + this.byteBody = v; + return execute(); + }); + } + + for (RequestInterceptor interceptor : client.requestInterceptors) { + interceptor.intercept(this); + } + + finalizeRequest(); + + List filterState = new ArrayList<>(client.nettyClientFilter.size()); + for (OciNettyClientFilter filter : client.nettyClientFilter) { + filterState.add(filter.beforeRequest(this)); + } + + return Mono.from(client.upstreamHttpClient.exchange(mnRequest, byteBody, blockHint)) + .toFuture() + .thenApply(r -> (HttpResponse) new MicronautHttpResponse(client.jsonMapper, r, offloadExecutor)) + .exceptionallyCompose(e -> runResponseFilters(filterState, null, e)) + .thenCompose(r -> runResponseFilters(filterState, r, null)); + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + private CompletableFuture runResponseFilters(List filterState, HttpResponse response, Throwable exception) { + if (exception instanceof CompletionException && exception.getCause() != null) { + exception = exception.getCause(); + } + + for (int i = client.nettyClientFilter.size() - 1; i >= 0; i--) { + try { + ((OciNettyClientFilter) client.nettyClientFilter.get(i)) + .afterResponse(this, response, exception, filterState.get(i)); + } catch (Exception e) { + if (exception == null) { + response.close(); + response = null; + } else { + e.addSuppressed(exception); + } + exception = e; + } + } + if (exception != null) { + return CompletableFuture.failedFuture(exception); + } else { + return CompletableFuture.completedFuture(response); + } + } + + private void finalizeRequest() { + String uriString = buildUri(); + + URI uri = URI.create(uriString); + mnRequest.uri(uri); + if (!mnRequest.getHeaders().contains(HttpHeaders.HOST)) { + mnRequest.getHeaders().add(HttpHeaderNames.HOST, uri.getHost()); + } + + if (!mnRequest.getHeaders().contains(HttpHeaders.CONTENT_LENGTH) && !mnRequest.getHeaders().contains(HttpHeaders.TRANSFER_ENCODING)) { + // the RawHttpClient would set these headers, but they need to be visible from filters + OptionalLong contentLength = byteBody == null ? OptionalLong.of(0) : byteBody.expectedLength(); + if (contentLength.isPresent()) { + mnRequest.getHeaders().add(HttpHeaders.CONTENT_LENGTH, String.valueOf(contentLength.getAsLong())); + } else { + mnRequest.getHeaders().add(HttpHeaders.TRANSFER_ENCODING, "chunked"); + } + } + } +} diff --git a/oraclecloud-httpclient-netty/src/main/java/io/micronaut/oraclecloud/httpclient/netty/MicronautHttpResponse.java b/oraclecloud-httpclient-netty/src/main/java/io/micronaut/oraclecloud/httpclient/netty/MicronautHttpResponse.java new file mode 100644 index 000000000..5592f7dde --- /dev/null +++ b/oraclecloud-httpclient-netty/src/main/java/io/micronaut/oraclecloud/httpclient/netty/MicronautHttpResponse.java @@ -0,0 +1,152 @@ +/* + * Copyright 2017-2022 original authors + * + * 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 + * + * https://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.micronaut.oraclecloud.httpclient.netty; + +import com.oracle.bmc.http.client.HttpResponse; +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.type.Argument; +import io.micronaut.http.ByteBodyHttpResponse; +import io.micronaut.http.body.AvailableByteBody; +import io.micronaut.http.body.ByteBody; +import io.micronaut.json.JsonMapper; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.Executor; +import java.util.function.Function; + +@Internal +final class MicronautHttpResponse implements HttpResponse { + private final JsonMapper jsonMapper; + private final io.micronaut.http.HttpResponse mnResponse; + private final Executor offloadExecutor; + private LimitedBufferingSubscriber limitedBufferingSubscriber; + + MicronautHttpResponse(JsonMapper jsonMapper, io.micronaut.http.HttpResponse mnResponse, Executor offloadExecutor) { + this.jsonMapper = jsonMapper; + this.mnResponse = mnResponse; + this.offloadExecutor = offloadExecutor; + } + + @Override + public int status() { + return mnResponse.code(); + } + + @Override + public Map> headers() { + return new MicronautHeaderMap(mnResponse.getHeaders()); + } + + private ByteBody byteBody() { + if (!(mnResponse instanceof ByteBodyHttpResponse bbhr)) { + throw new UnsupportedOperationException("A micronaut client filter replaced the HTTP response. This is not supported for the micronaut-oracle-cloud HTTP client."); + } + return bbhr.byteBody(); + } + + @Override + public CompletionStage streamBody() { + ByteBody byteBody = byteBody(); + limitedBufferingSubscriber = new LimitedBufferingSubscriber(4096); + byteBody.split(ByteBody.SplitBackpressureMode.SLOWEST).toByteBufferPublisher().subscribe(limitedBufferingSubscriber); + return CompletableFuture.completedFuture(byteBody.toInputStream()); + } + + /** + * Get the body as a buffer, falling back to {@link LimitedBufferingBodyHandler} if the body has already been + * requested previously as another type. + */ + private CompletableFuture bodyAsBuffer() { + if (limitedBufferingSubscriber != null) { + return limitedBufferingSubscriber.future; + } else { + return byteBody().buffer().thenApply(AvailableByteBody::toByteArray); + } + } + + @Override + public CompletionStage body(Class type) { + return thenApply(bodyAsBuffer(), buf -> { + try { + if (buf.length == 0) { + /* This is a bit weird. jax-rs Response.readEntity says: + * "for a zero-length response entities returns a corresponding Java object + * that represents zero-length data." + * This appears to refer to types like byte[] and String, which return an empty + * array or string when the body is empty. + * + * For complex types, this behavior comes from jackson, and is explicitly + * against the jax-rs standard: + * https://github.com/FasterXML/jackson-jaxrs-providers/issues/49 + * Basically, by default (which oci-sdk uses), jackson returns null when the + * body is empty. + * + * We replicate the jackson behavior here. We don't replicate the behavior for + * byte[] and String, those should usually go through textBody or other body + * methods anyway. + */ + return null; + } + + return jsonMapper.readValue(buf, type); + } catch (IOException e) { + throw new CompletionException(e); + } + }); + } + + @Override + public CompletionStage> listBody(Class type) { + Argument> listArgument = Argument.listOf(type); + return thenApply(bodyAsBuffer(), buf -> { + try { + return jsonMapper.readValue(buf, listArgument); + } catch (IOException e) { + throw new CompletionException(e); + } + }); + } + + @Override + public CompletionStage textBody() { + return thenApply(bodyAsBuffer(), buf -> new String(buf, StandardCharsets.UTF_8)); + } + + private CompletionStage thenApply(CompletionStage stage, Function fn) { + if (offloadExecutor == null) { + return stage.thenApply(fn); + } else { + return stage.thenApplyAsync(fn, offloadExecutor); + } + } + + @Override + public void close() { + if (limitedBufferingSubscriber != null) { + limitedBufferingSubscriber.close(); + } + if (mnResponse instanceof ByteBodyHttpResponse c) { + c.close(); + } + } +} diff --git a/oraclecloud-httpclient-netty/src/main/java/io/micronaut/oraclecloud/httpclient/netty/NettyHttpClient.java b/oraclecloud-httpclient-netty/src/main/java/io/micronaut/oraclecloud/httpclient/netty/NettyHttpClient.java index 5286eeb37..6d164eba4 100644 --- a/oraclecloud-httpclient-netty/src/main/java/io/micronaut/oraclecloud/httpclient/netty/NettyHttpClient.java +++ b/oraclecloud-httpclient-netty/src/main/java/io/micronaut/oraclecloud/httpclient/netty/NettyHttpClient.java @@ -23,9 +23,12 @@ import com.oracle.bmc.http.client.Method; import com.oracle.bmc.http.client.RequestInterceptor; import com.oracle.bmc.http.client.StandardClientProperties; +import io.micronaut.core.annotation.Internal; import io.micronaut.core.order.OrderUtil; import io.micronaut.http.client.DefaultHttpClientConfiguration; import io.micronaut.http.client.HttpVersionSelection; +import io.micronaut.http.client.RawHttpClient; +import io.micronaut.http.client.exceptions.ResponseClosedException; import io.micronaut.http.client.netty.ConnectionManager; import io.micronaut.http.client.netty.DefaultHttpClient; import io.micronaut.json.JsonMapper; @@ -34,7 +37,6 @@ import io.netty.handler.codec.PrematureChannelClosureException; import io.netty.handler.timeout.ReadTimeoutException; -import java.io.Closeable; import java.io.IOException; import java.io.UncheckedIOException; import java.net.URI; @@ -50,13 +52,17 @@ import static io.micronaut.oraclecloud.httpclient.netty.NettyClientProperties.OCI_NETTY_CLIENT_FILTERS_KEY; +@Internal final class NettyHttpClient implements HttpClient { /** - * Default settings of {@link com.oracle.bmc.ClientConfiguration}. They are set by BaseClient, + * Default settings of {@link ClientConfiguration}. They are set by BaseClient, * so we ignore them if they are the default value. */ private static final Map, Object> EXPECTED_PROPERTIES; + private static final boolean LEGACY_NETTY_CLIENT = Boolean.getBoolean("io.micronaut.oraclecloud.httpclient.netty.legacy-netty-client"); + + final boolean legacyNettyClient; final boolean hasContext; final boolean ownsThreadPool; final URI baseUri; @@ -66,8 +72,8 @@ final class NettyHttpClient implements HttpClient { final String host; final int port; final boolean buffered; - final Closeable upstreamHttpClient; final ConnectionManager connectionManager; + final RawHttpClient upstreamHttpClient; final DefaultHttpClient.RequestKey requestKey; final JsonMapper jsonMapper; @@ -81,7 +87,8 @@ final class NettyHttpClient implements HttpClient { } NettyHttpClient(NettyHttpClientBuilder builder) { - DefaultHttpClient mnClient; + this.legacyNettyClient = LEGACY_NETTY_CLIENT || (builder.managedProvider != null && builder.managedProvider.configuration.legacyNettyClient()); + RawHttpClient mnClient; if (builder.managedProvider == null) { hasContext = false; ownsThreadPool = true; @@ -92,7 +99,7 @@ final class NettyHttpClient implements HttpClient { if (builder.properties.containsKey(StandardClientProperties.READ_TIMEOUT)) { cfg.setReadTimeout((Duration) builder.properties.get(StandardClientProperties.READ_TIMEOUT)); } - mnClient = new DefaultHttpClient((URI) null, cfg); + mnClient = RawHttpClient.create(null, cfg); blockingIoExecutor = Executors.newCachedThreadPool(); jsonMapper = OciSdkMicronautSerializer.getDefaultObjectMapper(); } else { @@ -103,9 +110,9 @@ final class NettyHttpClient implements HttpClient { } } if (builder.managedProvider.mnHttpClient != null) { - mnClient = (DefaultHttpClient) builder.managedProvider.mnHttpClient; + mnClient = builder.managedProvider.mnHttpClient; } else { - mnClient = (DefaultHttpClient) builder.managedProvider.mnHttpClientRegistry.getClient( + mnClient = builder.managedProvider.mnHttpClientRegistry.getRawClient( HttpVersionSelection.forClientConfiguration(new DefaultHttpClientConfiguration()), builder.serviceId, null @@ -121,7 +128,7 @@ final class NettyHttpClient implements HttpClient { jsonMapper = builder.managedProvider.jsonMapper; } upstreamHttpClient = mnClient; - connectionManager = mnClient.connectionManager(); + connectionManager = legacyNettyClient ? ((DefaultHttpClient) mnClient).connectionManager() : null; baseUri = Objects.requireNonNull(builder.baseUri, "baseUri"); requestInterceptors = builder.requestInterceptors.stream() .sorted(Comparator.comparingInt(p -> p.priority)) @@ -134,25 +141,30 @@ final class NettyHttpClient implements HttpClient { nettyClientFilter = Collections.emptyList(); } - requestKey = new DefaultHttpClient.RequestKey(mnClient, builder.baseUri); + requestKey = legacyNettyClient ? new DefaultHttpClient.RequestKey((DefaultHttpClient) mnClient, builder.baseUri) : null; this.port = builder.baseUri.getPort(); this.host = builder.baseUri.getHost(); this.buffered = builder.buffered; } ByteBufAllocator alloc() { - return connectionManager.alloc(); + return connectionManager == null ? ByteBufAllocator.DEFAULT : connectionManager.alloc(); } + @SuppressWarnings("deprecation") @Override public HttpRequest createRequest(Method method) { - return new NettyHttpRequest(this, method); + return legacyNettyClient ? new NettyHttpRequest(this, method) : new MicronautHttpRequest(this, method); } @Override public boolean isProcessingException(Exception e) { // these exceptions will allow the client to retry the request - return e instanceof JacksonException || e instanceof PrematureChannelClosureException || e instanceof ReadTimeoutException; + return e instanceof JacksonException || + e instanceof PrematureChannelClosureException || + e instanceof ReadTimeoutException || + e instanceof io.micronaut.http.client.exceptions.ReadTimeoutException || + e instanceof ResponseClosedException; } @Override diff --git a/oraclecloud-httpclient-netty/src/main/java/io/micronaut/oraclecloud/httpclient/netty/NettyHttpClientBuilder.java b/oraclecloud-httpclient-netty/src/main/java/io/micronaut/oraclecloud/httpclient/netty/NettyHttpClientBuilder.java index 485bc1740..8937a08d1 100644 --- a/oraclecloud-httpclient-netty/src/main/java/io/micronaut/oraclecloud/httpclient/netty/NettyHttpClientBuilder.java +++ b/oraclecloud-httpclient-netty/src/main/java/io/micronaut/oraclecloud/httpclient/netty/NettyHttpClientBuilder.java @@ -20,6 +20,7 @@ import com.oracle.bmc.http.client.HttpClientBuilder; import com.oracle.bmc.http.client.RequestInterceptor; import com.oracle.bmc.http.client.StandardClientProperties; +import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.Nullable; import java.net.URI; @@ -31,6 +32,7 @@ import static io.micronaut.oraclecloud.httpclient.netty.NettyClientProperties.OCI_NETTY_CLIENT_FILTERS_KEY; +@Internal final class NettyHttpClientBuilder implements HttpClientBuilder { final Collection> requestInterceptors = new ArrayList<>(); diff --git a/oraclecloud-httpclient-netty/src/main/java/io/micronaut/oraclecloud/httpclient/netty/NettyHttpRequest.java b/oraclecloud-httpclient-netty/src/main/java/io/micronaut/oraclecloud/httpclient/netty/NettyHttpRequest.java index 25f916eed..289332b56 100644 --- a/oraclecloud-httpclient-netty/src/main/java/io/micronaut/oraclecloud/httpclient/netty/NettyHttpRequest.java +++ b/oraclecloud-httpclient-netty/src/main/java/io/micronaut/oraclecloud/httpclient/netty/NettyHttpRequest.java @@ -59,6 +59,7 @@ import static io.micronaut.oraclecloud.httpclient.netty.NettyClientProperties.CLASS_AND_METHOD_KEY_NAME; +@Deprecated final class NettyHttpRequest implements HttpRequest { private static final long UNKNOWN_CONTENT_LENGTH = -1; diff --git a/oraclecloud-httpclient-netty/src/main/java/io/micronaut/oraclecloud/httpclient/netty/NettyHttpResponse.java b/oraclecloud-httpclient-netty/src/main/java/io/micronaut/oraclecloud/httpclient/netty/NettyHttpResponse.java index cb7e1718a..871efafed 100644 --- a/oraclecloud-httpclient-netty/src/main/java/io/micronaut/oraclecloud/httpclient/netty/NettyHttpResponse.java +++ b/oraclecloud-httpclient-netty/src/main/java/io/micronaut/oraclecloud/httpclient/netty/NettyHttpResponse.java @@ -32,6 +32,7 @@ import java.util.concurrent.Executor; import java.util.function.Function; +@Deprecated final class NettyHttpResponse implements HttpResponse { private final JsonMapper jsonMapper; private final io.netty.handler.codec.http.HttpResponse nettyResponse; diff --git a/oraclecloud-httpclient-netty/src/main/java/io/micronaut/oraclecloud/httpclient/netty/OciNettyConfiguration.java b/oraclecloud-httpclient-netty/src/main/java/io/micronaut/oraclecloud/httpclient/netty/OciNettyConfiguration.java new file mode 100644 index 000000000..224fe8a57 --- /dev/null +++ b/oraclecloud-httpclient-netty/src/main/java/io/micronaut/oraclecloud/httpclient/netty/OciNettyConfiguration.java @@ -0,0 +1,38 @@ +/* + * Copyright 2017-2024 original authors + * + * 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 + * + * https://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.micronaut.oraclecloud.httpclient.netty; + +import io.micronaut.context.annotation.ConfigurationProperties; +import io.micronaut.core.annotation.Experimental; +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.bind.annotation.Bindable; + +/** + * Configuration properties specific to the managed client. + * + * @param legacyNettyClient Use the legacy implementation of the netty client. + * @author Jonas Konrad + * @since 4.3.0 + */ +@Internal +@ConfigurationProperties(OciNettyConfiguration.PREFIX) +record OciNettyConfiguration( + @Experimental + @Bindable(defaultValue = "false") + boolean legacyNettyClient +) { + static final String PREFIX = "oci.netty"; +} diff --git a/oraclecloud-httpclient-netty/src/main/java/io/micronaut/oraclecloud/httpclient/netty/StreamReadingHandler.java b/oraclecloud-httpclient-netty/src/main/java/io/micronaut/oraclecloud/httpclient/netty/StreamReadingHandler.java index 1b5c33dc4..7a4edc8e0 100644 --- a/oraclecloud-httpclient-netty/src/main/java/io/micronaut/oraclecloud/httpclient/netty/StreamReadingHandler.java +++ b/oraclecloud-httpclient-netty/src/main/java/io/micronaut/oraclecloud/httpclient/netty/StreamReadingHandler.java @@ -26,6 +26,7 @@ /** * Channel handler that exposes inbound data as an {@link InputStream}. */ +@Deprecated class StreamReadingHandler extends DecidedBodyHandler { private final Object monitor = new Object(); private CompositeByteBuf buffer; diff --git a/oraclecloud-httpclient-netty/src/main/java/io/micronaut/oraclecloud/httpclient/netty/StreamWritingHandler.java b/oraclecloud-httpclient-netty/src/main/java/io/micronaut/oraclecloud/httpclient/netty/StreamWritingHandler.java index b56f7a5c5..79e0926bd 100644 --- a/oraclecloud-httpclient-netty/src/main/java/io/micronaut/oraclecloud/httpclient/netty/StreamWritingHandler.java +++ b/oraclecloud-httpclient-netty/src/main/java/io/micronaut/oraclecloud/httpclient/netty/StreamWritingHandler.java @@ -27,6 +27,7 @@ /** * Channel handler that writes data from a given {@link InputStream} to the channel. */ +@Deprecated final class StreamWritingHandler extends ChannelInboundHandlerAdapter { public static final int MAX_WRITE_TARGET = 1024 * 16; private final InputStream stream; diff --git a/oraclecloud-httpclient-netty/src/main/java/io/micronaut/oraclecloud/httpclient/netty/UndecidedBodyHandler.java b/oraclecloud-httpclient-netty/src/main/java/io/micronaut/oraclecloud/httpclient/netty/UndecidedBodyHandler.java index b8fcb59fb..a86f49a05 100644 --- a/oraclecloud-httpclient-netty/src/main/java/io/micronaut/oraclecloud/httpclient/netty/UndecidedBodyHandler.java +++ b/oraclecloud-httpclient-netty/src/main/java/io/micronaut/oraclecloud/httpclient/netty/UndecidedBodyHandler.java @@ -32,6 +32,7 @@ * Handler that buffers some input data until the user decides whether they want it all buffered or as a stream. After * that, handling is delegated to {@link StreamReadingHandler} or {@link BufferFutureHandler}. */ +@Deprecated final class UndecidedBodyHandler extends ChannelInboundHandlerAdapter { private final Runnable release; private final ByteBufAllocator alloc; diff --git a/oraclecloud-httpclient-netty/src/test/java/io/micronaut/oraclecloud/httpclient/netty/BufferFutureHandlerTest.java b/oraclecloud-httpclient-netty/src/test/java/io/micronaut/oraclecloud/httpclient/netty/BufferFutureHandlerTest.java index 828459313..6ea68bce5 100644 --- a/oraclecloud-httpclient-netty/src/test/java/io/micronaut/oraclecloud/httpclient/netty/BufferFutureHandlerTest.java +++ b/oraclecloud-httpclient-netty/src/test/java/io/micronaut/oraclecloud/httpclient/netty/BufferFutureHandlerTest.java @@ -12,6 +12,7 @@ import java.util.concurrent.ExecutionException; import java.util.concurrent.atomic.AtomicBoolean; +@Deprecated class BufferFutureHandlerTest { @Test public void normal() throws Exception { diff --git a/oraclecloud-httpclient-netty/src/test/java/io/micronaut/oraclecloud/httpclient/netty/HeaderMapTest.java b/oraclecloud-httpclient-netty/src/test/java/io/micronaut/oraclecloud/httpclient/netty/HeaderMapTest.java index 87ca5f4f8..9b9d49a52 100644 --- a/oraclecloud-httpclient-netty/src/test/java/io/micronaut/oraclecloud/httpclient/netty/HeaderMapTest.java +++ b/oraclecloud-httpclient-netty/src/test/java/io/micronaut/oraclecloud/httpclient/netty/HeaderMapTest.java @@ -1,6 +1,7 @@ package io.micronaut.oraclecloud.httpclient.netty; -import io.netty.handler.codec.http.DefaultHttpHeaders; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.MutableHttpHeaders; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; @@ -10,10 +11,10 @@ class HeaderMapTest { @Test public void containsCaseInsensitive() { - DefaultHttpHeaders headers = new DefaultHttpHeaders(); + MutableHttpHeaders headers = HttpRequest.GET("").getHeaders(); headers.add("Foo", "bar"); - HeaderMap map = new HeaderMap(headers); + MicronautHeaderMap map = new MicronautHeaderMap(headers); Assertions.assertTrue(map.containsKey("foo")); Assertions.assertTrue(map.containsKey("FOO")); Assertions.assertTrue(map.containsKey("Foo")); diff --git a/oraclecloud-httpclient-netty/src/test/java/io/micronaut/oraclecloud/httpclient/netty/LegacyNettyManagedTest.java b/oraclecloud-httpclient-netty/src/test/java/io/micronaut/oraclecloud/httpclient/netty/LegacyNettyManagedTest.java new file mode 100644 index 000000000..1a663291f --- /dev/null +++ b/oraclecloud-httpclient-netty/src/test/java/io/micronaut/oraclecloud/httpclient/netty/LegacyNettyManagedTest.java @@ -0,0 +1,24 @@ +package io.micronaut.oraclecloud.httpclient.netty; + +import io.micronaut.context.ApplicationContext; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import java.util.Map; + +@Deprecated +public class LegacyNettyManagedTest extends NettyManagedTest { + @Override + @BeforeEach + public void setUp() { + ctx = ApplicationContext.run(Map.of("oci.netty.legacy-netty-client", true)); + } + + @Override + @Test + @Disabled // response filter order was fixed in the new client impl + void simpleRequestTestFilters() throws Exception { + super.simpleRequestTestFilters(); + } +} diff --git a/oraclecloud-httpclient-netty/src/test/java/io/micronaut/oraclecloud/httpclient/netty/ManagedPropertyTest.java b/oraclecloud-httpclient-netty/src/test/java/io/micronaut/oraclecloud/httpclient/netty/ManagedPropertyTest.java index 97ace8d42..54ba9a139 100644 --- a/oraclecloud-httpclient-netty/src/test/java/io/micronaut/oraclecloud/httpclient/netty/ManagedPropertyTest.java +++ b/oraclecloud-httpclient-netty/src/test/java/io/micronaut/oraclecloud/httpclient/netty/ManagedPropertyTest.java @@ -8,21 +8,16 @@ import com.oracle.bmc.common.ClientBuilderBase; import com.oracle.bmc.http.internal.BaseSyncClient; import io.micronaut.context.ApplicationContext; -import io.micronaut.context.BeanContext; import io.micronaut.context.annotation.Property; import io.micronaut.context.annotation.Replaces; import io.micronaut.context.annotation.Requires; -import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; -import io.micronaut.http.client.HttpClient; -import io.micronaut.http.client.HttpClientConfiguration; -import io.micronaut.http.client.HttpClientRegistry; import io.micronaut.http.client.HttpVersionSelection; -import io.micronaut.http.client.LoadBalancer; +import io.micronaut.http.client.RawHttpClient; +import io.micronaut.http.client.RawHttpClientRegistry; import io.micronaut.http.client.netty.DefaultHttpClient; import io.micronaut.http.client.netty.DefaultNettyHttpClientRegistry; -import io.micronaut.inject.InjectionPoint; import io.micronaut.test.extensions.junit5.annotation.MicronautTest; import jakarta.inject.Inject; import jakarta.inject.Singleton; @@ -62,28 +57,14 @@ public void unmanagedClientUsesManagedProviderProperty() { @Singleton @Replaces(DefaultNettyHttpClientRegistry.class) @Requires(property = "spec.name", value = "ManagedPropertyTest") - static class MockHttpClientRegistry implements HttpClientRegistry { + static class MockHttpClientRegistry implements RawHttpClientRegistry { boolean clientRegistered = false; @Override - public @NonNull HttpClient getClient(@NonNull AnnotationMetadata annotationMetadata) { - return new DefaultHttpClient(); - } - - @Override - public @NonNull HttpClient getClient(@NonNull HttpVersionSelection httpVersion, @NonNull String clientId, @Nullable String path) { + public @NonNull RawHttpClient getRawClient(@NonNull HttpVersionSelection httpVersion, @NonNull String clientId, @Nullable String path) { clientRegistered = true; - return new DefaultHttpClient(); - } - - @Override - public @NonNull HttpClient resolveClient(@Nullable InjectionPoint injectionPoint, @Nullable LoadBalancer loadBalancer, @Nullable HttpClientConfiguration configuration, @NonNull BeanContext beanContext) { - return new DefaultHttpClient(); - } - - @Override - public void disposeClient(AnnotationMetadata annotationMetadata) { + return DefaultHttpClient.builder().build(); } } diff --git a/oraclecloud-httpclient-netty/src/test/java/io/micronaut/oraclecloud/httpclient/netty/ManagedTest.java b/oraclecloud-httpclient-netty/src/test/java/io/micronaut/oraclecloud/httpclient/netty/ManagedTest.java index 18238b7a2..e52bc8f4c 100644 --- a/oraclecloud-httpclient-netty/src/test/java/io/micronaut/oraclecloud/httpclient/netty/ManagedTest.java +++ b/oraclecloud-httpclient-netty/src/test/java/io/micronaut/oraclecloud/httpclient/netty/ManagedTest.java @@ -8,7 +8,7 @@ import io.micronaut.context.annotation.Property; import io.micronaut.context.annotation.Replaces; import io.micronaut.context.annotation.Requires; -import io.micronaut.http.client.HttpClientRegistry; +import io.micronaut.http.client.RawHttpClientRegistry; import io.micronaut.oraclecloud.serde.OciSerdeConfiguration; import io.micronaut.oraclecloud.serde.OciSerializationConfiguration; import io.micronaut.scheduling.TaskExecutors; @@ -51,11 +51,11 @@ public static class MockProvider extends ManagedNettyHttpProvider { int buildersCreated = 0; public MockProvider( - HttpClientRegistry mnHttpClientRegistry, @Named(TaskExecutors.BLOCKING) ExecutorService ioExecutor, + RawHttpClientRegistry mnHttpClientRegistry, @Named(TaskExecutors.BLOCKING) ExecutorService ioExecutor, ObjectMapper jsonMapper, OciSerdeConfiguration ociSerdeConfiguration, OciSerializationConfiguration ociSerializationConfiguration, - List> nettyClientFilters + List> nettyClientFilters, OciNettyConfiguration configuration ) { - super(mnHttpClientRegistry, ioExecutor, jsonMapper, ociSerdeConfiguration, ociSerializationConfiguration, nettyClientFilters); + super(mnHttpClientRegistry, ioExecutor, jsonMapper, ociSerdeConfiguration, ociSerializationConfiguration, nettyClientFilters, configuration); } @Override diff --git a/oraclecloud-httpclient-netty/src/test/java/io/micronaut/oraclecloud/httpclient/netty/NettyHttpClientTest.java b/oraclecloud-httpclient-netty/src/test/java/io/micronaut/oraclecloud/httpclient/netty/NettyHttpClientTest.java index b3c97d834..f96abe18f 100644 --- a/oraclecloud-httpclient-netty/src/test/java/io/micronaut/oraclecloud/httpclient/netty/NettyHttpClientTest.java +++ b/oraclecloud-httpclient-netty/src/test/java/io/micronaut/oraclecloud/httpclient/netty/NettyHttpClientTest.java @@ -9,8 +9,8 @@ import io.micronaut.http.annotation.Controller; import io.micronaut.http.annotation.Get; import io.micronaut.http.annotation.Post; +import io.micronaut.http.client.exceptions.ReadTimeoutException; import io.micronaut.runtime.server.EmbeddedServer; -import io.netty.handler.timeout.ReadTimeoutException; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.reactivestreams.Publisher; @@ -22,6 +22,7 @@ import java.nio.charset.StandardCharsets; import java.time.Duration; import java.util.Map; +import java.util.concurrent.CompletionException; import java.util.concurrent.CompletionStage; class NettyHttpClientTest { @@ -116,6 +117,9 @@ public void readTimeoutText() throws Exception { .appendPathPart("/slow") .execute() .exceptionally(e -> { + if (e instanceof CompletionException) { + e = e.getCause(); + } Assertions.assertInstanceOf(ReadTimeoutException.class, e); return null; }); diff --git a/oraclecloud-httpclient-netty/src/test/java/io/micronaut/oraclecloud/httpclient/netty/NettyUnmanagedTest.java b/oraclecloud-httpclient-netty/src/test/java/io/micronaut/oraclecloud/httpclient/netty/NettyUnmanagedTest.java index 1aea034a2..64e072dcb 100644 --- a/oraclecloud-httpclient-netty/src/test/java/io/micronaut/oraclecloud/httpclient/netty/NettyUnmanagedTest.java +++ b/oraclecloud-httpclient-netty/src/test/java/io/micronaut/oraclecloud/httpclient/netty/NettyUnmanagedTest.java @@ -97,6 +97,6 @@ void simpleRequestTestFilters() throws Exception { Assertions.assertTrue(firstTestNettyClientFilter.getStartTime() < secondTestNettyClientFilter.getStartTime()); Assertions.assertTrue(firstTestNettyClientFilter.getOrder() < secondTestNettyClientFilter.getOrder()); - Assertions.assertTrue(firstTestNettyClientFilter.getEndTime() < secondTestNettyClientFilter.getEndTime()); + Assertions.assertTrue(firstTestNettyClientFilter.getEndTime() > secondTestNettyClientFilter.getEndTime()); } } diff --git a/oraclecloud-httpclient-netty/src/test/java/io/micronaut/oraclecloud/httpclient/netty/StreamReadingHandlerTest.java b/oraclecloud-httpclient-netty/src/test/java/io/micronaut/oraclecloud/httpclient/netty/StreamReadingHandlerTest.java index 55129d84c..c9be11d5c 100644 --- a/oraclecloud-httpclient-netty/src/test/java/io/micronaut/oraclecloud/httpclient/netty/StreamReadingHandlerTest.java +++ b/oraclecloud-httpclient-netty/src/test/java/io/micronaut/oraclecloud/httpclient/netty/StreamReadingHandlerTest.java @@ -20,6 +20,7 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; +@Deprecated class StreamReadingHandlerTest { private ExecutorService executor; diff --git a/oraclecloud-httpclient-netty/src/test/java/io/micronaut/oraclecloud/httpclient/netty/StreamWritingHandlerTest.java b/oraclecloud-httpclient-netty/src/test/java/io/micronaut/oraclecloud/httpclient/netty/StreamWritingHandlerTest.java index abe548c89..091000071 100644 --- a/oraclecloud-httpclient-netty/src/test/java/io/micronaut/oraclecloud/httpclient/netty/StreamWritingHandlerTest.java +++ b/oraclecloud-httpclient-netty/src/test/java/io/micronaut/oraclecloud/httpclient/netty/StreamWritingHandlerTest.java @@ -14,6 +14,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertSame; +@Deprecated class StreamWritingHandlerTest { @Test public void test() { @@ -104,4 +105,4 @@ public int read(byte[] b, int off, int len) throws IOException { } } } -} \ No newline at end of file +} diff --git a/oraclecloud-httpclient-netty/src/test/java/io/micronaut/oraclecloud/httpclient/netty/UndecidedBodyHandlerTest.java b/oraclecloud-httpclient-netty/src/test/java/io/micronaut/oraclecloud/httpclient/netty/UndecidedBodyHandlerTest.java index 70b23dbc6..f7b42673a 100644 --- a/oraclecloud-httpclient-netty/src/test/java/io/micronaut/oraclecloud/httpclient/netty/UndecidedBodyHandlerTest.java +++ b/oraclecloud-httpclient-netty/src/test/java/io/micronaut/oraclecloud/httpclient/netty/UndecidedBodyHandlerTest.java @@ -15,6 +15,7 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.atomic.AtomicBoolean; +@Deprecated class UndecidedBodyHandlerTest { @Test public void fullyBufferedStream() throws Exception {