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

feign.hc5.AsyncApacheHttp5Client Maven / Gradle / Ivy

The newest version!
/*
 * Copyright © 2012 The Feign Authors ([email protected])
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package feign.hc5;

import static feign.Util.enumForName;

import feign.*;
import feign.Request.Options;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.zip.GZIPOutputStream;
import org.apache.hc.client5.http.async.methods.SimpleHttpRequest;
import org.apache.hc.client5.http.async.methods.SimpleHttpResponse;
import org.apache.hc.client5.http.config.Configurable;
import org.apache.hc.client5.http.config.RequestConfig;
import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient;
import org.apache.hc.client5.http.impl.async.HttpAsyncClients;
import org.apache.hc.client5.http.protocol.HttpClientContext;
import org.apache.hc.core5.concurrent.FutureCallback;
import org.apache.hc.core5.http.ContentType;
import org.apache.hc.core5.http.Header;
import org.apache.hc.core5.io.CloseMode;

/**
 * This module directs Feign's http requests to Apache's
 * HttpClient 5. Ex.
 *
 * 
 * GitHub github = Feign.builder().client(new ApacheHttp5Client()).target(GitHub.class,
 * "https://api.github.com");
 */
/*
 */
public final class AsyncApacheHttp5Client implements AsyncClient, AutoCloseable {

  private static final String ACCEPT_HEADER_NAME = "Accept";

  private final CloseableHttpAsyncClient client;

  public AsyncApacheHttp5Client() {
    this(createStartedClient());
  }

  public AsyncApacheHttp5Client(CloseableHttpAsyncClient client) {
    this.client = client;
  }

  private static CloseableHttpAsyncClient createStartedClient() {
    final CloseableHttpAsyncClient client = HttpAsyncClients.custom().build();
    client.start();
    return client;
  }

  @Override
  public CompletableFuture execute(
      Request request, Options options, Optional requestContext) {
    final SimpleHttpRequest httpUriRequest = toClassicHttpRequest(request, options);

    final CompletableFuture result = new CompletableFuture<>();
    final FutureCallback callback =
        new FutureCallback() {

          @Override
          public void completed(SimpleHttpResponse httpResponse) {
            result.complete(toFeignResponse(httpResponse, request));
          }

          @Override
          public void failed(Exception ex) {
            result.completeExceptionally(ex);
          }

          @Override
          public void cancelled() {
            result.cancel(false);
          }
        };

    client.execute(
        httpUriRequest,
        configureTimeoutsAndRedirection(options, requestContext.orElseGet(HttpClientContext::new)),
        callback);

    return result;
  }

  protected HttpClientContext configureTimeoutsAndRedirection(
      Request.Options options, HttpClientContext context) {
    // per request timeouts
    final RequestConfig requestConfig =
        (client instanceof Configurable
                ? RequestConfig.copy(((Configurable) client).getConfig())
                : RequestConfig.custom())
            .setConnectTimeout(options.connectTimeout(), options.connectTimeoutUnit())
            .setResponseTimeout(options.readTimeout(), options.readTimeoutUnit())
            .setRedirectsEnabled(options.isFollowRedirects())
            .build();
    context.setRequestConfig(requestConfig);
    return context;
  }

  SimpleHttpRequest toClassicHttpRequest(Request request, Request.Options options) {
    final SimpleHttpRequest httpRequest =
        new SimpleHttpRequest(request.httpMethod().name(), request.url());

    // request headers
    boolean hasAcceptHeader = false;
    boolean isGzip = false;
    for (final Map.Entry> headerEntry : request.headers().entrySet()) {
      final String headerName = headerEntry.getKey();
      if (headerName.equalsIgnoreCase(ACCEPT_HEADER_NAME)) {
        hasAcceptHeader = true;
      }

      if (headerName.equalsIgnoreCase(Util.CONTENT_LENGTH)) {
        // The 'Content-Length' header is always set by the Apache client and it
        // doesn't like us to set it as well.
        continue;
      }
      if (headerName.equalsIgnoreCase(Util.CONTENT_ENCODING)) {
        isGzip = headerEntry.getValue().stream().anyMatch(Util.ENCODING_GZIP::equalsIgnoreCase);
        boolean isDeflate =
            headerEntry.getValue().stream().anyMatch(Util.ENCODING_DEFLATE::equalsIgnoreCase);
        if (isDeflate) {
          // DeflateCompressingEntity not available in hc5 yet
          throw new IllegalArgumentException(
              "Deflate Content-Encoding is not supported by feign-hc5");
        }
      }

      for (final String headerValue : headerEntry.getValue()) {
        httpRequest.addHeader(headerName, headerValue);
      }
    }
    // some servers choke on the default accept string, so we'll set it to anything
    if (!hasAcceptHeader) {
      httpRequest.addHeader(ACCEPT_HEADER_NAME, "*/*");
    }

    // request body
    // final Body requestBody = request.requestBody();
    byte[] data = request.body();
    if (isGzip && data != null && data.length > 0) {
      // compress if needed
      try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
          GZIPOutputStream gzipOs = new GZIPOutputStream(baos, true)) {
        gzipOs.write(data);
        gzipOs.flush();
        data = baos.toByteArray();
      } catch (IOException suppressed) { // NOPMD
      }
    }
    if (data != null) {
      httpRequest.setBody(data, getContentType(request));
    }

    return httpRequest;
  }

  private ContentType getContentType(Request request) {
    ContentType contentType = null;
    for (final Map.Entry> entry : request.headers().entrySet()) {
      if (entry.getKey().equalsIgnoreCase("Content-Type")) {
        final Collection values = entry.getValue();
        if (values != null && !values.isEmpty()) {
          contentType = ContentType.parse(values.iterator().next());
          if (contentType.getCharset() == null) {
            contentType = contentType.withCharset(request.charset());
          }
          break;
        }
      }
    }
    return contentType;
  }

  Response toFeignResponse(SimpleHttpResponse httpResponse, Request request) {
    final int statusCode = httpResponse.getCode();

    final String reason = httpResponse.getReasonPhrase();

    final Map> headers = new HashMap>();
    for (final Header header : httpResponse.getHeaders()) {
      final String name = header.getName();
      final String value = header.getValue();

      Collection headerValues = headers.get(name);
      if (headerValues == null) {
        headerValues = new ArrayList();
        headers.put(name, headerValues);
      }
      headerValues.add(value);
    }

    return Response.builder()
        .protocolVersion(
            enumForName(Request.ProtocolVersion.class, httpResponse.getVersion().format()))
        .status(statusCode)
        .reason(reason)
        .headers(headers)
        .request(request)
        .body(httpResponse.getBodyBytes())
        .build();
  }

  @Override
  public void close() throws Exception {
    client.close(CloseMode.GRACEFUL);
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy