All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.arangodb.shaded.vertx.core.http.impl.Http1xClientConnection Maven / Gradle / Ivy

There is a newer version: 7.8.0
Show newest version
/*
 * Copyright (c) 2011-2019 Contributors to the Eclipse Foundation
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
 * which is available at https://www.apache.org/licenses/LICENSE-2.0.
 *
 * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
 */

package com.arangodb.shaded.vertx.core.http.impl;

import com.arangodb.shaded.netty.buffer.ByteBuf;
import com.arangodb.shaded.netty.buffer.Unpooled;
import com.arangodb.shaded.netty.channel.*;
import com.arangodb.shaded.netty.handler.codec.DecoderResult;
import com.arangodb.shaded.netty.handler.codec.compression.Brotli;
import com.arangodb.shaded.netty.handler.codec.compression.ZlibCodecFactory;
import com.arangodb.shaded.netty.handler.codec.http.DefaultHttpContent;
import com.arangodb.shaded.netty.handler.codec.http.DefaultHttpHeaders;
import com.arangodb.shaded.netty.handler.codec.http.DefaultHttpRequest;
import com.arangodb.shaded.netty.handler.codec.http.DefaultLastHttpContent;
import com.arangodb.shaded.netty.handler.codec.http.FullHttpRequest;
import com.arangodb.shaded.netty.handler.codec.http.HttpContent;
import com.arangodb.shaded.netty.handler.codec.http.HttpContentDecompressor;
import com.arangodb.shaded.netty.handler.codec.http.HttpHeaderNames;
import com.arangodb.shaded.netty.handler.codec.http.HttpHeaderValues;
import com.arangodb.shaded.netty.handler.codec.http.HttpHeaders;
import com.arangodb.shaded.netty.handler.codec.http.HttpObject;
import com.arangodb.shaded.netty.handler.codec.http.HttpRequest;
import com.arangodb.shaded.netty.handler.codec.http.HttpResponseStatus;
import com.arangodb.shaded.netty.handler.codec.http.HttpUtil;
import com.arangodb.shaded.netty.handler.codec.http.LastHttpContent;
import com.arangodb.shaded.netty.handler.codec.http.websocketx.WebSocket07FrameDecoder;
import com.arangodb.shaded.netty.handler.codec.http.websocketx.WebSocket08FrameDecoder;
import com.arangodb.shaded.netty.handler.codec.http.websocketx.WebSocket13FrameDecoder;
import com.arangodb.shaded.netty.handler.codec.http.websocketx.WebSocketClientHandshaker;
import com.arangodb.shaded.netty.handler.codec.http.websocketx.WebSocketClientHandshaker00;
import com.arangodb.shaded.netty.handler.codec.http.websocketx.WebSocketClientHandshaker07;
import com.arangodb.shaded.netty.handler.codec.http.websocketx.WebSocketClientHandshaker08;
import com.arangodb.shaded.netty.handler.codec.http.websocketx.WebSocketClientHandshaker13;
import com.arangodb.shaded.netty.handler.codec.http.websocketx.WebSocketDecoderConfig;
import com.arangodb.shaded.netty.handler.codec.http.websocketx.WebSocketFrame;
import com.arangodb.shaded.netty.handler.codec.http.websocketx.WebSocketFrameDecoder;
import com.arangodb.shaded.netty.handler.codec.http.websocketx.WebSocketHandshakeException;
import com.arangodb.shaded.netty.handler.codec.http.websocketx.WebSocketVersion;
import com.arangodb.shaded.netty.handler.codec.http.websocketx.extensions.WebSocketClientExtensionHandler;
import com.arangodb.shaded.netty.handler.codec.http.websocketx.extensions.WebSocketClientExtensionHandshaker;
import com.arangodb.shaded.netty.handler.codec.http.websocketx.extensions.compression.DeflateFrameClientExtensionHandshaker;
import com.arangodb.shaded.netty.handler.codec.http.websocketx.extensions.compression.PerMessageDeflateClientExtensionHandshaker;
import com.arangodb.shaded.netty.handler.codec.http.websocketx.extensions.compression.PerMessageDeflateServerExtensionHandshaker;
import com.arangodb.shaded.netty.handler.timeout.IdleStateEvent;
import com.arangodb.shaded.netty.util.ReferenceCountUtil;
import com.arangodb.shaded.netty.util.concurrent.FutureListener;
import com.arangodb.shaded.vertx.core.AsyncResult;
import com.arangodb.shaded.vertx.core.Future;
import com.arangodb.shaded.vertx.core.Handler;
import com.arangodb.shaded.vertx.core.MultiMap;
import com.arangodb.shaded.vertx.core.Promise;
import com.arangodb.shaded.vertx.core.VertxException;
import com.arangodb.shaded.vertx.core.buffer.Buffer;
import com.arangodb.shaded.vertx.core.http.HttpClientOptions;
import com.arangodb.shaded.vertx.core.http.HttpFrame;
import com.arangodb.shaded.vertx.core.http.HttpMethod;
import com.arangodb.shaded.vertx.core.http.HttpVersion;
import com.arangodb.shaded.vertx.core.http.StreamPriority;
import com.arangodb.shaded.vertx.core.http.WebSocket;
import com.arangodb.shaded.vertx.core.http.WebsocketVersion;
import com.arangodb.shaded.vertx.core.http.impl.headers.HeadersAdaptor;
import com.arangodb.shaded.vertx.core.impl.ContextInternal;
import com.arangodb.shaded.vertx.core.impl.future.PromiseInternal;
import com.arangodb.shaded.vertx.core.impl.logging.Logger;
import com.arangodb.shaded.vertx.core.impl.logging.LoggerFactory;
import com.arangodb.shaded.vertx.core.net.SocketAddress;
import com.arangodb.shaded.vertx.core.net.impl.NetSocketImpl;
import com.arangodb.shaded.vertx.core.net.impl.NetSocketInternal;
import com.arangodb.shaded.vertx.core.net.impl.VertxHandler;
import com.arangodb.shaded.vertx.core.spi.metrics.ClientMetrics;
import com.arangodb.shaded.vertx.core.spi.metrics.HttpClientMetrics;
import com.arangodb.shaded.vertx.core.spi.tracing.SpanKind;
import com.arangodb.shaded.vertx.core.spi.tracing.TagExtractor;
import com.arangodb.shaded.vertx.core.spi.tracing.VertxTracer;
import com.arangodb.shaded.vertx.core.streams.WriteStream;
import com.arangodb.shaded.vertx.core.streams.impl.InboundBuffer;

import java.net.URI;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.function.BiConsumer;

import static com.arangodb.shaded.netty.handler.codec.http.websocketx.WebSocketVersion.*;
import static com.arangodb.shaded.vertx.core.http.HttpHeaders.*;

/**
 *
 * This class is optimised for performance when used on the same event loop. However it can be used safely from other threads.
 *
 * The internal state is protected using the synchronized keyword. If always used on the same event loop, then
 * we benefit from biased locking which makes the overhead of synchronized near zero.
 *
 * @author Tim Fox
 */
public class Http1xClientConnection extends Http1xConnectionBase implements HttpClientConnection {

  private static final Logger log = LoggerFactory.getLogger(Http1xClientConnection.class);

  private static final Handler INVALID_MSG_HANDLER = msg -> {
    ReferenceCountUtil.release(msg);
    throw new IllegalStateException("Invalid object " + msg);
  };

  private final HttpClientImpl client;
  private final HttpClientOptions options;
  private final boolean ssl;
  private final SocketAddress server;
  public final ClientMetrics metrics;
  private final HttpVersion version;
  private final long lowWaterMark;
  private final long highWaterMark;

  private Deque requests = new ArrayDeque<>();
  private Deque responses = new ArrayDeque<>();
  private boolean closed;
  private boolean evicted;

  private Handler evictionHandler = DEFAULT_EVICTION_HANDLER;
  private Handler invalidMessageHandler = INVALID_MSG_HANDLER;
  private boolean close;
  private boolean shutdown;
  private long shutdownTimerID = -1L;
  private boolean isConnect;
  private int keepAliveTimeout;
  private long expirationTimestamp;
  private int seq = 1;
  private long readWindow;
  private long writeWindow;
  private boolean writeOverflow;

  private long lastResponseReceivedTimestamp;

  Http1xClientConnection(HttpVersion version,
                         HttpClientImpl client,
                         ChannelHandlerContext channel,
                         boolean ssl,
                         SocketAddress server,
                         ContextInternal context,
                         ClientMetrics metrics) {
    super(context, channel);
    this.client = client;
    this.options = client.options();
    this.ssl = ssl;
    this.server = server;
    this.metrics = metrics;
    this.version = version;
    this.readWindow = 0L;
    this.writeWindow = 0L;
    this.highWaterMark = channel.channel().config().getWriteBufferHighWaterMark();
    this.lowWaterMark = channel.channel().config().getWriteBufferLowWaterMark();
    this.keepAliveTimeout = options.getKeepAliveTimeout();
    this.expirationTimestamp = expirationTimestampOf(keepAliveTimeout);
  }

  @Override
  public HttpClientConnection evictionHandler(Handler handler) {
    evictionHandler = handler;
    return this;
  }

  @Override
  public HttpClientConnection concurrencyChangeHandler(Handler handler) {
    // Never changes
    return this;
  }

  @Override
  public long concurrency() {
    return options.isPipelining() ? options.getPipeliningLimit() : 1;
  }

  /**
   * @return a raw {@code NetSocket} - for internal use
   */
  public NetSocketInternal toNetSocket() {
    removeChannelHandlers();
    NetSocketImpl socket = new NetSocketImpl(context, chctx, null, metrics(), false);
    socket.metric(metric());
    evictionHandler.handle(null);
    chctx.pipeline().replace("handler", "handler", VertxHandler.create(ctx -> socket));
    return socket;
  }

  private HttpRequest createRequest(
    HttpMethod method,
    String uri,
    MultiMap headerMap,
    String authority,
    boolean chunked,
    ByteBuf buf,
    boolean end) {
    HttpRequest request = new DefaultHttpRequest(HttpUtils.toNettyHttpVersion(version), method.toNetty(), uri, false);
    HttpHeaders headers = request.headers();
    if (headerMap != null) {
      for (Map.Entry header : headerMap) {
        headers.add(header.getKey(), header.getValue());
      }
    }
    if (!headers.contains(HOST)) {
      request.headers().set(HOST, authority);
    } else {
      headers.remove(TRANSFER_ENCODING);
    }
    if (chunked) {
      HttpUtil.setTransferEncodingChunked(request, true);
    }
    if (options.isTryUseCompression() && request.headers().get(ACCEPT_ENCODING) == null) {
      // if compression should be used but nothing is specified by the user support deflate and gzip.
      CharSequence acceptEncoding = determineCompressionAcceptEncoding();
      request.headers().set(ACCEPT_ENCODING, acceptEncoding);
    }
    if (!options.isKeepAlive() && options.getProtocolVersion() == io.vertx.core.http.HttpVersion.HTTP_1_1) {
      request.headers().set(CONNECTION, CLOSE);
    } else if (options.isKeepAlive() && options.getProtocolVersion() == io.vertx.core.http.HttpVersion.HTTP_1_0) {
      request.headers().set(CONNECTION, KEEP_ALIVE);
    }
    if (end) {
      if (buf != null) {
        request = new AssembledFullHttpRequest(request, buf);
      } else {
        request = new AssembledFullHttpRequest(request);
      }
    } else if (buf != null) {
      request = new AssembledHttpRequest(request, buf);
    }
    return request;
  }

  static CharSequence determineCompressionAcceptEncoding() {
    if (isBrotliAvailable()) {
      return DEFLATE_GZIP_BR;
    } else {
      return DEFLATE_GZIP;
    }
  }

  // Encapsulated in a method, so GraalVM can substitute it
  private static boolean isBrotliAvailable() {
    return Brotli.isAvailable();
  }

  private void beginRequest(Stream stream, HttpRequestHead request, boolean chunked, ByteBuf buf, boolean end, boolean connect, Handler> handler) {
    request.id = stream.id;
    request.remoteAddress = remoteAddress();
    stream.bytesWritten += buf != null ? buf.readableBytes() : 0L;
    HttpRequest nettyRequest = createRequest(request.method, request.uri, request.headers, request.authority, chunked, buf, end);
    synchronized (this) {
      responses.add(stream);
      this.isConnect = connect;
      if (this.metrics != null) {
        stream.metric = this.metrics.requestBegin(request.uri, request);
      }
      VertxTracer tracer = context.tracer();
      if (tracer != null) {
        BiConsumer headers = (key, val) -> new HeadersAdaptor(nettyRequest.headers()).add(key, val);
        String operation = request.traceOperation;
        if (operation == null) {
          operation = request.method.name();
        }
        stream.trace = tracer.sendRequest(stream.context, SpanKind.RPC, options.getTracingPolicy(), request, operation, headers, HttpUtils.CLIENT_HTTP_REQUEST_TAG_EXTRACTOR);
      }
    }
    writeToChannel(nettyRequest, handler == null ? null : context.promise(handler));
    if (end) {
      endRequest(stream);
    }
  }

  private void writeBuffer(Stream s, ByteBuf buff, boolean end, FutureListener listener) {
    s.bytesWritten += buff != null ? buff.readableBytes() : 0L;
    Object msg;
    if (isConnect) {
      msg = buff != null ? buff : Unpooled.EMPTY_BUFFER;
      if (end) {
        writeToChannel(msg, channelFuture()
          .addListener(listener)
          .addListener(v -> close())
        );
      } else {
        writeToChannel(msg);
      }
    } else {
      if (end) {
        if (buff != null && buff.isReadable()) {
          msg = new DefaultLastHttpContent(buff, false);
        } else {
          msg = LastHttpContent.EMPTY_LAST_CONTENT;
        }
      } else {
        msg = new DefaultHttpContent(buff);
      }
      writeToChannel(msg, listener);
      if (end) {
        endRequest(s);
      }
    }
  }

  private void endRequest(Stream s) {
    Stream next;
    boolean checkLifecycle;
    synchronized (this) {
      requests.pop();
      next = requests.peek();
      checkLifecycle = s.responseEnded;
      if (metrics != null) {
        metrics.requestEnd(s.metric, s.bytesWritten);
      }
    }
    flushBytesWritten();
    if (next != null) {
      next.promise.complete((HttpClientStream) next);
    }
    if (checkLifecycle) {
      checkLifecycle();
    }
  }

  /**
   * Resets the given {@code stream}.
   *
   * @param stream to reset
   * @return whether the stream should be considered as closed
   */
  private boolean reset(Stream stream) {
    boolean inflight;
    synchronized (this) {
      inflight = responses.contains(stream) || stream.responseEnded;
      if (!inflight) {
        requests.remove(stream);
      }
      close = inflight;
    }
    checkLifecycle();
    return !inflight;
  }

  private void receiveBytes(int len) {
    boolean le = readWindow <= highWaterMark;
    readWindow += len;
    boolean gt = readWindow > highWaterMark;
    if (le && gt) {
      doPause();
    }
  }

  private void ackBytes(int len) {
    EventLoop eventLoop = context.nettyEventLoop();
    if (eventLoop.inEventLoop()) {
      boolean gt = readWindow > lowWaterMark;
      readWindow -= len;
      boolean le = readWindow <= lowWaterMark;
      if (gt && le) {
        doResume();
      }
    } else {
      eventLoop.execute(() -> ackBytes(len));
    }
  }

  private abstract static class Stream {

    protected final Promise promise;
    protected final ContextInternal context;
    protected final int id;

    private Object trace;
    private Object metric;
    private HttpResponseHead response;
    private boolean responseEnded;
    private long bytesRead;
    private long bytesWritten;

    Stream(ContextInternal context, int id) {
      this.context = context;
      this.id = id;
      this.promise = context.promise();
    }

    // Not really elegant... but well
    Object metric() {
      return metric;
    }

    Object trace() {
      return trace;
    }

    abstract void handleContinue();
    abstract void handleEarlyHints(MultiMap headers);
    abstract void handleHead(HttpResponseHead response);
    abstract void handleChunk(Buffer buff);
    abstract void handleEnd(LastHttpContent trailer);
    abstract void handleWritabilityChanged(boolean writable);
    abstract void handleException(Throwable cause);
    abstract void handleClosed();

  }

  /**
   * We split the stream class in two classes so that the base {@link #Stream} class defines the (mutable)
   * state managed by the connection and this class defines the state managed by the stream implementation
   */
  private static class StreamImpl extends Stream implements HttpClientStream {

    private final Http1xClientConnection conn;
    private final InboundBuffer queue;
    private boolean reset;
    private boolean closed;
    private HttpRequestHead request;
    private Handler headHandler;
    private Handler chunkHandler;
    private Handler endHandler;
    private Handler drainHandler;
    private Handler continueHandler;

    private Handler earlyHintsHandler;
    private Handler exceptionHandler;
    private Handler closeHandler;

    StreamImpl(ContextInternal context, Http1xClientConnection conn, int id) {
      super(context, id);

      this.conn = conn;
      this.queue = new InboundBuffer<>(context, 5)
        .handler(item -> {
          if (!reset) {
            if (item instanceof MultiMap) {
              Handler handler = endHandler;
              if (handler != null) {
                handler.handle((MultiMap) item);
              }
            } else {
              Buffer buffer = (Buffer) item;
              int len = buffer.length();
              conn.ackBytes(len);
              Handler handler = chunkHandler;
              if (handler != null) {
                handler.handle(buffer);
              }
            }
          }
        })
        .exceptionHandler(context::reportException);
    }

    @Override
    public void continueHandler(Handler handler) {
      continueHandler = handler;
    }

    @Override
    public void earlyHintsHandler(Handler handler) {
      earlyHintsHandler = handler;
    }

    @Override
    public StreamImpl drainHandler(Handler handler) {
      drainHandler = handler;
      return this;
    }

    @Override
    public StreamImpl exceptionHandler(Handler handler) {
      exceptionHandler = handler;
      return this;
    }

    @Override
    public WriteStream setWriteQueueMaxSize(int maxSize) {
      return null;
    }

    @Override
    public boolean writeQueueFull() {
      return false;
    }

    @Override
    public void headHandler(Handler handler) {
      this.headHandler = handler;
    }

    @Override
    public void closeHandler(Handler handler) {
      closeHandler = handler;
    }

    @Override
    public void priorityHandler(Handler handler) {
      // No op
    }

    @Override
    public void pushHandler(Handler handler) {
      // No op
    }

    @Override
    public void unknownFrameHandler(Handler handler) {
      // No op
    }

    @Override
    public int id() {
      return id;
    }

    @Override
    public Object metric() {
      return super.metric();
    }

    @Override
    public Object trace() {
      return super.trace();
    }

    @Override
    public HttpVersion version() {
      return conn.version;
    }

    @Override
    public HttpClientConnection connection() {
      return conn;
    }

    @Override
    public ContextInternal getContext() {
      return context;
    }

    @Override
    public void writeHead(HttpRequestHead request, boolean chunked, ByteBuf buf, boolean end, StreamPriority priority, boolean connect, Handler> handler) {
      writeHead(request, chunked, buf, end, connect, handler == null ? null : context.promise(handler));
    }

    private void writeHead(HttpRequestHead request, boolean chunked, ByteBuf buf, boolean end, boolean connect, Handler> handler) {
      EventLoop eventLoop = conn.context.nettyEventLoop();
      if (eventLoop.inEventLoop()) {
        this.request = request;
        conn.beginRequest(this, request, chunked, buf, end, connect, handler);
      } else {
        eventLoop.execute(() -> writeHead(request, chunked, buf, end, connect, handler));
      }
    }

    @Override
    public void writeBuffer(ByteBuf buff, boolean end, Handler> handler) {
      if (buff != null || end) {
        FutureListener listener = handler == null ? null : context.promise(handler);
        writeBuffer(buff, end, listener);
      }
    }

    private void writeBuffer(ByteBuf buff, boolean end, FutureListener listener) {
      FutureListener l;
      if (buff != null) {
        int size = buff.readableBytes();
        l = future -> {
          Handler drain;
          synchronized (conn) {
            conn.writeWindow -= size;
            if (conn.writeOverflow && conn.writeWindow < conn.lowWaterMark) {
              drain = drainHandler;
              conn.writeOverflow = false;
            } else {
              drain = null;
            }
          }
          if (drain != null) {
            context.emit(drain);
          }
          if (listener != null) {
            listener.operationComplete(future);
          }
        };
        synchronized (conn) {
          conn.writeWindow += size;
          if (conn.writeWindow > conn.highWaterMark) {
            conn.writeOverflow = true;
          }
        }
      } else {
        l = listener;
      }
      EventLoop eventLoop = conn.context.nettyEventLoop();
      if (eventLoop.inEventLoop()) {
        conn.writeBuffer(this, buff, end, l);
      } else {
        eventLoop.execute(() -> writeBuffer(buff, end, l));
      }
    }

    @Override
    public void writeFrame(int type, int flags, ByteBuf payload) {
      throw new IllegalStateException("Cannot write an HTTP/2 frame over an HTTP/1.x connection");
    }

    @Override
    public void doSetWriteQueueMaxSize(int size) {
      conn.doSetWriteQueueMaxSize(size);
    }

    @Override
    public boolean isNotWritable() {
      synchronized (conn) {
        return conn.writeWindow > conn.highWaterMark;
      }
    }

    @Override
    public void doPause() {
      queue.pause();
    }

    @Override
    public void doFetch(long amount) {
      queue.fetch(amount);
    }

    @Override
    public void reset(Throwable cause) {
      synchronized (conn) {
        if (reset) {
          return;
        }
        reset = true;
      }
      EventLoop eventLoop = conn.context.nettyEventLoop();
      if (eventLoop.inEventLoop()) {
        _reset(cause);
      } else {
        eventLoop.execute(() -> _reset(cause));
      }
    }

    private void _reset(Throwable cause) {
      boolean removed = conn.reset(this);
      context.execute(cause, this::handleException);
      if (removed) {
        context.execute(this::handleClosed);
      }
    }

    @Override
    public StreamPriority priority() {
      return null;
    }

    @Override
    public void updatePriority(StreamPriority streamPriority) {
    }

    @Override
    void handleWritabilityChanged(boolean writable) {
    }

    void handleContinue() {
      if (continueHandler != null) {
        continueHandler.handle(null);
      }
    }

    void handleEarlyHints(MultiMap headers) {
      if (earlyHintsHandler != null) {
        earlyHintsHandler.handle(headers);
      }
    }

    @Override
    void handleHead(HttpResponseHead response) {
      Handler handler = headHandler;
      if (handler != null) {
        context.emit(response, handler);
      }
    }

    @Override
    public void chunkHandler(Handler handler) {
      chunkHandler = handler;
    }

    @Override
    public void endHandler(Handler handler) {
      endHandler = handler;
    }

    void handleChunk(Buffer buff) {
      queue.write(buff);
    }

    void handleEnd(LastHttpContent trailer) {
      queue.write(new HeadersAdaptor(trailer.trailingHeaders()));
      tryClose();
    }

    void handleException(Throwable cause) {
      if (exceptionHandler != null) {
        exceptionHandler.handle(cause);
      }
    }

    @Override
    void handleClosed() {
      handleException(HttpUtils.CONNECTION_CLOSED_EXCEPTION);
      tryClose();
    }

    /**
     * Attempt to close the stream.
     */
    private void tryClose() {
      if (!closed) {
        closed = true;
        if (closeHandler != null) {
          closeHandler.handle(null);
        }
      }
    }
  }

  private void checkLifecycle() {
    if (close || (shutdown && requests.isEmpty() && responses.isEmpty())) {
      close();
    } else if (!isConnect) {
      expirationTimestamp = expirationTimestampOf(keepAliveTimeout);
    }
  }

  @Override
  public Future close() {
    if (!evicted) {
      evicted = true;
      if (evictionHandler != null) {
        evictionHandler.handle(null);
      }
    }
    return super.close();
  }

  private Throwable validateMessage(Object msg) {
    if (msg instanceof HttpObject) {
      HttpObject obj = (HttpObject) msg;
      DecoderResult result = obj.decoderResult();
      if (result.isFailure()) {
        return result.cause();
      } else if (obj instanceof io.netty.handler.codec.http.HttpResponse) {
        io.netty.handler.codec.http.HttpVersion version = ((com.arangodb.shaded.netty.handler.codec.http.HttpResponse) obj).protocolVersion();
        if (version != io.netty.handler.codec.http.HttpVersion.HTTP_1_0 && version != io.netty.handler.codec.http.HttpVersion.HTTP_1_1) {
          return new IllegalStateException("Unsupported HTTP version: " + version);
        }
      }
    }
    return null;
  }

  public void handleMessage(Object msg) {
    Throwable error = validateMessage(msg);
    if (error != null) {
      ReferenceCountUtil.release(msg);
      fail(error);
    } else if (msg instanceof HttpObject) {
      handleHttpMessage((HttpObject) msg);
    } else if (msg instanceof ByteBuf && isConnect) {
      handleChunk((ByteBuf) msg);
    } else if (msg instanceof WebSocketFrame) {
      handleWsFrame((WebSocketFrame) msg);
    } else {
      invalidMessageHandler.handle(msg);
    }
  }

  private void handleHttpMessage(HttpObject obj) {
    Stream stream;
    synchronized (this) {
      stream = responses.peekFirst();
    }
    if (stream == null) {
      fail(new VertxException("Received HTTP message with no request in progress"));
    } else if (obj instanceof io.netty.handler.codec.http.HttpResponse) {
      io.netty.handler.codec.http.HttpResponse response = (com.arangodb.shaded.netty.handler.codec.http.HttpResponse) obj;
      HttpVersion version;
      if (response.protocolVersion() == io.netty.handler.codec.http.HttpVersion.HTTP_1_0) {
        version = io.vertx.core.http.HttpVersion.HTTP_1_0;
      } else {
        version = io.vertx.core.http.HttpVersion.HTTP_1_1;
      }
      handleResponseBegin(stream, new HttpResponseHead(
        version,
        response.status().code(),
        response.status().reasonPhrase(),
        new HeadersAdaptor(response.headers())));
    } else if (obj instanceof HttpContent) {
      HttpContent chunk = (HttpContent) obj;
      if (chunk.content().isReadable()) {
        handleResponseChunk(stream, chunk.content());
      }
      if (!isConnect && chunk instanceof LastHttpContent) {
        handleResponseEnd(stream, (LastHttpContent) chunk);
      }
    }
  }

  private void handleChunk(ByteBuf chunk) {
    Stream stream;
    synchronized (this) {
      stream = responses.peekFirst();
      if (stream == null) {
        return;
      }
    }
    if (chunk.isReadable()) {
      handleResponseChunk(stream, chunk);
    }
  }

  private void handleResponseBegin(Stream stream, HttpResponseHead response) {
    // How can we handle future undefined 1xx informational response codes?
    if (response.statusCode == HttpResponseStatus.CONTINUE.code()) {
      stream.context.execute(null, v -> stream.handleContinue());
    } else if (response.statusCode == HttpResponseStatus.EARLY_HINTS.code()) {
      stream.context.execute(null, v -> stream.handleEarlyHints(response.headers));
    } else {
      HttpRequestHead request;
      synchronized (this) {
        request = ((StreamImpl)stream).request;
        stream.response = response;

        if (metrics != null) {
          metrics.responseBegin(stream.metric, response);
        }

        //
        if (response.statusCode != 100 && request.method != HttpMethod.CONNECT) {
          // See https://tools.ietf.org/html/rfc7230#section-6.3
          String responseConnectionHeader = response.headers.get(HttpHeaderNames.CONNECTION);
          String requestConnectionHeader = request.headers != null ? request.headers.get(HttpHeaderNames.CONNECTION) : null;
          // We don't need to protect against concurrent changes on forceClose as it only goes from false -> true
          if (HttpHeaderValues.CLOSE.contentEqualsIgnoreCase(responseConnectionHeader) || HttpHeaderValues.CLOSE.contentEqualsIgnoreCase(requestConnectionHeader)) {
            // In all cases, if we have a close connection option then we SHOULD NOT treat the connection as persistent
            this.close = true;
          } else if (response.version == HttpVersion.HTTP_1_0 && !HttpHeaderValues.KEEP_ALIVE.contentEqualsIgnoreCase(responseConnectionHeader)) {
            // In the HTTP/1.0 case both request/response need a keep-alive connection header the connection to be persistent
            // currently Vertx forces the Connection header if keepalive is enabled for 1.0
            this.close = true;
          }
          String keepAliveHeader = response.headers.get(HttpHeaderNames.KEEP_ALIVE);
          if (keepAliveHeader != null) {
            int timeout = HttpUtils.parseKeepAliveHeaderTimeout(keepAliveHeader);
            if (timeout != -1) {
              this.keepAliveTimeout = timeout;
            }
          }
        }
      }

      //
      stream.handleHead(response);

      if (isConnect) {
        if ((request.method == HttpMethod.CONNECT &&
             response.statusCode == 200) || (
             request.method == HttpMethod.GET &&
             request.headers != null && request.headers.contains(CONNECTION, UPGRADE, true) &&
             response.statusCode == 101)) {
          removeChannelHandlers();
        } else {
          isConnect = false;
        }
      }
    }
  }

  /**
   * Remove all HTTP channel handlers of this connection
   *
   * @return the messages emitted by the removed handlers during their removal
   */
  private void removeChannelHandlers() {
    ChannelPipeline pipeline = chctx.pipeline();
    ChannelHandler inflater = pipeline.get(HttpContentDecompressor.class);
    if (inflater != null) {
      pipeline.remove(inflater);
    }
    // removing this codec might fire pending buffers in the HTTP decoder
    // this happens when the channel reads the HTTP response and the following data in a single buffer
    Handler prev = invalidMessageHandler;
    invalidMessageHandler = msg -> {
      ReferenceCountUtil.release(msg);
    };
    try {
      pipeline.remove("codec");
    } finally {
      invalidMessageHandler = prev;
    }
  }

  private void handleResponseChunk(Stream stream, ByteBuf chunk) {
    Buffer buff = Buffer.buffer(VertxHandler.safeBuffer(chunk));
    int len = buff.length();
    receiveBytes(len);
    stream.bytesRead += len;
    stream.context.execute(buff, stream::handleChunk);
  }

  private void handleResponseEnd(Stream stream, LastHttpContent trailer) {
    boolean check;
    synchronized (this) {
      if (stream.response == null) {
        // 100-continue
        return;
      }
      responses.pop();
      close |= !options.isKeepAlive();
      stream.responseEnded = true;
      check = requests.peek() != stream;
    }
    VertxTracer tracer = context.tracer();
    if (tracer != null) {
      tracer.receiveResponse(stream.context, stream.response, stream.trace, null, HttpUtils.CLIENT_RESPONSE_TAG_EXTRACTOR);
    }
    if (metrics != null) {
      metrics.responseEnd(stream.metric, stream.bytesRead);
    }
    flushBytesRead();
    if (check) {
      checkLifecycle();
    }
    lastResponseReceivedTimestamp = System.currentTimeMillis();
    stream.context.execute(trailer, stream::handleEnd);
  }

  public HttpClientMetrics metrics() {
    return client.metrics();
  }

  synchronized void toWebSocket(
    ContextInternal context,
    String requestURI,
    MultiMap headers,
    boolean allowOriginHeader,
    WebsocketVersion vers,
    List subProtocols,
    long handshakeTimeout,
    boolean registerWriteHandlers,
    int maxWebSocketFrameSize,
    Promise promise) {
    try {
      URI wsuri = new URI(requestURI);
      if (!wsuri.isAbsolute()) {
        // Netty requires an absolute url
        wsuri = new URI((ssl ? "https:" : "http:") + "//" + server.host() + ":" + server.port() + requestURI);
      }
      WebSocketVersion version =
         WebSocketVersion.valueOf((vers == null ?
           WebSocketVersion.V13 : vers).toString());
      HttpHeaders nettyHeaders;
      if (headers != null) {
        nettyHeaders = new DefaultHttpHeaders();
        for (Map.Entry entry: headers) {
          nettyHeaders.add(entry.getKey(), entry.getValue());
        }
      } else {
        nettyHeaders = null;
      }

      long timer;
      if (handshakeTimeout > 0L) {
        timer = vertx.setTimer(handshakeTimeout, id -> {
          close();
        });
      } else {
        timer = -1;
      }

      ChannelPipeline p = chctx.channel().pipeline();
      ArrayList extensionHandshakers = initializeWebSocketExtensionHandshakers(client.options());
      if (!extensionHandshakers.isEmpty()) {
        p.addBefore("handler", "webSocketsExtensionsHandler", new WebSocketClientExtensionHandler(
          extensionHandshakers.toArray(new WebSocketClientExtensionHandshaker[0])));
      }

      String subp = null;
      if (subProtocols != null) {
        subp = String.join(",", subProtocols);
      }
      WebSocketClientHandshaker handshaker = newHandshaker(
        wsuri,
        version,
        subp,
        !extensionHandshakers.isEmpty(),
        allowOriginHeader,
        nettyHeaders,
        maxWebSocketFrameSize,
        !options.isSendUnmaskedFrames());

      Handler> webSocketHandshakeComplete = ar -> {
        if (timer > 0L) {
          vertx.cancelTimer(timer);
        }
        if (ar.failed()) {
          close();
          promise.fail(ar.cause());
        } else {
          WebSocketImpl ws = finish(context, version, registerWriteHandlers, handshaker, ar.result());
          webSocket = ws;
          getContext().emit(ws, w -> {
            promise.handle(Future.succeededFuture(w));
            webSocket.headers(null);
          });
        }
      };
      WebSocketHandshakeInboundHandler handshakeInboundHandler = new WebSocketHandshakeInboundHandler(handshaker, webSocketHandshakeComplete);
      p.addBefore("handler", "handshakeCompleter", handshakeInboundHandler);
    } catch (Exception e) {
      handleException(e);
    }
  }

  private WebSocketImpl finish(ContextInternal context,
                               WebSocketVersion version,
                               boolean registerWriteHandlers,
                               WebSocketClientHandshaker handshaker,
                               MultiMap headers) {
    WebSocketImpl ws = new WebSocketImpl(
      context,
      Http1xClientConnection.this,
      version != V00,
      options.getWebSocketClosingTimeout(),
      options.getMaxWebSocketFrameSize(),
      options.getMaxWebSocketMessageSize(),
      registerWriteHandlers);
    ws.subProtocol(handshaker.actualSubprotocol());
    ws.registerHandler(vertx.eventBus());
    log.debug("WebSocket handshake complete");
    HttpClientMetrics metrics = client.metrics();
    if (metrics != null) {
      ws.setMetric(metrics.connected(ws));
    }
    ws.headers(headers);
    return ws;
  }

  static WebSocketClientHandshaker newHandshaker(
    URI webSocketURL, WebSocketVersion version, String subprotocol,
    boolean allowExtensions, boolean allowOriginHeader, HttpHeaders customHeaders, int maxFramePayloadLength,
    boolean performMasking) {
    WebSocketDecoderConfig config = WebSocketDecoderConfig.newBuilder()
      .expectMaskedFrames(false)
      .allowExtensions(allowExtensions)
      .maxFramePayloadLength(maxFramePayloadLength)
      .allowMaskMismatch(false)
      .closeOnProtocolViolation(false)
      .build();
    if (version == V13) {
      return new WebSocketClientHandshaker13(
        webSocketURL, V13, subprotocol, allowExtensions, customHeaders,
        maxFramePayloadLength, performMasking, false, -1) {
        @Override
        protected WebSocketFrameDecoder newWebsocketDecoder() {
          return new WebSocket13FrameDecoder(config);
        }

        @Override
        protected FullHttpRequest newHandshakeRequest() {
          FullHttpRequest request = super.newHandshakeRequest();
          if (!allowOriginHeader) {
            request.headers().remove(ORIGIN);
          }
          return request;
        }
      };
    }
    if (version == V08) {
      return new WebSocketClientHandshaker08(
        webSocketURL, V08, subprotocol, allowExtensions, customHeaders,
        maxFramePayloadLength, performMasking, false, -1) {
        @Override
        protected WebSocketFrameDecoder newWebsocketDecoder() {
          return new WebSocket08FrameDecoder(config);
        }

        @Override
        protected FullHttpRequest newHandshakeRequest() {
          FullHttpRequest request = super.newHandshakeRequest();
          if (!allowOriginHeader) {
            request.headers().remove(HttpHeaderNames.SEC_WEBSOCKET_ORIGIN);
          }
          return request;
        }
      };
    }
    if (version == V07) {
      return new WebSocketClientHandshaker07(
        webSocketURL, V07, subprotocol, allowExtensions, customHeaders,
        maxFramePayloadLength, performMasking, false, -1) {
        @Override
        protected WebSocketFrameDecoder newWebsocketDecoder() {
          return new WebSocket07FrameDecoder(config);
        }

        @Override
        protected FullHttpRequest newHandshakeRequest() {
          FullHttpRequest request = super.newHandshakeRequest();
          if (!allowOriginHeader) {
            request.headers().remove(HttpHeaderNames.SEC_WEBSOCKET_ORIGIN);
          }
          return request;
        }
      };
    }
    if (version == V00) {
      return new WebSocketClientHandshaker00(
        webSocketURL, V00, subprotocol, customHeaders, maxFramePayloadLength, -1) {
        @Override
        protected FullHttpRequest newHandshakeRequest() {
          FullHttpRequest request = super.newHandshakeRequest();
          if (!allowOriginHeader) {
            request.headers().remove(ORIGIN);
          }
          return request;
        }
      };
    }

    throw new WebSocketHandshakeException("Protocol version " + version + " not supported.");
  }

  ArrayList initializeWebSocketExtensionHandshakers(HttpClientOptions options) {
    ArrayList extensionHandshakers = new ArrayList<>();
    if (options.getTryWebSocketDeflateFrameCompression()) {
      extensionHandshakers.add(new DeflateFrameClientExtensionHandshaker(options.getWebSocketCompressionLevel(),
        false));
    }

    if (options.getTryUsePerMessageWebSocketCompression()) {
      extensionHandshakers.add(new PerMessageDeflateClientExtensionHandshaker(options.getWebSocketCompressionLevel(),
        ZlibCodecFactory.isSupportingWindowSizeAndMemLevel(), PerMessageDeflateServerExtensionHandshaker.MAX_WINDOW_SIZE,
        options.getWebSocketCompressionAllowClientNoContext(), options.getWebSocketCompressionRequestServerNoContext()));
    }

    return extensionHandshakers;
  }

  @Override
  public  void handleInterestedOpsChanged() {
    boolean writable = !isNotWritable();
    ContextInternal context;
    Handler handler;
    synchronized (this) {
      Stream current = requests.peek();
      if (current != null) {
        context = current.context;
        handler = current::handleWritabilityChanged;
      } else if (webSocket != null) {
        context = webSocket.context;
        handler = webSocket::handleWritabilityChanged;
      } else {
        return;
      }
    }
    context.execute(writable, handler);
  }

  protected void handleClosed() {
    super.handleClosed();
    long timerID = shutdownTimerID;
    if (timerID != -1) {
      shutdownTimerID = -1L;
      vertx.cancelTimer(timerID);
    }
    closed = true;
    if (metrics != null) {
      HttpClientMetrics met = client.metrics();
      met.endpointDisconnected(metrics);
    }
    if (!evicted) {
      evicted = true;
      if (evictionHandler != null) {
        evictionHandler.handle(null);
      }
    }
    WebSocketImpl ws;
    VertxTracer tracer = context.tracer();
    List allocatedStreams;
    List sentStreams;
    synchronized (this) {
      ws = webSocket;
      sentStreams = new ArrayList<>(responses);
      allocatedStreams = new ArrayList<>(requests);
      allocatedStreams.removeAll(responses);
    }
    if (ws != null) {
      ws.handleConnectionClosed();
    }
    for (Stream stream : allocatedStreams) {
      stream.context.execute(null, v -> stream.handleClosed());
    }
    for (Stream stream : sentStreams) {
      if (metrics != null) {
        metrics.requestReset(stream.metric);
      }
      Object trace = stream.trace;
      if (tracer != null && trace != null) {
        tracer.receiveResponse(stream.context, null, trace, HttpUtils.CONNECTION_CLOSED_EXCEPTION, TagExtractor.empty());
      }
      stream.context.execute(null, v -> stream.handleClosed());
    }
  }

  protected void handleIdle(IdleStateEvent event) {
    synchronized (this) {
      if (webSocket == null && responses.isEmpty() && requests.isEmpty()) {
        return;
      }
    }
    super.handleIdle(event);
  }

  @Override
  protected void handleException(Throwable e) {
    super.handleException(e);
    WebSocketImpl ws;
    LinkedHashSet allStreams = new LinkedHashSet<>();
    synchronized (this) {
      ws = webSocket;
      allStreams.addAll(requests);
      allStreams.addAll(responses);
    }
    if (ws != null) {
      ws.handleException(e);
    }
    for (Stream stream : allStreams) {
      stream.handleException(e);
    }
  }

  @Override
  public void createStream(ContextInternal context, Handler> handler) {
    EventLoop eventLoop = context.nettyEventLoop();
    if (eventLoop.inEventLoop()) {
      StreamImpl stream;
      synchronized (this) {
        if (closed) {
          stream = null;
        } else {
          stream = new StreamImpl(context, this, seq++);
          requests.add(stream);
          if (requests.size() == 1) {
            stream.promise.complete(stream);
          }
        }
      }
      if (stream != null) {
        stream.promise.future().onComplete(handler);
      } else {
        handler.handle(Future.failedFuture(HttpUtils.CONNECTION_CLOSED_EXCEPTION));
      }
    } else {
      eventLoop.execute(() -> {
        createStream(context, handler);
      });
    }
  }

  @Override
  public long lastResponseReceivedTimestamp() {
    return lastResponseReceivedTimestamp;
  }

  @Override
  public boolean isValid() {
    return expirationTimestamp == 0 || System.currentTimeMillis() <= expirationTimestamp;
  }

  @Override
  public void shutdown(long timeout, Handler> handler) {
    shutdown(timeout, vertx.promise(handler));
  }

  @Override
  public Future shutdown(long timeoutMs) {
    PromiseInternal promise = vertx.promise();
    shutdown(timeoutMs, promise);
    return promise.future();
  }

  private synchronized void shutdownNow() {
    shutdownTimerID = -1L;
    close();
  }

  private void shutdown(long timeoutMs, PromiseInternal promise) {
    synchronized (this) {
      if (shutdown) {
        promise.fail("Already shutdown");
        return;
      }
      shutdown = true;
      closeFuture().onComplete(promise);
    }
    synchronized (this) {
      if (!closed) {
        if (timeoutMs > 0L) {
          shutdownTimerID = context.setTimer(timeoutMs, id -> shutdownNow());
        } else {
          close = true;
        }
      }
    }
    checkLifecycle();
  }

  /**
   * Compute the expiration timeout of the connection, relative to the current time.
   *
   * @param timeout the timeout
   * @return the expiration timestamp
   */
  private static long expirationTimestampOf(long timeout) {
    return timeout == 0 ? 0L : System.currentTimeMillis() + timeout * 1000;
  }
}