package core.framework.api.http; import core.framework.api.log.ActionLogContext; import core.framework.api.log.Markers; import core.framework.api.util.Charsets; import core.framework.api.util.InputStreams; import core.framework.api.util.Maps; import core.framework.api.util.StopWatch; import core.framework.impl.log.LogParam; import org.apache.http.Header; import org.apache.http.HttpEntity; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpUriRequest; import org.apache.http.impl.client.CloseableHttpClient; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.io.InputStream; import java.io.UncheckedIOException; import java.time.Duration; import java.util.HashMap; import java.util.Map; /** * @author neo */ public final class HTTPClient { private static final Map<Integer, HTTPStatus> HTTP_STATUSES; static { // allow server ssl cert change during renegotiation // http client uses pooled connection, multiple requests to same host may hit different server behind LB System.setProperty("jdk.tls.allowUnsafeServerCertChange", "true"); HTTPStatus[] values = HTTPStatus.values(); HTTP_STATUSES = new HashMap<>(values.length); for (HTTPStatus status : values) { HTTP_STATUSES.put(status.code, status); } } static HTTPStatus parseHTTPStatus(int statusCode) { HTTPStatus status = HTTP_STATUSES.get(statusCode); if (status == null) throw new HTTPClientException("unsupported http status code, code=" + statusCode, "UNKNOWN_HTTP_STATUS_CODE"); return status; } static byte[] responseBody(HttpEntity entity) throws IOException { if (entity == null) return new byte[0]; // for HEAD request, 204/304/205, http client will not create entity try (InputStream stream = entity.getContent()) { int length = (int) entity.getContentLength(); if (length >= 0) { return InputStreams.bytesWithExpectedLength(stream, length); } else { return InputStreams.bytes(stream, 4096); } } } private final Logger logger = LoggerFactory.getLogger(HTTPClient.class); private final CloseableHttpClient client; private final String userAgent; private final long slowOperationThresholdInNanos; HTTPClient(CloseableHttpClient client, String userAgent, Duration slowOperationThreshold) { this.client = client; this.userAgent = userAgent; slowOperationThresholdInNanos = slowOperationThreshold.toNanos(); } public void close() { logger.info("close http client, userAgent={}", userAgent); try { client.close(); } catch (IOException e) { throw new UncheckedIOException(e); } } public HTTPResponse execute(HTTPRequest request) { StopWatch watch = new StopWatch(); HttpUriRequest httpRequest = request.builder.build(); try (CloseableHttpResponse httpResponse = client.execute(httpRequest)) { int statusCode = httpResponse.getStatusLine().getStatusCode(); logger.debug("[response] status={}", statusCode); Map<String, String> headers = Maps.newHashMap(); for (Header header : httpResponse.getAllHeaders()) { logger.debug("[response:header] {}={}", header.getName(), header.getValue()); headers.putIfAbsent(header.getName(), header.getValue()); } HttpEntity entity = httpResponse.getEntity(); byte[] body = responseBody(entity); HTTPResponse response = new HTTPResponse(parseHTTPStatus(statusCode), headers, body); logResponseText(response); return response; } catch (IOException | UncheckedIOException e) { throw new HTTPClientException(e.getMessage(), "HTTP_COMMUNICATION_FAILED", e); } finally { long elapsedTime = watch.elapsedTime(); ActionLogContext.track("http", elapsedTime); logger.debug("execute, elapsedTime={}", elapsedTime); if (elapsedTime > slowOperationThresholdInNanos) { logger.warn(Markers.errorCode("SLOW_HTTP"), "slow http operation, elapsedTime={}", elapsedTime); } } } private void logResponseText(HTTPResponse response) { response.contentType().ifPresent(contentType -> { String mediaType = contentType.mediaType(); if (mediaType.contains("text") || mediaType.contains("json")) { logger.debug("[response] body={}", LogParam.of(response.body(), contentType.charset().orElse(Charsets.UTF_8))); } }); } }