Implementing a Lightweight HTTP Client Wrapper with Apache HttpComponents
The subsequent implementation transitions from scattered utility functions to a centralized routing architecture. By consolidating network operations into a single dispatcher, duplicate connnection setup and teardown logic are eliminated. The design employs a factory approach to instantiate distinct HTTP verbs based on invocation context, while leveraging standard try-with-resources guarantees to prevent socket leaks during both successful transactions and abrupt network failures.
import org.apache.http.Header;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.*;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.entity.ContentType;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
public class HttpTransportHelper {
private static final RequestConfig SAFE_CONFIG = RequestConfig.custom()
.setConnectTimeout(5000)
.setSocketTimeout(5000)
.build();
public static String sendStandardGet(String address) {
return dispatch(address, null, null, "GET");
}
public static String sendPostWithData(String address, Map<String, String> fields) {
return dispatch(address, null, fields, "POST");
}
public static String sendGetWithQueries(String address, Map<String, String> queries) {
return dispatch(address, queries, null, "GET");
}
private static String dispatch(String url, Map<String, String> queryMap, Map<String, String> payloadMap, String verb) {
String output = "";
try (CloseableHttpClient channel = HttpClients.custom().setDefaultRequestConfig(SAFE_CONFIG).build()) {
URI finalUri = assembleUri(url, queryMap);
HttpUriRequest action = createOperation(verb, finalUri, payloadMap);
try (CloseableHttpResponse response = channel.execute(action)) {
HttpEntity content = response.getEntity();
if (content != null) {
output = EntityUtils.toString(content, "UTF-8");
}
}
} catch (Exception interruption) {
System.err.println("Request failed: " + interruption.getMessage());
}
return output;
}
private static URI assembleUri(String base, Map<String, String> map) throws Exception {
if (map == null || map.isEmpty()) return new URI(base);
URIBuilder builder = new URIBuilder(base);
map.forEach(builder::setParameter);
return builder.build();
}
private static HttpUriRequest createOperation(String command, URI uri, Map<String, String> data) {
if ("POST".equalsIgnoreCase(command)) {
HttpPost request = new HttpPost(uri);
if (data != null && !data.isEmpty()) {
List<BasicNameValuePair> params = new ArrayList<>(data.size());
data.forEach((k, v) -> params.add(new BasicNameValuePair(k, v)));
request.setEntity(new UrlEncodedFormEntity(params, ContentType.TEXT_PLAIN));
}
return request;
}
return new HttpGet(uri);
}
}
Key architectural decisions in this revision include embedding explicit connectivity thresholds to mitigate hanging threads during high-latency scenarios. The URI assembly routine dynamically mutates query string mappings rather than relying on external builders, ensuring predictable parameter injection. Furthermore, response payload extraction is isolated into a dedicated validation layer, decoupling business logic from raw byte-stream conversion. When adapting this pattern to legacy environments, the diamond operator and functional idioms may require explicit type declarations depending on the target compiler specification.