io.vertx.core.http.impl.Http1xClientConnection.java Source code

Java tutorial

Introduction

Here is the source code for io.vertx.core.http.impl.Http1xClientConnection.java

Source

/*
 * Copyright (c) 2011-2017 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 io.vertx.core.http.impl;

import io.netty.buffer.ByteBuf;
import io.netty.channel.*;
import io.netty.handler.codec.DecoderResult;
import io.netty.handler.codec.compression.ZlibCodecFactory;
import io.netty.handler.codec.http.*;
import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.codec.http.websocketx.*;
import io.netty.handler.codec.http.websocketx.WebSocketFrame;
import io.netty.handler.codec.http.websocketx.extensions.WebSocketClientExtensionHandler;
import io.netty.handler.codec.http.websocketx.extensions.WebSocketClientExtensionHandshaker;
import io.netty.handler.codec.http.websocketx.extensions.compression.DeflateFrameClientExtensionHandshaker;
import io.netty.handler.codec.http.websocketx.extensions.compression.PerMessageDeflateClientExtensionHandshaker;
import io.netty.handler.codec.http.websocketx.extensions.compression.PerMessageDeflateServerExtensionHandshaker;
import io.netty.util.ReferenceCountUtil;
import io.vertx.core.*;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.http.*;
import io.vertx.core.http.HttpMethod;
import io.vertx.core.http.HttpVersion;
import io.vertx.core.http.impl.pool.ConnectionListener;
import io.vertx.core.http.impl.ws.WebSocketFrameInternal;
import io.vertx.core.impl.ContextInternal;
import io.vertx.core.logging.Logger;
import io.vertx.core.logging.LoggerFactory;
import io.vertx.core.net.NetSocket;
import io.vertx.core.net.impl.NetSocketImpl;
import io.vertx.core.net.impl.VertxNetHandler;
import io.vertx.core.spi.metrics.HttpClientMetrics;
import io.vertx.core.queue.Queue;

import java.net.URI;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;

import static io.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 <a href="http://tfox.org">Tim Fox</a>
 */
class Http1xClientConnection extends Http1xConnectionBase implements HttpClientConnection {

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

    private final ConnectionListener<HttpClientConnection> listener;
    private final HttpClientImpl client;
    private final HttpClientOptions options;
    private final boolean ssl;
    private final String host;
    private final int port;
    private final Object endpointMetric;
    private final HttpClientMetrics metrics;
    private final HttpVersion version;

    private WebSocketClientHandshaker handshaker;
    private WebSocketImpl ws;
    private boolean closeFrameSent;

    private StreamImpl requestInProgress; // The request being sent
    private StreamImpl responseInProgress; // The request waiting for a response

    private boolean close;
    private boolean upgraded;
    private int keepAliveTimeout;
    private int seq = 1;

    Http1xClientConnection(ConnectionListener<HttpClientConnection> listener, HttpVersion version,
            HttpClientImpl client, Object endpointMetric, ChannelHandlerContext channel, boolean ssl, String host,
            int port, ContextInternal context, HttpClientMetrics metrics) {
        super(client.getVertx(), channel, context);
        this.listener = listener;
        this.client = client;
        this.options = client.getOptions();
        this.ssl = ssl;
        this.host = host;
        this.port = port;
        this.metrics = metrics;
        this.version = version;
        this.endpointMetric = endpointMetric;
        this.keepAliveTimeout = options.getKeepAliveTimeout();
    }

    Object endpointMetric() {
        return endpointMetric;
    }

    ConnectionListener<HttpClientConnection> listener() {
        return listener;
    }

    private synchronized NetSocket upgrade(StreamImpl stream) {
        if (options.isPipelining()) {
            throw new IllegalStateException("Cannot upgrade a pipe-lined request");
        }
        if (upgraded) {
            throw new IllegalStateException("Request already upgraded to NetSocket");
        }
        upgraded = true;

        // connection was upgraded to raw TCP socket
        AtomicBoolean paused = new AtomicBoolean(false);
        NetSocketImpl socket = new NetSocketImpl(vertx, chctx, context, client.getSslHelper(), metrics) {
            {
                super.pause();
            }

            @Override
            public synchronized NetSocket handler(Handler<Buffer> dataHandler) {
                return super.handler(dataHandler);
            }

            @Override
            public synchronized NetSocket pause() {
                paused.set(true);
                return super.pause();
            }

            @Override
            public synchronized NetSocket resume() {
                paused.set(false);
                return super.resume();
            }
        };
        socket.metric(metric());

        // Flush out all pending data
        endReadAndFlush();

        // remove old http handlers and replace the old handler with one that handle plain sockets
        ChannelPipeline pipeline = chctx.pipeline();
        ChannelHandler inflater = pipeline.get(HttpContentDecompressor.class);
        if (inflater != null) {
            pipeline.remove(inflater);
        }
        pipeline.replace("handler", "handler", new VertxNetHandler(socket) {
            @Override
            public void channelRead(ChannelHandlerContext chctx, Object msg) throws Exception {
                if (msg instanceof HttpContent) {
                    if (msg instanceof LastHttpContent) {
                        stream.endResponse((LastHttpContent) msg);
                    }
                    ReferenceCountUtil.release(msg);
                    return;
                }
                super.channelRead(chctx, msg);
            }
        }.removeHandler(sock -> listener.onDiscard()));

        // 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
        pipeline.remove("codec");

        // Async check to deliver the pending messages
        // because the netSocket access in HttpClientResponse is synchronous
        // we need to pause the NetSocket to avoid losing or reordering buffers
        // and then asynchronously un-pause it unless it was actually paused by the application
        context.runOnContext(v -> {
            if (!paused.get()) {
                socket.resume();
            }
        });

        return socket;
    }

    private static class StreamImpl implements HttpClientStream {

        private final int id;
        private final Http1xClientConnection conn;
        private final Future<HttpClientStream> fut;
        private HttpClientRequestImpl request;
        private HttpClientResponseImpl response;
        private boolean requestEnded;
        private boolean responseEnded;
        private boolean reset;
        private Queue<Buffer> queue;
        private MultiMap trailers;
        private StreamImpl next;

        StreamImpl(Http1xClientConnection conn, int id, Handler<AsyncResult<HttpClientStream>> handler) {
            this.conn = conn;
            this.fut = Future.<HttpClientStream>future().setHandler(handler);
            this.id = id;
            this.queue = Queue.queue(conn.context, 0);
        }

        private void append(StreamImpl s) {
            StreamImpl c = this;
            while (c.next != null) {
                c = c.next;
            }
            c.next = s;
        }

        @Override
        public void reportBytesWritten(long numberOfBytes) {
            conn.reportBytesWritten(numberOfBytes);
        }

        @Override
        public void reportBytesRead(long numberOfBytes) {
            conn.reportBytesRead(numberOfBytes);
        }

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

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

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

        @Override
        public Context getContext() {
            return conn.context;
        }

        public void writeHead(HttpMethod method, String rawMethod, String uri, MultiMap headers, String hostHeader,
                boolean chunked, ByteBuf buf, boolean end) {
            HttpRequest request = createRequest(method, rawMethod, uri, headers);
            prepareRequestHeaders(request, hostHeader, chunked);
            sendRequest(request, buf, end);
            if (conn.responseInProgress == null) {
                conn.responseInProgress = this;
            } else {
                conn.responseInProgress.append(this);
            }
            next = null;
        }

        private HttpRequest createRequest(HttpMethod method, String rawMethod, String uri, MultiMap headers) {
            DefaultHttpRequest request = new DefaultHttpRequest(HttpUtils.toNettyHttpVersion(conn.version),
                    HttpUtils.toNettyHttpMethod(method, rawMethod), uri, false);
            if (headers != null) {
                for (Map.Entry<String, String> header : headers) {
                    // Todo : multi valued headers
                    request.headers().add(header.getKey(), header.getValue());
                }
            }
            return request;
        }

        private void prepareRequestHeaders(HttpRequest request, String hostHeader, boolean chunked) {
            HttpHeaders headers = request.headers();
            headers.remove(TRANSFER_ENCODING);
            if (!headers.contains(HOST)) {
                request.headers().set(HOST, hostHeader);
            }
            if (chunked) {
                HttpUtil.setTransferEncodingChunked(request, true);
            }
            if (conn.options.isTryUseCompression() && request.headers().get(ACCEPT_ENCODING) == null) {
                // if compression should be used but nothing is specified by the user support deflate and gzip.
                request.headers().set(ACCEPT_ENCODING, DEFLATE_GZIP);
            }
            if (!conn.options.isKeepAlive()
                    && conn.options.getProtocolVersion() == io.vertx.core.http.HttpVersion.HTTP_1_1) {
                request.headers().set(CONNECTION, CLOSE);
            } else if (conn.options.isKeepAlive()
                    && conn.options.getProtocolVersion() == io.vertx.core.http.HttpVersion.HTTP_1_0) {
                request.headers().set(CONNECTION, KEEP_ALIVE);
            }
        }

        private void sendRequest(HttpRequest request, ByteBuf buf, boolean end) {
            if (end) {
                if (buf != null) {
                    request = new AssembledFullHttpRequest(request, buf);
                } else {
                    request = new AssembledFullHttpRequest(request);
                }
            } else {
                if (buf != null) {
                    request = new AssembledHttpRequest(request, buf);
                }
            }
            conn.writeToChannel(request);
        }

        private boolean handleChunk(Buffer buff) {
            return queue.add(buff);
        }

        @Override
        public void writeBuffer(ByteBuf buff, boolean end) {
            if (end) {
                if (buff != null && buff.isReadable()) {
                    conn.writeToChannel(new DefaultLastHttpContent(buff, false));
                } else {
                    conn.writeToChannel(LastHttpContent.EMPTY_LAST_CONTENT);
                }
            } else if (buff != null) {
                conn.writeToChannel(new DefaultHttpContent(buff));
            }
        }

        @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() {
            return conn.isNotWritable();
        }

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

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

        @Override
        public void doResume() {
            queue.resume();
        }

        @Override
        public void reset(long code) {
            synchronized (conn) {
                if (!reset) {
                    reset = true;
                    if (conn.requestInProgress == this) {
                        if (request == null) {
                            conn.requestInProgress = null;
                            conn.recycle();
                        } else {
                            conn.close();
                        }
                    } else if (!responseEnded) {
                        conn.close();
                    }
                }
            }
        }

        @Override
        public void beginRequest(HttpClientRequestImpl req) {
            synchronized (conn) {
                if (request != null) {
                    throw new IllegalStateException("Already writing a request");
                }
                if (conn.requestInProgress != this) {
                    throw new IllegalStateException("Connection is already writing another request");
                }
                request = req;
                if (conn.metrics != null) {
                    Object reqMetric = conn.metrics.requestBegin(conn.endpointMetric, conn.metric(),
                            conn.localAddress(), conn.remoteAddress(), request);
                    request.metric(reqMetric);
                }
            }
        }

        public void endRequest() {
            boolean doRecycle;
            synchronized (conn) {
                StreamImpl s = conn.requestInProgress;
                if (s != this) {
                    throw new IllegalStateException("No write in progress");
                }
                if (requestEnded) {
                    throw new IllegalStateException("Request already sent");
                }
                requestEnded = true;
                if (conn.metrics != null) {
                    conn.metrics.requestEnd(request.metric());
                }
                doRecycle = responseEnded;
            }
            conn.handleRequestEnd(doRecycle);
        }

        @Override
        public NetSocket createNetSocket() {
            synchronized (conn) {
                if (responseEnded) {
                    throw new IllegalStateException("Response already ended");
                }
                return conn.upgrade(this);
            }
        }

        private HttpClientResponseImpl beginResponse(HttpResponse resp) {
            HttpVersion version;
            if (resp.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;
            }
            response = new HttpClientResponseImpl(request, version, this, resp.status().code(),
                    resp.status().reasonPhrase(), new HeadersAdaptor(resp.headers()));
            if (conn.metrics != null) {
                conn.metrics.responseBegin(request.metric(), response);
            }
            if (resp.status().code() != 100 && request.method() != io.vertx.core.http.HttpMethod.CONNECT) {
                // See https://tools.ietf.org/html/rfc7230#section-6.3
                String responseConnectionHeader = resp.headers().get(HttpHeaderNames.CONNECTION);
                io.netty.handler.codec.http.HttpVersion protocolVersion = resp.protocolVersion();
                String requestConnectionHeader = request.headers().get(HttpHeaderNames.CONNECTION);
                // 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
                    conn.close = true;
                } else if (protocolVersion == io.netty.handler.codec.http.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
                    conn.close = true;
                }
                String keepAliveHeader = resp.headers().get(HttpHeaderNames.KEEP_ALIVE);
                if (keepAliveHeader != null) {
                    int timeout = HttpUtils.parseKeepAliveHeaderTimeout(keepAliveHeader);
                    if (timeout != -1) {
                        conn.keepAliveTimeout = timeout;
                    }
                }
            }
            queue.handler(buf -> response.handleChunk(buf));
            queue.emptyHandler(v -> {
                if (responseEnded) {
                    response.handleEnd(trailers);
                }
            });
            queue.writableHandler(v -> {
                if (!responseEnded) {
                    conn.doResume();
                }
            });
            return response;
        }

        private boolean endResponse(LastHttpContent trailer) {
            synchronized (conn) {
                if (conn.metrics != null) {
                    HttpClientRequestBase req = request;
                    Object reqMetric = req.metric();
                    if (req.exceptionOccurred != null) {
                        conn.metrics.requestReset(reqMetric);
                    } else {
                        conn.metrics.responseEnd(reqMetric, response);
                    }
                }
                trailers = new HeadersAdaptor(trailer.trailingHeaders());
                if (queue.isEmpty()) {
                    response.handleEnd(trailers);
                }
                responseEnded = true;
                conn.close |= !conn.options.isKeepAlive();
                conn.doResume();
                return requestEnded;
            }
        }

        void handleException(Throwable cause) {
            synchronized (conn) {
                if (request != null) {
                    if (response == null) {
                        request.handleException(cause);
                    } else {
                        if (!requestEnded) {
                            request.handleException(cause);
                        }
                        response.handleException(cause);
                    }
                } else {
                    fut.tryFail(cause);
                }
            }
        }
    }

    private void checkLifecycle() {
        if (upgraded) {
            // Do nothing
        } else if (close) {
            close();
        } else {
            recycle();
        }
    }

    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 HttpResponse) {
                io.netty.handler.codec.http.HttpVersion version = ((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) {
            fail(error);
        } else if (msg instanceof HttpObject) {
            HttpObject obj = (HttpObject) msg;
            handleHttpMessage(obj);
        } else if (msg instanceof WebSocketFrame) {
            WebSocketFrameInternal frame = decodeFrame((WebSocketFrame) msg);
            switch (frame.type()) {
            case BINARY:
            case CONTINUATION:
            case TEXT:
            case PONG:
                handleWsFrame(frame);
                break;
            case PING:
                // Echo back the content of the PING frame as PONG frame as specified in RFC 6455 Section 5.5.2
                chctx.writeAndFlush(new PongWebSocketFrame(frame.getBinaryData().copy()));
                break;
            case CLOSE:
                handleWsFrame(frame);
                if (!closeFrameSent) {
                    // Echo back close frame and close the connection once it was written.
                    // This is specified in the WebSockets RFC 6455 Section  5.4.1
                    CloseWebSocketFrame closeFrame = new CloseWebSocketFrame(frame.closeStatusCode(),
                            frame.closeReason());
                    chctx.writeAndFlush(closeFrame).addListener(ChannelFutureListener.CLOSE);
                    closeFrameSent = true;
                }
                break;
            default:
                throw new IllegalStateException("Invalid type: " + frame.type());
            }
        } else {
            throw new IllegalStateException("Invalid object " + msg);
        }
    }

    private void handleHttpMessage(HttpObject obj) {
        if (obj instanceof HttpResponse) {
            handleResponseBegin((HttpResponse) obj);
        } else if (obj instanceof HttpContent) {
            HttpContent chunk = (HttpContent) obj;
            if (chunk.content().isReadable()) {
                Buffer buff = Buffer.buffer(VertxHttpHandler.safeBuffer(chunk.content(), chctx.alloc()));
                handleResponseChunk(buff);
            }
            if (chunk instanceof LastHttpContent) {
                handleResponseEnd((LastHttpContent) chunk);
            }
        }
    }

    private void handleResponseBegin(HttpResponse resp) {
        StreamImpl stream;
        HttpClientResponseImpl response;
        HttpClientRequestImpl request;
        Throwable err;
        synchronized (this) {
            stream = responseInProgress;
            request = stream.request;
            try {
                response = stream.beginResponse(resp);
                err = null;
            } catch (Exception e) {
                err = e;
                response = null;
            }
        }
        if (response != null) {
            request.handleResponse(response);
        } else {
            request.handleException(err);
        }
    }

    private void handleResponseChunk(Buffer buff) {
        StreamImpl resp;
        synchronized (this) {
            resp = responseInProgress;
        }
        if (resp != null) {
            if (!resp.handleChunk(buff)) {
                doPause();
            }
        }
    }

    private void handleResponseEnd(LastHttpContent trailer) {
        StreamImpl stream;
        synchronized (this) {
            stream = responseInProgress;
            // We don't signal response end for a 100-continue response as a real response will follow
            if (stream.response == null || stream.response.statusCode() != 100) {
                responseInProgress = responseInProgress.next;
            }
        }
        if (stream.endResponse(trailer)) {
            checkLifecycle();
        }
    }

    private void handleRequestEnd(boolean recycle) {
        StreamImpl next;
        synchronized (this) {
            next = requestInProgress.next;
            requestInProgress = next;
        }
        if (recycle) {
            checkLifecycle();
        }
        if (next != null) {
            next.fut.complete(next);
        }
    }

    public HttpClientMetrics metrics() {
        return metrics;
    }

    synchronized void toWebSocket(String requestURI, MultiMap headers, WebsocketVersion vers, String subProtocols,
            int maxWebSocketFrameSize, Handler<WebSocket> wsConnect) {
        if (ws != null) {
            throw new IllegalStateException("Already websocket");
        }

        try {
            URI wsuri = new URI(requestURI);
            if (!wsuri.isAbsolute()) {
                // Netty requires an absolute url
                wsuri = new URI((ssl ? "https:" : "http:") + "//" + host + ":" + port + requestURI);
            }
            WebSocketVersion version = WebSocketVersion
                    .valueOf((vers == null ? WebSocketVersion.V13 : vers).toString());
            HttpHeaders nettyHeaders;
            if (headers != null) {
                nettyHeaders = new DefaultHttpHeaders();
                for (Map.Entry<String, String> entry : headers) {
                    nettyHeaders.add(entry.getKey(), entry.getValue());
                }
            } else {
                nettyHeaders = null;
            }

            ChannelPipeline p = chctx.channel().pipeline();
            ArrayList<WebSocketClientExtensionHandshaker> extensionHandshakers = initializeWebsocketExtensionHandshakers(
                    client.getOptions());
            if (!extensionHandshakers.isEmpty()) {
                p.addBefore("handler", "websocketsExtensionsHandler",
                        new WebSocketClientExtensionHandler(extensionHandshakers
                                .toArray(new WebSocketClientExtensionHandshaker[extensionHandshakers.size()])));
            }

            handshaker = WebSocketClientHandshakerFactory.newHandshaker(wsuri, version, subProtocols,
                    !extensionHandshakers.isEmpty(), nettyHeaders, maxWebSocketFrameSize,
                    !options.isSendUnmaskedFrames(), false);

            p.addBefore("handler", "handshakeCompleter",
                    new HandshakeInboundHandler(wsConnect, version != WebSocketVersion.V00));
            handshaker.handshake(chctx.channel()).addListener(future -> {
                Handler<Throwable> handler = exceptionHandler();
                if (!future.isSuccess() && handler != null) {
                    handler.handle(future.cause());
                }
            });
        } catch (Exception e) {
            handleException(e);
        }
    }

    ArrayList<WebSocketClientExtensionHandshaker> initializeWebsocketExtensionHandshakers(
            HttpClientOptions options) {
        ArrayList<WebSocketClientExtensionHandshaker> extensionHandshakers = new ArrayList<WebSocketClientExtensionHandshaker>();
        if (options.tryWebsocketDeflateFrameCompression()) {
            extensionHandshakers
                    .add(new DeflateFrameClientExtensionHandshaker(options.websocketCompressionLevel(), false));
        }

        if (options.tryUsePerMessageWebsocketCompression()) {
            extensionHandshakers.add(new PerMessageDeflateClientExtensionHandshaker(
                    options.websocketCompressionLevel(), ZlibCodecFactory.isSupportingWindowSizeAndMemLevel(),
                    PerMessageDeflateServerExtensionHandshaker.MAX_WINDOW_SIZE,
                    options.getWebsocketCompressionAllowClientNoContext(),
                    options.getWebsocketCompressionRequestServerNoContext()));
        }

        return extensionHandshakers;
    }

    private final class HandshakeInboundHandler extends ChannelInboundHandlerAdapter {

        private final boolean supportsContinuation;
        private final Handler<WebSocket> wsConnect;
        private final ContextInternal context;
        private final Deque<Object> buffered = new ArrayDeque<>();
        private FullHttpResponse response;
        private boolean handshaking = true;

        public HandshakeInboundHandler(Handler<WebSocket> wsConnect, boolean supportsContinuation) {
            this.supportsContinuation = supportsContinuation;
            this.wsConnect = wsConnect;
            this.context = vertx.getContext();
        }

        @Override
        public void channelInactive(ChannelHandlerContext ctx) throws Exception {
            super.channelInactive(ctx);
            // if still handshaking this means we not got any response back from the server and so need to notify the client
            // about it as otherwise the client would never been notified.
            if (handshaking) {
                handleException(new WebSocketHandshakeException("Connection closed while handshake in process"));
            }
        }

        @Override
        public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
            if (handshaker != null && handshaking) {
                if (msg instanceof HttpResponse) {
                    HttpResponse resp = (HttpResponse) msg;
                    HttpResponseStatus status = resp.status();
                    if (status.code() != 101) {
                        handshaker = null;
                        close();
                        handleException(new WebsocketRejectedException(status.code()));
                        return;
                    }
                    response = new DefaultFullHttpResponse(resp.protocolVersion(), status);
                    response.headers().add(resp.headers());
                }

                if (msg instanceof HttpContent) {
                    if (response != null) {
                        response.content().writeBytes(((HttpContent) msg).content());
                        if (msg instanceof LastHttpContent) {
                            response.trailingHeaders().add(((LastHttpContent) msg).trailingHeaders());
                            try {
                                handshakeComplete(ctx, response);
                                chctx.pipeline().remove(HandshakeInboundHandler.this);
                                for (;;) {
                                    Object m = buffered.poll();
                                    if (m == null) {
                                        break;
                                    }
                                    ctx.fireChannelRead(m);
                                }
                            } catch (WebSocketHandshakeException e) {
                                close();
                                handleException(e);
                            }
                        }
                    }
                }
            } else {
                buffered.add(msg);
            }
        }

        private void handleException(Exception e) {
            handshaking = false;
            buffered.clear();
            Handler<Throwable> handler = exceptionHandler();
            if (handler != null) {
                context.executeFromIO(v -> {
                    handler.handle(e);
                });
            } else {
                log.error("Error in websocket handshake", e);
            }
        }

        private void handshakeComplete(ChannelHandlerContext ctx, FullHttpResponse response) {
            handshaking = false;
            ChannelHandler handler = ctx.pipeline().get(HttpContentDecompressor.class);
            if (handler != null) {
                // remove decompressor as its not needed anymore once connection was upgraded to websockets
                ctx.pipeline().remove(handler);
            }
            WebSocketImpl webSocket = new WebSocketImpl(vertx, Http1xClientConnection.this, supportsContinuation,
                    options.getMaxWebsocketFrameSize(), options.getMaxWebsocketMessageSize());
            ws = webSocket;
            handshaker.finishHandshake(chctx.channel(), response);
            ws.subProtocol(handshaker.actualSubprotocol());
            context.executeFromIO(v -> {
                log.debug("WebSocket handshake complete");
                if (metrics != null) {
                    webSocket.setMetric(metrics.connected(endpointMetric, metric(), webSocket));
                }
                webSocket.registerHandler(vertx.eventBus());
                wsConnect.handle(webSocket);
            });
        }
    }

    @Override
    public synchronized void handleInterestedOpsChanged() {
        if (!isNotWritable()) {
            if (requestInProgress != null) {
                requestInProgress.request.handleDrained();
            } else if (ws != null) {
                ws.writable();
            }
        }
    }

    synchronized void handleWsFrame(WebSocketFrameInternal frame) {
        if (ws != null) {
            ws.handleFrame(frame);
        }
    }

    protected synchronized void handleClosed() {
        super.handleClosed();
        if (ws != null) {
            ws.handleClosed();
        }
        if (metrics != null) {
            for (StreamImpl r = responseInProgress; r != null; r = r.next) {
                metrics.requestReset(r.request.metric());
            }
        }
        failStreams(CLOSED_EXCEPTION);
    }

    private void failStreams(Throwable cause) {
        StreamImpl stream;
        for (StreamImpl r = responseInProgress; r != null; r = r.next) {
            r.handleException(cause);
        }
    }

    @Override
    protected synchronized void handleException(Throwable e) {
        super.handleException(e);
        for (StreamImpl r = responseInProgress; r != null; r = r.next) {
            r.handleException(e);
        }
    }

    @Override
    public synchronized void close() {
        closeWithPayload(null);
    }

    @Override
    public void closeWithPayload(ByteBuf byteBuf) {
        if (handshaker == null) {
            super.close();
        } else {
            // make sure everything is flushed out on close
            endReadAndFlush();
            // close the websocket connection by sending a close frame with specified payload.
            CloseWebSocketFrame closeFrame;
            if (byteBuf != null) {
                closeFrame = new CloseWebSocketFrame(true, 0, byteBuf);
            } else {
                closeFrame = new CloseWebSocketFrame(true, 0, 1000, null);
            }
            handshaker.close(chctx.channel(), closeFrame);
        }
    }

    @Override
    public void createStream(Handler<AsyncResult<HttpClientStream>> handler) {
        StreamImpl stream;
        synchronized (this) {
            stream = new StreamImpl(this, seq++, handler);
            if (requestInProgress != null) {
                requestInProgress.append(stream);
                return;
            }
            requestInProgress = stream;
        }
        stream.fut.complete(stream);
    }

    private void recycle() {
        long expiration = keepAliveTimeout == 0 ? 0L : System.currentTimeMillis() + keepAliveTimeout * 1000;
        listener.onRecycle(expiration);
    }
}