Implementación de un Cliente HTTP en Java con Apache HttpClient y Bypass de SSL

Introducción

En el desarrollo de software moderno, interactuar con servicios web mediante APIs REST es una tarea fundamental. A continuación, se presenta la implementación de una clase utilitaria personalizada (ApiRequestExecutor) diseñada para simplificar el envío de peticiones HTTP. Esta solución, construida sobre Apache HttpClient, incluye la capacidad de omitir la validación de certificados HTTPS (ideal para entornos de prueba y desarrollo), gestión de grupos de conexiones (connection pooling) y descarga de recursos binarios.

Dependencias Necesarias

Para utilizar esta implementación, es necesario incluir la biblioteca de Apache HttpClient en el archivo de configuración de Maven:

<dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpclient</artifactId>
    <version>4.5.13</version>
</dependency>

Implementación de la Clase Utilitaria

El siguiente código implementa el cliente HTTP con soporte para múltiples verbos, manejo de payloads JSON, omisión de certificados SSL y trazabilidad mediante logs.

import lombok.extern.slf4j.Slf4j;
import org.apache.http.HttpEntity;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.*;
import org.apache.http.config.Registry;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.conn.socket.PlainConnectionSocketFactory;
import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.util.EntityUtils;
import org.springframework.http.HttpHeaders;

import javax.imageio.ImageIO;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.cert.X509Certificate;
import java.util.Map;

@Slf4j
public class ApiRequestExecutor {

    private static final PoolingHttpClientConnectionManager poolManager;
    private static final CloseableHttpClient restClient;

    static {
        try {
            SSLContext trustAllCtx = buildInsecureSslContext();
            SSLConnectionSocketFactory sslFactory = new SSLConnectionSocketFactory(trustAllCtx, NoopHostnameVerifier.INSTANCE);
            
            Registry<ConnectionSocketFactory> socketRegistry = RegistryBuilder.<ConnectionSocketFactory>create()
                    .register("http", PlainConnectionSocketFactory.getSocketFactory())
                    .register("https", sslFactory)
                    .build();
                    
            poolManager = new PoolingHttpClientConnectionManager(socketRegistry);
            poolManager.setMaxTotal(100);
            poolManager.setDefaultMaxPerRoute(20);
            
            RequestConfig globalConfig = RequestConfig.custom()
                    .setConnectTimeout(5000)
                    .setSocketTimeout(10000)
                    .build();

            restClient = HttpClients.custom()
                    .setConnectionManager(poolManager)
                    .setDefaultRequestConfig(globalConfig)
                    .build();
        } catch (Exception e) {
            throw new RuntimeException("Initialization of HTTP client failed", e);
        }
    }

    public static String executeApiCall(HttpVerb verb, String endpoint, String payload, Map<String, String> customHeaders) throws IOException {
        HttpUriRequest req = buildRequest(verb, endpoint, payload, customHeaders);
        logRequest(req);
        
        try (CloseableHttpResponse res = restClient.execute(req)) {
            return processResponse(res);
        } catch (IOException ex) {
            log.error("Request execution failed: {}", ex.getMessage());
            return null;
        }
    }

    public static void downloadResource(HttpVerb verb, String endpoint, String payload, Map<String, String> customHeaders, String destinationPath, String format) throws IOException {
        HttpUriRequest req = buildRequest(verb, endpoint, payload, customHeaders);
        logRequest(req);
        
        try (CloseableHttpResponse res = restClient.execute(req)) {
            saveImageToFile(res, destinationPath, format);
        } catch (IOException ex) {
            log.error("Resource download failed: {}", ex.getMessage());
        }
    }

    private static String processResponse(CloseableHttpResponse res) throws IOException {
        int status = res.getStatusLine().getStatusCode();
        if (status == 200) {
            return extractAndLogResponse(res);
        }
        log.error("Request failed with HTTP status: {}", status);
        return null;
    }

    private static void logRequest(HttpUriRequest req) {
        log.debug("=== HTTP REQUEST START ===");
        log.debug("Verb: {} | URI: {}", req.getMethod(), req.getURI());
        
        for (org.apache.http.Header h : req.getAllHeaders()) {
            log.debug("Header -> {}: {}", h.getName(), h.getValue());
        }
        
        if (req instanceof HttpEntityEnclosingRequestBase) {
            HttpEntity entity = ((HttpEntityEnclosingRequestBase) req).getEntity();
            if (entity != null) {
                try {
                    log.debug("Payload: {}", EntityUtils.toString(entity, StandardCharsets.UTF_8));
                } catch (IOException e) {
                    log.error("Error reading payload");
                }
            }
        }
        log.debug("=== HTTP REQUEST END ===");
    }

    private static String extractAndLogResponse(CloseableHttpResponse res) throws IOException {
        log.debug("=== HTTP RESPONSE START ===");
        log.debug("Status: {}", res.getStatusLine());
        
        for (org.apache.http.Header h : res.getAllHeaders()) {
            log.debug("Header -> {}: {}", h.getName(), h.getValue());
        }
        
        HttpEntity entity = res.getEntity();
        if (entity != null) {
            String body = EntityUtils.toString(entity, StandardCharsets.UTF_8);
            log.debug("Body: {}", body);
            log.debug("=== HTTP RESPONSE END ===");
            return body;
        }
        log.debug("=== HTTP RESPONSE END (Empty Body) ===");
        return null;
    }

    private static void saveImageToFile(CloseableHttpResponse res, String path, String format) {
        HttpEntity entity = res.getEntity();
        if (entity != null) {
            try {
                byte[] imgData = EntityUtils.toByteArray(entity);
                BufferedImage img = ImageIO.read(new ByteArrayInputStream(imgData));
                File outFile = new File(path);
                
                File parentDir = outFile.getParentFile();
                if (parentDir != null && !parentDir.exists()) {
                    parentDir.mkdirs();
                }
                
                ImageIO.write(img, format, outFile);
                log.info("Image successfully saved to {}", path);
            } catch (IOException e) {
                log.error("Failed to save image", e);
            }
        }
    }

    private static HttpUriRequest buildRequest(HttpVerb verb, String url, String jsonPayload, Map<String, String> headers) {
        HttpUriRequest request;
        switch (verb) {
            case GET: request = new HttpGet(url); break;
            case POST: 
                request = new HttpPost(url); 
                ((HttpEntityEnclosingRequestBase) request).setEntity(new StringEntity(jsonPayload, StandardCharsets.UTF_8));
                break;
            case PUT: 
                request = new HttpPut(url); 
                ((HttpEntityEnclosingRequestBase) request).setEntity(new StringEntity(jsonPayload, StandardCharsets.UTF_8));
                break;
            case DELETE: request = new HttpDelete(url); break;
            default: throw new UnsupportedOperationException("HTTP verb not supported: " + verb);
        }

        request.setHeader(HttpHeaders.ACCEPT, "application/json");
        request.setHeader(HttpHeaders.CONTENT_TYPE, "application/json");

        if (headers != null) {
            headers.forEach(request::setHeader);
        }
        return request;
    }

    private static SSLContext buildInsecureSslContext() throws NoSuchAlgorithmException, KeyManagementException {
        SSLContext ctx = SSLContext.getInstance("TLS");
        ctx.init(null, new TrustManager[]{
            new X509TrustManager() {
                public void checkClientTrusted(X509Certificate[] chain, String authType) {}
                public void checkServerTrusted(X509Certificate[] chain, String authType) {}
                public X509Certificate[] getAcceptedIssuers() { return new X509Certificate[0]; }
            }
        }, new SecureRandom());
        return ctx;
    }

    public enum HttpVerb {
        GET, POST, PUT, DELETE
    }
}

Características Principales

  • Gestión de Seguridad y Conexiones: El método buildInsecureSslContext genera un contexto SSL que acepta cualquier certificado, evitando errores de handshake en entornos locales. Además, se utiliza PoolingHttpClientConnectionManager para reutilizar conexiones TCP, optimizando el rendimiento.
  • Ejecución Flexible de Peticiones: Soporta los verbos HTTP estándar (GET, POST, PUT, DELETE). Permite inyectar payloads en formato JSON y cabeceras personalizadas de manera dinámica.
  • Descarga de Recursos Binarios: La función downloadResource procesa la respuesta como un flujo de bytes, lo que permite guardar imágenes u otros archivos directamente en el sistema de archivos local.
  • Trazabilidad: Se han implementado métodos de logging (logRequest y extractAndLogResponse) que registran verbos, URIs, cabeceras y cuerpos de las peticiones y respuestas, facilitando la depuración.

Ejemplos de Uso

Petición JSON Estándar

String endpoint = "https://api.dominio.com/recursos";
String jsonPayload = "{\"nombre\":\"test\",\"valor\":123}";
Map<String, String> cabeceras = new HashMap<>();
cabeceras.put("Authorization", "Bearer token_de_acceso");

try {
    String respuesta = ApiRequestExecutor.executeApiCall(
        ApiRequestExecutor.HttpVerb.POST, endpoint, jsonPayload, cabeceras
    );
    System.out.println("Respuesta recibida: " + respuesta);
} catch (Exception e) {
    e.printStackTrace();
}

Descarga de Imágenes

String urlImagen = "https://dominio.com/imagenes/captcha.png";
String rutaGuardado = "/tmp/descargas/captcha_local.png";

try {
    ApiRequestExecutor.downloadResource(
        ApiRequestExecutor.HttpVerb.GET, urlImagen, "", null, rutaGuardado, "png"
    );
} catch (Exception e) {
    e.printStackTrace();
}

Extensión de Verbos HTTP

Para añadir soporte a otros verbos HTTP como PATCH o HEAD, simplemente se deben seguir dos pasos:

  1. Agregar el nuevo verbo al enum HttpVerb (por ejemplo, PATCH).
  2. Incluir un nuevo caso en el bloque switch dentro del método buildRequest, instanciando la clase correspondiente de Apache HttpClient (como HttpPatch).

Etiquetas: apache-httpclient java ssl-bypass rest-api connection-pooling

Publicado el 7-2 23:05