From 0ce726230580b0a11fdd491f204f1d1e07cc7ef1 Mon Sep 17 00:00:00 2001
From: Jonas Konrad <jonas.konrad@oracle.com>
Date: Fri, 8 Nov 2024 09:50:05 +0100
Subject: [PATCH] Minor client optimizations (#11295)

* Migrate netty HttpClient to ExecutionFlow
Also adds cancellation support to ExecutionFlow.

* review

* fix double cancel

* Remove context propagation filter
Implement "proper" context propagation in the client to get rid of the old ClientServerContextFilter.

Because context propagation uses its own key for the reactor context instead of ServerRequestContext.KEY, I've deprecated ServerRequestContext.KEY and added a more general API that can use the propagated context from the ContextView.

* review

* Minor client optimizations

* Minor client optimizations

* trigger build
---
 .../io/micronaut/core/util/StringUtils.java   | 35 ++++++++++++++-----
 .../http/client/netty/DefaultHttpClient.java  | 12 +++++--
 .../http/netty/NettyHttpHeaders.java          |  5 +++
 .../body/NettyCharSequenceBodyWriter.java     |  3 +-
 .../netty/body/NettyWritableBodyWriter.java   |  4 +--
 .../netty/body/AbstractFileBodyWriter.java    |  7 ++--
 .../java/io/micronaut/http/HttpHeaders.java   | 11 ++++++
 7 files changed, 59 insertions(+), 18 deletions(-)

diff --git a/core/src/main/java/io/micronaut/core/util/StringUtils.java b/core/src/main/java/io/micronaut/core/util/StringUtils.java
index 78abd3b5468..ec1e77e08c9 100644
--- a/core/src/main/java/io/micronaut/core/util/StringUtils.java
+++ b/core/src/main/java/io/micronaut/core/util/StringUtils.java
@@ -366,18 +366,35 @@ public static String convertDotToUnderscore(String dottedProperty, boolean upper
      * @return A combined uri string
      */
     public static String prependUri(String baseUri, String uri) {
-        if (!uri.startsWith("/") && !uri.startsWith("?")) {
-            uri = "/" + uri;
+        StringBuilder builder = new StringBuilder(baseUri);
+        if (!uri.isEmpty() && (uri.length() != 1 || uri.charAt(0) != '/')) {
+            if (!uri.startsWith("/") && !uri.startsWith("?")) {
+                builder.append('/');
+            }
+            builder.append(uri);
+        }
+        if (builder.isEmpty()) {
+            return "";
         }
-        if (uri.length() == 1 && uri.charAt(0) == '/') {
-            uri = "";
+
+        int i = 0;
+        if (builder.charAt(0) != '/' && builder.indexOf("://") != -1) {
+            // skip until after scheme
+            while (i < builder.length() && builder.charAt(i) != ':') {
+                i++;
+            }
+            i += 2;
         }
-        uri = baseUri + uri;
-        if (uri.startsWith("/")) {
-            return uri.replaceAll("/{2,}", "/");
-        } else {
-            return uri.replaceAll("(?<=[^:])/{2,}", "/");
+        // replace double slashes
+        for (; i < builder.length() - 1; i++) {
+            if (builder.charAt(i) == '/' && builder.charAt(i + 1) == '/') {
+                builder.deleteCharAt(i);
+                i--;
+            } else if (builder.charAt(i) == '?') {
+                break;
+            }
         }
+        return builder.toString();
     }
 
     /**
diff --git a/http-client/src/main/java/io/micronaut/http/client/netty/DefaultHttpClient.java b/http-client/src/main/java/io/micronaut/http/client/netty/DefaultHttpClient.java
index 274a01739fd..22423b97a91 100644
--- a/http-client/src/main/java/io/micronaut/http/client/netty/DefaultHttpClient.java
+++ b/http-client/src/main/java/io/micronaut/http/client/netty/DefaultHttpClient.java
@@ -1284,14 +1284,14 @@ private NettyByteBody buildNettyRequest(
         boolean permitsBody,
         EventLoop eventLoop) throws HttpPostRequestEncoder.ErrorDataEncoderException {
 
-        if (!request.getHeaders().contains(io.micronaut.http.HttpHeaders.HOST)) {
+        if (!request.getHeaders().contains(HttpHeaderNames.HOST)) {
             request.getHeaders().set(HttpHeaderNames.HOST, getHostHeader(requestURI));
         }
 
         if (permitsBody) {
             Optional<?> body = request.getBody();
             if (body.isPresent()) {
-                if (!request.getHeaders().contains(io.micronaut.http.HttpHeaders.CONTENT_TYPE)) {
+                if (!request.getHeaders().contains(HttpHeaderNames.CONTENT_TYPE)) {
                     MediaType mediaType = request.getContentType().orElse(MediaType.APPLICATION_JSON_TYPE);
                     request.getHeaders().set(HttpHeaderNames.CONTENT_TYPE, mediaType);
                 }
@@ -1971,6 +1971,14 @@ private static MessageBodyHandlerRegistry createDefaultMessageBodyHandlerRegistr
     }
 
     static boolean isSecureScheme(String scheme) {
+        // fast path
+        if (scheme.equals("http")) {
+            return false;
+        }
+        if (scheme.equals("https")) {
+            return true;
+        }
+        // actual case-insensitive check
         return io.micronaut.http.HttpRequest.SCHEME_HTTPS.equalsIgnoreCase(scheme) || SCHEME_WSS.equalsIgnoreCase(scheme);
     }
 
diff --git a/http-netty/src/main/java/io/micronaut/http/netty/NettyHttpHeaders.java b/http-netty/src/main/java/io/micronaut/http/netty/NettyHttpHeaders.java
index 0c740ce15a1..55019ca132e 100644
--- a/http-netty/src/main/java/io/micronaut/http/netty/NettyHttpHeaders.java
+++ b/http-netty/src/main/java/io/micronaut/http/netty/NettyHttpHeaders.java
@@ -104,6 +104,11 @@ public final boolean contains(String name) {
         return nettyHeaders.contains(name);
     }
 
+    @Override
+    public final boolean contains(CharSequence name) {
+        return nettyHeaders.contains(name);
+    }
+
     @Override
     public <T> Optional<T> get(CharSequence name, ArgumentConversionContext<T> conversionContext) {
         List<String> values = nettyHeaders.getAll(name);
diff --git a/http-netty/src/main/java/io/micronaut/http/netty/body/NettyCharSequenceBodyWriter.java b/http-netty/src/main/java/io/micronaut/http/netty/body/NettyCharSequenceBodyWriter.java
index f52c55d6ae4..667612991cb 100644
--- a/http-netty/src/main/java/io/micronaut/http/netty/body/NettyCharSequenceBodyWriter.java
+++ b/http-netty/src/main/java/io/micronaut/http/netty/body/NettyCharSequenceBodyWriter.java
@@ -22,7 +22,6 @@
 import io.micronaut.core.type.MutableHeaders;
 import io.micronaut.http.ByteBodyHttpResponse;
 import io.micronaut.http.ByteBodyHttpResponseWrapper;
-import io.micronaut.http.HttpHeaders;
 import io.micronaut.http.HttpRequest;
 import io.micronaut.http.MediaType;
 import io.micronaut.http.MutableHttpHeaders;
@@ -63,7 +62,7 @@ public ByteBodyHttpResponse<?> write(ByteBufferFactory<?, ?> bufferFactory, Http
             ByteBufUtil.writeUtf8(ByteBufAllocator.DEFAULT, object) :
             ByteBufUtil.encodeString(ByteBufAllocator.DEFAULT, CharBuffer.wrap(object), charset);
         NettyHttpHeaders nettyHttpHeaders = (NettyHttpHeaders) headers;
-        if (!nettyHttpHeaders.contains(HttpHeaders.CONTENT_TYPE)) {
+        if (!nettyHttpHeaders.contains(HttpHeaderNames.CONTENT_TYPE)) {
             nettyHttpHeaders.set(HttpHeaderNames.CONTENT_TYPE, mediaType);
         }
         return ByteBodyHttpResponseWrapper.wrap(outgoingResponse, new AvailableNettyByteBody(byteBuf));
diff --git a/http-netty/src/main/java/io/micronaut/http/netty/body/NettyWritableBodyWriter.java b/http-netty/src/main/java/io/micronaut/http/netty/body/NettyWritableBodyWriter.java
index 63ac58fd5e5..0a31a039fc1 100644
--- a/http-netty/src/main/java/io/micronaut/http/netty/body/NettyWritableBodyWriter.java
+++ b/http-netty/src/main/java/io/micronaut/http/netty/body/NettyWritableBodyWriter.java
@@ -26,7 +26,6 @@
 import io.micronaut.core.type.MutableHeaders;
 import io.micronaut.http.ByteBodyHttpResponse;
 import io.micronaut.http.ByteBodyHttpResponseWrapper;
-import io.micronaut.http.HttpHeaders;
 import io.micronaut.http.HttpRequest;
 import io.micronaut.http.MediaType;
 import io.micronaut.http.MutableHttpHeaders;
@@ -41,6 +40,7 @@
 import io.micronaut.runtime.ApplicationConfiguration;
 import io.netty.buffer.ByteBufAllocator;
 import io.netty.buffer.ByteBufOutputStream;
+import io.netty.handler.codec.http.HttpHeaderNames;
 import jakarta.inject.Singleton;
 import org.reactivestreams.Publisher;
 
@@ -76,7 +76,7 @@ public boolean isBlocking() {
     @Override
     public ByteBodyHttpResponse<?> write(ByteBufferFactory<?, ?> bufferFactory, HttpRequest<?> request, MutableHttpResponse<Writable> outgoingResponse, Argument<Writable> type, MediaType mediaType, Writable object) throws CodecException {
         MutableHttpHeaders outgoingHeaders = outgoingResponse.getHeaders();
-        if (mediaType != null && !outgoingHeaders.contains(HttpHeaders.CONTENT_TYPE)) {
+        if (mediaType != null && !outgoingHeaders.contains(HttpHeaderNames.CONTENT_TYPE)) {
             outgoingHeaders.contentType(mediaType);
         }
         ByteBufOutputStream outputStream = new ByteBufOutputStream(ByteBufAllocator.DEFAULT.buffer());
diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/body/AbstractFileBodyWriter.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/body/AbstractFileBodyWriter.java
index 389e5d23985..23de9f08de3 100644
--- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/body/AbstractFileBodyWriter.java
+++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/body/AbstractFileBodyWriter.java
@@ -27,6 +27,7 @@
 import io.micronaut.http.netty.body.AvailableNettyByteBody;
 import io.micronaut.http.server.netty.configuration.NettyHttpServerConfiguration;
 import io.micronaut.http.server.types.files.FileCustomizableResponseType;
+import io.netty.handler.codec.http.HttpHeaderNames;
 
 import java.time.LocalDateTime;
 import java.time.ZonedDateTime;
@@ -70,8 +71,8 @@ protected boolean handleIfModifiedAndHeaders(HttpRequest<?> request, MutableHttp
             }
         }
 
-        if (!response.getHeaders().contains(HttpHeaders.CONTENT_TYPE)) {
-            response.header(HttpHeaders.CONTENT_TYPE, systemFile.getMediaType().toString());
+        if (!response.getHeaders().contains(HttpHeaderNames.CONTENT_TYPE)) {
+            response.header(HttpHeaderNames.CONTENT_TYPE, systemFile.getMediaType().toString());
         }
         setDateAndCacheHeaders(response, lastModified);
         systemFile.process(nettyResponse);
@@ -86,7 +87,7 @@ protected void setDateAndCacheHeaders(MutableHttpResponse response, long lastMod
         // Date header
         MutableHttpHeaders headers = response.getHeaders();
         LocalDateTime now = LocalDateTime.now();
-        if (!headers.contains(HttpHeaders.DATE)) {
+        if (!headers.contains(HttpHeaderNames.DATE)) {
             headers.date(now);
         }
 
diff --git a/http/src/main/java/io/micronaut/http/HttpHeaders.java b/http/src/main/java/io/micronaut/http/HttpHeaders.java
index 7aeba92751c..26e43217ffa 100644
--- a/http/src/main/java/io/micronaut/http/HttpHeaders.java
+++ b/http/src/main/java/io/micronaut/http/HttpHeaders.java
@@ -622,6 +622,17 @@ public interface HttpHeaders extends Headers {
         X_AUTH_TOKEN
     ));
 
+    /**
+     * Whether the given key is contained within these values.
+     *
+     * @param name The key name
+     * @return True if it is
+     * @since 4.8.0
+     */
+    default boolean contains(CharSequence name) {
+        return contains(name.toString());
+    }
+
     /**
      * Obtain the date header.
      *