Error Handling

KissRequests throws a single exception type for all execution failures: HttpException.

Philosophy

Errors should be actionable. When a request fails, you need to know:

All of this is captured in HttpException.

HttpException

Extends RuntimeException. All execution failures throw this exception.

public class HttpException extends RuntimeException {
    public String method();
    public String url();
    public String curl();
    public List<HttpAttempt> attempts();
    public Duration totalDuration();
    public int statusCode();       // -1 if no response received
    public Map<String, List<String>> responseHeaders();
    public String responseBody();  // null if no response body, truncated to 4KB
    public Throwable rootCause();  // null if status rejection
    public Throwable getCause();   // overridden from RuntimeException
    public String report();        // human-readable multi-line report
}

Basic Usage

try {
    HttpResult result = http.request(HttpMethod.GET, "https://api.example.com/users/999").execute();
} catch (HttpException e) {
    System.err.println("Failed: " + e.getMessage());
    // Failed: HTTP 404 for GET https://api.example.com/users/999 (1 attempt, 234ms)
}

Error Report

The report() method returns a human-readable multi-line report containing all available debug information:

try {
    http.request(HttpMethod.GET, "https://api.example.com/users/999").execute();
} catch (HttpException e) {
    System.err.println(e.report());
    // Output:
    // HTTP 404 for GET https://api.example.com/users/999 (1 attempt, 234ms)
    //   Attempt 1: status=404, duration=234ms - HTTP 404
    //   Curl: curl 'https://api.example.com/users/999'
    //   Response: {"error": "Not found"}
}

Error Categories

HTTP Error (status >= 400)

try {
    http.request(HttpMethod.GET, "https://api.example.com/missing").execute();
} catch (HttpException e) {
    System.out.println(e.statusCode());     // 404
    System.out.println(e.responseHeaders()); // response headers, if available
    System.out.println(e.responseBody());   // {"error": "Not found"}
    System.out.println(e.rootCause());      // null
}

Timeout

try {
    http.request(HttpMethod.GET, "https://api.example.com/slow").execute();
} catch (HttpException e) {
    System.out.println(e.statusCode());     // -1
    System.out.println(e.rootCause());      // HttpTimeoutException
    // Message includes the method, URL, cause message, attempts, and duration.
}

Transport Error (connection refused, DNS failure, etc.)

try {
    http.request(HttpMethod.GET, "https://nonexistent.host.example.com/api").execute();
} catch (HttpException e) {
    System.out.println(e.statusCode());     // -1
    System.out.println(e.rootCause());      // IOException
    // Message includes the method, URL, cause message, attempts, and duration.
}

Unexpected Error

try {
    http.request(HttpMethod.GET, "https://api.example.com/users").execute();
} catch (HttpException e) {
    if (e.rootCause() != null) {
        e.rootCause().printStackTrace();
    }
}

Inspecting Retry Attempts

When retry is configured, you can inspect all attempts:

Http http = Http.builder()
        .retryPolicy(RetryPolicy.of(3, Duration.ofMillis(500)))
        .build();

try {
    http.request(HttpMethod.GET, "https://api.example.com/flaky").execute();
} catch (HttpException e) {
    System.out.println("Total attempts: " + e.attempts().size());
    for (HttpAttempt attempt : e.attempts()) {
        System.out.printf("  Attempt %d: status=%d, duration=%dms, failure=%s%n",
                attempt.attemptNumber(),
                attempt.statusCode(),
                attempt.duration().toMillis(),
                attempt.failureMessage());
    }
    System.out.println("Total duration: " + e.totalDuration().toMillis() + "ms");
    System.out.println("Curl: " + e.curl());
}

Response Body Truncation

Response bodies in exceptions are truncated to 4KB to prevent memory issues:

try {
    http.request(HttpMethod.GET, "https://api.example.com/large-error").execute();
} catch (HttpException e) {
    String body = e.responseBody();
    // body is truncated to 4096 bytes if the response was larger
    // ends with "\n... [truncated]" if truncated
}

Interrupted Threads

If the thread is interrupted during execution, the interrupt flag is restored:

Thread t = new Thread(() -> {
    try {
        http.request(HttpMethod.GET, "https://api.example.com/slow").execute();
    } catch (HttpException e) {
        System.out.println(Thread.currentThread().isInterrupted()); // true
    }
});
t.start();
t.interrupt();

Use HttpMethod constants for standard methods. Raw strings are accepted for custom or uncommon methods.