public Protocol(
      ChannelManager channelManager,
      AsyncHttpClientConfig config,
      NettyAsyncHttpProviderConfig nettyConfig,
      NettyRequestSender requestSender) {
    this.channelManager = channelManager;
    this.config = config;
    this.requestSender = requestSender;
    this.nettyConfig = nettyConfig;

    hasResponseFilters = !config.getResponseFilters().isEmpty();
    hasIOExceptionFilters = !config.getIOExceptionFilters().isEmpty();
    timeConverter = config.getTimeConverter();
  }
  private void addGeneralHeaders(final Request request, final HttpRequestPacket requestPacket) {

    if (request.hasHeaders()) {
      final FluentCaseInsensitiveStringsMap map = request.getHeaders();
      for (final Map.Entry<String, List<String>> entry : map.entrySet()) {
        final String headerName = entry.getKey();
        final List<String> headerValues = entry.getValue();
        if (isNonEmpty(headerValues)) {
          for (int i = 0, len = headerValues.size(); i < len; i++) {
            requestPacket.addHeader(headerName, headerValues.get(i));
          }
        }
      }
    }

    final MimeHeaders headers = requestPacket.getHeaders();
    if (!headers.contains(Header.Connection)) {
      // final boolean canCache = context.provider.clientConfig.getAllowPoolingConnection();
      requestPacket.addHeader(Header.Connection, /*(canCache ? */ "keep-alive" /*: "close")*/);
    }

    if (!headers.contains(Header.Accept)) {
      requestPacket.addHeader(Header.Accept, "*/*");
    }

    if (!headers.contains(Header.UserAgent)) {
      requestPacket.addHeader(Header.UserAgent, config.getUserAgent());
    }
  }
  public NettyAsyncHttpProvider(AsyncHttpClientConfig config) {

    this.config = config;
    NettyAsyncHttpProviderConfig nettyConfig =
        config.getAsyncHttpProviderConfig() instanceof NettyAsyncHttpProviderConfig
            ? //
            (NettyAsyncHttpProviderConfig) config.getAsyncHttpProviderConfig()
            : new NettyAsyncHttpProviderConfig();

    allowStopNettyTimer = nettyConfig.getNettyTimer() == null;
    nettyTimer = allowStopNettyTimer ? newNettyTimer() : nettyConfig.getNettyTimer();

    channelManager = new ChannelManager(config, nettyConfig, nettyTimer);
    requestSender = new NettyRequestSender(config, channelManager, nettyTimer, closed);
    channelManager.configureBootstraps(requestSender, closed);
  }
  private void addCookies(final Request request, final HttpRequestPacket requestPacket) {

    final Collection<Cookie> cookies = request.getCookies();
    if (isNonEmpty(cookies)) {
      StringBuilder sb = new StringBuilder(128);
      org.glassfish.grizzly.http.Cookie[] gCookies =
          new org.glassfish.grizzly.http.Cookie[cookies.size()];
      convertCookies(cookies, gCookies);
      CookieSerializerUtils.serializeClientCookies(
          sb, false, config.isRfc6265CookieEncoding(), gCookies);
      requestPacket.addHeader(Header.Cookie, sb.toString());
    }
  }
  public void close() {
    if (closed.compareAndSet(false, true)) {
      try {
        channelManager.close();

        // FIXME shouldn't close if not allowed
        config.executorService().shutdown();

        if (allowStopNettyTimer) nettyTimer.stop();

      } catch (Throwable t) {
        LOGGER.warn("Unexpected error on close", t);
      }
    }
  }
  @SuppressWarnings({"rawtypes", "unchecked"})
  protected boolean exitAfterProcessingFilters( //
      Channel channel, //
      NettyResponseFuture<?> future, //
      AsyncHandler<?> handler, //
      HttpResponseStatus status, //
      HttpResponseHeaders responseHeaders)
      throws IOException {

    if (hasResponseFilters) {
      FilterContext fc =
          new FilterContext.FilterContextBuilder()
              .asyncHandler(handler)
              .request(future.getRequest())
              .responseStatus(status)
              .responseHeaders(responseHeaders)
              .build();

      for (ResponseFilter asyncFilter : config.getResponseFilters()) {
        try {
          fc = asyncFilter.filter(fc);
          // FIXME Is it worth protecting against this?
          if (fc == null) {
            throw new NullPointerException("FilterContext is null");
          }
        } catch (FilterException efe) {
          requestSender.abort(channel, future, efe);
        }
      }

      // The handler may have been wrapped.
      future.setAsyncHandler(fc.getAsyncHandler());

      // The request has changed
      if (fc.replayRequest()) {
        requestSender.replayRequest(future, fc, channel);
        return true;
      }
    }
    return false;
  }
  protected boolean exitAfterHandlingRedirect( //
      Channel channel, //
      NettyResponseFuture<?> future, //
      HttpResponse response, //
      Request request, //
      int statusCode)
      throws Exception {

    if (followRedirect(config, request) && REDIRECT_STATUSES.contains(statusCode)) {
      if (future.incrementAndGetCurrentRedirectCount() >= config.getMaxRedirects()) {
        throw new MaxRedirectException("Maximum redirect reached: " + config.getMaxRedirects());

      } else {
        // We must allow 401 handling again.
        future.getAndSetAuth(false);

        HttpHeaders responseHeaders = response.headers();
        String location = responseHeaders.get(HttpHeaders.Names.LOCATION);
        Uri uri = Uri.create(future.getUri(), location);

        if (!uri.equals(future.getUri())) {
          final RequestBuilder requestBuilder = new RequestBuilder(future.getRequest());

          if (!config.isRemoveQueryParamOnRedirect())
            requestBuilder.addQueryParams(future.getRequest().getQueryParams());

          // if we are to strictly handle 302, we should keep the original method (which browsers
          // don't)
          // 303 must force GET
          if ((statusCode == FOUND.code() && !config.isStrict302Handling())
              || statusCode == SEE_OTHER.code()) requestBuilder.setMethod("GET");

          // in case of a redirect from HTTP to HTTPS, future attributes might change
          final boolean initialConnectionKeepAlive = future.isKeepAlive();
          final String initialPoolKey = channelManager.getPartitionId(future);

          future.setUri(uri);
          String newUrl = uri.toUrl();
          if (request.getUri().getScheme().startsWith(WEBSOCKET)) {
            newUrl = newUrl.replaceFirst(HTTP, WEBSOCKET);
          }

          logger.debug("Redirecting to {}", newUrl);

          for (String cookieStr : responseHeaders.getAll(HttpHeaders.Names.SET_COOKIE)) {
            Cookie c = CookieDecoder.decode(cookieStr, timeConverter);
            if (c != null) requestBuilder.addOrReplaceCookie(c);
          }

          Callback callback =
              channelManager.newDrainCallback(
                  future, channel, initialConnectionKeepAlive, initialPoolKey);

          if (HttpHeaders.isTransferEncodingChunked(response)) {
            // We must make sure there is no bytes left before
            // executing the next request.
            // FIXME investigate this
            Channels.setAttribute(channel, callback);
          } else {
            // FIXME don't understand: this offers the connection to the pool, or even closes it,
            // while the
            // request has not been sent, right?
            callback.call();
          }

          Request redirectRequest = requestBuilder.setUrl(newUrl).build();
          // FIXME why not reuse the channel is same host?
          requestSender.sendNextRequest(redirectRequest, future);
          return true;
        }
      }
    }
    return false;
  }