/* * Copyright 2015 Netflix, Inc. * * 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.httpclient; import org.apache.http.Header; import org.apache.http.HttpEntity; import org.apache.http.HttpResponse; import org.apache.http.NameValuePair; import org.apache.http.StatusLine; import org.apache.http.client.HttpClient; import org.apache.http.client.config.RequestConfig; import org.apache.http.client.methods.HttpUriRequest; import org.apache.http.client.methods.RequestBuilder; import org.apache.http.client.utils.URIBuilder; import org.apache.http.client.utils.URLEncodedUtils; import org.apache.http.entity.ByteArrayEntity; import org.apache.http.entity.ContentType; import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.HttpClientBuilder; import org.apache.http.util.EntityUtils; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.Reader; import java.io.UnsupportedEncodingException; import java.net.MalformedURLException; import java.net.URI; import java.net.URISyntaxException; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; import feign.Client; import feign.Request; import feign.Response; import feign.Util; import static feign.Util.UTF_8; /** * This module directs Feign's http requests to Apache's * <a href="https://hc.apache.org/httpcomponents-client-ga/">HttpClient</a>. Ex. * <pre> * GitHub github = Feign.builder().client(new ApacheHttpClient()).target(GitHub.class, * "https://api.github.com"); */ /* * Based on Square, Inc's Retrofit ApacheClient implementation */ public final class ApacheHttpClient implements Client { private static final String ACCEPT_HEADER_NAME = "Accept"; private final HttpClient client; public ApacheHttpClient() { this(HttpClientBuilder.create().build()); } public ApacheHttpClient(HttpClient client) { this.client = client; } @Override public Response execute(Request request, Request.Options options) throws IOException { HttpUriRequest httpUriRequest; try { httpUriRequest = toHttpUriRequest(request, options); } catch (URISyntaxException e) { throw new IOException("URL '" + request.url() + "' couldn't be parsed into a URI", e); } HttpResponse httpResponse = client.execute(httpUriRequest); return toFeignResponse(httpResponse).toBuilder().request(request).build(); } HttpUriRequest toHttpUriRequest(Request request, Request.Options options) throws UnsupportedEncodingException, MalformedURLException, URISyntaxException { RequestBuilder requestBuilder = RequestBuilder.create(request.method()); //per request timeouts RequestConfig requestConfig = RequestConfig .custom() .setConnectTimeout(options.connectTimeoutMillis()) .setSocketTimeout(options.readTimeoutMillis()) .build(); requestBuilder.setConfig(requestConfig); URI uri = new URIBuilder(request.url()).build(); requestBuilder.setUri(uri.getScheme() + "://" + uri.getAuthority() + uri.getRawPath()); //request query params List<NameValuePair> queryParams = URLEncodedUtils.parse(uri, requestBuilder.getCharset().name()); for (NameValuePair queryParam: queryParams) { requestBuilder.addParameter(queryParam); } //request headers boolean hasAcceptHeader = false; for (Map.Entry<String, Collection<String>> headerEntry : request.headers().entrySet()) { 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; } for (String headerValue : headerEntry.getValue()) { requestBuilder.addHeader(headerName, headerValue); } } //some servers choke on the default accept string, so we'll set it to anything if (!hasAcceptHeader) { requestBuilder.addHeader(ACCEPT_HEADER_NAME, "*/*"); } //request body if (request.body() != null) { HttpEntity entity = null; if (request.charset() != null) { ContentType contentType = getContentType(request); String content = new String(request.body(), request.charset()); entity = new StringEntity(content, contentType); } else { entity = new ByteArrayEntity(request.body()); } requestBuilder.setEntity(entity); } return requestBuilder.build(); } private ContentType getContentType(Request request) { ContentType contentType = ContentType.DEFAULT_TEXT; for (Map.Entry<String, Collection<String>> entry : request.headers().entrySet()) if (entry.getKey().equalsIgnoreCase("Content-Type")) { Collection<String> values = entry.getValue(); if (values != null && !values.isEmpty()) { contentType = ContentType.parse(values.iterator().next()); break; } } return contentType; } Response toFeignResponse(HttpResponse httpResponse) throws IOException { StatusLine statusLine = httpResponse.getStatusLine(); int statusCode = statusLine.getStatusCode(); String reason = statusLine.getReasonPhrase(); Map<String, Collection<String>> headers = new HashMap<String, Collection<String>>(); for (Header header : httpResponse.getAllHeaders()) { String name = header.getName(); String value = header.getValue(); Collection<String> headerValues = headers.get(name); if (headerValues == null) { headerValues = new ArrayList<String>(); headers.put(name, headerValues); } headerValues.add(value); } return Response.builder() .status(statusCode) .reason(reason) .headers(headers) .body(toFeignBody(httpResponse)) .build(); } Response.Body toFeignBody(HttpResponse httpResponse) throws IOException { final HttpEntity entity = httpResponse.getEntity(); if (entity == null) { return null; } return new Response.Body() { @Override public Integer length() { return entity.getContentLength() >= 0 && entity.getContentLength() <= Integer.MAX_VALUE ? (int) entity.getContentLength() : null; } @Override public boolean isRepeatable() { return entity.isRepeatable(); } @Override public InputStream asInputStream() throws IOException { return entity.getContent(); } @Override public Reader asReader() throws IOException { return new InputStreamReader(asInputStream(), UTF_8); } @Override public void close() throws IOException { EntityUtils.consume(entity); } }; } }