diff --git a/sdk/clientcore/http-netty4/checkstyle-suppressions.xml b/sdk/clientcore/http-netty4/checkstyle-suppressions.xml index ee6d12f1a0e0..8d1d4b4e5869 100644 --- a/sdk/clientcore/http-netty4/checkstyle-suppressions.xml +++ b/sdk/clientcore/http-netty4/checkstyle-suppressions.xml @@ -3,9 +3,10 @@ + - + diff --git a/sdk/clientcore/http-netty4/spotbugs-exclude.xml b/sdk/clientcore/http-netty4/spotbugs-exclude.xml index 66ac446dc9a3..9b8d71f16243 100644 --- a/sdk/clientcore/http-netty4/spotbugs-exclude.xml +++ b/sdk/clientcore/http-netty4/spotbugs-exclude.xml @@ -8,6 +8,7 @@ + @@ -20,11 +21,11 @@ - + - + @@ -32,7 +33,7 @@ - + @@ -42,7 +43,7 @@ - + diff --git a/sdk/clientcore/http-netty4/src/main/java/io/clientcore/http/netty4/NettyHttpClient.java b/sdk/clientcore/http-netty4/src/main/java/io/clientcore/http/netty4/NettyHttpClient.java index a48d2f88609c..90fbafd96b16 100644 --- a/sdk/clientcore/http-netty4/src/main/java/io/clientcore/http/netty4/NettyHttpClient.java +++ b/sdk/clientcore/http-netty4/src/main/java/io/clientcore/http/netty4/NettyHttpClient.java @@ -33,7 +33,6 @@ import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelInitializer; import io.netty.channel.EventLoopGroup; -import io.netty.handler.codec.http.HttpClientCodec; import io.netty.handler.codec.http2.Http2SecurityUtil; import io.netty.handler.proxy.ProxyHandler; import io.netty.handler.ssl.ApplicationProtocolConfig; @@ -55,6 +54,8 @@ import static io.clientcore.core.utils.ServerSentEventUtils.attemptRetry; import static io.clientcore.core.utils.ServerSentEventUtils.processTextEventStream; +import static io.clientcore.http.netty4.implementation.Netty4HandlerNames.HTTP_RESPONSE; +import static io.clientcore.http.netty4.implementation.Netty4HandlerNames.PROGRESS_AND_TIMEOUT; import static io.clientcore.http.netty4.implementation.Netty4Utility.awaitLatch; import static io.clientcore.http.netty4.implementation.Netty4Utility.createCodec; import static io.clientcore.http.netty4.implementation.Netty4Utility.sendHttp11Request; @@ -166,11 +167,10 @@ protected void initChannel(Channel channel) throws SSLException { channel.pipeline().addLast(Netty4HandlerNames.SSL, ssl.newHandler(channel.alloc(), host, port)); channel.pipeline() .addLast(Netty4HandlerNames.SSL_INITIALIZER, new Netty4SslInitializationHandler()); - } - if (isHttps) { channel.pipeline() - .addLast(new Netty4AlpnHandler(request, addProgressAndTimeoutHandler, errorReference, latch)); + .addLast(Netty4HandlerNames.ALPN, + new Netty4AlpnHandler(request, responseReference, errorReference, latch)); } } }); @@ -198,14 +198,10 @@ protected void initChannel(Channel channel) throws SSLException { // effectively be a no-op. if (addProgressAndTimeoutHandler) { channel.pipeline() - .addLast(Netty4HandlerNames.PROGRESS_AND_TIMEOUT, new Netty4ProgressAndTimeoutHandler( - progressReporter, writeTimeoutMillis, responseTimeoutMillis, readTimeoutMillis)); + .addLast(PROGRESS_AND_TIMEOUT, new Netty4ProgressAndTimeoutHandler(progressReporter, + writeTimeoutMillis, responseTimeoutMillis, readTimeoutMillis)); } - Netty4ResponseHandler responseHandler - = new Netty4ResponseHandler(request, responseReference, errorReference, latch); - channel.pipeline().addLast(Netty4HandlerNames.RESPONSE, responseHandler); - Throwable earlyError = errorReference.get(); if (earlyError != null) { // If an error occurred between the connect and the request being sent, don't proceed with sending @@ -237,15 +233,14 @@ protected void initChannel(Channel channel) throws SSLException { } else { // If there isn't an SslHandler, we can send the request immediately. // Add the HTTP/1.1 codec, as we only support HTTP/2 when using SSL ALPN. - HttpClientCodec codec = createCodec(); - if (addProgressAndTimeoutHandler) { - channel.pipeline() - .addBefore(Netty4HandlerNames.PROGRESS_AND_TIMEOUT, Netty4HandlerNames.HTTP_1_1_CODEC, codec); - } else { - channel.pipeline().addBefore(Netty4HandlerNames.RESPONSE, Netty4HandlerNames.HTTP_1_1_CODEC, codec); - } + Netty4ResponseHandler responseHandler + = new Netty4ResponseHandler(request, responseReference, errorReference, latch); + channel.pipeline().addLast(HTTP_RESPONSE, responseHandler); + + String addBefore = addProgressAndTimeoutHandler ? PROGRESS_AND_TIMEOUT : HTTP_RESPONSE; + channel.pipeline().addBefore(addBefore, Netty4HandlerNames.HTTP_CODEC, createCodec()); - sendHttp11Request(request, channel, addProgressAndTimeoutHandler, errorReference) + sendHttp11Request(request, channel, errorReference) .addListener((ChannelFutureListener) sendListener -> { if (!sendListener.isSuccess()) { setOrSuppressError(errorReference, sendListener.cause()); @@ -288,7 +283,7 @@ protected void initChannel(Channel channel) throws SSLException { // We're ignoring the response content. CountDownLatch drainLatch = new CountDownLatch(1); channel.pipeline().addLast(new Netty4EagerConsumeChannelHandler(drainLatch, ignored -> { - })); + }, info.isHttp2())); channel.config().setAutoRead(true); awaitLatch(drainLatch); } else if (bodyHandling == ResponseBodyHandling.STREAM) { @@ -306,7 +301,7 @@ protected void initChannel(Channel channel) throws SSLException { } } - body = new Netty4ChannelBinaryData(info.getEagerContent(), channel, length); + body = new Netty4ChannelBinaryData(info.getEagerContent(), channel, length, info.isHttp2()); } else { // All cases otherwise assume BUFFER. CountDownLatch drainLatch = new CountDownLatch(1); @@ -316,7 +311,7 @@ protected void initChannel(Channel channel) throws SSLException { } catch (IOException ex) { throw LOGGER.throwableAtError().log(ex, CoreException::from); } - })); + }, info.isHttp2())); channel.config().setAutoRead(true); awaitLatch(drainLatch); diff --git a/sdk/clientcore/http-netty4/src/main/java/io/clientcore/http/netty4/NettyHttpClientBuilder.java b/sdk/clientcore/http-netty4/src/main/java/io/clientcore/http/netty4/NettyHttpClientBuilder.java index d2f02a37afca..b74853327e3a 100644 --- a/sdk/clientcore/http-netty4/src/main/java/io/clientcore/http/netty4/NettyHttpClientBuilder.java +++ b/sdk/clientcore/http-netty4/src/main/java/io/clientcore/http/netty4/NettyHttpClientBuilder.java @@ -133,7 +133,7 @@ private static Class getChannelClass(String className) private Duration readTimeout; private Duration responseTimeout; private Duration writeTimeout; - // private HttpProtocolVersion maximumHttpVersion = HttpProtocolVersion.HTTP_2; + private HttpProtocolVersion maximumHttpVersion = HttpProtocolVersion.HTTP_2; /** * Creates a new instance of {@link NettyHttpClientBuilder}. @@ -260,26 +260,26 @@ public NettyHttpClientBuilder proxy(ProxyOptions proxyOptions) { return this; } - // /** - // * Sets the maximum {@link HttpProtocolVersion HTTP protocol version} that the HTTP client will support. - // *

- // * By default, the maximum HTTP protocol version is set to {@link HttpProtocolVersion#HTTP_2 HTTP_2}. - // *

- // * If {@code httpVersion} is null, it will reset the maximum HTTP protocol version to - // * {@link HttpProtocolVersion#HTTP_2 HTTP_2}. - // * - // * @param httpVersion The maximum HTTP protocol version that the HTTP client will support. - // * @return The updated {@link JdkHttpClientBuilder} object. - // */ - // public NettyHttpClientBuilder maximumHttpVersion(HttpProtocolVersion httpVersion) { - // if (httpVersion != null) { - // this.maximumHttpVersion = httpVersion; - // } else { - // this.maximumHttpVersion = HttpProtocolVersion.HTTP_2; - // } - // - // return this; - // } + /** + * Sets the maximum {@link HttpProtocolVersion HTTP protocol version} that the HTTP client will support. + *

+ * By default, the maximum HTTP protocol version is set to {@link HttpProtocolVersion#HTTP_2 HTTP_2}. + *

+ * If {@code httpVersion} is null, it will reset the maximum HTTP protocol version to + * {@link HttpProtocolVersion#HTTP_2 HTTP_2}. + * + * @param httpVersion The maximum HTTP protocol version that the HTTP client will support. + * @return The updated builder. + */ + public NettyHttpClientBuilder maximumHttpVersion(HttpProtocolVersion httpVersion) { + if (httpVersion != null) { + this.maximumHttpVersion = httpVersion; + } else { + this.maximumHttpVersion = HttpProtocolVersion.HTTP_2; + } + + return this; + } /** * Builds the NettyHttpClient. @@ -312,7 +312,7 @@ public HttpClient build() { ProxyOptions buildProxyOptions = (proxyOptions == null) ? ProxyOptions.fromConfiguration(buildConfiguration, true) : proxyOptions; - return new NettyHttpClient(bootstrap, sslContextModifier, HttpProtocolVersion.HTTP_1_1, + return new NettyHttpClient(bootstrap, sslContextModifier, maximumHttpVersion, new ChannelInitializationProxyHandler(buildProxyOptions), getTimeoutMillis(readTimeout), getTimeoutMillis(responseTimeout), getTimeoutMillis(writeTimeout)); } diff --git a/sdk/clientcore/http-netty4/src/main/java/io/clientcore/http/netty4/implementation/Netty4AlpnHandler.java b/sdk/clientcore/http-netty4/src/main/java/io/clientcore/http/netty4/implementation/Netty4AlpnHandler.java index 2d048fcfc059..024dc9f7686a 100644 --- a/sdk/clientcore/http-netty4/src/main/java/io/clientcore/http/netty4/implementation/Netty4AlpnHandler.java +++ b/sdk/clientcore/http-netty4/src/main/java/io/clientcore/http/netty4/implementation/Netty4AlpnHandler.java @@ -4,13 +4,16 @@ import io.clientcore.core.http.models.HttpRequest; import io.netty.channel.ChannelFutureListener; -import io.netty.channel.ChannelHandlerAdapter; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelPipeline; -import io.netty.handler.codec.http2.Http2FrameCodec; -import io.netty.handler.codec.http2.Http2FrameCodecBuilder; -import io.netty.handler.codec.http2.Http2MultiplexHandler; -import io.netty.handler.flush.FlushConsolidationHandler; +import io.netty.handler.codec.http2.DefaultHttp2Connection; +import io.netty.handler.codec.http2.DelegatingDecompressorFrameListener; +import io.netty.handler.codec.http2.Http2Connection; +import io.netty.handler.codec.http2.Http2FrameListener; +import io.netty.handler.codec.http2.Http2Settings; +import io.netty.handler.codec.http2.HttpToHttp2ConnectionHandler; +import io.netty.handler.codec.http2.HttpToHttp2ConnectionHandlerBuilder; +import io.netty.handler.codec.http2.InboundHttp2ToHttpAdapterBuilder; import io.netty.handler.ssl.ApplicationProtocolNames; import io.netty.handler.ssl.ApplicationProtocolNegotiationHandler; @@ -19,7 +22,6 @@ import static io.clientcore.http.netty4.implementation.Netty4Utility.createCodec; import static io.clientcore.http.netty4.implementation.Netty4Utility.sendHttp11Request; -import static io.clientcore.http.netty4.implementation.Netty4Utility.sendHttp2Request; import static io.clientcore.http.netty4.implementation.Netty4Utility.setOrSuppressError; /** @@ -27,8 +29,9 @@ * either HTTP/1.1 or HTTP/2 based on the result of negotiation. */ public final class Netty4AlpnHandler extends ApplicationProtocolNegotiationHandler { + private static final int TWO_FIFTY_SIX_KB = 256 * 1024; private final HttpRequest request; - private final boolean addProgressAndTimeoutHandler; + private final AtomicReference responseReference; private final AtomicReference errorReference; private final CountDownLatch latch; @@ -36,15 +39,14 @@ public final class Netty4AlpnHandler extends ApplicationProtocolNegotiationHandl * Creates a new instance of {@link Netty4AlpnHandler} with a fallback to using HTTP/1.1. * * @param request The request to send once ALPN negotiation completes. - * @param addProgressAndTimeoutHandler Whether the progress and timeout handler was added to the ChannelPipeline. * @param errorReference An AtomicReference keeping track of errors during the request lifecycle. * @param latch A CountDownLatch that will be released once the request completes. */ - public Netty4AlpnHandler(HttpRequest request, boolean addProgressAndTimeoutHandler, + public Netty4AlpnHandler(HttpRequest request, AtomicReference responseReference, AtomicReference errorReference, CountDownLatch latch) { super(ApplicationProtocolNames.HTTP_1_1); this.request = request; - this.addProgressAndTimeoutHandler = addProgressAndTimeoutHandler; + this.responseReference = responseReference; this.errorReference = errorReference; this.latch = latch; } @@ -57,16 +59,41 @@ public boolean isSharable() { @Override protected void configurePipeline(ChannelHandlerContext ctx, String protocol) { if (ApplicationProtocolNames.HTTP_2.equals(protocol)) { - FlushConsolidationHandler flushConsolidationHandler = new FlushConsolidationHandler(1024, true); - Http2FrameCodec http2FrameCodec = Http2FrameCodecBuilder.forClient().validateHeaders(true).build(); - Http2MultiplexHandler http2MultiplexHandler = new Http2MultiplexHandler(NoOpHandler.INSTANCE); - ChannelPipeline pipeline = ctx.pipeline(); - pipeline.addAfter(Netty4HandlerNames.SSL, Netty4HandlerNames.HTTP_2_FLUSH, flushConsolidationHandler); - pipeline.addAfter(Netty4HandlerNames.HTTP_2_FLUSH, Netty4HandlerNames.HTTP_2_CODEC, http2FrameCodec); - pipeline.addAfter(Netty4HandlerNames.HTTP_2_CODEC, Netty4HandlerNames.HTTP_2_MULTIPLEX, - http2MultiplexHandler); + // TODO (alzimmer): InboundHttp2ToHttpAdapter buffers the entire response into a FullHttpResponse. Need to + // create a streaming version of this to support huge response payloads. + Http2Connection http2Connection = new DefaultHttp2Connection(false); + Http2Settings settings = new Http2Settings().headerTableSize(4096) + .maxHeaderListSize(TWO_FIFTY_SIX_KB) + .pushEnabled(false) + .initialWindowSize(TWO_FIFTY_SIX_KB); + Http2FrameListener frameListener = new DelegatingDecompressorFrameListener(http2Connection, + new InboundHttp2ToHttpAdapterBuilder(http2Connection).maxContentLength(Integer.MAX_VALUE) + .propagateSettings(true) + .validateHttpHeaders(true) + .build()); - sendHttp2Request(request, ctx.channel(), addProgressAndTimeoutHandler, errorReference) + HttpToHttp2ConnectionHandler connectionHandler + = new HttpToHttp2ConnectionHandlerBuilder().initialSettings(settings) + .frameListener(frameListener) + .connection(http2Connection) + .validateHeaders(true) + .build(); + + if (ctx.pipeline().get(Netty4HandlerNames.PROGRESS_AND_TIMEOUT) != null) { + ctx.pipeline() + .addAfter(Netty4HandlerNames.PROGRESS_AND_TIMEOUT, Netty4HandlerNames.HTTP_RESPONSE, + new Netty4ResponseHandler(request, responseReference, errorReference, latch)); + ctx.pipeline() + .addBefore(Netty4HandlerNames.PROGRESS_AND_TIMEOUT, Netty4HandlerNames.HTTP_CODEC, + connectionHandler); + } else { + ctx.pipeline().addAfter(Netty4HandlerNames.SSL, Netty4HandlerNames.HTTP_CODEC, connectionHandler); + ctx.pipeline() + .addAfter(Netty4HandlerNames.HTTP_CODEC, Netty4HandlerNames.HTTP_RESPONSE, + new Netty4ResponseHandler(request, responseReference, errorReference, latch)); + } + + sendHttp11Request(request, ctx.channel(), errorReference) .addListener((ChannelFutureListener) sendListener -> { if (!sendListener.isSuccess()) { setOrSuppressError(errorReference, sendListener.cause()); @@ -77,9 +104,20 @@ protected void configurePipeline(ChannelHandlerContext ctx, String protocol) { } }); } else if (ApplicationProtocolNames.HTTP_1_1.equals(protocol)) { - ctx.pipeline().addAfter(Netty4HandlerNames.SSL, Netty4HandlerNames.HTTP_1_1_CODEC, createCodec()); + if (ctx.pipeline().get(Netty4HandlerNames.PROGRESS_AND_TIMEOUT) != null) { + ctx.pipeline() + .addAfter(Netty4HandlerNames.PROGRESS_AND_TIMEOUT, Netty4HandlerNames.HTTP_RESPONSE, + new Netty4ResponseHandler(request, responseReference, errorReference, latch)); + ctx.pipeline() + .addBefore(Netty4HandlerNames.PROGRESS_AND_TIMEOUT, Netty4HandlerNames.HTTP_CODEC, createCodec()); + } else { + ctx.pipeline().addAfter(Netty4HandlerNames.SSL, Netty4HandlerNames.HTTP_CODEC, createCodec()); + ctx.pipeline() + .addAfter(Netty4HandlerNames.HTTP_CODEC, Netty4HandlerNames.HTTP_RESPONSE, + new Netty4ResponseHandler(request, responseReference, errorReference, latch)); + } - sendHttp11Request(request, ctx.channel(), addProgressAndTimeoutHandler, errorReference) + sendHttp11Request(request, ctx.channel(), errorReference) .addListener((ChannelFutureListener) sendListener -> { if (!sendListener.isSuccess()) { setOrSuppressError(errorReference, sendListener.cause()); @@ -93,13 +131,4 @@ protected void configurePipeline(ChannelHandlerContext ctx, String protocol) { throw new IllegalStateException("unknown protocol: " + protocol); } } - - private static final class NoOpHandler extends ChannelHandlerAdapter { - private static final NoOpHandler INSTANCE = new NoOpHandler(); - - @Override - public boolean isSharable() { - return true; - } - } } diff --git a/sdk/clientcore/http-netty4/src/main/java/io/clientcore/http/netty4/implementation/Netty4ChannelBinaryData.java b/sdk/clientcore/http-netty4/src/main/java/io/clientcore/http/netty4/implementation/Netty4ChannelBinaryData.java index ba46556faf72..837f4d164b1b 100644 --- a/sdk/clientcore/http-netty4/src/main/java/io/clientcore/http/netty4/implementation/Netty4ChannelBinaryData.java +++ b/sdk/clientcore/http-netty4/src/main/java/io/clientcore/http/netty4/implementation/Netty4ChannelBinaryData.java @@ -35,6 +35,7 @@ public final class Netty4ChannelBinaryData extends BinaryData { private final Channel channel; private final Long length; + private final boolean isHttp2; // Non-final to allow nulling out after use. private ByteArrayOutputStream eagerContent; @@ -47,11 +48,13 @@ public final class Netty4ChannelBinaryData extends BinaryData { * @param eagerContent Response body content that was eagerly read by Netty while processing the HTTP headers. * @param channel The Netty {@link Channel}. * @param length Size of the response body (if known). + * @param isHttp2 Flag indicating whether the handler is used for HTTP/2 or not. */ - public Netty4ChannelBinaryData(ByteArrayOutputStream eagerContent, Channel channel, Long length) { + public Netty4ChannelBinaryData(ByteArrayOutputStream eagerContent, Channel channel, Long length, boolean isHttp2) { this.eagerContent = eagerContent; this.channel = channel; this.length = length; + this.isHttp2 = isHttp2; } @Override @@ -62,8 +65,8 @@ public byte[] toBytes() { if (bytes == null) { CountDownLatch latch = new CountDownLatch(1); - Netty4EagerConsumeChannelHandler handler - = new Netty4EagerConsumeChannelHandler(latch, buf -> buf.readBytes(eagerContent, buf.readableBytes())); + Netty4EagerConsumeChannelHandler handler = new Netty4EagerConsumeChannelHandler(latch, + buf -> buf.readBytes(eagerContent, buf.readableBytes()), isHttp2); channel.pipeline().addLast(Netty4HandlerNames.EAGER_CONSUME, handler); channel.config().setAutoRead(true); @@ -102,7 +105,7 @@ public T toObject(Type type, ObjectSerializer serializer) { @Override public InputStream toStream() { if (bytes == null) { - return new Netty4ChannelInputStream(eagerContent, channel); + return new Netty4ChannelInputStream(eagerContent, channel, isHttp2); } else { return new ByteArrayInputStream(bytes); } @@ -130,7 +133,7 @@ public void writeTo(OutputStream outputStream) { CountDownLatch latch = new CountDownLatch(1); Netty4EagerConsumeChannelHandler handler = new Netty4EagerConsumeChannelHandler(latch, - buf -> buf.readBytes(outputStream, buf.readableBytes())); + buf -> buf.readBytes(outputStream, buf.readableBytes()), isHttp2); channel.pipeline().addLast(Netty4HandlerNames.EAGER_CONSUME, handler); channel.config().setAutoRead(true); diff --git a/sdk/clientcore/http-netty4/src/main/java/io/clientcore/http/netty4/implementation/Netty4ChannelInputStream.java b/sdk/clientcore/http-netty4/src/main/java/io/clientcore/http/netty4/implementation/Netty4ChannelInputStream.java index e97927ae3fc4..418df7945248 100644 --- a/sdk/clientcore/http-netty4/src/main/java/io/clientcore/http/netty4/implementation/Netty4ChannelInputStream.java +++ b/sdk/clientcore/http-netty4/src/main/java/io/clientcore/http/netty4/implementation/Netty4ChannelInputStream.java @@ -15,6 +15,7 @@ */ public final class Netty4ChannelInputStream extends InputStream { private final Channel channel; + private final boolean isHttp2; // Indicator for the Channel being fully read. // This will become true before 'streamDone' becomes true, but both may become true in the same operation. @@ -44,8 +45,9 @@ public final class Netty4ChannelInputStream extends InputStream { * @param eagerContent Any response body content eagerly read from the {@link Channel} when processing the initial * status line and response headers. * @param channel The {@link Channel} to read from. + * @param isHttp2 Flag indicating whether the Channel is used for HTTP/2 or not. */ - Netty4ChannelInputStream(ByteArrayOutputStream eagerContent, Channel channel) { + Netty4ChannelInputStream(ByteArrayOutputStream eagerContent, Channel channel, boolean isHttp2) { if (eagerContent != null && eagerContent.size() > 0) { this.currentBuffer = eagerContent.toByteArray(); } else { @@ -57,6 +59,7 @@ public final class Netty4ChannelInputStream extends InputStream { if (channel.pipeline().get(Netty4InitiateOneReadHandler.class) != null) { channel.pipeline().remove(Netty4InitiateOneReadHandler.class); } + this.isHttp2 = isHttp2; } byte[] getCurrentBuffer() { @@ -168,8 +171,10 @@ public long skip(long n) throws IOException { public void close() { currentBuffer = null; additionalBuffers.clear(); - channel.disconnect(); - channel.close(); + if (channel.isOpen() || channel.isActive()) { + channel.disconnect(); + channel.close(); + } } private boolean setupNextBuffer() throws IOException { @@ -210,7 +215,7 @@ private boolean readMore() throws IOException { byteBuf.readBytes(buffer); additionalBuffers.add(buffer); - }); + }, isHttp2); channel.pipeline().addLast(Netty4HandlerNames.READ_ONE, handler); } diff --git a/sdk/clientcore/http-netty4/src/main/java/io/clientcore/http/netty4/implementation/Netty4EagerConsumeChannelHandler.java b/sdk/clientcore/http-netty4/src/main/java/io/clientcore/http/netty4/implementation/Netty4EagerConsumeChannelHandler.java index 2d7aef28a00d..74cf9911a145 100644 --- a/sdk/clientcore/http-netty4/src/main/java/io/clientcore/http/netty4/implementation/Netty4EagerConsumeChannelHandler.java +++ b/sdk/clientcore/http-netty4/src/main/java/io/clientcore/http/netty4/implementation/Netty4EagerConsumeChannelHandler.java @@ -9,6 +9,7 @@ import io.netty.channel.ChannelInboundHandler; import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.handler.codec.http.LastHttpContent; +import io.netty.handler.codec.http2.Http2DataFrame; import io.netty.util.ReferenceCountUtil; import java.io.IOException; @@ -22,6 +23,7 @@ public final class Netty4EagerConsumeChannelHandler extends ChannelInboundHandlerAdapter { private final CountDownLatch latch; private final IOExceptionCheckedConsumer byteBufConsumer; + private final boolean isHttp2; private boolean lastRead; private Throwable exception; @@ -31,10 +33,13 @@ public final class Netty4EagerConsumeChannelHandler extends ChannelInboundHandle * * @param latch The latch to count down when the response is fully read, or an exception occurs. * @param byteBufConsumer The consumer to process the {@link ByteBuf ByteBufs} as they are read. + * @param isHttp2 Flag indicating whether the handler is used for HTTP/2 or not. */ - public Netty4EagerConsumeChannelHandler(CountDownLatch latch, IOExceptionCheckedConsumer byteBufConsumer) { + public Netty4EagerConsumeChannelHandler(CountDownLatch latch, IOExceptionCheckedConsumer byteBufConsumer, + boolean isHttp2) { this.latch = latch; this.byteBufConsumer = byteBufConsumer; + this.isHttp2 = isHttp2; } @Override @@ -56,7 +61,11 @@ public void channelRead(ChannelHandlerContext ctx, Object msg) { } } - lastRead = msg instanceof LastHttpContent; + if (isHttp2) { + lastRead = msg instanceof Http2DataFrame && ((Http2DataFrame) msg).isEndStream(); + } else { + lastRead = msg instanceof LastHttpContent; + } ctx.fireChannelRead(msg); } diff --git a/sdk/clientcore/http-netty4/src/main/java/io/clientcore/http/netty4/implementation/Netty4HandlerNames.java b/sdk/clientcore/http-netty4/src/main/java/io/clientcore/http/netty4/implementation/Netty4HandlerNames.java index 9f90763e4716..eee40c7c4fe7 100644 --- a/sdk/clientcore/http-netty4/src/main/java/io/clientcore/http/netty4/implementation/Netty4HandlerNames.java +++ b/sdk/clientcore/http-netty4/src/main/java/io/clientcore/http/netty4/implementation/Netty4HandlerNames.java @@ -3,9 +3,6 @@ package io.clientcore.http.netty4.implementation; import io.netty.handler.codec.http.HttpClientCodec; -import io.netty.handler.codec.http2.Http2FrameCodec; -import io.netty.handler.codec.http2.Http2MultiplexHandler; -import io.netty.handler.flush.FlushConsolidationHandler; import io.netty.handler.proxy.ProxyHandler; import io.netty.handler.ssl.SslHandler; import io.netty.handler.stream.ChunkedWriteHandler; @@ -38,29 +35,14 @@ public final class Netty4HandlerNames { public static final String SSL_INITIALIZER = "clientcore.sslinitializer"; /** - * Name for the {@link Netty4H2OrHttp11Handler}. + * Name for the {@link Netty4AlpnHandler}. */ - public static final String HTTP_VERSION_PICKER = "clientcore.httpversionpicker"; + public static final String ALPN = "clientcore.alpn"; /** * Name for the HTTP/1.1 {@link HttpClientCodec} */ - public static final String HTTP_1_1_CODEC = "clientcore.http11codec"; - - /** - * Name for the HTTP/2 {@link FlushConsolidationHandler}. - */ - public static final String HTTP_2_FLUSH = "clientcore.http2flush"; - - /** - * Name for the HTTP/2 {@link Http2FrameCodec}. - */ - public static final String HTTP_2_CODEC = "clientcore.http2codec"; - - /** - * Name for the HTTP/2 {@link Http2MultiplexHandler}. - */ - public static final String HTTP_2_MULTIPLEX = "clientcore.http2multiplex"; + public static final String HTTP_CODEC = "clientcore.httpcodec"; /** * Name for the {@link Netty4ProgressAndTimeoutHandler}. @@ -75,7 +57,7 @@ public final class Netty4HandlerNames { /** * Name for the {@link Netty4ResponseHandler}. */ - public static final String RESPONSE = "clientcore.response"; + public static final String HTTP_RESPONSE = "clientcore.httpresponse"; /** * Name for the {@link Netty4EagerConsumeChannelHandler}. diff --git a/sdk/clientcore/http-netty4/src/main/java/io/clientcore/http/netty4/implementation/Netty4InitiateOneReadHandler.java b/sdk/clientcore/http-netty4/src/main/java/io/clientcore/http/netty4/implementation/Netty4InitiateOneReadHandler.java index 8ddc1c9a4fc6..6a242ed03246 100644 --- a/sdk/clientcore/http-netty4/src/main/java/io/clientcore/http/netty4/implementation/Netty4InitiateOneReadHandler.java +++ b/sdk/clientcore/http-netty4/src/main/java/io/clientcore/http/netty4/implementation/Netty4InitiateOneReadHandler.java @@ -9,6 +9,7 @@ import io.netty.channel.ChannelInboundHandler; import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.handler.codec.http.LastHttpContent; +import io.netty.handler.codec.http2.Http2DataFrame; import io.netty.util.ReferenceCountUtil; import java.io.IOException; @@ -21,6 +22,8 @@ */ public final class Netty4InitiateOneReadHandler extends ChannelInboundHandlerAdapter { private final IOExceptionCheckedConsumer byteBufConsumer; + private final boolean isHttp2; + private CountDownLatch latch; private boolean lastRead; @@ -35,10 +38,13 @@ public final class Netty4InitiateOneReadHandler extends ChannelInboundHandlerAda * * @param latch The latch to count down when the channel read completes. * @param byteBufConsumer The consumer to process the {@link ByteBuf ByteBufs} as they are read. + * @param isHttp2 Flag indicating whether the handler is used for HTTP/2 or not. */ - public Netty4InitiateOneReadHandler(CountDownLatch latch, IOExceptionCheckedConsumer byteBufConsumer) { + public Netty4InitiateOneReadHandler(CountDownLatch latch, IOExceptionCheckedConsumer byteBufConsumer, + boolean isHttp2) { this.latch = latch; this.byteBufConsumer = byteBufConsumer; + this.isHttp2 = isHttp2; } /** @@ -77,7 +83,11 @@ public void channelRead(ChannelHandlerContext ctx, Object msg) { } } - lastRead = msg instanceof LastHttpContent; + if (isHttp2) { + lastRead = msg instanceof Http2DataFrame && ((Http2DataFrame) msg).isEndStream(); + } else { + lastRead = msg instanceof LastHttpContent; + } ctx.fireChannelRead(msg); } diff --git a/sdk/clientcore/http-netty4/src/main/java/io/clientcore/http/netty4/implementation/Netty4ResponseHandler.java b/sdk/clientcore/http-netty4/src/main/java/io/clientcore/http/netty4/implementation/Netty4ResponseHandler.java index a9478416f01a..5da09d7c8924 100644 --- a/sdk/clientcore/http-netty4/src/main/java/io/clientcore/http/netty4/implementation/Netty4ResponseHandler.java +++ b/sdk/clientcore/http-netty4/src/main/java/io/clientcore/http/netty4/implementation/Netty4ResponseHandler.java @@ -25,8 +25,8 @@ import static io.clientcore.http.netty4.implementation.Netty4Utility.setOrSuppressError; /** - * A {@link ChannelInboundHandler} implementation that appropriately handles the response reading from the server based - * on the information provided from the headers. + * A {@link ChannelInboundHandler} implementation that appropriately handles {@code HTTP/1.1} responses by using the + * response headers to determine how to read the response from the server. *

* When used with {@code NettyHttpClient} this handler must be added to the pipeline so that the {@link HttpClientCodec} * is able to decode the data of the response. @@ -77,7 +77,7 @@ public boolean isSharable() { } @Override - public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { setOrSuppressError(errorReference, cause); latch.countDown(); } @@ -126,8 +126,8 @@ public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception started = true; HttpResponse response = (HttpResponse) msg; this.statusCode = response.status().code(); - this.headers = (response.headers() instanceof WrappedHttpHeaders) - ? ((WrappedHttpHeaders) response.headers()).getCoreHeaders() + this.headers = (response.headers() instanceof WrappedHttp11Headers) + ? ((WrappedHttp11Headers) response.headers()).getCoreHeaders() : Netty4Utility.convertHeaders(response.headers()); if (msg instanceof FullHttpResponse) { @@ -173,11 +173,7 @@ public void channelReadComplete(ChannelHandlerContext ctx) throws Exception { } responseReference.set(new ResponseStateInfo(ctx.channel(), complete, statusCode, headers, eagerContent, - ResponseBodyHandling.getBodyHandling(request, headers))); + ResponseBodyHandling.getBodyHandling(request, headers), false)); latch.countDown(); } - - private enum BodyHandling { - IGNORE, STREAM, BUFFER - } } diff --git a/sdk/clientcore/http-netty4/src/main/java/io/clientcore/http/netty4/implementation/Netty4StreamingHttp2Adapter.java b/sdk/clientcore/http-netty4/src/main/java/io/clientcore/http/netty4/implementation/Netty4StreamingHttp2Adapter.java new file mode 100644 index 000000000000..9c15c5b66098 --- /dev/null +++ b/sdk/clientcore/http-netty4/src/main/java/io/clientcore/http/netty4/implementation/Netty4StreamingHttp2Adapter.java @@ -0,0 +1,139 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package io.clientcore.http.netty4.implementation; + +import io.clientcore.core.instrumentation.logging.ClientLogger; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.http.DefaultFullHttpResponse; +import io.netty.handler.codec.http.DefaultHttpContent; +import io.netty.handler.codec.http.DefaultHttpResponse; +import io.netty.handler.codec.http.DefaultLastHttpContent; +import io.netty.handler.codec.http.FullHttpMessage; +import io.netty.handler.codec.http.FullHttpResponse; +import io.netty.handler.codec.http.HttpHeaders; +import io.netty.handler.codec.http.HttpResponse; +import io.netty.handler.codec.http.HttpVersion; +import io.netty.handler.codec.http2.Http2CodecUtil; +import io.netty.handler.codec.http2.Http2Connection; +import io.netty.handler.codec.http2.Http2Error; +import io.netty.handler.codec.http2.Http2EventAdapter; +import io.netty.handler.codec.http2.Http2Exception; +import io.netty.handler.codec.http2.Http2Headers; +import io.netty.handler.codec.http2.Http2Settings; +import io.netty.handler.codec.http2.Http2Stream; +import io.netty.handler.codec.http2.HttpConversionUtil; +import io.netty.handler.codec.http2.InboundHttp2ToHttpAdapter; + +import static io.netty.handler.codec.http2.Http2Error.PROTOCOL_ERROR; +import static io.netty.handler.codec.http2.Http2Exception.connectionError; +import static io.netty.handler.codec.http2.HttpConversionUtil.addHttp2ToHttpHeaders; +import static io.netty.handler.codec.http2.HttpConversionUtil.parseStatus; + +/** + * Implementation of {@link Http2EventAdapter} that converts HTTP/2 frames into HTTP/1.1 objects. + *

+ * This is similar to {@link InboundHttp2ToHttpAdapter} but it doesn't buffer the entire response into a + * {@link FullHttpResponse}. Rather it streams frames as they arrive, allowing for more efficient memory usage. + */ +final class Netty4StreamingHttp2Adapter extends Http2EventAdapter { + private static final ClientLogger LOGGER = new ClientLogger(Netty4StreamingHttp2Adapter.class); + + private final Http2Connection connection; + + Netty4StreamingHttp2Adapter(Http2Connection connection) { + this.connection = connection; + } + + // TODO (alzimmer): This implementation is close but needs a way to control when WINDOWS_UPDATE frames are sent to + // prevent race conditions between switching from the initial response data handling in Netty4ResponseHandler and + // either eager or deferred content reading in the custom handlers. + // For now, while huge responses don't need to be supported yet, use InboundHttp2ToHttpAdapter to buffer the + // entire response into a FullHttpResponse. + @Override + public int onDataRead(ChannelHandlerContext ctx, int streamId, ByteBuf data, int padding, boolean endOfStream) + throws Http2Exception { + Http2Stream stream = connection.stream(streamId); + if (stream == null) { + throw LOGGER.throwableAtError() + .addKeyValue("streamId", streamId) + .log("Data Frame received for unknown stream", message -> connectionError(PROTOCOL_ERROR, message)); + } + + // data may be using pooled buffers (can't find a way to determine if it is pooled or not), and downstream may + // not eagerly consume the data. Create a copy to ensure that the data is not reclaimed / corrupted before use. + int dataReadableBytes = data.readableBytes(); + data = Unpooled.copiedBuffer(data); + if (endOfStream) { + ctx.fireChannelRead(new DefaultLastHttpContent(data)); + } else { + ctx.fireChannelRead(new DefaultHttpContent(data)); + } + + // All bytes have been processed. + return dataReadableBytes + padding; + } + + @Override + public void onHeadersRead(ChannelHandlerContext ctx, int streamId, Http2Headers headers, int padding, + boolean endOfStream) throws Http2Exception { + onHeadersRead(ctx, streamId, headers, -1, (short) -1, false, padding, endOfStream); + } + + @Override + public void onHeadersRead(ChannelHandlerContext ctx, int streamId, Http2Headers headers, int streamDependency, + short weight, boolean exclusive, int padding, boolean endOfStream) throws Http2Exception { + Http2Stream stream = connection.stream(streamId); + if (stream == null) { + throw LOGGER.throwableAtError() + .addKeyValue("streamId", streamId) + .log("Header Frame received for unknown stream", message -> connectionError(PROTOCOL_ERROR, message)); + } + + HttpHeaders httpHeaders = new WrappedHttp11Headers(new io.clientcore.core.http.models.HttpHeaders()); + addHttp2ToHttpHeaders(streamId, headers, httpHeaders, HttpVersion.HTTP_1_1, false, false); + + HttpResponse response; + if (endOfStream) { + response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, parseStatus(headers.status()), + Unpooled.EMPTY_BUFFER, new WrappedHttp11Headers(new io.clientcore.core.http.models.HttpHeaders()), + new WrappedHttp11Headers(new io.clientcore.core.http.models.HttpHeaders())); + addHttp2ToHttpHeaders(streamId, headers, (FullHttpMessage) response, false); + } else { + HttpHeaders wrappedHeaders = new WrappedHttp11Headers(new io.clientcore.core.http.models.HttpHeaders()); + response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, parseStatus(headers.status()), wrappedHeaders); + addHttp2ToHttpHeaders(streamId, headers, wrappedHeaders, HttpVersion.HTTP_1_1, false, false); + } + + // Add special headers for stream dependency and weight. + if (streamDependency > Http2CodecUtil.CONNECTION_STREAM_ID) { + response.headers() + .setInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_DEPENDENCY_ID.text(), streamDependency); + } + if (weight > 0) { + response.headers().setShort(HttpConversionUtil.ExtensionHeaderNames.STREAM_WEIGHT.text(), weight); + } + + ctx.fireChannelRead(response); + } + + @Override + public void onRstStreamRead(ChannelHandlerContext ctx, int streamId, long errorCode) throws Http2Exception { + ctx.fireExceptionCaught(LOGGER.throwableAtError() + .log("HTTP/2 to HTTP layer caught stream reset", + message -> connectionError(Http2Error.valueOf(errorCode), message))); + } + + @Override + public void onPushPromiseRead(ChannelHandlerContext ctx, int streamId, int promisedStreamId, Http2Headers headers, + int padding) throws Http2Exception { + ctx.fireExceptionCaught(new UnsupportedOperationException("Push promises are not supported.")); + } + + @Override + public void onSettingsRead(ChannelHandlerContext ctx, Http2Settings settings) throws Http2Exception { + // Propagate settings for downstream handlers to process. + ctx.fireChannelRead(settings); + } +} diff --git a/sdk/clientcore/http-netty4/src/main/java/io/clientcore/http/netty4/implementation/Netty4Utility.java b/sdk/clientcore/http-netty4/src/main/java/io/clientcore/http/netty4/implementation/Netty4Utility.java index 07cae2037b9a..bc0daf8efe8c 100644 --- a/sdk/clientcore/http-netty4/src/main/java/io/clientcore/http/netty4/implementation/Netty4Utility.java +++ b/sdk/clientcore/http-netty4/src/main/java/io/clientcore/http/netty4/implementation/Netty4Utility.java @@ -19,20 +19,15 @@ import io.netty.buffer.Unpooled; import io.netty.channel.Channel; import io.netty.channel.ChannelFuture; -import io.netty.channel.ChannelPipeline; import io.netty.handler.codec.http.DefaultFullHttpRequest; import io.netty.handler.codec.http.DefaultHttpRequest; +import io.netty.handler.codec.http.HttpChunkedInput; import io.netty.handler.codec.http.HttpClientCodec; import io.netty.handler.codec.http.HttpDecoderConfig; +import io.netty.handler.codec.http.HttpHeaderNames; import io.netty.handler.codec.http.HttpHeadersFactory; import io.netty.handler.codec.http.HttpMethod; import io.netty.handler.codec.http.HttpVersion; -import io.netty.handler.codec.http2.DefaultHttp2DataFrame; -import io.netty.handler.codec.http2.DefaultHttp2Headers; -import io.netty.handler.codec.http2.DefaultHttp2HeadersFrame; -import io.netty.handler.codec.http2.Http2DataChunkedInput; -import io.netty.handler.codec.http2.Http2Headers; -import io.netty.handler.codec.http2.Http2HeadersFrame; import io.netty.handler.stream.ChunkedInput; import io.netty.handler.stream.ChunkedNioFile; import io.netty.handler.stream.ChunkedStream; @@ -74,16 +69,10 @@ public final class Netty4Utility { private static final List OPTIONAL_NETTY_VERSION_ARTIFACTS = Arrays .asList("netty-transport-native-unix-common", "netty-transport-native-epoll", "netty-transport-native-kqueue"); - /** - * Name given to the {@link Netty4ProgressAndTimeoutHandler} used in the {@link ChannelPipeline} created by - * {@code NettyHttpClient}. - */ - public static final String PROGRESS_AND_TIMEOUT_HANDLER_NAME = "Netty4-Progress-And-Timeout-Handler"; - /** * Converts Netty HttpHeaders to ClientCore HttpHeaders. *

- * Most Netty requests should store headers in {@link WrappedHttpHeaders}, but if that doesn't happen this method + * Most Netty requests should store headers in {@link WrappedHttp11Headers}, but if that doesn't happen this method * can be used to convert the Netty headers to ClientCore headers. * * @param nettyHeaders Netty HttpHeaders. @@ -144,7 +133,7 @@ static void readByteBufIntoOutputStream(ByteBuf byteBuf, OutputStream stream) th /** * Creates an {@link HttpClientCodec} that uses a custom {@link HttpDecoderConfig} that injects - * {@link WrappedHttpHeaders} functionality. + * {@link WrappedHttp11Headers} functionality. * * @return A new {@link HttpClientCodec} instance. */ @@ -156,20 +145,20 @@ public static HttpClientCodec createCodec() { } /** - * Custom implementation of {@link HttpHeadersFactory} that creates {@link WrappedHttpHeaders}. + * Custom implementation of {@link HttpHeadersFactory} that creates {@link WrappedHttp11Headers}. *

- * Using {@link WrappedHttpHeaders} is a performance optimization to remove converting Netty's HttpHeaders to + * Using {@link WrappedHttp11Headers} is a performance optimization to remove converting Netty's HttpHeaders to * ClientCore's HttpHeaders and vice versa. */ private static final class WrappedHttpHeadersFactory implements HttpHeadersFactory { @Override public io.netty.handler.codec.http.HttpHeaders newHeaders() { - return new WrappedHttpHeaders(new io.clientcore.core.http.models.HttpHeaders()); + return new WrappedHttp11Headers(new io.clientcore.core.http.models.HttpHeaders()); } @Override public io.netty.handler.codec.http.HttpHeaders newEmptyHeaders() { - return new WrappedHttpHeaders(new io.clientcore.core.http.models.HttpHeaders()); + return new WrappedHttp11Headers(new io.clientcore.core.http.models.HttpHeaders()); } } @@ -178,15 +167,15 @@ public io.netty.handler.codec.http.HttpHeaders newEmptyHeaders() { * {@link io.netty.handler.codec.http.HttpHeaders}. *

* This method inspects the Netty {@link io.netty.handler.codec.http.HttpHeaders} for being an instance of - * {@link WrappedHttpHeaders}. If it is not an instanceof it will use the {@code nettyHeaderName} to retrieve all + * {@link WrappedHttp11Headers}. If it is not an instanceof it will use the {@code nettyHeaderName} to retrieve all * values. If it is an instanceof it will use the {@code clientCoreHeaderName}. *

* This method is an attempt to optimize retrieval as Netty and ClientCore use different structures for managing * headers, where in many cases lookup is faster for ClientCore headers. * * @param headers The Netty {@link io.netty.handler.codec.http.HttpHeaders} to retrieve all header values from. - * @param nettyHeaderName The header name to use when retrieving from a non-{@link WrappedHttpHeaders}. - * @param clientCoreHeaderName The header name to use when retrieving from a {@link WrappedHttpHeaders}. + * @param nettyHeaderName The header name to use when retrieving from a non-{@link WrappedHttp11Headers}. + * @param clientCoreHeaderName The header name to use when retrieving from a {@link WrappedHttp11Headers}. * @return The value for the header name, or null if the header didn't exist in the headers. */ public static String get(io.netty.handler.codec.http.HttpHeaders headers, CharSequence nettyHeaderName, @@ -200,21 +189,21 @@ public static String get(io.netty.handler.codec.http.HttpHeaders headers, CharSe * {@link io.netty.handler.codec.http.HttpHeaders}. *

* This method inspects the Netty {@link io.netty.handler.codec.http.HttpHeaders} for being an instance of - * {@link WrappedHttpHeaders}. If it is not an instanceof it will use the {@code nettyHeaderName} to retrieve all + * {@link WrappedHttp11Headers}. If it is not an instanceof it will use the {@code nettyHeaderName} to retrieve all * values. If it is an instanceof it will use the {@code clientCoreHeaderName}. *

* This method is an attempt to optimize retrieval as Netty and ClientCore use different structures for managing * headers, where in many cases lookup is faster for ClientCore headers. * * @param headers The Netty {@link io.netty.handler.codec.http.HttpHeaders} to retrieve all header values from. - * @param nettyHeaderName The header name to use when retrieving from a non-{@link WrappedHttpHeaders}. - * @param clientCoreHeaderName The header name to use when retrieving from a {@link WrappedHttpHeaders}. + * @param nettyHeaderName The header name to use when retrieving from a non-{@link WrappedHttp11Headers}. + * @param clientCoreHeaderName The header name to use when retrieving from a {@link WrappedHttp11Headers}. * @return The list of values for the header name, or an empty list if the header didn't exist in the headers. */ public static List getAll(io.netty.handler.codec.http.HttpHeaders headers, CharSequence nettyHeaderName, HttpHeaderName clientCoreHeaderName) { - if (headers instanceof WrappedHttpHeaders) { - HttpHeader header = ((WrappedHttpHeaders) headers).getCoreHeaders().get(clientCoreHeaderName); + if (headers instanceof WrappedHttp11Headers) { + HttpHeader header = ((WrappedHttp11Headers) headers).getCoreHeaders().get(clientCoreHeaderName); return (header == null) ? Collections.emptyList() : header.getValues(); } else { return headers.getAll(nettyHeaderName); @@ -250,20 +239,18 @@ public static void setOrSuppressError(AtomicReference errorReference, * * @param request The HTTP request to send. * @param channel The Channel to send the request. - * @param progressAndTimeoutHandlerAdded Whether the ChannelPipeline associated with the Channel had the progress - * and timeout handler added. * @param errorReference An AtomicReference tracking exceptions seen during the request lifecycle. * @return A ChannelFuture that will complete once the request has been sent. */ public static ChannelFuture sendHttp11Request(HttpRequest request, Channel channel, - boolean progressAndTimeoutHandlerAdded, AtomicReference errorReference) { + AtomicReference errorReference) { HttpMethod nettyMethod = HttpMethod.valueOf(request.getHttpMethod().toString()); String uri = request.getUri().toString(); - WrappedHttpHeaders wrappedHttpHeaders = new WrappedHttpHeaders(request.getHeaders()); + WrappedHttp11Headers wrappedHttp11Headers = new WrappedHttp11Headers(request.getHeaders()); // TODO (alzimmer): This will mutate the underlying ClientCore HttpHeaders. Will need to think about this design // more once it's closer to completion. - wrappedHttpHeaders.getCoreHeaders().set(HttpHeaderName.HOST, request.getUri().getHost()); + wrappedHttp11Headers.getCoreHeaders().set(HttpHeaderName.HOST, request.getUri().getHost()); BinaryData requestBody = request.getBody(); if (requestBody instanceof FileBinaryData) { @@ -272,15 +259,14 @@ public static ChannelFuture sendHttp11Request(HttpRequest request, Channel chann return sendChunkedHttp11(channel, new ChunkedNioFile(FileChannel.open(fileBinaryData.getFile(), StandardOpenOption.READ), fileBinaryData.getPosition(), fileBinaryData.getLength(), 8192), - new DefaultHttpRequest(HttpVersion.HTTP_1_1, nettyMethod, uri, wrappedHttpHeaders), - progressAndTimeoutHandlerAdded, errorReference); + new DefaultHttpRequest(HttpVersion.HTTP_1_1, nettyMethod, uri, wrappedHttp11Headers), + errorReference); } catch (IOException ex) { return channel.newFailedFuture(ex); } } else if (requestBody instanceof InputStreamBinaryData) { return sendChunkedHttp11(channel, new ChunkedStream(requestBody.toStream()), - new DefaultHttpRequest(HttpVersion.HTTP_1_1, nettyMethod, uri, wrappedHttpHeaders), - progressAndTimeoutHandlerAdded, errorReference); + new DefaultHttpRequest(HttpVersion.HTTP_1_1, nettyMethod, uri, wrappedHttp11Headers), errorReference); } else { ByteBuf body = Unpooled.EMPTY_BUFFER; if (requestBody != null && requestBody != BinaryData.empty()) { @@ -291,7 +277,7 @@ public static ChannelFuture sendHttp11Request(HttpRequest request, Channel chann if (body.readableBytes() > 0) { // TODO (alzimmer): Should we be setting Content-Length here again? Shouldn't this be handled externally // by the creator of the HttpRequest? - wrappedHttpHeaders.getCoreHeaders() + wrappedHttp11Headers.getCoreHeaders() .set(HttpHeaderName.CONTENT_LENGTH, String.valueOf(body.readableBytes())); } @@ -301,23 +287,16 @@ public static ChannelFuture sendHttp11Request(HttpRequest request, Channel chann } return channel.writeAndFlush(new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, nettyMethod, uri, body, - wrappedHttpHeaders, trailersFactory().newHeaders())); + wrappedHttp11Headers, trailersFactory().newHeaders())); } } private static ChannelFuture sendChunkedHttp11(Channel channel, ChunkedInput chunkedInput, - io.netty.handler.codec.http.HttpRequest initialLineAndHeaders, boolean progressAndTimeoutHandlerAdded, - AtomicReference errorReference) { + io.netty.handler.codec.http.HttpRequest initialLineAndHeaders, AtomicReference errorReference) { if (channel.pipeline().get(Netty4HandlerNames.CHUNKED_WRITER) == null) { // Add the ChunkedWriteHandler which will handle sending the chunkedInput. - ChunkedWriteHandler chunkedWriteHandler = new ChunkedWriteHandler(); - if (progressAndTimeoutHandlerAdded) { - channel.pipeline() - .addBefore(Netty4HandlerNames.PROGRESS_AND_TIMEOUT, Netty4HandlerNames.CHUNKED_WRITER, - chunkedWriteHandler); - } else { - channel.pipeline().addLast(Netty4HandlerNames.CHUNKED_WRITER, chunkedWriteHandler); - } + channel.pipeline() + .addAfter(Netty4HandlerNames.HTTP_CODEC, Netty4HandlerNames.CHUNKED_WRITER, new ChunkedWriteHandler()); } Throwable error = errorReference.get(); @@ -326,99 +305,7 @@ private static ChannelFuture sendChunkedHttp11(Channel channel, ChunkedInput errorReference) { - // HTTP/2 requests are more complicated than HTTP/1.1 as they are a stream of frames with specific purposes. - // Additionally, since we're using multiplexing, we need to associate a stream ID with each frame. - - // Send the headers frame(s). - // Unlike in HTTP/1.1, there isn't a status line on requests. Rather pseudo headers are used. - // TODO (alzimmer): Create an Http2Headers implementations similar to WrappedHttpHeaders. - Http2Headers headers = new DefaultHttp2Headers(); - headers.method(request.getHttpMethod().toString()); - headers.scheme(request.getUri().getScheme()); - headers.authority(request.getUri().getAuthority()); - if (request.getUri().getPath() != null) { - headers.path(request.getUri().getPath()); - } - - // If the request doesn't have a body or is a HEAD request, only a headers frame should be sent before the - // client indicates closure of its half of the stream. - BinaryData requestBody = request.getBody(); - Long bodyLength = requestBody == null ? null : requestBody.getLength(); - boolean headersOnly = (bodyLength == null || bodyLength == 0) - || request.getHttpMethod() == io.clientcore.core.http.models.HttpMethod.HEAD; - - request.getHeaders() - .stream() - .forEach(httpHeader -> headers.add(httpHeader.getName().getCaseInsensitiveName(), httpHeader.getValues())); - Http2HeadersFrame headersFrame = new DefaultHttp2HeadersFrame(headers, headersOnly); - - if (headersOnly) { - return channel.write(headersFrame); - } - - channel.write(headersFrame); - - // Now it's time to write the data frames. - if (requestBody instanceof FileBinaryData) { - FileBinaryData fileBinaryData = (FileBinaryData) requestBody; - try { - return sendChunkedHttp2(channel, - new ChunkedNioFile(FileChannel.open(fileBinaryData.getFile(), StandardOpenOption.READ), - fileBinaryData.getPosition(), fileBinaryData.getLength(), 8192), - progressAndTimeoutHandlerAdded, errorReference); - } catch (IOException ex) { - return channel.newFailedFuture(ex); - } - } else if (requestBody instanceof InputStreamBinaryData) { - return sendChunkedHttp2(channel, new ChunkedStream(requestBody.toStream()), progressAndTimeoutHandlerAdded, - errorReference); - } else { - ByteBuf body = Unpooled.wrappedBuffer(requestBody.toBytes()); - - Throwable error = errorReference.get(); - if (error != null) { - return channel.newFailedFuture(error); - } - - return channel.writeAndFlush(new DefaultHttp2DataFrame(body, true)); - } - } - - private static ChannelFuture sendChunkedHttp2(Channel channel, ChunkedInput chunkedInput, - boolean progressAndTimeoutHandlerAdded, AtomicReference errorReference) { - if (channel.pipeline().get(Netty4HandlerNames.CHUNKED_WRITER) == null) { - // Add the ChunkedWriteHandler which will handle sending the chunkedInput. - ChunkedWriteHandler chunkedWriteHandler = new ChunkedWriteHandler(); - if (progressAndTimeoutHandlerAdded) { - channel.pipeline() - .addBefore(Netty4HandlerNames.PROGRESS_AND_TIMEOUT, Netty4HandlerNames.CHUNKED_WRITER, - chunkedWriteHandler); - } else { - channel.pipeline().addLast(Netty4HandlerNames.CHUNKED_WRITER, chunkedWriteHandler); - } - } - - Throwable error = errorReference.get(); - if (error != null) { - return channel.newFailedFuture(error); - } - - return channel.writeAndFlush(new Http2DataChunkedInput(chunkedInput, null)); + return channel.writeAndFlush(new HttpChunkedInput(chunkedInput)); } /** @@ -510,6 +397,47 @@ private void log() { } } + /** + * Helper method that hot paths some well-known AsciiString HttpHeaderNames that are known to be used by Netty + * internally. + * + * @param asciiString The CharSequence to check for a known HttpHeaderName. + * @return The corresponding HttpHeaderName if it matches a known one, otherwise a new HttpHeaderName created from + * the given CharSequence. + */ + @SuppressWarnings("deprecation") + public static HttpHeaderName fromPossibleAsciiString(CharSequence asciiString) { + if (HttpHeaderNames.ACCEPT_ENCODING == asciiString) { + return HttpHeaderName.ACCEPT_ENCODING; + } else if (HttpHeaderNames.CONNECTION == asciiString) { + return HttpHeaderName.CONNECTION; + } else if (HttpHeaderNames.CONTENT_ENCODING == asciiString) { + return HttpHeaderName.CONTENT_ENCODING; + } else if (HttpHeaderNames.CONTENT_LENGTH == asciiString) { + return HttpHeaderName.CONTENT_LENGTH; + } else if (HttpHeaderNames.CONTENT_TYPE == asciiString) { + return HttpHeaderName.CONTENT_TYPE; + } else if (HttpHeaderNames.COOKIE == asciiString) { + return HttpHeaderName.COOKIE; + } else if (HttpHeaderNames.EXPECT == asciiString) { + return HttpHeaderName.EXPECT; + } else if (HttpHeaderNames.HOST == asciiString) { + return HttpHeaderName.HOST; + } else if (HttpHeaderNames.KEEP_ALIVE == asciiString) { + return HttpHeaderName.KEEP_ALIVE; + } else if (HttpHeaderNames.PROXY_AUTHORIZATION == asciiString) { + return HttpHeaderName.PROXY_AUTHORIZATION; + } else if (HttpHeaderNames.TE == asciiString) { + return HttpHeaderName.TE; + } else if (HttpHeaderNames.TRAILER == asciiString) { + return HttpHeaderName.TRAILER; + } else if (HttpHeaderNames.TRANSFER_ENCODING == asciiString) { + return HttpHeaderName.TRANSFER_ENCODING; + } else { + return HttpHeaderName.fromString(asciiString.toString()); + } + } + private Netty4Utility() { } } diff --git a/sdk/clientcore/http-netty4/src/main/java/io/clientcore/http/netty4/implementation/ResponseStateInfo.java b/sdk/clientcore/http-netty4/src/main/java/io/clientcore/http/netty4/implementation/ResponseStateInfo.java index feb01a1d42af..c31e381c7b60 100644 --- a/sdk/clientcore/http-netty4/src/main/java/io/clientcore/http/netty4/implementation/ResponseStateInfo.java +++ b/sdk/clientcore/http-netty4/src/main/java/io/clientcore/http/netty4/implementation/ResponseStateInfo.java @@ -19,38 +19,80 @@ public final class ResponseStateInfo { private final HttpHeaders headers; private final ByteArrayOutputStream eagerContent; private final ResponseBodyHandling responseBodyHandling; + private final boolean isHttp2; ResponseStateInfo(Channel responseChannel, boolean channelConsumptionComplete, int statusCode, HttpHeaders headers, - ByteArrayOutputStream eagerContent, ResponseBodyHandling responseBodyHandling) { + ByteArrayOutputStream eagerContent, ResponseBodyHandling responseBodyHandling, boolean isHttp2) { this.responseChannel = responseChannel; this.channelConsumptionComplete = channelConsumptionComplete; this.statusCode = statusCode; this.headers = headers; this.eagerContent = eagerContent; this.responseBodyHandling = responseBodyHandling; + this.isHttp2 = isHttp2; } + /** + * Gets the Netty {@link Channel} that holds the connection to the response. + * + * @return The Netty {@link Channel} that holds the connection to the response. + */ public Channel getResponseChannel() { return responseChannel; } + /** + * Flag indicating whether the channel consumption is complete. + * + * @return Whether the channel consumption is complete. + */ public boolean isChannelConsumptionComplete() { return channelConsumptionComplete; } + /** + * Gets the HTTP status code of the response. + * + * @return The HTTP status code of the response. + */ public int getStatusCode() { return statusCode; } + /** + * Gets the HTTP headers of the response. + * + * @return The HTTP headers of the response. + */ public HttpHeaders getHeaders() { return headers; } + /** + * Gets the content that was eagerly read from the Netty pipeline when processing the initial status line and + * headers. + * + * @return The content that was eagerly read from the Netty pipeline. + */ public ByteArrayOutputStream getEagerContent() { return eagerContent; } + /** + * Gets the response body handling strategy. + * + * @return The response body handling strategy. + */ public ResponseBodyHandling getResponseBodyHandling() { return responseBodyHandling; } + + /** + * Flag indicating whether the connection is using HTTP/2 or not. + * + * @return Whether the connection is using HTTP/2. + */ + public boolean isHttp2() { + return isHttp2; + } } diff --git a/sdk/clientcore/http-netty4/src/main/java/io/clientcore/http/netty4/implementation/WrappedHttpHeaders.java b/sdk/clientcore/http-netty4/src/main/java/io/clientcore/http/netty4/implementation/WrappedHttp11Headers.java similarity index 83% rename from sdk/clientcore/http-netty4/src/main/java/io/clientcore/http/netty4/implementation/WrappedHttpHeaders.java rename to sdk/clientcore/http-netty4/src/main/java/io/clientcore/http/netty4/implementation/WrappedHttp11Headers.java index bb79c85fdd66..ae1a3a1c2c62 100644 --- a/sdk/clientcore/http-netty4/src/main/java/io/clientcore/http/netty4/implementation/WrappedHttpHeaders.java +++ b/sdk/clientcore/http-netty4/src/main/java/io/clientcore/http/netty4/implementation/WrappedHttp11Headers.java @@ -6,7 +6,6 @@ import io.clientcore.core.http.models.HttpHeaderName; import io.clientcore.core.instrumentation.logging.ClientLogger; import io.netty.handler.codec.DateFormatter; -import io.netty.handler.codec.http.HttpHeaderNames; import io.netty.handler.codec.http.HttpHeaders; import java.util.AbstractMap; @@ -22,6 +21,8 @@ import java.util.function.BiFunction; import java.util.stream.Collectors; +import static io.clientcore.http.netty4.implementation.Netty4Utility.fromPossibleAsciiString; + /** * Implementation of Netty's {@link HttpHeaders} wrapping an instance of ClientCore's * {@link io.clientcore.core.http.models.HttpHeader}, eliminating the need to convert between the two HTTP header @@ -31,18 +32,18 @@ * converted to Netty's {@link io.netty.handler.codec.http2.Http2Headers}, a future optimization if we begin seeing * more usage of {@code HTTP/2} would be having a similar wrapper for the {@code HTTP/2} headers. */ -public final class WrappedHttpHeaders extends HttpHeaders { - private static final ClientLogger LOGGER = new ClientLogger(WrappedHttpHeaders.class); +public final class WrappedHttp11Headers extends HttpHeaders { + private static final ClientLogger LOGGER = new ClientLogger(WrappedHttp11Headers.class); private io.clientcore.core.http.models.HttpHeaders coreHeaders; /** - * Creates a new instance of {@link WrappedHttpHeaders} wrapping the provided {@code coreHeaders}. + * Creates a new instance of {@link WrappedHttp11Headers} wrapping the provided {@code coreHeaders}. * * @param coreHeaders The ClientCore {@link io.clientcore.core.http.models.HttpHeaders} to wrap providing * integration with Netty {@link HttpHeaders}. * @throws NullPointerException If {@code coreHeaders} is null. */ - public WrappedHttpHeaders(io.clientcore.core.http.models.HttpHeaders coreHeaders) { + public WrappedHttp11Headers(io.clientcore.core.http.models.HttpHeaders coreHeaders) { this.coreHeaders = Objects.requireNonNull(coreHeaders, "'coreHeaders' cannot be null."); } @@ -301,8 +302,8 @@ public HttpHeaders add(CharSequence name, Iterable values) { @Override public HttpHeaders add(HttpHeaders headers) { - if (headers instanceof WrappedHttpHeaders) { - coreHeaders.addAll(((WrappedHttpHeaders) headers).coreHeaders); + if (headers instanceof WrappedHttp11Headers) { + coreHeaders.addAll(((WrappedHttp11Headers) headers).coreHeaders); return this; } else { return super.add(headers); @@ -355,8 +356,8 @@ public HttpHeaders set(CharSequence name, Iterable values) { @Override public HttpHeaders set(HttpHeaders headers) { - if (headers instanceof WrappedHttpHeaders) { - coreHeaders = new io.clientcore.core.http.models.HttpHeaders(((WrappedHttpHeaders) headers).coreHeaders); + if (headers instanceof WrappedHttp11Headers) { + coreHeaders = new io.clientcore.core.http.models.HttpHeaders(((WrappedHttp11Headers) headers).coreHeaders); } else { super.set(headers); } @@ -366,8 +367,8 @@ public HttpHeaders set(HttpHeaders headers) { @Override public HttpHeaders setAll(HttpHeaders headers) { - if (headers instanceof WrappedHttpHeaders) { - coreHeaders.setAll(((WrappedHttpHeaders) headers).coreHeaders); + if (headers instanceof WrappedHttp11Headers) { + coreHeaders.setAll(((WrappedHttp11Headers) headers).coreHeaders); } else { super.setAll(headers); } @@ -377,7 +378,7 @@ public HttpHeaders setAll(HttpHeaders headers) { @Override public HttpHeaders copy() { - return new WrappedHttpHeaders(new io.clientcore.core.http.models.HttpHeaders(coreHeaders)); + return new WrappedHttp11Headers(new io.clientcore.core.http.models.HttpHeaders(coreHeaders)); } @Override @@ -397,39 +398,4 @@ public HttpHeaders clear() { coreHeaders = new io.clientcore.core.http.models.HttpHeaders(); return this; } - - // Helper method that hot paths some well-known AsciiString HttpHeaderNames that are known to be used by Netty - // internally. - @SuppressWarnings("deprecation") - private static HttpHeaderName fromPossibleAsciiString(CharSequence asciiString) { - if (HttpHeaderNames.ACCEPT_ENCODING == asciiString) { - return HttpHeaderName.ACCEPT_ENCODING; - } else if (HttpHeaderNames.CONNECTION == asciiString) { - return HttpHeaderName.CONNECTION; - } else if (HttpHeaderNames.CONTENT_ENCODING == asciiString) { - return HttpHeaderName.CONTENT_ENCODING; - } else if (HttpHeaderNames.CONTENT_LENGTH == asciiString) { - return HttpHeaderName.CONTENT_LENGTH; - } else if (HttpHeaderNames.CONTENT_TYPE == asciiString) { - return HttpHeaderName.CONTENT_TYPE; - } else if (HttpHeaderNames.COOKIE == asciiString) { - return HttpHeaderName.COOKIE; - } else if (HttpHeaderNames.EXPECT == asciiString) { - return HttpHeaderName.EXPECT; - } else if (HttpHeaderNames.HOST == asciiString) { - return HttpHeaderName.HOST; - } else if (HttpHeaderNames.KEEP_ALIVE == asciiString) { - return HttpHeaderName.KEEP_ALIVE; - } else if (HttpHeaderNames.PROXY_AUTHORIZATION == asciiString) { - return HttpHeaderName.PROXY_AUTHORIZATION; - } else if (HttpHeaderNames.TE == asciiString) { - return HttpHeaderName.TE; - } else if (HttpHeaderNames.TRAILER == asciiString) { - return HttpHeaderName.TRAILER; - } else if (HttpHeaderNames.TRANSFER_ENCODING == asciiString) { - return HttpHeaderName.TRANSFER_ENCODING; - } else { - return HttpHeaderName.fromString(asciiString.toString()); - } - } } diff --git a/sdk/clientcore/http-netty4/src/test/java/io/clientcore/http/netty4/NettyHttp2HttpClientTests.java b/sdk/clientcore/http-netty4/src/test/java/io/clientcore/http/netty4/NettyHttp2HttpClientTests.java index f0484462d652..9fb189ccd741 100644 --- a/sdk/clientcore/http-netty4/src/test/java/io/clientcore/http/netty4/NettyHttp2HttpClientTests.java +++ b/sdk/clientcore/http-netty4/src/test/java/io/clientcore/http/netty4/NettyHttp2HttpClientTests.java @@ -5,19 +5,25 @@ import io.clientcore.core.http.client.HttpClient; import io.clientcore.core.http.client.HttpProtocolVersion; +import io.clientcore.core.http.models.HttpMethod; +import io.clientcore.core.http.models.HttpRequest; +import io.clientcore.core.http.models.Response; +import io.clientcore.core.models.binarydata.BinaryData; import io.clientcore.core.shared.HttpClientTests; import io.clientcore.core.shared.HttpClientTestsServer; import io.clientcore.core.shared.InsecureTrustManager; import io.clientcore.core.shared.LocalTestServer; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; import java.security.SecureRandom; +import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.TimeUnit; -@Disabled("Support will be added in the future, in another PR.") +import static org.junit.jupiter.api.Assertions.assertArrayEquals; + @Timeout(value = 3, unit = TimeUnit.MINUTES) public class NettyHttp2HttpClientTests extends HttpClientTests { private static LocalTestServer server; @@ -28,7 +34,7 @@ public class NettyHttp2HttpClientTests extends HttpClientTests { HTTP_CLIENT_INSTANCE = new NettyHttpClientBuilder() .sslContextModifier( builder -> builder.trustManager(new InsecureTrustManager()).secureRandom(new SecureRandom())) - //.maximumHttpVersion(HttpProtocolVersion.HTTP_2) + .maximumHttpVersion(HttpProtocolVersion.HTTP_2) .build(); } @@ -71,4 +77,17 @@ protected String getServerUri(boolean secure) { protected HttpClient getHttpClient() { return HTTP_CLIENT_INSTANCE; } + + @Test + public void canSendBinaryDataDebug() { + byte[] expectedBytes = new byte[1024 * 1024]; + ThreadLocalRandom.current().nextBytes(expectedBytes); + HttpRequest request = new HttpRequest().setMethod(HttpMethod.PUT) + .setUri(getRequestUri("echo")) + .setBody(BinaryData.fromBytes(expectedBytes)); + + try (Response response = getHttpClient().send(request)) { + assertArrayEquals(expectedBytes, response.getValue().toBytes()); + } + } } diff --git a/sdk/clientcore/http-netty4/src/test/java/io/clientcore/http/netty4/implementation/Netty4ChannelBinaryDataTests.java b/sdk/clientcore/http-netty4/src/test/java/io/clientcore/http/netty4/implementation/Netty4Http11ChannelBinaryDataTests.java similarity index 92% rename from sdk/clientcore/http-netty4/src/test/java/io/clientcore/http/netty4/implementation/Netty4ChannelBinaryDataTests.java rename to sdk/clientcore/http-netty4/src/test/java/io/clientcore/http/netty4/implementation/Netty4Http11ChannelBinaryDataTests.java index 6e82642d4754..a51af8e6be19 100644 --- a/sdk/clientcore/http-netty4/src/test/java/io/clientcore/http/netty4/implementation/Netty4ChannelBinaryDataTests.java +++ b/sdk/clientcore/http-netty4/src/test/java/io/clientcore/http/netty4/implementation/Netty4Http11ChannelBinaryDataTests.java @@ -30,11 +30,11 @@ * Tests {@link Netty4ChannelBinaryData}. */ @Timeout(value = 3, unit = TimeUnit.MINUTES) -public class Netty4ChannelBinaryDataTests { +public class Netty4Http11ChannelBinaryDataTests { @Test public void toBytesWillThrowIsLengthIsTooLarge() { assertThrows(IllegalStateException.class, - () -> new Netty4ChannelBinaryData(null, null, Long.MAX_VALUE).toBytes()); + () -> new Netty4ChannelBinaryData(null, null, Long.MAX_VALUE, false).toBytes()); } @Test @@ -45,7 +45,7 @@ public void toBytesCaches() throws IOException { eagerContent.write(expected); Netty4ChannelBinaryData binaryData - = new Netty4ChannelBinaryData(eagerContent, channelWithNoData(), (long) expected.length); + = new Netty4ChannelBinaryData(eagerContent, channelWithNoData(), (long) expected.length, false); assertArraysEqual(expected, binaryData.toBytes()); assertArraysEqual(expected, binaryData.toBytes()); @@ -60,7 +60,7 @@ public void toStringTest() throws IOException { eagerContent.write(bytes); Netty4ChannelBinaryData binaryData - = new Netty4ChannelBinaryData(eagerContent, channelWithNoData(), (long) bytes.length); + = new Netty4ChannelBinaryData(eagerContent, channelWithNoData(), (long) bytes.length, false); assertEquals(expected, binaryData.toString()); } @@ -74,7 +74,7 @@ public void toByteBuffer() throws IOException { eagerContent.write(bytes); Netty4ChannelBinaryData binaryData - = new Netty4ChannelBinaryData(eagerContent, channelWithNoData(), (long) bytes.length); + = new Netty4ChannelBinaryData(eagerContent, channelWithNoData(), (long) bytes.length, false); assertEquals(expected, binaryData.toByteBuffer()); } @@ -87,7 +87,7 @@ public void toStreamUsesBytesIfToBytesWasAlreadyCalled() throws IOException { eagerContent.write(expected); Netty4ChannelBinaryData binaryData - = new Netty4ChannelBinaryData(eagerContent, channelWithNoData(), (long) expected.length); + = new Netty4ChannelBinaryData(eagerContent, channelWithNoData(), (long) expected.length, false); assertArraysEqual(expected, binaryData.toBytes()); @@ -112,7 +112,7 @@ public void writeToOutputStreamUsesBytesIfToBytesWasAlreadyCalled() throws IOExc eagerContent.write(expected); Netty4ChannelBinaryData binaryData - = new Netty4ChannelBinaryData(eagerContent, channelWithNoData(), (long) expected.length); + = new Netty4ChannelBinaryData(eagerContent, channelWithNoData(), (long) expected.length, false); assertArraysEqual(expected, binaryData.toBytes()); @@ -124,17 +124,17 @@ public void writeToOutputStreamUsesBytesIfToBytesWasAlreadyCalled() throws IOExc @Test public void channelBinaryDataLengthIsKnown() { - assertEquals(1, new Netty4ChannelBinaryData(null, null, 1L).getLength()); + assertEquals(1, new Netty4ChannelBinaryData(null, null, 1L, false).getLength()); } @Test public void channelBinaryDataLengthIsUnknown() { - assertNull(new Netty4ChannelBinaryData(null, null, null).getLength()); + assertNull(new Netty4ChannelBinaryData(null, null, null, false).getLength()); } @Test public void channelBinaryDataIsNeverReplayable() { - assertFalse(new Netty4ChannelBinaryData(null, null, null).isReplayable()); + assertFalse(new Netty4ChannelBinaryData(null, null, null, false).isReplayable()); } @Test @@ -145,7 +145,7 @@ public void channelBinaryDataToReplayableReturnsAByteArrayBinaryData() throws IO eagerContent.write(expected); Netty4ChannelBinaryData binaryData - = new Netty4ChannelBinaryData(eagerContent, channelWithNoData(), (long) expected.length); + = new Netty4ChannelBinaryData(eagerContent, channelWithNoData(), (long) expected.length, false); BinaryData replayable = binaryData.toReplayableBinaryData(); assertInstanceOf(ByteArrayBinaryData.class, replayable); diff --git a/sdk/clientcore/http-netty4/src/test/java/io/clientcore/http/netty4/implementation/Netty4ChannelInputStreamTests.java b/sdk/clientcore/http-netty4/src/test/java/io/clientcore/http/netty4/implementation/Netty4Http11ChannelInputStreamTests.java similarity index 95% rename from sdk/clientcore/http-netty4/src/test/java/io/clientcore/http/netty4/implementation/Netty4ChannelInputStreamTests.java rename to sdk/clientcore/http-netty4/src/test/java/io/clientcore/http/netty4/implementation/Netty4Http11ChannelInputStreamTests.java index d070769fe761..67df3b7ac43e 100644 --- a/sdk/clientcore/http-netty4/src/test/java/io/clientcore/http/netty4/implementation/Netty4ChannelInputStreamTests.java +++ b/sdk/clientcore/http-netty4/src/test/java/io/clientcore/http/netty4/implementation/Netty4Http11ChannelInputStreamTests.java @@ -37,11 +37,11 @@ * Tests {@link Netty4ChannelInputStream}. */ @Timeout(value = 3, unit = TimeUnit.MINUTES) -public class Netty4ChannelInputStreamTests { +public class Netty4Http11ChannelInputStreamTests { @Test public void nullEagerContentResultsInEmptyInitialCurrentBuffer() { try (Netty4ChannelInputStream channelInputStream - = new Netty4ChannelInputStream(null, createCloseableChannel())) { + = new Netty4ChannelInputStream(null, createCloseableChannel(), false)) { assertEquals(0, channelInputStream.getCurrentBuffer().length); } } @@ -49,7 +49,7 @@ public void nullEagerContentResultsInEmptyInitialCurrentBuffer() { @Test public void emptyEagerContentResultsInEmptyInitialCurrentBuffer() { try (Netty4ChannelInputStream channelInputStream - = new Netty4ChannelInputStream(new ByteArrayOutputStream(), createCloseableChannel())) { + = new Netty4ChannelInputStream(new ByteArrayOutputStream(), createCloseableChannel(), false)) { assertEquals(0, channelInputStream.getCurrentBuffer().length); } } @@ -63,7 +63,8 @@ public void readConsumesCurrentBufferAndHasNoMoreData() throws IOException { eagerContent.write(expected); // MockChannels aren't active by default, so once the eagerContent is consumed the stream will be done. - Netty4ChannelInputStream channelInputStream = new Netty4ChannelInputStream(eagerContent, new MockChannel()); + Netty4ChannelInputStream channelInputStream + = new Netty4ChannelInputStream(eagerContent, new MockChannel(), false); // Make sure the Netty4ChannelInputStream copied the eager content correctly. assertArraysEqual(expected, channelInputStream.getCurrentBuffer()); @@ -95,7 +96,7 @@ public void readConsumesCurrentBufferAndRequestsMoreData() throws IOException { handler.channelRead(ctx, wrappedBuffer(expected, 16, 16)); handler.channelRead(ctx, LastHttpContent.EMPTY_LAST_CONTENT); handler.channelReadComplete(ctx); - })); + }), false); int index = 0; byte[] actual = new byte[32]; @@ -117,7 +118,7 @@ public void multipleSmallerSkips() throws IOException { // MockChannels aren't active by default, so once the eagerContent is consumed the stream will be done. try (Netty4ChannelInputStream channelInputStream - = new Netty4ChannelInputStream(eagerContent, createCloseableChannel())) { + = new Netty4ChannelInputStream(eagerContent, createCloseableChannel(), false)) { long skipped = channelInputStream.skip(16); assertEquals(16, skipped); @@ -140,7 +141,7 @@ public void largeReadTriggersMultipleChannelReads() throws IOException { ThreadLocalRandom.current().nextBytes(expected); try (Netty4ChannelInputStream channelInputStream - = new Netty4ChannelInputStream(null, createChannelThatReads8Kb(expected))) { + = new Netty4ChannelInputStream(null, createChannelThatReads8Kb(expected), false)) { byte[] actual = new byte[8192]; int read = channelInputStream.read(actual); @@ -161,7 +162,7 @@ public void largeSkipTriggersMultipleChannelReads() throws IOException { ThreadLocalRandom.current().nextBytes(expected); try (Netty4ChannelInputStream channelInputStream - = new Netty4ChannelInputStream(null, createChannelThatReads8Kb(expected))) { + = new Netty4ChannelInputStream(null, createChannelThatReads8Kb(expected), false)) { long skipped = channelInputStream.skip(8192); assertEquals(8192, skipped); @@ -176,7 +177,7 @@ public void closingStreamClosesChannel() { AtomicInteger disconnectCount = new AtomicInteger(); new Netty4ChannelInputStream(null, - createCloseableChannel(closeCount::incrementAndGet, disconnectCount::incrementAndGet)).close(); + createCloseableChannel(closeCount::incrementAndGet, disconnectCount::incrementAndGet), false).close(); assertEquals(1, closeCount.get()); } @@ -184,7 +185,8 @@ public void closingStreamClosesChannel() { @ParameterizedTest @MethodSource("errorSupplier") public void streamPropagatesErrorFiredInChannel(Throwable expected) { - InputStream inputStream = new Netty4ChannelInputStream(null, createPartialReadThenErrorChannel(expected)); + InputStream inputStream + = new Netty4ChannelInputStream(null, createPartialReadThenErrorChannel(expected), false); Throwable actual = assertThrows(Throwable.class, () -> inputStream.read(new byte[8192])); diff --git a/sdk/clientcore/http-netty4/src/test/java/io/clientcore/http/netty4/implementation/Netty4HttpProxyHandlerTests.java b/sdk/clientcore/http-netty4/src/test/java/io/clientcore/http/netty4/implementation/Netty4HttpProxyHandlerTests.java index 47960fa7021c..76014c56b9e5 100644 --- a/sdk/clientcore/http-netty4/src/test/java/io/clientcore/http/netty4/implementation/Netty4HttpProxyHandlerTests.java +++ b/sdk/clientcore/http-netty4/src/test/java/io/clientcore/http/netty4/implementation/Netty4HttpProxyHandlerTests.java @@ -42,10 +42,13 @@ public void validateProxyAuthenticationInfoThrowsOnMismatch(String infoHeader, S } private static Stream mismatchData() { - return Stream.of(Arguments.of("cnonce=1", "cnonce=2", - "Property received in the 'Proxy-Authentication-Info' header doesn't match the value sent in the 'Proxy-Authorization' header; {\"propertyName\":\"cnonce\",\"received\":\"1\",\"sent\":\"2\"}"), + return Stream.of( + Arguments.of("cnonce=1", "cnonce=2", + "Property received in the 'Proxy-Authentication-Info' header doesn't match the value sent in the " + + "'Proxy-Authorization' header; {\"propertyName\":\"cnonce\",\"received\":\"1\",\"sent\":\"2\"}"), Arguments.of("nc=1", "nc=2", - "Property received in the 'Proxy-Authentication-Info' header doesn't match the value sent in the 'Proxy-Authorization' header; {\"propertyName\":\"nc\",\"received\":\"1\",\"sent\":\"2\"}")); + "Property received in the 'Proxy-Authentication-Info' header doesn't match the value sent in the " + + "'Proxy-Authorization' header; {\"propertyName\":\"nc\",\"received\":\"1\",\"sent\":\"2\"}")); } @Test diff --git a/sdk/clientcore/http-netty4/src/test/java/io/clientcore/http/netty4/implementation/WrappedHttp11HeadersTests.java b/sdk/clientcore/http-netty4/src/test/java/io/clientcore/http/netty4/implementation/WrappedHttp11HeadersTests.java new file mode 100644 index 000000000000..e396a5b508f5 --- /dev/null +++ b/sdk/clientcore/http-netty4/src/test/java/io/clientcore/http/netty4/implementation/WrappedHttp11HeadersTests.java @@ -0,0 +1,656 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package io.clientcore.http.netty4.implementation; + +import io.clientcore.core.http.models.HttpHeader; +import io.clientcore.core.http.models.HttpHeaderName; +import io.clientcore.core.http.models.HttpHeaders; +import io.clientcore.core.utils.DateTimeRfc1123; +import io.netty.handler.codec.http.DefaultHttpHeaders; +import io.netty.handler.codec.http.HttpHeaderNames; +import org.junit.jupiter.api.Test; + +import java.time.OffsetDateTime; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertLinesMatch; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Unit tests for {@link WrappedHttp11Headers}. + */ +public class WrappedHttp11HeadersTests { + @Test + public void throwsOnNullClientCoreHttpHeaders() { + assertThrows(NullPointerException.class, () -> new WrappedHttp11Headers(null)); + } + + @Test + public void addCharSequenceIterable() { + WrappedHttp11Headers wrappedHttp11Headers = new WrappedHttp11Headers(new HttpHeaders()); + wrappedHttp11Headers.add(HttpHeaderNames.ACCEPT_ENCODING, Arrays.asList("gzip", "deflate")); + + HttpHeader header = wrappedHttp11Headers.getCoreHeaders().get(HttpHeaderName.ACCEPT_ENCODING); + assertNotNull(header); + assertLinesMatch(Arrays.asList("gzip", "deflate"), header.getValues()); + } + + @Test + public void addCharSequenceIterableThrowsOnNull() { + WrappedHttp11Headers wrappedHttp11Headers = new WrappedHttp11Headers(new HttpHeaders()); + assertThrows(NullPointerException.class, + () -> wrappedHttp11Headers.add(HttpHeaderNames.ACCEPT_ENCODING, (Iterable) null)); + } + + @Test + public void addCharSequenceIterableThrowsIfAnyNull() { + WrappedHttp11Headers wrappedHttp11Headers = new WrappedHttp11Headers(new HttpHeaders()); + assertThrows(NullPointerException.class, + () -> wrappedHttp11Headers.add(HttpHeaderNames.ACCEPT_ENCODING, Arrays.asList("gzip", null))); + } + + @Test + public void addCharSequenceObject() { + WrappedHttp11Headers wrappedHttp11Headers = new WrappedHttp11Headers(new HttpHeaders()); + wrappedHttp11Headers.add(HttpHeaderNames.CONTENT_LENGTH, 42); + + HttpHeader header = wrappedHttp11Headers.getCoreHeaders().get(HttpHeaderName.CONTENT_LENGTH); + assertNotNull(header); + assertEquals("42", header.getValue()); + } + + @Test + public void addCharSequenceObjectThrowsOnNull() { + WrappedHttp11Headers wrappedHttp11Headers = new WrappedHttp11Headers(new HttpHeaders()); + assertThrows(NullPointerException.class, + () -> wrappedHttp11Headers.add(HttpHeaderNames.CONTENT_LENGTH, (Object) null)); + } + + @Test + public void addStringIterable() { + WrappedHttp11Headers wrappedHttp11Headers = new WrappedHttp11Headers(new HttpHeaders()); + wrappedHttp11Headers.add("Accept-Encoding", Arrays.asList("gzip", "deflate")); + + HttpHeader header = wrappedHttp11Headers.getCoreHeaders().get(HttpHeaderName.ACCEPT_ENCODING); + assertNotNull(header); + assertLinesMatch(Arrays.asList("gzip", "deflate"), header.getValues()); + } + + @Test + public void addStringIterableThrowsOnNull() { + WrappedHttp11Headers wrappedHttp11Headers = new WrappedHttp11Headers(new HttpHeaders()); + assertThrows(NullPointerException.class, () -> wrappedHttp11Headers.add("Accept-Encoding", (Iterable) null)); + } + + @Test + public void addStringIterableThrowsIfAnyNull() { + WrappedHttp11Headers wrappedHttp11Headers = new WrappedHttp11Headers(new HttpHeaders()); + assertThrows(NullPointerException.class, + () -> wrappedHttp11Headers.add("Accept-Encoding", Arrays.asList("gzip", null))); + } + + @Test + public void addStringObject() { + WrappedHttp11Headers wrappedHttp11Headers = new WrappedHttp11Headers(new HttpHeaders()); + wrappedHttp11Headers.add("Content-Length", 42); + + HttpHeader header = wrappedHttp11Headers.getCoreHeaders().get(HttpHeaderName.CONTENT_LENGTH); + assertNotNull(header); + assertEquals("42", header.getValue()); + } + + @Test + public void addStringObjectThrowsOnNull() { + WrappedHttp11Headers wrappedHttp11Headers = new WrappedHttp11Headers(new HttpHeaders()); + assertThrows(NullPointerException.class, () -> wrappedHttp11Headers.add("Content-Length", (Object) null)); + } + + @Test + public void addCharSequenceInt() { + WrappedHttp11Headers wrappedHttp11Headers = new WrappedHttp11Headers(new HttpHeaders()); + wrappedHttp11Headers.addInt("Content-Length", 42); + + HttpHeader header = wrappedHttp11Headers.getCoreHeaders().get(HttpHeaderName.CONTENT_LENGTH); + assertNotNull(header); + assertEquals("42", header.getValue()); + } + + @Test + public void addCharSequenceShort() { + WrappedHttp11Headers wrappedHttp11Headers = new WrappedHttp11Headers(new HttpHeaders()); + wrappedHttp11Headers.addShort("Content-Length", (short) 42); + + HttpHeader header = wrappedHttp11Headers.getCoreHeaders().get(HttpHeaderName.CONTENT_LENGTH); + assertNotNull(header); + assertEquals("42", header.getValue()); + } + + @Test + public void addHttpHeadersHotPathsWrappedHttpHeaders() { + WrappedHttp11Headers wrappedHttp11Headers = new WrappedHttp11Headers(new HttpHeaders()); + WrappedHttp11Headers toAdd = new WrappedHttp11Headers( + new HttpHeaders().set(HttpHeaderName.KEEP_ALIVE, "true").set(HttpHeaderName.CONTENT_LENGTH, "42")); + + wrappedHttp11Headers.add(toAdd); + + HttpHeaders coreHeaders = wrappedHttp11Headers.getCoreHeaders(); + assertEquals(2, coreHeaders.getSize()); + assertEquals("true", coreHeaders.getValue(HttpHeaderName.KEEP_ALIVE)); + assertEquals("42", coreHeaders.getValue(HttpHeaderName.CONTENT_LENGTH)); + } + + @SuppressWarnings("deprecation") + @Test + public void addHttpHeadersFallsBackToSuperImplementation() { + WrappedHttp11Headers wrappedHttp11Headers = new WrappedHttp11Headers(new HttpHeaders()); + io.netty.handler.codec.http.HttpHeaders toAdd = new DefaultHttpHeaders().add(HttpHeaderNames.KEEP_ALIVE, "true") + .add(HttpHeaderNames.CONTENT_LENGTH, "42"); + + wrappedHttp11Headers.add(toAdd); + + HttpHeaders coreHeaders = wrappedHttp11Headers.getCoreHeaders(); + assertEquals(2, coreHeaders.getSize()); + assertEquals("true", coreHeaders.getValue(HttpHeaderName.KEEP_ALIVE)); + assertEquals("42", coreHeaders.getValue(HttpHeaderName.CONTENT_LENGTH)); + } + + @Test + public void addHttpHeadersThrowsOnNull() { + WrappedHttp11Headers wrappedHttp11Headers = new WrappedHttp11Headers(new HttpHeaders()); + assertThrows(NullPointerException.class, + () -> wrappedHttp11Headers.add((io.netty.handler.codec.http.HttpHeaders) null)); + } + + @Test + public void clearUsesNewClientCoreHttpHeaders() { + HttpHeaders initial = new HttpHeaders(); + WrappedHttp11Headers wrappedHttp11Headers = new WrappedHttp11Headers(initial); + wrappedHttp11Headers.clear(); + + assertNotSame(initial, wrappedHttp11Headers.getCoreHeaders()); + } + + @Test + public void containsCharSequenceReturnsFalseWhenHeaderDoesNotExist() { + WrappedHttp11Headers wrappedHttp11Headers = new WrappedHttp11Headers(new HttpHeaders()); + + assertFalse(wrappedHttp11Headers.contains(HttpHeaderNames.CONTENT_LENGTH)); + } + + @Test + public void containsCharSequence() { + WrappedHttp11Headers wrappedHttp11Headers + = new WrappedHttp11Headers(new HttpHeaders().set(HttpHeaderName.CONNECTION, "connection")); + + assertTrue(wrappedHttp11Headers.contains(HttpHeaderNames.CONNECTION)); + } + + @Test + public void containsStringReturnsFalseWhenHeaderDoesNotExist() { + WrappedHttp11Headers wrappedHttp11Headers = new WrappedHttp11Headers(new HttpHeaders()); + + assertFalse(wrappedHttp11Headers.contains("Content-Length")); + } + + @Test + public void containsString() { + WrappedHttp11Headers wrappedHttp11Headers + = new WrappedHttp11Headers(new HttpHeaders().set(HttpHeaderName.CONNECTION, "connection")); + + assertTrue(wrappedHttp11Headers.contains("Connection")); + } + + @Test + public void copyUsesNewClientCoreHeaders() { + HttpHeaders initial = new HttpHeaders().set(HttpHeaderName.CONTENT_LENGTH, "42"); + WrappedHttp11Headers wrappedHttp11Headers = new WrappedHttp11Headers(initial); + + WrappedHttp11Headers copy = (WrappedHttp11Headers) wrappedHttp11Headers.copy(); + + assertNotSame(initial, copy.getCoreHeaders()); + assertEquals("42", wrappedHttp11Headers.get(HttpHeaderNames.CONTENT_LENGTH)); + } + + @Test + public void getCharSequenceReturnsNullWhenHeaderDoesNotExist() { + WrappedHttp11Headers wrappedHttp11Headers = new WrappedHttp11Headers(new HttpHeaders()); + + assertNull(wrappedHttp11Headers.get(HttpHeaderNames.CONTENT_ENCODING)); + } + + @Test + public void getCharSequenceReturnsFirstValue() { + WrappedHttp11Headers wrappedHttp11Headers = new WrappedHttp11Headers(new HttpHeaders() + .set(HttpHeaderName.CONTENT_ENCODING, Arrays.asList("application/json", "application/xml"))); + + assertEquals("application/json", wrappedHttp11Headers.get(HttpHeaderNames.CONTENT_ENCODING)); + } + + @Test + public void getStringReturnsNullWhenHeaderDoesNotExist() { + WrappedHttp11Headers wrappedHttp11Headers = new WrappedHttp11Headers(new HttpHeaders()); + + assertNull(wrappedHttp11Headers.get("Content-Encoding")); + } + + @Test + public void getStringReturnsFirstValue() { + WrappedHttp11Headers wrappedHttp11Headers = new WrappedHttp11Headers(new HttpHeaders() + .set(HttpHeaderName.CONTENT_ENCODING, Arrays.asList("application/json", "application/xml"))); + + assertEquals("application/json", wrappedHttp11Headers.get("Content-Encoding")); + } + + @Test + public void getAllCharSequenceReturnsEmptyListWhenHeaderDoesNotExist() { + WrappedHttp11Headers wrappedHttp11Headers = new WrappedHttp11Headers(new HttpHeaders()); + + assertSame(Collections.emptyList(), wrappedHttp11Headers.getAll(HttpHeaderNames.CONTENT_TYPE)); + } + + @Test + public void getAllCharSequence() { + WrappedHttp11Headers wrappedHttp11Headers = new WrappedHttp11Headers( + new HttpHeaders().set(HttpHeaderName.CONTENT_TYPE, Arrays.asList("application/json", "application/xml"))); + + assertLinesMatch(Arrays.asList("application/json", "application/xml"), + wrappedHttp11Headers.getAll(HttpHeaderNames.CONTENT_TYPE)); + } + + @Test + public void getAllStringReturnsEmptyListWhenHeaderDoesNotExist() { + WrappedHttp11Headers wrappedHttp11Headers = new WrappedHttp11Headers(new HttpHeaders()); + + assertSame(Collections.emptyList(), wrappedHttp11Headers.getAll("Content-Type")); + } + + @Test + public void getAllString() { + WrappedHttp11Headers wrappedHttp11Headers = new WrappedHttp11Headers( + new HttpHeaders().set(HttpHeaderName.CONTENT_TYPE, Arrays.asList("application/json", "application/xml"))); + + assertLinesMatch(Arrays.asList("application/json", "application/xml"), + wrappedHttp11Headers.getAll("Content-Type")); + } + + @Test + public void getIntReturnsNullWhenHeaderDoesNotExist() { + WrappedHttp11Headers wrappedHttp11Headers = new WrappedHttp11Headers(new HttpHeaders()); + + assertNull(wrappedHttp11Headers.getInt(HttpHeaderNames.CONTENT_LENGTH)); + } + + @Test + public void getIntReturnsFirstValue() { + WrappedHttp11Headers wrappedHttp11Headers + = new WrappedHttp11Headers(new HttpHeaders().set(HttpHeaderName.COOKIE, Arrays.asList("1", "2"))); + + assertEquals(1, wrappedHttp11Headers.getInt(HttpHeaderNames.COOKIE)); + } + + @Test + public void getIntReturnsNullOnInvalidParse() { + WrappedHttp11Headers wrappedHttp11Headers + = new WrappedHttp11Headers(new HttpHeaders().set(HttpHeaderName.EXPECT, "expect")); + + assertNull(wrappedHttp11Headers.getInt(HttpHeaderNames.EXPECT)); + } + + @Test + public void getIntWithDefaultReturnsDefaultOnNull() { + WrappedHttp11Headers wrappedHttp11Headers = new WrappedHttp11Headers(new HttpHeaders()); + + assertEquals(1, wrappedHttp11Headers.getInt(HttpHeaderNames.CONTENT_LENGTH, 1)); + } + + @Test + public void getIntWithDefaultReturnsDefaultOnInvalidParse() { + WrappedHttp11Headers wrappedHttp11Headers + = new WrappedHttp11Headers(new HttpHeaders().set(HttpHeaderName.HOST, "host")); + + assertEquals(1, wrappedHttp11Headers.getInt(HttpHeaderNames.HOST, 1)); + } + + @Test + public void getIntReturnsActualValue() { + WrappedHttp11Headers wrappedHttp11Headers + = new WrappedHttp11Headers(new HttpHeaders().set(HttpHeaderName.CONTENT_LENGTH, "42")); + + assertEquals(42, wrappedHttp11Headers.getInt(HttpHeaderNames.CONTENT_LENGTH, 24)); + } + + @Test + public void getShortReturnsNullWhenHeaderDoesNotExist() { + WrappedHttp11Headers wrappedHttp11Headers = new WrappedHttp11Headers(new HttpHeaders()); + + assertNull(wrappedHttp11Headers.getShort(HttpHeaderNames.CONTENT_LENGTH)); + } + + @Test + public void getShortReturnsFirstValue() { + WrappedHttp11Headers wrappedHttp11Headers + = new WrappedHttp11Headers(new HttpHeaders().set(HttpHeaderName.COOKIE, Arrays.asList("1", "2"))); + + assertEquals((short) 1, wrappedHttp11Headers.getShort(HttpHeaderNames.COOKIE)); + } + + @Test + public void getShortReturnsNullOnInvalidParse() { + WrappedHttp11Headers wrappedHttp11Headers + = new WrappedHttp11Headers(new HttpHeaders().set(HttpHeaderName.PROXY_AUTHORIZATION, "authorization")); + + assertNull(wrappedHttp11Headers.getShort(HttpHeaderNames.PROXY_AUTHORIZATION)); + } + + @Test + public void getShortWithDefaultReturnsDefaultOnNull() { + WrappedHttp11Headers wrappedHttp11Headers = new WrappedHttp11Headers(new HttpHeaders()); + + assertEquals((short) 1, wrappedHttp11Headers.getShort(HttpHeaderNames.CONTENT_LENGTH, (short) 1)); + } + + @Test + public void getShortWithDefaultReturnsDefaultOnInvalidParse() { + WrappedHttp11Headers wrappedHttp11Headers + = new WrappedHttp11Headers(new HttpHeaders().set(HttpHeaderName.TE, "TE")); + + assertEquals((short) 1, wrappedHttp11Headers.getShort(HttpHeaderNames.TE, (short) 1)); + } + + @Test + public void getShortReturnsActualValue() { + WrappedHttp11Headers wrappedHttp11Headers + = new WrappedHttp11Headers(new HttpHeaders().set(HttpHeaderName.CONTENT_LENGTH, "42")); + + assertEquals((short) 42, wrappedHttp11Headers.getShort(HttpHeaderNames.CONTENT_LENGTH, (short) 24)); + } + + @Test + public void isEmptyIsBasedOnClientCoreHeaders() { + WrappedHttp11Headers wrappedHttp11Headers = new WrappedHttp11Headers(new HttpHeaders()); + assertTrue(wrappedHttp11Headers.isEmpty()); + + wrappedHttp11Headers.getCoreHeaders().set(HttpHeaderName.CONTENT_LENGTH, "42"); + assertFalse(wrappedHttp11Headers.isEmpty()); + } + + @Test + public void removeCharSequence() { + WrappedHttp11Headers wrappedHttp11Headers + = new WrappedHttp11Headers(new HttpHeaders().set(HttpHeaderName.TRAILER, "trailer")); + wrappedHttp11Headers.remove(HttpHeaderNames.TRAILER); + + assertTrue(wrappedHttp11Headers.isEmpty()); + assertNull(wrappedHttp11Headers.getCoreHeaders().get(HttpHeaderName.TRAILER)); + } + + @Test + public void removeString() { + WrappedHttp11Headers wrappedHttp11Headers + = new WrappedHttp11Headers(new HttpHeaders().set(HttpHeaderName.TRAILER, "trailer")); + wrappedHttp11Headers.remove("Trailer"); + + assertTrue(wrappedHttp11Headers.isEmpty()); + assertNull(wrappedHttp11Headers.getCoreHeaders().get(HttpHeaderName.TRAILER)); + } + + @Test + public void setCharSequenceIterable() { + WrappedHttp11Headers wrappedHttp11Headers + = new WrappedHttp11Headers(new HttpHeaders().set(HttpHeaderName.ACCEPT_ENCODING, "*")); + wrappedHttp11Headers.set(HttpHeaderNames.ACCEPT_ENCODING, Arrays.asList("gzip", "deflate")); + + HttpHeader header = wrappedHttp11Headers.getCoreHeaders().get(HttpHeaderName.ACCEPT_ENCODING); + assertNotNull(header); + assertLinesMatch(Arrays.asList("gzip", "deflate"), header.getValues()); + } + + @Test + public void setCharSequenceIterableThrowsOnNull() { + WrappedHttp11Headers wrappedHttp11Headers = new WrappedHttp11Headers(new HttpHeaders()); + assertThrows(NullPointerException.class, + () -> wrappedHttp11Headers.set(HttpHeaderNames.ACCEPT_ENCODING, (Iterable) null)); + } + + @Test + public void setCharSequenceIterableThrowsIfAnyNull() { + WrappedHttp11Headers wrappedHttp11Headers = new WrappedHttp11Headers(new HttpHeaders()); + assertThrows(NullPointerException.class, + () -> wrappedHttp11Headers.set(HttpHeaderNames.ACCEPT_ENCODING, Arrays.asList("gzip", null))); + } + + @SuppressWarnings("deprecation") + @Test + public void setCharSequenceObject() { + WrappedHttp11Headers wrappedHttp11Headers + = new WrappedHttp11Headers(new HttpHeaders().set(HttpHeaderName.KEEP_ALIVE, "false")); + wrappedHttp11Headers.set(HttpHeaderNames.KEEP_ALIVE, "true"); + + HttpHeader header = wrappedHttp11Headers.getCoreHeaders().get(HttpHeaderName.KEEP_ALIVE); + assertNotNull(header); + assertEquals("true", header.getValue()); + } + + @Test + public void setCharSequenceObjectThrowsOnNull() { + WrappedHttp11Headers wrappedHttp11Headers = new WrappedHttp11Headers(new HttpHeaders()); + assertThrows(NullPointerException.class, + () -> wrappedHttp11Headers.set(HttpHeaderNames.CONTENT_LENGTH, (Object) null)); + } + + @Test + public void setStringIterable() { + WrappedHttp11Headers wrappedHttp11Headers + = new WrappedHttp11Headers(new HttpHeaders().set(HttpHeaderName.ACCEPT_ENCODING, "*")); + wrappedHttp11Headers.set("Accept-Encoding", Arrays.asList("gzip", "deflate")); + + HttpHeader header = wrappedHttp11Headers.getCoreHeaders().get(HttpHeaderName.ACCEPT_ENCODING); + assertNotNull(header); + assertLinesMatch(Arrays.asList("gzip", "deflate"), header.getValues()); + } + + @Test + public void setStringIterableThrowsOnNull() { + WrappedHttp11Headers wrappedHttp11Headers = new WrappedHttp11Headers(new HttpHeaders()); + assertThrows(NullPointerException.class, () -> wrappedHttp11Headers.set("Accept-Encoding", (Iterable) null)); + } + + @Test + public void setStringIterableThrowsIfAnyNull() { + WrappedHttp11Headers wrappedHttp11Headers = new WrappedHttp11Headers(new HttpHeaders()); + assertThrows(NullPointerException.class, + () -> wrappedHttp11Headers.set("Accept-Encoding", Arrays.asList("gzip", null))); + } + + @Test + public void setStringObject() { + WrappedHttp11Headers wrappedHttp11Headers + = new WrappedHttp11Headers(new HttpHeaders().set(HttpHeaderName.CONTENT_LENGTH, "24")); + wrappedHttp11Headers.set("Content-Length", 42); + + HttpHeader header = wrappedHttp11Headers.getCoreHeaders().get(HttpHeaderName.CONTENT_LENGTH); + assertNotNull(header); + assertEquals("42", header.getValue()); + } + + @Test + public void setStringObjectThrowsOnNull() { + WrappedHttp11Headers wrappedHttp11Headers = new WrappedHttp11Headers(new HttpHeaders()); + assertThrows(NullPointerException.class, () -> wrappedHttp11Headers.set("Content-Length", (Object) null)); + } + + @Test + public void setCharSequenceInt() { + WrappedHttp11Headers wrappedHttp11Headers + = new WrappedHttp11Headers(new HttpHeaders().set(HttpHeaderName.CONTENT_LENGTH, "24")); + wrappedHttp11Headers.setInt("Content-Length", 42); + + HttpHeader header = wrappedHttp11Headers.getCoreHeaders().get(HttpHeaderName.CONTENT_LENGTH); + assertNotNull(header); + assertEquals("42", header.getValue()); + } + + @Test + public void setCharSequenceShort() { + WrappedHttp11Headers wrappedHttp11Headers + = new WrappedHttp11Headers(new HttpHeaders().set(HttpHeaderName.CONTENT_LENGTH, "24")); + wrappedHttp11Headers.setShort("Content-Length", (short) 42); + + HttpHeader header = wrappedHttp11Headers.getCoreHeaders().get(HttpHeaderName.CONTENT_LENGTH); + assertNotNull(header); + assertEquals("42", header.getValue()); + } + + @Test + public void setHttpHeadersHotPathsWrappedHttpHeaders() { + HttpHeaders initial = new HttpHeaders(0); + WrappedHttp11Headers wrappedHttp11Headers = new WrappedHttp11Headers(initial); + WrappedHttp11Headers toAdd = new WrappedHttp11Headers( + new HttpHeaders().set(HttpHeaderName.KEEP_ALIVE, "true").set(HttpHeaderName.CONTENT_LENGTH, "42")); + + wrappedHttp11Headers.set(toAdd); + + HttpHeaders coreHeaders = wrappedHttp11Headers.getCoreHeaders(); + assertNotSame(initial, coreHeaders); + assertNotSame(toAdd.getCoreHeaders(), coreHeaders); + assertEquals(2, coreHeaders.getSize()); + assertEquals("true", coreHeaders.getValue(HttpHeaderName.KEEP_ALIVE)); + assertEquals("42", coreHeaders.getValue(HttpHeaderName.CONTENT_LENGTH)); + } + + @Test + public void setHttpHeadersFallsBackToSuperImplementation() { + HttpHeaders initial = new HttpHeaders(); + WrappedHttp11Headers wrappedHttp11Headers = new WrappedHttp11Headers(initial); + io.netty.handler.codec.http.HttpHeaders toAdd + = new DefaultHttpHeaders().add("Keep-Alive", "true").add(HttpHeaderNames.CONTENT_LENGTH, "42"); + + wrappedHttp11Headers.set(toAdd); + + HttpHeaders coreHeaders = wrappedHttp11Headers.getCoreHeaders(); + assertNotSame(initial, coreHeaders); + assertEquals(2, coreHeaders.getSize()); + assertEquals("true", coreHeaders.getValue(HttpHeaderName.KEEP_ALIVE)); + assertEquals("42", coreHeaders.getValue(HttpHeaderName.CONTENT_LENGTH)); + } + + @Test + public void setHttpHeadersThrowsOnNull() { + WrappedHttp11Headers wrappedHttp11Headers = new WrappedHttp11Headers(new HttpHeaders()); + assertThrows(NullPointerException.class, + () -> wrappedHttp11Headers.set((io.netty.handler.codec.http.HttpHeaders) null)); + } + + @Test + public void setAllHttpHeadersHotPathsWrappedHttpHeaders() { + HttpHeaders initial = new HttpHeaders(0); + WrappedHttp11Headers wrappedHttp11Headers = new WrappedHttp11Headers(initial); + WrappedHttp11Headers toAdd = new WrappedHttp11Headers( + new HttpHeaders().set(HttpHeaderName.KEEP_ALIVE, "true").set(HttpHeaderName.CONTENT_LENGTH, "42")); + + wrappedHttp11Headers.setAll(toAdd); + + HttpHeaders coreHeaders = wrappedHttp11Headers.getCoreHeaders(); + assertSame(initial, coreHeaders); + assertEquals(2, coreHeaders.getSize()); + assertEquals("true", coreHeaders.getValue(HttpHeaderName.KEEP_ALIVE)); + assertEquals("42", coreHeaders.getValue(HttpHeaderName.CONTENT_LENGTH)); + } + + @Test + public void setAllHttpHeadersFallsBackToSuperImplementation() { + HttpHeaders initial = new HttpHeaders(); + WrappedHttp11Headers wrappedHttp11Headers = new WrappedHttp11Headers(initial); + io.netty.handler.codec.http.HttpHeaders toAdd + = new DefaultHttpHeaders().add("Keep-Alive", "true").add(HttpHeaderNames.CONTENT_LENGTH, "42"); + + wrappedHttp11Headers.setAll(toAdd); + + HttpHeaders coreHeaders = wrappedHttp11Headers.getCoreHeaders(); + assertSame(initial, coreHeaders); + assertEquals(2, coreHeaders.getSize()); + assertEquals("true", coreHeaders.getValue(HttpHeaderName.KEEP_ALIVE)); + assertEquals("42", coreHeaders.getValue(HttpHeaderName.CONTENT_LENGTH)); + } + + @Test + public void setAllHttpHeadersThrowsOnNull() { + WrappedHttp11Headers wrappedHttp11Headers = new WrappedHttp11Headers(new HttpHeaders()); + assertThrows(NullPointerException.class, () -> wrappedHttp11Headers.setAll(null)); + } + + @Test + public void names() { + HttpHeaders clientCoreHeaders = new HttpHeaders().set(HttpHeaderName.CONTENT_LENGTH, "42") + .set(HttpHeaderName.CONTENT_TYPE, "application/json") + .set(HttpHeaderName.ACCEPT, Arrays.asList("application/json", "text/json")); + WrappedHttp11Headers wrappedHttp11Headers = new WrappedHttp11Headers(clientCoreHeaders); + + Set expectedNames = new HashSet<>(Arrays.asList(HttpHeaderName.CONTENT_LENGTH.getCaseSensitiveName(), + HttpHeaderName.CONTENT_TYPE.getCaseSensitiveName(), HttpHeaderName.ACCEPT.getCaseSensitiveName())); + Set names = wrappedHttp11Headers.names(); + + assertEquals(expectedNames, names); + } + + @Test + public void getTimeMillis() { + OffsetDateTime now = OffsetDateTime.now(); + WrappedHttp11Headers wrappedHttp11Headers = new WrappedHttp11Headers( + new HttpHeaders().set(HttpHeaderName.DATE, DateTimeRfc1123.toRfc1123String(now))); + + Long timeMillis = wrappedHttp11Headers.getTimeMillis(HttpHeaderNames.DATE); + assertNotNull(timeMillis); + + // Use OffsetDateTime.toEpochSecond() * 1000L to get the expected millis as DateTimeRfc1123 only has the ability + // to represent to seconds. If OffsetDateTime.toInstant().toEpochMilli() is used, it will return the current + // time in milliseconds, which cannot be represented. + assertEquals(now.toEpochSecond() * 1000L, timeMillis); + } + + @Test + public void getTimeMillisReturnsNullWhenHeaderDoesNotExist() { + WrappedHttp11Headers wrappedHttp11Headers = new WrappedHttp11Headers(new HttpHeaders()); + + assertNull(wrappedHttp11Headers.getTimeMillis(HttpHeaderNames.DATE)); + } + + @Test + public void getTimeMillisReturnsNullWhenHeaderIsNotADate() { + WrappedHttp11Headers wrappedHttp11Headers + = new WrappedHttp11Headers(new HttpHeaders().set(HttpHeaderName.DATE, "notADate")); + + assertNull(wrappedHttp11Headers.getTimeMillis(HttpHeaderNames.DATE)); + } + + @Test + public void getTimeMillisReturnsDefaultWhenHeaderIsNotADate() { + WrappedHttp11Headers wrappedHttp11Headers + = new WrappedHttp11Headers(new HttpHeaders().set(HttpHeaderName.DATE, "notADate")); + + assertEquals(42L, wrappedHttp11Headers.getTimeMillis(HttpHeaderNames.DATE, 42L)); + } + + @Test + public void getTimeMillisReturnsActualValue() { + OffsetDateTime now = OffsetDateTime.now(); + WrappedHttp11Headers wrappedHttp11Headers = new WrappedHttp11Headers( + new HttpHeaders().set(HttpHeaderName.DATE, DateTimeRfc1123.toRfc1123String(now))); + + // Use OffsetDateTime.toEpochSecond() * 1000L to get the expected millis as DateTimeRfc1123 only has the ability + // to represent to seconds. If OffsetDateTime.toInstant().toEpochMilli() is used, it will return the current + // time in milliseconds, which cannot be represented. + assertEquals(now.toEpochSecond() * 1000L, + wrappedHttp11Headers.getTimeMillis(HttpHeaderNames.DATE, now.toInstant().toEpochMilli())); + } +} diff --git a/sdk/clientcore/http-netty4/src/test/java/io/clientcore/http/netty4/implementation/WrappedHttpHeadersTests.java b/sdk/clientcore/http-netty4/src/test/java/io/clientcore/http/netty4/implementation/WrappedHttpHeadersTests.java deleted file mode 100644 index 260bed58a757..000000000000 --- a/sdk/clientcore/http-netty4/src/test/java/io/clientcore/http/netty4/implementation/WrappedHttpHeadersTests.java +++ /dev/null @@ -1,655 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -package io.clientcore.http.netty4.implementation; - -import io.clientcore.core.http.models.HttpHeader; -import io.clientcore.core.http.models.HttpHeaderName; -import io.clientcore.core.http.models.HttpHeaders; -import io.clientcore.core.utils.DateTimeRfc1123; -import io.netty.handler.codec.http.DefaultHttpHeaders; -import io.netty.handler.codec.http.HttpHeaderNames; -import org.junit.jupiter.api.Test; - -import java.time.OffsetDateTime; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashSet; -import java.util.Set; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertLinesMatch; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNotSame; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertSame; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -/** - * Unit tests for {@link WrappedHttpHeaders}. - */ -public class WrappedHttpHeadersTests { - @Test - public void throwsOnNullClientCoreHttpHeaders() { - assertThrows(NullPointerException.class, () -> new WrappedHttpHeaders(null)); - } - - @Test - public void addCharSequenceIterable() { - WrappedHttpHeaders wrappedHttpHeaders = new WrappedHttpHeaders(new HttpHeaders()); - wrappedHttpHeaders.add(HttpHeaderNames.ACCEPT_ENCODING, Arrays.asList("gzip", "deflate")); - - HttpHeader header = wrappedHttpHeaders.getCoreHeaders().get(HttpHeaderName.ACCEPT_ENCODING); - assertNotNull(header); - assertLinesMatch(Arrays.asList("gzip", "deflate"), header.getValues()); - } - - @Test - public void addCharSequenceIterableThrowsOnNull() { - WrappedHttpHeaders wrappedHttpHeaders = new WrappedHttpHeaders(new HttpHeaders()); - assertThrows(NullPointerException.class, - () -> wrappedHttpHeaders.add(HttpHeaderNames.ACCEPT_ENCODING, (Iterable) null)); - } - - @Test - public void addCharSequenceIterableThrowsIfAnyNull() { - WrappedHttpHeaders wrappedHttpHeaders = new WrappedHttpHeaders(new HttpHeaders()); - assertThrows(NullPointerException.class, - () -> wrappedHttpHeaders.add(HttpHeaderNames.ACCEPT_ENCODING, Arrays.asList("gzip", null))); - } - - @Test - public void addCharSequenceObject() { - WrappedHttpHeaders wrappedHttpHeaders = new WrappedHttpHeaders(new HttpHeaders()); - wrappedHttpHeaders.add(HttpHeaderNames.CONTENT_LENGTH, 42); - - HttpHeader header = wrappedHttpHeaders.getCoreHeaders().get(HttpHeaderName.CONTENT_LENGTH); - assertNotNull(header); - assertEquals("42", header.getValue()); - } - - @Test - public void addCharSequenceObjectThrowsOnNull() { - WrappedHttpHeaders wrappedHttpHeaders = new WrappedHttpHeaders(new HttpHeaders()); - assertThrows(NullPointerException.class, - () -> wrappedHttpHeaders.add(HttpHeaderNames.CONTENT_LENGTH, (Object) null)); - } - - @Test - public void addStringIterable() { - WrappedHttpHeaders wrappedHttpHeaders = new WrappedHttpHeaders(new HttpHeaders()); - wrappedHttpHeaders.add("Accept-Encoding", Arrays.asList("gzip", "deflate")); - - HttpHeader header = wrappedHttpHeaders.getCoreHeaders().get(HttpHeaderName.ACCEPT_ENCODING); - assertNotNull(header); - assertLinesMatch(Arrays.asList("gzip", "deflate"), header.getValues()); - } - - @Test - public void addStringIterableThrowsOnNull() { - WrappedHttpHeaders wrappedHttpHeaders = new WrappedHttpHeaders(new HttpHeaders()); - assertThrows(NullPointerException.class, () -> wrappedHttpHeaders.add("Accept-Encoding", (Iterable) null)); - } - - @Test - public void addStringIterableThrowsIfAnyNull() { - WrappedHttpHeaders wrappedHttpHeaders = new WrappedHttpHeaders(new HttpHeaders()); - assertThrows(NullPointerException.class, - () -> wrappedHttpHeaders.add("Accept-Encoding", Arrays.asList("gzip", null))); - } - - @Test - public void addStringObject() { - WrappedHttpHeaders wrappedHttpHeaders = new WrappedHttpHeaders(new HttpHeaders()); - wrappedHttpHeaders.add("Content-Length", 42); - - HttpHeader header = wrappedHttpHeaders.getCoreHeaders().get(HttpHeaderName.CONTENT_LENGTH); - assertNotNull(header); - assertEquals("42", header.getValue()); - } - - @Test - public void addStringObjectThrowsOnNull() { - WrappedHttpHeaders wrappedHttpHeaders = new WrappedHttpHeaders(new HttpHeaders()); - assertThrows(NullPointerException.class, () -> wrappedHttpHeaders.add("Content-Length", (Object) null)); - } - - @Test - public void addCharSequenceInt() { - WrappedHttpHeaders wrappedHttpHeaders = new WrappedHttpHeaders(new HttpHeaders()); - wrappedHttpHeaders.addInt("Content-Length", 42); - - HttpHeader header = wrappedHttpHeaders.getCoreHeaders().get(HttpHeaderName.CONTENT_LENGTH); - assertNotNull(header); - assertEquals("42", header.getValue()); - } - - @Test - public void addCharSequenceShort() { - WrappedHttpHeaders wrappedHttpHeaders = new WrappedHttpHeaders(new HttpHeaders()); - wrappedHttpHeaders.addShort("Content-Length", (short) 42); - - HttpHeader header = wrappedHttpHeaders.getCoreHeaders().get(HttpHeaderName.CONTENT_LENGTH); - assertNotNull(header); - assertEquals("42", header.getValue()); - } - - @Test - public void addHttpHeadersHotPathsWrappedHttpHeaders() { - WrappedHttpHeaders wrappedHttpHeaders = new WrappedHttpHeaders(new HttpHeaders()); - WrappedHttpHeaders toAdd = new WrappedHttpHeaders( - new HttpHeaders().set(HttpHeaderName.KEEP_ALIVE, "true").set(HttpHeaderName.CONTENT_LENGTH, "42")); - - wrappedHttpHeaders.add(toAdd); - - HttpHeaders coreHeaders = wrappedHttpHeaders.getCoreHeaders(); - assertEquals(2, coreHeaders.getSize()); - assertEquals("true", coreHeaders.getValue(HttpHeaderName.KEEP_ALIVE)); - assertEquals("42", coreHeaders.getValue(HttpHeaderName.CONTENT_LENGTH)); - } - - @SuppressWarnings("deprecation") - @Test - public void addHttpHeadersFallsBackToSuperImplementation() { - WrappedHttpHeaders wrappedHttpHeaders = new WrappedHttpHeaders(new HttpHeaders()); - io.netty.handler.codec.http.HttpHeaders toAdd = new DefaultHttpHeaders().add(HttpHeaderNames.KEEP_ALIVE, "true") - .add(HttpHeaderNames.CONTENT_LENGTH, "42"); - - wrappedHttpHeaders.add(toAdd); - - HttpHeaders coreHeaders = wrappedHttpHeaders.getCoreHeaders(); - assertEquals(2, coreHeaders.getSize()); - assertEquals("true", coreHeaders.getValue(HttpHeaderName.KEEP_ALIVE)); - assertEquals("42", coreHeaders.getValue(HttpHeaderName.CONTENT_LENGTH)); - } - - @Test - public void addHttpHeadersThrowsOnNull() { - WrappedHttpHeaders wrappedHttpHeaders = new WrappedHttpHeaders(new HttpHeaders()); - assertThrows(NullPointerException.class, - () -> wrappedHttpHeaders.add((io.netty.handler.codec.http.HttpHeaders) null)); - } - - @Test - public void clearUsesNewClientCoreHttpHeaders() { - HttpHeaders initial = new HttpHeaders(); - WrappedHttpHeaders wrappedHttpHeaders = new WrappedHttpHeaders(initial); - wrappedHttpHeaders.clear(); - - assertNotSame(initial, wrappedHttpHeaders.getCoreHeaders()); - } - - @Test - public void containsCharSequenceReturnsFalseWhenHeaderDoesNotExist() { - WrappedHttpHeaders wrappedHttpHeaders = new WrappedHttpHeaders(new HttpHeaders()); - - assertFalse(wrappedHttpHeaders.contains(HttpHeaderNames.CONTENT_LENGTH)); - } - - @Test - public void containsCharSequence() { - WrappedHttpHeaders wrappedHttpHeaders - = new WrappedHttpHeaders(new HttpHeaders().set(HttpHeaderName.CONNECTION, "connection")); - - assertTrue(wrappedHttpHeaders.contains(HttpHeaderNames.CONNECTION)); - } - - @Test - public void containsStringReturnsFalseWhenHeaderDoesNotExist() { - WrappedHttpHeaders wrappedHttpHeaders = new WrappedHttpHeaders(new HttpHeaders()); - - assertFalse(wrappedHttpHeaders.contains("Content-Length")); - } - - @Test - public void containsString() { - WrappedHttpHeaders wrappedHttpHeaders - = new WrappedHttpHeaders(new HttpHeaders().set(HttpHeaderName.CONNECTION, "connection")); - - assertTrue(wrappedHttpHeaders.contains("Connection")); - } - - @Test - public void copyUsesNewClientCoreHeaders() { - HttpHeaders initial = new HttpHeaders().set(HttpHeaderName.CONTENT_LENGTH, "42"); - WrappedHttpHeaders wrappedHttpHeaders = new WrappedHttpHeaders(initial); - - WrappedHttpHeaders copy = (WrappedHttpHeaders) wrappedHttpHeaders.copy(); - - assertNotSame(initial, copy.getCoreHeaders()); - assertEquals("42", wrappedHttpHeaders.get(HttpHeaderNames.CONTENT_LENGTH)); - } - - @Test - public void getCharSequenceReturnsNullWhenHeaderDoesNotExist() { - WrappedHttpHeaders wrappedHttpHeaders = new WrappedHttpHeaders(new HttpHeaders()); - - assertNull(wrappedHttpHeaders.get(HttpHeaderNames.CONTENT_ENCODING)); - } - - @Test - public void getCharSequenceReturnsFirstValue() { - WrappedHttpHeaders wrappedHttpHeaders = new WrappedHttpHeaders(new HttpHeaders() - .set(HttpHeaderName.CONTENT_ENCODING, Arrays.asList("application/json", "application/xml"))); - - assertEquals("application/json", wrappedHttpHeaders.get(HttpHeaderNames.CONTENT_ENCODING)); - } - - @Test - public void getStringReturnsNullWhenHeaderDoesNotExist() { - WrappedHttpHeaders wrappedHttpHeaders = new WrappedHttpHeaders(new HttpHeaders()); - - assertNull(wrappedHttpHeaders.get("Content-Encoding")); - } - - @Test - public void getStringReturnsFirstValue() { - WrappedHttpHeaders wrappedHttpHeaders = new WrappedHttpHeaders(new HttpHeaders() - .set(HttpHeaderName.CONTENT_ENCODING, Arrays.asList("application/json", "application/xml"))); - - assertEquals("application/json", wrappedHttpHeaders.get("Content-Encoding")); - } - - @Test - public void getAllCharSequenceReturnsEmptyListWhenHeaderDoesNotExist() { - WrappedHttpHeaders wrappedHttpHeaders = new WrappedHttpHeaders(new HttpHeaders()); - - assertSame(Collections.emptyList(), wrappedHttpHeaders.getAll(HttpHeaderNames.CONTENT_TYPE)); - } - - @Test - public void getAllCharSequence() { - WrappedHttpHeaders wrappedHttpHeaders = new WrappedHttpHeaders( - new HttpHeaders().set(HttpHeaderName.CONTENT_TYPE, Arrays.asList("application/json", "application/xml"))); - - assertLinesMatch(Arrays.asList("application/json", "application/xml"), - wrappedHttpHeaders.getAll(HttpHeaderNames.CONTENT_TYPE)); - } - - @Test - public void getAllStringReturnsEmptyListWhenHeaderDoesNotExist() { - WrappedHttpHeaders wrappedHttpHeaders = new WrappedHttpHeaders(new HttpHeaders()); - - assertSame(Collections.emptyList(), wrappedHttpHeaders.getAll("Content-Type")); - } - - @Test - public void getAllString() { - WrappedHttpHeaders wrappedHttpHeaders = new WrappedHttpHeaders( - new HttpHeaders().set(HttpHeaderName.CONTENT_TYPE, Arrays.asList("application/json", "application/xml"))); - - assertLinesMatch(Arrays.asList("application/json", "application/xml"), - wrappedHttpHeaders.getAll("Content-Type")); - } - - @Test - public void getIntReturnsNullWhenHeaderDoesNotExist() { - WrappedHttpHeaders wrappedHttpHeaders = new WrappedHttpHeaders(new HttpHeaders()); - - assertNull(wrappedHttpHeaders.getInt(HttpHeaderNames.CONTENT_LENGTH)); - } - - @Test - public void getIntReturnsFirstValue() { - WrappedHttpHeaders wrappedHttpHeaders - = new WrappedHttpHeaders(new HttpHeaders().set(HttpHeaderName.COOKIE, Arrays.asList("1", "2"))); - - assertEquals(1, wrappedHttpHeaders.getInt(HttpHeaderNames.COOKIE)); - } - - @Test - public void getIntReturnsNullOnInvalidParse() { - WrappedHttpHeaders wrappedHttpHeaders - = new WrappedHttpHeaders(new HttpHeaders().set(HttpHeaderName.EXPECT, "expect")); - - assertNull(wrappedHttpHeaders.getInt(HttpHeaderNames.EXPECT)); - } - - @Test - public void getIntWithDefaultReturnsDefaultOnNull() { - WrappedHttpHeaders wrappedHttpHeaders = new WrappedHttpHeaders(new HttpHeaders()); - - assertEquals(1, wrappedHttpHeaders.getInt(HttpHeaderNames.CONTENT_LENGTH, 1)); - } - - @Test - public void getIntWithDefaultReturnsDefaultOnInvalidParse() { - WrappedHttpHeaders wrappedHttpHeaders - = new WrappedHttpHeaders(new HttpHeaders().set(HttpHeaderName.HOST, "host")); - - assertEquals(1, wrappedHttpHeaders.getInt(HttpHeaderNames.HOST, 1)); - } - - @Test - public void getIntReturnsActualValue() { - WrappedHttpHeaders wrappedHttpHeaders - = new WrappedHttpHeaders(new HttpHeaders().set(HttpHeaderName.CONTENT_LENGTH, "42")); - - assertEquals(42, wrappedHttpHeaders.getInt(HttpHeaderNames.CONTENT_LENGTH, 24)); - } - - @Test - public void getShortReturnsNullWhenHeaderDoesNotExist() { - WrappedHttpHeaders wrappedHttpHeaders = new WrappedHttpHeaders(new HttpHeaders()); - - assertNull(wrappedHttpHeaders.getShort(HttpHeaderNames.CONTENT_LENGTH)); - } - - @Test - public void getShortReturnsFirstValue() { - WrappedHttpHeaders wrappedHttpHeaders - = new WrappedHttpHeaders(new HttpHeaders().set(HttpHeaderName.COOKIE, Arrays.asList("1", "2"))); - - assertEquals((short) 1, wrappedHttpHeaders.getShort(HttpHeaderNames.COOKIE)); - } - - @Test - public void getShortReturnsNullOnInvalidParse() { - WrappedHttpHeaders wrappedHttpHeaders - = new WrappedHttpHeaders(new HttpHeaders().set(HttpHeaderName.PROXY_AUTHORIZATION, "authorization")); - - assertNull(wrappedHttpHeaders.getShort(HttpHeaderNames.PROXY_AUTHORIZATION)); - } - - @Test - public void getShortWithDefaultReturnsDefaultOnNull() { - WrappedHttpHeaders wrappedHttpHeaders = new WrappedHttpHeaders(new HttpHeaders()); - - assertEquals((short) 1, wrappedHttpHeaders.getShort(HttpHeaderNames.CONTENT_LENGTH, (short) 1)); - } - - @Test - public void getShortWithDefaultReturnsDefaultOnInvalidParse() { - WrappedHttpHeaders wrappedHttpHeaders = new WrappedHttpHeaders(new HttpHeaders().set(HttpHeaderName.TE, "TE")); - - assertEquals((short) 1, wrappedHttpHeaders.getShort(HttpHeaderNames.TE, (short) 1)); - } - - @Test - public void getShortReturnsActualValue() { - WrappedHttpHeaders wrappedHttpHeaders - = new WrappedHttpHeaders(new HttpHeaders().set(HttpHeaderName.CONTENT_LENGTH, "42")); - - assertEquals((short) 42, wrappedHttpHeaders.getShort(HttpHeaderNames.CONTENT_LENGTH, (short) 24)); - } - - @Test - public void isEmptyIsBasedOnClientCoreHeaders() { - WrappedHttpHeaders wrappedHttpHeaders = new WrappedHttpHeaders(new HttpHeaders()); - assertTrue(wrappedHttpHeaders.isEmpty()); - - wrappedHttpHeaders.getCoreHeaders().set(HttpHeaderName.CONTENT_LENGTH, "42"); - assertFalse(wrappedHttpHeaders.isEmpty()); - } - - @Test - public void removeCharSequence() { - WrappedHttpHeaders wrappedHttpHeaders - = new WrappedHttpHeaders(new HttpHeaders().set(HttpHeaderName.TRAILER, "trailer")); - wrappedHttpHeaders.remove(HttpHeaderNames.TRAILER); - - assertTrue(wrappedHttpHeaders.isEmpty()); - assertNull(wrappedHttpHeaders.getCoreHeaders().get(HttpHeaderName.TRAILER)); - } - - @Test - public void removeString() { - WrappedHttpHeaders wrappedHttpHeaders - = new WrappedHttpHeaders(new HttpHeaders().set(HttpHeaderName.TRAILER, "trailer")); - wrappedHttpHeaders.remove("Trailer"); - - assertTrue(wrappedHttpHeaders.isEmpty()); - assertNull(wrappedHttpHeaders.getCoreHeaders().get(HttpHeaderName.TRAILER)); - } - - @Test - public void setCharSequenceIterable() { - WrappedHttpHeaders wrappedHttpHeaders - = new WrappedHttpHeaders(new HttpHeaders().set(HttpHeaderName.ACCEPT_ENCODING, "*")); - wrappedHttpHeaders.set(HttpHeaderNames.ACCEPT_ENCODING, Arrays.asList("gzip", "deflate")); - - HttpHeader header = wrappedHttpHeaders.getCoreHeaders().get(HttpHeaderName.ACCEPT_ENCODING); - assertNotNull(header); - assertLinesMatch(Arrays.asList("gzip", "deflate"), header.getValues()); - } - - @Test - public void setCharSequenceIterableThrowsOnNull() { - WrappedHttpHeaders wrappedHttpHeaders = new WrappedHttpHeaders(new HttpHeaders()); - assertThrows(NullPointerException.class, - () -> wrappedHttpHeaders.set(HttpHeaderNames.ACCEPT_ENCODING, (Iterable) null)); - } - - @Test - public void setCharSequenceIterableThrowsIfAnyNull() { - WrappedHttpHeaders wrappedHttpHeaders = new WrappedHttpHeaders(new HttpHeaders()); - assertThrows(NullPointerException.class, - () -> wrappedHttpHeaders.set(HttpHeaderNames.ACCEPT_ENCODING, Arrays.asList("gzip", null))); - } - - @SuppressWarnings("deprecation") - @Test - public void setCharSequenceObject() { - WrappedHttpHeaders wrappedHttpHeaders - = new WrappedHttpHeaders(new HttpHeaders().set(HttpHeaderName.KEEP_ALIVE, "false")); - wrappedHttpHeaders.set(HttpHeaderNames.KEEP_ALIVE, "true"); - - HttpHeader header = wrappedHttpHeaders.getCoreHeaders().get(HttpHeaderName.KEEP_ALIVE); - assertNotNull(header); - assertEquals("true", header.getValue()); - } - - @Test - public void setCharSequenceObjectThrowsOnNull() { - WrappedHttpHeaders wrappedHttpHeaders = new WrappedHttpHeaders(new HttpHeaders()); - assertThrows(NullPointerException.class, - () -> wrappedHttpHeaders.set(HttpHeaderNames.CONTENT_LENGTH, (Object) null)); - } - - @Test - public void setStringIterable() { - WrappedHttpHeaders wrappedHttpHeaders - = new WrappedHttpHeaders(new HttpHeaders().set(HttpHeaderName.ACCEPT_ENCODING, "*")); - wrappedHttpHeaders.set("Accept-Encoding", Arrays.asList("gzip", "deflate")); - - HttpHeader header = wrappedHttpHeaders.getCoreHeaders().get(HttpHeaderName.ACCEPT_ENCODING); - assertNotNull(header); - assertLinesMatch(Arrays.asList("gzip", "deflate"), header.getValues()); - } - - @Test - public void setStringIterableThrowsOnNull() { - WrappedHttpHeaders wrappedHttpHeaders = new WrappedHttpHeaders(new HttpHeaders()); - assertThrows(NullPointerException.class, () -> wrappedHttpHeaders.set("Accept-Encoding", (Iterable) null)); - } - - @Test - public void setStringIterableThrowsIfAnyNull() { - WrappedHttpHeaders wrappedHttpHeaders = new WrappedHttpHeaders(new HttpHeaders()); - assertThrows(NullPointerException.class, - () -> wrappedHttpHeaders.set("Accept-Encoding", Arrays.asList("gzip", null))); - } - - @Test - public void setStringObject() { - WrappedHttpHeaders wrappedHttpHeaders - = new WrappedHttpHeaders(new HttpHeaders().set(HttpHeaderName.CONTENT_LENGTH, "24")); - wrappedHttpHeaders.set("Content-Length", 42); - - HttpHeader header = wrappedHttpHeaders.getCoreHeaders().get(HttpHeaderName.CONTENT_LENGTH); - assertNotNull(header); - assertEquals("42", header.getValue()); - } - - @Test - public void setStringObjectThrowsOnNull() { - WrappedHttpHeaders wrappedHttpHeaders = new WrappedHttpHeaders(new HttpHeaders()); - assertThrows(NullPointerException.class, () -> wrappedHttpHeaders.set("Content-Length", (Object) null)); - } - - @Test - public void setCharSequenceInt() { - WrappedHttpHeaders wrappedHttpHeaders - = new WrappedHttpHeaders(new HttpHeaders().set(HttpHeaderName.CONTENT_LENGTH, "24")); - wrappedHttpHeaders.setInt("Content-Length", 42); - - HttpHeader header = wrappedHttpHeaders.getCoreHeaders().get(HttpHeaderName.CONTENT_LENGTH); - assertNotNull(header); - assertEquals("42", header.getValue()); - } - - @Test - public void setCharSequenceShort() { - WrappedHttpHeaders wrappedHttpHeaders - = new WrappedHttpHeaders(new HttpHeaders().set(HttpHeaderName.CONTENT_LENGTH, "24")); - wrappedHttpHeaders.setShort("Content-Length", (short) 42); - - HttpHeader header = wrappedHttpHeaders.getCoreHeaders().get(HttpHeaderName.CONTENT_LENGTH); - assertNotNull(header); - assertEquals("42", header.getValue()); - } - - @Test - public void setHttpHeadersHotPathsWrappedHttpHeaders() { - HttpHeaders initial = new HttpHeaders(0); - WrappedHttpHeaders wrappedHttpHeaders = new WrappedHttpHeaders(initial); - WrappedHttpHeaders toAdd = new WrappedHttpHeaders( - new HttpHeaders().set(HttpHeaderName.KEEP_ALIVE, "true").set(HttpHeaderName.CONTENT_LENGTH, "42")); - - wrappedHttpHeaders.set(toAdd); - - HttpHeaders coreHeaders = wrappedHttpHeaders.getCoreHeaders(); - assertNotSame(initial, coreHeaders); - assertNotSame(toAdd.getCoreHeaders(), coreHeaders); - assertEquals(2, coreHeaders.getSize()); - assertEquals("true", coreHeaders.getValue(HttpHeaderName.KEEP_ALIVE)); - assertEquals("42", coreHeaders.getValue(HttpHeaderName.CONTENT_LENGTH)); - } - - @Test - public void setHttpHeadersFallsBackToSuperImplementation() { - HttpHeaders initial = new HttpHeaders(); - WrappedHttpHeaders wrappedHttpHeaders = new WrappedHttpHeaders(initial); - io.netty.handler.codec.http.HttpHeaders toAdd - = new DefaultHttpHeaders().add("Keep-Alive", "true").add(HttpHeaderNames.CONTENT_LENGTH, "42"); - - wrappedHttpHeaders.set(toAdd); - - HttpHeaders coreHeaders = wrappedHttpHeaders.getCoreHeaders(); - assertNotSame(initial, coreHeaders); - assertEquals(2, coreHeaders.getSize()); - assertEquals("true", coreHeaders.getValue(HttpHeaderName.KEEP_ALIVE)); - assertEquals("42", coreHeaders.getValue(HttpHeaderName.CONTENT_LENGTH)); - } - - @Test - public void setHttpHeadersThrowsOnNull() { - WrappedHttpHeaders wrappedHttpHeaders = new WrappedHttpHeaders(new HttpHeaders()); - assertThrows(NullPointerException.class, - () -> wrappedHttpHeaders.set((io.netty.handler.codec.http.HttpHeaders) null)); - } - - @Test - public void setAllHttpHeadersHotPathsWrappedHttpHeaders() { - HttpHeaders initial = new HttpHeaders(0); - WrappedHttpHeaders wrappedHttpHeaders = new WrappedHttpHeaders(initial); - WrappedHttpHeaders toAdd = new WrappedHttpHeaders( - new HttpHeaders().set(HttpHeaderName.KEEP_ALIVE, "true").set(HttpHeaderName.CONTENT_LENGTH, "42")); - - wrappedHttpHeaders.setAll(toAdd); - - HttpHeaders coreHeaders = wrappedHttpHeaders.getCoreHeaders(); - assertSame(initial, coreHeaders); - assertEquals(2, coreHeaders.getSize()); - assertEquals("true", coreHeaders.getValue(HttpHeaderName.KEEP_ALIVE)); - assertEquals("42", coreHeaders.getValue(HttpHeaderName.CONTENT_LENGTH)); - } - - @Test - public void setAllHttpHeadersFallsBackToSuperImplementation() { - HttpHeaders initial = new HttpHeaders(); - WrappedHttpHeaders wrappedHttpHeaders = new WrappedHttpHeaders(initial); - io.netty.handler.codec.http.HttpHeaders toAdd - = new DefaultHttpHeaders().add("Keep-Alive", "true").add(HttpHeaderNames.CONTENT_LENGTH, "42"); - - wrappedHttpHeaders.setAll(toAdd); - - HttpHeaders coreHeaders = wrappedHttpHeaders.getCoreHeaders(); - assertSame(initial, coreHeaders); - assertEquals(2, coreHeaders.getSize()); - assertEquals("true", coreHeaders.getValue(HttpHeaderName.KEEP_ALIVE)); - assertEquals("42", coreHeaders.getValue(HttpHeaderName.CONTENT_LENGTH)); - } - - @Test - public void setAllHttpHeadersThrowsOnNull() { - WrappedHttpHeaders wrappedHttpHeaders = new WrappedHttpHeaders(new HttpHeaders()); - assertThrows(NullPointerException.class, () -> wrappedHttpHeaders.setAll(null)); - } - - @Test - public void names() { - HttpHeaders clientCoreHeaders = new HttpHeaders().set(HttpHeaderName.CONTENT_LENGTH, "42") - .set(HttpHeaderName.CONTENT_TYPE, "application/json") - .set(HttpHeaderName.ACCEPT, Arrays.asList("application/json", "text/json")); - WrappedHttpHeaders wrappedHttpHeaders = new WrappedHttpHeaders(clientCoreHeaders); - - Set expectedNames = new HashSet<>(Arrays.asList(HttpHeaderName.CONTENT_LENGTH.getCaseSensitiveName(), - HttpHeaderName.CONTENT_TYPE.getCaseSensitiveName(), HttpHeaderName.ACCEPT.getCaseSensitiveName())); - Set names = wrappedHttpHeaders.names(); - - assertEquals(expectedNames, names); - } - - @Test - public void getTimeMillis() { - OffsetDateTime now = OffsetDateTime.now(); - WrappedHttpHeaders wrappedHttpHeaders - = new WrappedHttpHeaders(new HttpHeaders().set(HttpHeaderName.DATE, DateTimeRfc1123.toRfc1123String(now))); - - Long timeMillis = wrappedHttpHeaders.getTimeMillis(HttpHeaderNames.DATE); - assertNotNull(timeMillis); - - // Use OffsetDateTime.toEpochSecond() * 1000L to get the expected millis as DateTimeRfc1123 only has the ability - // to represent to seconds. If OffsetDateTime.toInstant().toEpochMilli() is used, it will return the current - // time in milliseconds, which cannot be represented. - assertEquals(now.toEpochSecond() * 1000L, timeMillis); - } - - @Test - public void getTimeMillisReturnsNullWhenHeaderDoesNotExist() { - WrappedHttpHeaders wrappedHttpHeaders = new WrappedHttpHeaders(new HttpHeaders()); - - assertNull(wrappedHttpHeaders.getTimeMillis(HttpHeaderNames.DATE)); - } - - @Test - public void getTimeMillisReturnsNullWhenHeaderIsNotADate() { - WrappedHttpHeaders wrappedHttpHeaders - = new WrappedHttpHeaders(new HttpHeaders().set(HttpHeaderName.DATE, "notADate")); - - assertNull(wrappedHttpHeaders.getTimeMillis(HttpHeaderNames.DATE)); - } - - @Test - public void getTimeMillisReturnsDefaultWhenHeaderIsNotADate() { - WrappedHttpHeaders wrappedHttpHeaders - = new WrappedHttpHeaders(new HttpHeaders().set(HttpHeaderName.DATE, "notADate")); - - assertEquals(42L, wrappedHttpHeaders.getTimeMillis(HttpHeaderNames.DATE, 42L)); - } - - @Test - public void getTimeMillisReturnsActualValue() { - OffsetDateTime now = OffsetDateTime.now(); - WrappedHttpHeaders wrappedHttpHeaders - = new WrappedHttpHeaders(new HttpHeaders().set(HttpHeaderName.DATE, DateTimeRfc1123.toRfc1123String(now))); - - // Use OffsetDateTime.toEpochSecond() * 1000L to get the expected millis as DateTimeRfc1123 only has the ability - // to represent to seconds. If OffsetDateTime.toInstant().toEpochMilli() is used, it will return the current - // time in milliseconds, which cannot be represented. - assertEquals(now.toEpochSecond() * 1000L, - wrappedHttpHeaders.getTimeMillis(HttpHeaderNames.DATE, now.toInstant().toEpochMilli())); - } -}