CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/maven-dev-langchain4j--langchain4j-http-client

HTTP client abstraction for LangChain4j with synchronous/asynchronous execution and Server-Sent Events (SSE) streaming support

Overview
Eval results
Files

error-handling.mddocs/guides/

Error Handling Guide

This guide covers error handling strategies for both synchronous and asynchronous HTTP requests.

Exception Types

HttpException

Thrown when the server returns a client error (4XX) or server error (5XX) status code.

import dev.langchain4j.exception.HttpException;

try {
    SuccessfulHttpResponse response = client.execute(request);
    System.out.println("Success: " + response.body());
} catch (HttpException e) {
    // Handle 4XX/5XX errors
    System.err.println("HTTP error: " + e.getMessage());
}

When thrown:

  • 4XX client errors (400, 401, 403, 404, etc.)
  • 5XX server errors (500, 502, 503, etc.)

RuntimeException

Thrown for unexpected errors during request execution.

try {
    SuccessfulHttpResponse response = client.execute(request);
    System.out.println("Success: " + response.body());
} catch (RuntimeException e) {
    // Handle network errors, timeouts, etc.
    System.err.println("Request failed: " + e.getMessage());
}

When thrown:

  • Network connectivity issues
  • Connection timeout (exceeds connectTimeout)
  • Read timeout (exceeds readTimeout)
  • I/O errors
  • Invalid request configuration

Synchronous Request Error Handling

Basic Error Handling

import dev.langchain4j.exception.HttpException;

try {
    SuccessfulHttpResponse response = client.execute(request);
    System.out.println("Success: " + response.body());
} catch (HttpException e) {
    System.err.println("HTTP error: " + e.getMessage());
} catch (RuntimeException e) {
    System.err.println("Request failed: " + e.getMessage());
}

Handling Specific HTTP Status Codes

try {
    SuccessfulHttpResponse response = client.execute(request);
    System.out.println("Success: " + response.body());
} catch (HttpException e) {
    String message = e.getMessage();

    if (message.contains("401") || message.contains("403")) {
        System.err.println("Authentication failed");
    } else if (message.contains("404")) {
        System.err.println("Resource not found");
    } else if (message.contains("429")) {
        System.err.println("Rate limit exceeded");
    } else if (message.contains("500") || message.contains("502") || message.contains("503")) {
        System.err.println("Server error - retry later");
    } else {
        System.err.println("HTTP error: " + message);
    }
} catch (RuntimeException e) {
    System.err.println("Request failed: " + e.getMessage());
}

Timeout Errors

import java.net.SocketTimeoutException;

try {
    SuccessfulHttpResponse response = client.execute(request);
    System.out.println("Success: " + response.body());
} catch (RuntimeException e) {
    if (e.getCause() instanceof SocketTimeoutException) {
        System.err.println("Request timed out");
        // Consider retrying or using longer timeout
    } else {
        System.err.println("Request failed: " + e.getMessage());
    }
}

Asynchronous SSE Error Handling

Basic SSE Error Handling

import dev.langchain4j.http.client.sse.*;

client.execute(request, new ServerSentEventListener() {
    @Override
    public void onEvent(ServerSentEvent event) {
        System.out.println("Received: " + event.data());
    }

    @Override
    public void onError(Throwable throwable) {
        System.err.println("Stream error: " + throwable.getMessage());
        throwable.printStackTrace();
    }

    @Override
    public void onClose() {
        System.out.println("Stream closed");
    }
});

SSE Error Types

client.execute(request, new ServerSentEventListener() {
    @Override
    public void onEvent(ServerSentEvent event) {
        System.out.println("Received: " + event.data());
    }

    @Override
    public void onError(Throwable throwable) {
        if (throwable instanceof HttpException) {
            System.err.println("HTTP error in stream: " + throwable.getMessage());
        } else if (throwable.getCause() instanceof SocketTimeoutException) {
            System.err.println("Stream timed out");
        } else if (throwable instanceof IOException) {
            System.err.println("I/O error: " + throwable.getMessage());
        } else {
            System.err.println("Unknown error: " + throwable.getMessage());
        }
    }

    @Override
    public void onClose() {
        System.out.println("Stream closed");
    }
});

Preventing Listener Exceptions from Terminating Stream

Important: If any method of the ServerSentEventListener throws an exception, the stream processing will be terminated immediately.

client.execute(request, new ServerSentEventListener() {
    @Override
    public void onEvent(ServerSentEvent event) {
        try {
            // Process event - may throw exceptions
            processEvent(event);
        } catch (Exception e) {
            // Handle exception without propagating
            System.err.println("Error processing event: " + e.getMessage());
            // Stream continues
        }
    }

    @Override
    public void onError(Throwable throwable) {
        System.err.println("Stream error: " + throwable.getMessage());
    }
});

Retry Strategies

Simple Retry

public SuccessfulHttpResponse executeWithRetry(HttpRequest request, int maxRetries) {
    int attempt = 0;
    while (attempt < maxRetries) {
        try {
            return client.execute(request);
        } catch (RuntimeException e) {
            attempt++;
            if (attempt >= maxRetries) {
                throw e;  // Re-throw on final attempt
            }
            System.err.println("Attempt " + attempt + " failed, retrying...");
            try {
                Thread.sleep(1000 * attempt);  // Exponential backoff
            } catch (InterruptedException ie) {
                Thread.currentThread().interrupt();
                throw new RuntimeException("Interrupted during retry", ie);
            }
        }
    }
    throw new RuntimeException("Should not reach here");
}

Exponential Backoff with Jitter

public SuccessfulHttpResponse executeWithBackoff(HttpRequest request, int maxRetries) {
    int attempt = 0;
    Random random = new Random();

    while (attempt < maxRetries) {
        try {
            return client.execute(request);
        } catch (HttpException e) {
            // Don't retry 4XX client errors
            if (e.getMessage().contains("4")) {
                throw e;
            }
            attempt++;
            if (attempt >= maxRetries) {
                throw e;
            }
        } catch (RuntimeException e) {
            attempt++;
            if (attempt >= maxRetries) {
                throw e;
            }
        }

        // Exponential backoff with jitter
        long baseDelay = 1000L * (1L << attempt);  // 1s, 2s, 4s, 8s...
        long jitter = random.nextInt(1000);
        long delay = baseDelay + jitter;

        System.err.println("Attempt " + attempt + " failed, retrying in " + delay + "ms");

        try {
            Thread.sleep(delay);
        } catch (InterruptedException ie) {
            Thread.currentThread().interrupt();
            throw new RuntimeException("Interrupted during retry", ie);
        }
    }
    throw new RuntimeException("Should not reach here");
}

Retry with Different Timeout

public SuccessfulHttpResponse executeWithTimeoutFallback(HttpRequest request) {
    HttpClient normalClient = HttpClientBuilderLoader.loadHttpClientBuilder()
        .connectTimeout(Duration.ofSeconds(10))
        .readTimeout(Duration.ofSeconds(30))
        .build();

    HttpClient lenientClient = HttpClientBuilderLoader.loadHttpClientBuilder()
        .connectTimeout(Duration.ofSeconds(30))
        .readTimeout(Duration.ofMinutes(2))
        .build();

    try {
        return normalClient.execute(request);
    } catch (RuntimeException e) {
        if (e.getCause() instanceof SocketTimeoutException) {
            System.err.println("Timeout with normal client, retrying with lenient timeout");
            return lenientClient.execute(request);
        }
        throw e;
    }
}

Circuit Breaker Pattern

public class CircuitBreakerHttpClient {
    private final HttpClient delegate;
    private final int failureThreshold;
    private final long retryTimeout;

    private int failureCount = 0;
    private long lastFailureTime = 0;
    private boolean circuitOpen = false;

    public CircuitBreakerHttpClient(HttpClient delegate, int failureThreshold, long retryTimeout) {
        this.delegate = delegate;
        this.failureThreshold = failureThreshold;
        this.retryTimeout = retryTimeout;
    }

    public SuccessfulHttpResponse execute(HttpRequest request) {
        if (circuitOpen) {
            if (System.currentTimeMillis() - lastFailureTime < retryTimeout) {
                throw new RuntimeException("Circuit breaker is OPEN");
            } else {
                // Try to close circuit
                circuitOpen = false;
                failureCount = 0;
            }
        }

        try {
            SuccessfulHttpResponse response = delegate.execute(request);
            // Success - reset failure count
            failureCount = 0;
            return response;
        } catch (RuntimeException e) {
            failureCount++;
            lastFailureTime = System.currentTimeMillis();

            if (failureCount >= failureThreshold) {
                circuitOpen = true;
                System.err.println("Circuit breaker OPENED after " + failureCount + " failures");
            }

            throw e;
        }
    }
}

Fallback Strategies

Fallback to Cache

private Map<String, String> cache = new ConcurrentHashMap<>();

public String fetchWithCache(String url) {
    HttpRequest request = HttpRequest.builder()
        .method(HttpMethod.GET)
        .url(url)
        .build();

    try {
        SuccessfulHttpResponse response = client.execute(request);
        String body = response.body();
        cache.put(url, body);  // Update cache
        return body;
    } catch (RuntimeException e) {
        System.err.println("Request failed, using cached value: " + e.getMessage());
        String cached = cache.get(url);
        if (cached != null) {
            return cached;
        }
        throw new RuntimeException("No cached value available", e);
    }
}

Fallback to Default Value

public String fetchWithDefault(String url, String defaultValue) {
    HttpRequest request = HttpRequest.builder()
        .method(HttpMethod.GET)
        .url(url)
        .build();

    try {
        SuccessfulHttpResponse response = client.execute(request);
        return response.body();
    } catch (RuntimeException e) {
        System.err.println("Request failed, using default: " + e.getMessage());
        return defaultValue;
    }
}

Logging Errors

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class ErrorLoggingClient {
    private static final Logger logger = LoggerFactory.getLogger(ErrorLoggingClient.class);
    private final HttpClient client;

    public SuccessfulHttpResponse execute(HttpRequest request) {
        try {
            SuccessfulHttpResponse response = client.execute(request);
            logger.info("Request successful: {} {}", request.method(), request.url());
            return response;
        } catch (HttpException e) {
            logger.error("HTTP error for {} {}: {}",
                request.method(), request.url(), e.getMessage());
            throw e;
        } catch (RuntimeException e) {
            logger.error("Request failed for {} {}: {}",
                request.method(), request.url(), e.getMessage(), e);
            throw e;
        }
    }
}

CompletableFuture Pattern

Async Error Handling

import java.util.concurrent.CompletableFuture;

public CompletableFuture<String> fetchAsync(String url) {
    CompletableFuture<String> future = new CompletableFuture<>();

    HttpRequest request = HttpRequest.builder()
        .method(HttpMethod.GET)
        .url(url)
        .build();

    try {
        SuccessfulHttpResponse response = client.execute(request);
        future.complete(response.body());
    } catch (Exception e) {
        future.completeExceptionally(e);
    }

    return future;
}

// Usage
fetchAsync("https://api.example.com/data")
    .thenAccept(body -> System.out.println("Success: " + body))
    .exceptionally(error -> {
        System.err.println("Error: " + error.getMessage());
        return null;
    });

Best Practices

  1. Differentiate error types: Handle HttpException (4XX/5XX) differently from RuntimeException (network issues, timeouts).

  2. Don't retry 4XX errors: Client errors usually indicate invalid requests that won't succeed on retry.

  3. Use exponential backoff: When retrying, increase the delay between attempts to avoid overwhelming the server.

  4. Set appropriate timeouts: Configure timeouts based on expected response times to fail fast when appropriate.

  5. Implement circuit breakers: Prevent cascading failures by stopping requests when a service is consistently failing.

  6. Log errors appropriately: Log enough information to diagnose issues but avoid logging sensitive data.

  7. Provide fallbacks: Consider fallback strategies like cached data or default values for non-critical requests.

  8. Monitor error rates: Track error rates in production to identify issues early.

Related Documentation

  • Timeout Configuration: Timeout Guide
  • SSE Error Handling: SSE Streaming Guide
  • Logging: Logging Configuration

Install with Tessl CLI

npx tessl i tessl/maven-dev-langchain4j--langchain4j-http-client

docs

index.md

installation.md

quick-start.md

tile.json