Skip to content

jsunsoftware/http-request

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

236 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

A powerful, fluent Java wrapper built on top of Apache HTTP Client 5 that simplifies making REST API calls.

Table of Contents

Overview

The http-request library is designed to create a simple REST client quickly, manage responses easily, and handle exceptions gracefully. It provides a fluent API that makes HTTP requests more readable and maintainable.

The main purpose is to simplify the process of making HTTP requests while providing powerful features for handling responses, managing connections, and processing data, building on top of the robust and performant Apache HttpClient 5.

Features

  • Fluent API: Build and execute HTTP requests with a clean, chainable API.
  • Thread-safe: HttpRequest objects are immutable and thread-safe after building.
  • Simplified Error Handling: All exceptions are wrapped with meaningful status codes.
  • Simplified Resource Management: Automatic closing of responses with ResponseHandler and AutoCloseable client resources.
  • Flexible Response Processing:
    • Convert responses to Java objects automatically.
    • Support for JSON and XML deserialization.
    • Type-safe generic response handling with TypeReference.
  • Connection Management:
    • Simplified connection pooling.
    • Configurable timeouts.
    • Easy SSL configuration.
  • Advanced Features:
    • Flexible retry mechanism for failed requests.
    • Custom response body readers and request body converters.
    • Secure password handling for basic authentication.
    • Request duration monitoring.

Requirements

  • Java 8 or higher
  • Dependencies (automatically managed by build tools):
    • Apache HttpClient 5.x
    • Jackson (for JSON/XML processing)
    • SLF4J (for logging)

Installation

Maven

Add this dependency to your pom.xml:

<dependency>
  <groupId>com.jsunsoft.http</groupId>
  <artifactId>http-request</artifactId>
  <version>3.5.0-rc2</version>
</dependency>

Gradle

Add this dependency to your build.gradle:

implementation 'com.jsunsoft.http:http-request:3.5.0-rc2'

Quick Start

Here's a simple example to get you started:

import com.jsunsoft.http.*;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;

public class QuickStart {
  public static void main(String[] args) {
    // Create an HTTP client and manage its resources with try-with-resources
    try (CloseableHttpClient httpClient = ClientBuilder.create().build()) {

      // Build an HTTP request
      HttpRequest httpRequest = HttpRequestBuilder.create(httpClient).build();

      // Make a GET request and get the response
      try (Response response = httpRequest.target("https://api.example.com/users/1").get()) {
        // Check the status code
        int statusCode = response.getCode();
        System.out.println("Status code: " + statusCode);

        if (response.isSuccess()) {
          // Read the response as a string
          String responseBody = response.readEntity(String.class);
          System.out.println("Response body: " + responseBody);
        }
      }
    }
  }
}

Resource Management

This library simplifies resource management to prevent connection leaks.

Concurrency and Thread Safety

  • HttpRequest: immutable and thread-safe after building; reuse it across threads.
  • WebTarget from target(...): mutable and not thread-safe; don't share between threads.
  • WebTarget from immutableTarget(...): safe to share and reuse across threads.

1. Using ResponseHandler (Recommended for most cases)

When you use methods that return a ResponseHandler<T> (e.g., get(User.class)), the response is consumed and closed automatically. This is the most convenient and safest way to handle responses.

// The response is closed automatically after the operation.
User user = httpRequest.target("https://api.example.com/users/1")
                .get(User.class)
                .orElseThrow();

2. Using Response with try-with-resources

When you need direct access to the Response object, always use a try-with-resources statement to ensure it's closed, even if exceptions occur.

try(Response response = httpRequest.target(uri).get()){
    int statusCode = response.getCode();
    SomeType body = response.readEntity(SomeType.class);
} // response.close() is called automatically

3. Managing the HttpClient

CloseableHttpClient manages the connection pool and should be shared across your application. It's a heavy-weight object. It's crucial to close it when your application shuts down.

// Use try-with-resources to manage the client lifecycle
try(CloseableHttpClient httpClient = ClientBuilder.create().build()){
        // ... use the client
}

Usage Examples

Creating an HttpRequest

The HttpRequestBuilder is used to create and configure HttpRequest instances. HttpRequest objects are immutable and thread-safe, so you can create one and reuse it.

// Create a simple HttpRequest
HttpRequest httpRequest = HttpRequestBuilder.create(httpClient).build();

// Create an HttpRequest with default headers and content type
HttpRequest httpRequest = HttpRequestBuilder.create(httpClient)
        .addDefaultHeader("User-Agent", "MyApp/1.0")
        .addContentType(ContentType.APPLICATION_JSON)
        .build();

// Create an HttpRequest with basic authentication
HttpRequest httpRequest = HttpRequestBuilder.create(httpClient)
        .basicAuth("username", "password")
        .build();

Making Simple Requests

The library provides convenient methods for all standard HTTP verbs.

  • get(), post(), put(), etc., returning a ResponseHandler<T> (eager processing): The response is automatically read and converted.
  • get(), post(), put(), etc., returning a Response (lazy processing): You have direct control over the response stream.
// Eager GET: Convert response body to a User object
ResponseHandler<User> userHandler = httpRequest.target("https://api.example.com/users/1").get(User.class);
User user = userHandler.orElseThrow(); // Throws exception if request was not successful

// Eager POST with a JSON payload
String jsonPayload = "{\"name\":\"John\",\"email\":\"john@example.com\"}";
ResponseHandler<String> postHandler = httpRequest.target("https://api.example.com/users").post(jsonPayload, String.class);
System.out.println("Response: " + postHandler.get());

// Lazy PUT: Get the Response object and handle it manually
try(
    Response putResponse = httpRequest.target("https://api.example.com/users/1").put(jsonPayload)){
            System.out.println("Status code: " + putResponse.getCode());
}

Working with Request Parameters

You can add query parameters to your requests easily.

// Add parameters
Response response = httpRequest.target("https://api.example.com/users")
                .addParameter("page", "1")
                .addParameter("limit", "10")
                .get();

// Add multiple parameters from a map
Map<String, String> params = new HashMap<>();
params.put("limit","10");
params.put("sort","name");

Response response = httpRequest.target("https://api.example.com/users")
        .addParameters(params)
        .get();

// Add parameters from a query string
Response response = httpRequest.target("https://api.example.com/users")
        .addParameters("page=1&limit=10&sort=name")
        .get();

Working with Headers

You can add headers to individual requests or set default headers on the HttpRequestBuilder.

// Add a header to a single request
Response response = httpRequest.target("https://api.example.com/users")
                .addHeader("X-API-Key", "your-api-key")
                .get();

// Add multiple headers
Response response = httpRequest.target("https://api.example.com/users")
        .addHeader("X-API-Key", "your-api-key")
        .addHeader("Accept-Language", "en-US")
        .get();

// Update or remove headers
Response response = httpRequest.target("https://api.example.com/users")
        .addHeader("Accept", "application/json")
        .updateHeader("Accept", "application/xml")
        .removeHeader("X-Temporary")
        .get();

// Set a default header for all requests made with this httpRequest instance
HttpRequest httpRequestWithAuth = HttpRequestBuilder.create(httpClient)
        .addDefaultHeader("Authorization", "Bearer your-token")
        .build();

Handling Responses

The ResponseHandler provides a powerful and fluent way to process responses.

ResponseHandler<User> handler = httpRequest.target("https://api.example.com/users/123").get(User.class);

// Chain success and error handling
handler.ifSuccess(h ->System.out.println("User: "+h.get().getName()))
        .otherwise(h ->System.err.println("Error: "+h.getErrorText()));

// Using filter and conditional processing
handler.filter(ResponseHandler::hasContent).ifPassed(h ->System.out.println("User: "+h.get().getName()))
        .otherwise(h ->System.out.println("No content"));

// Get the result or a default value
User user = handler.orElse(new User("Default"));

// Get the request duration
Duration duration = handler.getDuration();
System.out.println("Request took: "+duration.toMillis() +"ms");

// Status codes: mapped vs original (ResponseHandler only)
int code = handler.getCode(); // May be mapped for failures (e.g., 503/502)
int originalCode = handler.getOriginalCode(); // -1 if no response was received

Error Handling

The library wraps exceptions and provides convenient ways to handle errors.

  • Connection failuresRequestException (often with status code 503)
  • Deserialization failuresResponseBodyProcessingException (often with status code 502)
  • Non-2xx status codesUnexpectedStatusCodeException
try{
// Throws UnexpectedStatusCodeException for non-2xx responses
// Throws MissingResponseBodyException if the body is empty but required
User user = httpRequest.target("https://api.example.com/users/999")
        .get(User.class)
        .requiredGet();
}catch(ResponseException e){
        System.err.println("Response failed: " + e.getMessage());
        
        if(e instanceof UnexpectedStatusCodeException){
            System.err.println("Status code: " + ((UnexpectedStatusCodeException) e).getStatusCode());
        }
}

Working with JSON and XML

The library automatically handles JSON and XML serialization/deserialization using Jackson.

// POST a Java object as JSON
User newUser = new User("John Doe", "john@example.com");
Response response = httpRequest.target("https://api.example.com/users")
        .post(newUser); // Content-Type is automatically handled for Objects

// GET and parse a generic type (List<User>)
List<User> users = httpRequest.target("https://api.example.com/users")
        .get(new TypeReference<List<User>>() {
        })
        .orElseThrow();

// Custom date formats for JSON/XML
HttpRequest httpRequest = HttpRequestBuilder.create(httpClient)
        .addResponseDefaultDateDeserializationPattern(LocalDate.class, "yyyy-MM-dd")
        .addResponseDefaultDateDeserializationPattern(LocalDateTime.class, "yyyy-MM-dd HH:mm:ss")
        .addRequestDefaultDateSerializationPattern(LocalDateTime.class, "yyyy-MM-dd'T'HH:mm:ss")
        .build();

Advanced Features

Connection Pooling

Configure the connection pool using ClientBuilder.

CloseableHttpClient httpClient = ClientBuilder.create()
        .setMaxPoolSize(200) // Total connections
        .setDefaultMaxPoolSizePerRoute(50) // Connections per host
        .setMaxPoolSizePerRoute("api.example.com", 100) // Specific host
        .build();

By default, Apache HttpClient uses small pool limits (for example, 2 per route). When you use ClientBuilder, the defaults are set to 128 for max total and max per route. Override them based on your traffic patterns.

You can also configure a proxy:

CloseableHttpClient httpClient = ClientBuilder.create()
        .proxy("proxy.mycorp.local", 8080)
        .build();

Or customize the underlying HttpClientBuilder:

CloseableHttpClient httpClient = ClientBuilder.create()
        .addHttpClientCustomizer(builder ->
                builder.setKeepAliveStrategy((response, context) -> /* your strategy */ 30_000))
        .build();

Timeouts

Configure various timeouts for your HTTP client.

CloseableHttpClient httpClient = ClientBuilder.create()
        .setConnectTimeout(5000)               // Connection timeout in ms
        .setResponseTimeout(30000)             // Response timeout in ms
        .setConnectionRequestTimeout(30000)    // Time to wait for a pooled connection
        .setSocketTimeout(30000)               // Socket timeout in ms
        .build();

Default timeouts used by ClientBuilder:

  • Response timeout: 30000ms
  • Connection request timeout: 30000ms
  • Connect timeout: 10000ms
  • Socket timeout: 30000ms

Timeouts can be overridden per request:

HttpRequest httpRequest = HttpRequestBuilder.create(httpClient).build();

httpRequest.target(uri)
        .setRequestConfig(customRequestConfig)
        .get();

SSL Configuration

Easily configure SSL settings.

// Trust all certificates (INSECURE: for testing only)
CloseableHttpClient httpClient = ClientBuilder.create()
                .trustAllCertificates()
                .trustAllHosts()
                .build();

Authentication

The library supports basic authentication securely.

// Securely provide password as a char array (will be cleared after use)
HttpRequest httpRequest = HttpRequestBuilder.create(httpClient)
                .basicAuth("user", new char[]{'s', 'e', 'c', 'r', 'e', 't'})
                .build();

Redirects

Configure how redirects are handled. By default, they are disabled.

// Enable a standard redirect strategy
CloseableHttpClient httpClient = ClientBuilder.create()
                .enableDefaultRedirectStrategy()
                .build();

// Enable lax redirect strategy
CloseableHttpClient httpClient = ClientBuilder.create()
        .enableLaxRedirectStrategy()
        .build();

// Set a custom redirect strategy
CloseableHttpClient httpClient = ClientBuilder.create()
        .setRedirectStrategy(customRedirectStrategy)
        .build();

Retry Mechanism

The library offers two ways to handle retries:

1. RetryableWebTarget (Recommended)

This provides fine-grained control over retry logic.

// Retry 3 times with a 2-second delay if the status is 503
RetryContext retryContext = new RetryContext(3, 2000, response -> response.getCode() == 503);

Response response = httpRequest.retryableTarget("https://api.example.com/status/503", retryContext)
        .get();

2. Apache HttpClient's Automatic Retries

Enable the default, less flexible retry mechanism from the underlying client.

CloseableHttpClient httpClient = ClientBuilder.create()
        .enableAutomaticRetries()
        .build();

Custom Response Body Readers

Create custom readers for special response formats.

HttpRequest httpRequest = HttpRequestBuilder.create(httpClient)
        .addBodyReader(new MyCustomReader())
        .build();

You can also implement a ResponseBodyReader directly and (optionally) disable default readers:

HttpRequest httpRequest = HttpRequestBuilder.create(closeableHttpClient)
        // .disableDefaultBodyReader() // Disable defaults if needed
        .addBodyReader(new ResponseBodyReader<Map<String, String>>() {
            @Override
            public boolean isReadable(ResponseBodyReadableContext ctx) {
                return ctx.getType() == Map.class;
            }

            @Override
            public ResponseData read(ResponseBodyReaderContext<Map<String, String>> ctx)
                    throws IOException, ResponseBodyReaderException {
                return new ObjectMapper()
                        .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
                        .readValue(ctx.getContent(), ctx.getGenericType());
            }
        })
        .build();

Limiting Response Body Size

To protect your application from OutOfMemoryErrors caused by unexpectedly large HTTP responses, you can set a maximum size for the response body. This feature is now consistently applied to all response types (String, JSON/XML objects, etc.).

By default, the response body size is unlimited. You can configure a limit on the HttpRequestBuilder.

The limit is enforced by wrapping the response entity as soon as the response is received. If the response size exceeds this limit during reading, an InvalidContentLengthException is thrown. This exception is then wrapped and propagated to you.

  • When using a ResponseHandler, methods like .orElseThrow() will throw a ResponseException. The cause of this exception will be the InvalidContentLengthException.

Important: Even when this limit is exceeded, the underlying HTTP connection is always safely closed and released back to the pool. The library ensures resources are cleaned up correctly to prevent connection leaks.

// Configure a client with a 1KB limit
HttpRequest httpRequest = HttpRequestBuilder.create(httpClient)
                .setMaxResponseBodySizeBytes(1024)
                .build();

// This endpoint returns a 2KB response
server.stubFor(get(urlEqualTo("/large-response"))
        .willReturn(aResponse().withStatus(200).withBody("a".repeat(2048))));

try{
    httpRequest.target(httpUri("/large-response"))
        .get(String.class)
        .orElseThrow();

}catch(ResponseException e){
        System.out.println("Correctly caught expected exception: "+e.getClass().getName());

        // Check the cause to see if it was due to the size limit
        if(e.getCause() instanceof InvalidContentLengthException){
            System.out.println("Cause was InvalidContentLengthException, as expected.");
        // Handle the error appropriately
        }
}

A Note on Connection Handling: The library's closing behavior changes depending on whether a size limit is set.

  • Without a size limit: The library attempts to fully consume the response body before closing. This is a performance optimization that allows the underlying connection to be kept alive and reused for subsequent requests to the same host.
  • With a size limit: The library intentionally does not consume the rest of the body. It immediately closes the connection to avoid downloading potentially huge amounts of unwanted data. While this may prevent the connection from being reused, it guarantees safety and responsiveness, and the connection is always correctly returned to the pool.

Character Encoding

Specify charsets for URI paths/queries and for request bodies.

httpRequest.target("https://api.example.com/search")
    .setUriCharset(StandardCharsets.UTF_8) // For query params
    .setBodyCharset(StandardCharsets.ISO_8859_1) // For request body
    .addParameter("q","你好")
    .post("some-body");

Debugging

Enable request payload logging for easier debugging.

HttpRequest httpRequest = HttpRequestBuilder.create(httpClient)
        .enableRequestPayloadLogging()
        .build();

Why use http-request?

While Apache HttpClient is a powerful and flexible library, http-request provides a higher-level, fluent API that makes common tasks simpler, safer, and more readable.

Feature Pure Apache HttpClient 5 http-request Library
Request Creation Manually create ClassicRequestBuilder, set URI, method, entity. Fluent, chainable API: httpRequest.target(uri).get().
JSON/XML Handling Manually use ObjectMapper to serialize/deserialize entities. Automatic conversion: post(userObject), get(User.class).
Resource Management Manually close CloseableHttpResponse and CloseableHttpClient. Automatic with ResponseHandler or simple with try-with-resources.
Error Handling Check status codes manually, handle IOExceptions. Fluent ifSuccess().otherwise() chains, specific exceptions like UnexpectedStatusCodeException.
Configuration Verbose builders for client, request config, connection manager. Simplified ClientBuilder and HttpRequestBuilder for common settings.
Generics (List<T>) Manually handle TypeReference with ObjectMapper. Built-in support: get(new TypeReference<List<User>>() {}).
Retry Logic Implement a custom HttpRequestRetryStrategy. Simple and powerful RetryableWebTarget with RetryContext.
Readability Can become verbose and procedural. Clean, declarative, and easy to read.

In short, http-request accelerates development, reduces boilerplate, and helps you write more maintainable and robust HTTP client code.

API Documentation

Full API documentation is available here.

Additional documentation:

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'Add some amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. Open a Pull Request

License

This project is licensed under the Apache License 2.0 - see the LICENSE file for details.


Supported by JetBrains

Packages

No packages published

Contributors 2

  •  
  •  

Languages