CtrlK
BlogDocsLog inGet started
Tessl Logo

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

JDK HttpClient implementation for LangChain4j HTTP client interface

Pending
Quality

Pending

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

SecuritybySnyk

Pending

The risk profile of this skill

Overview
Eval results
Files

LangChain4j HTTP Client - JDK

JDK HttpClient implementation for LangChain4j HTTP client interface. Wraps java.net.http.HttpClient (Java 11+) with zero external dependencies.

Package Information

<dependency>
    <groupId>dev.langchain4j</groupId>
    <artifactId>langchain4j-http-client-jdk</artifactId>
    <version>1.11.0</version>
</dependency>
implementation 'dev.langchain4j:langchain4j-http-client-jdk:1.11.0'
  • Group ID: dev.langchain4j
  • Artifact ID: langchain4j-http-client-jdk
  • Package Type: maven
  • Language: Java
  • Minimum Java Version: Java 11
  • Dependencies: LangChain4j core interfaces only (zero external dependencies)

Core Imports

import dev.langchain4j.http.client.jdk.JdkHttpClient;
import dev.langchain4j.http.client.jdk.JdkHttpClientBuilder;
import dev.langchain4j.http.client.jdk.JdkHttpClientBuilderFactory;
import dev.langchain4j.http.client.HttpRequest;
import dev.langchain4j.http.client.HttpMethod;
import dev.langchain4j.http.client.SuccessfulHttpResponse;
import dev.langchain4j.http.client.FormDataFile;
import dev.langchain4j.http.client.sse.ServerSentEventParser;
import dev.langchain4j.http.client.sse.ServerSentEventListener;
import dev.langchain4j.http.client.sse.ServerSentEvent;
import dev.langchain4j.exception.HttpException;
import dev.langchain4j.exception.TimeoutException;
import java.time.Duration;
import java.net.http.HttpClient;

Quick Start

// Create client with timeouts
JdkHttpClient client = JdkHttpClient.builder()
    .connectTimeout(Duration.ofSeconds(30))
    .readTimeout(Duration.ofSeconds(60))
    .build();

// Execute synchronous request
HttpRequest request = HttpRequest.builder()
    .url("https://api.example.com/data")
    .method(HttpMethod.GET)
    .addHeader("Authorization", "Bearer token")
    .build();

SuccessfulHttpResponse response = client.execute(request);

Architecture

Design Patterns

Adapter Pattern: Wraps java.net.http.HttpClient to implement LangChain4j HttpClient interface, enabling seamless integration with the LangChain4j framework.

Service Provider Interface: JdkHttpClientBuilderFactory is registered in META-INF/services/dev.langchain4j.http.client.HttpClientBuilderFactory for automatic discovery via Java SPI mechanism.

Builder Pattern: Fluent API for configuration via JdkHttpClientBuilder.

Interface Implementations

  • JdkHttpClient implements dev.langchain4j.http.client.HttpClient
  • JdkHttpClientBuilder implements dev.langchain4j.http.client.HttpClientBuilder
  • JdkHttpClientBuilderFactory implements dev.langchain4j.http.client.HttpClientBuilderFactory

Key Characteristics

  • Thread Safety: JdkHttpClient instances are immutable and thread-safe after construction. Can be shared across threads.
  • Connection Pooling: Managed automatically by underlying JDK HttpClient. Pool size and behavior controlled via HttpClient.Builder.
  • HTTP Protocol Support: HTTP/1.1 (default), HTTP/2 (configurable via HttpClient.Builder).
  • Zero External Dependencies: Uses only Java 11+ standard library and LangChain4j core interfaces.
  • Error Mapping: JDK exceptions mapped to LangChain4j exception types (HttpException, TimeoutException).

API Reference

JdkHttpClient

Main HTTP client implementation wrapping JDK HttpClient.

package dev.langchain4j.http.client.jdk;

public class JdkHttpClient implements dev.langchain4j.http.client.HttpClient {
    // Constructor - typically not called directly, use builder()
    public JdkHttpClient(JdkHttpClientBuilder builder);

    // Static factory for builder
    public static JdkHttpClientBuilder builder();

    // Synchronous HTTP execution
    public SuccessfulHttpResponse execute(HttpRequest request)
        throws HttpException, TimeoutException;

    // Asynchronous SSE execution with custom parser
    public void execute(
        HttpRequest request,
        ServerSentEventParser parser,
        ServerSentEventListener listener
    );

    // Asynchronous SSE execution with default parser (from HttpClient interface)
    default void execute(
        HttpRequest request,
        ServerSentEventListener listener
    );
}

Thread Safety: Immutable and thread-safe after construction. Reuse across threads.

Resource Management: Does not require explicit closing. Underlying JDK HttpClient manages resources.

JdkHttpClientBuilder

Fluent builder for configuring JdkHttpClient instances.

package dev.langchain4j.http.client.jdk;

public class JdkHttpClientBuilder implements dev.langchain4j.http.client.HttpClientBuilder {
    // Timeout configuration
    public JdkHttpClientBuilder connectTimeout(Duration connectTimeout);
    public Duration connectTimeout();
    public JdkHttpClientBuilder readTimeout(Duration readTimeout);
    public Duration readTimeout();

    // Advanced JDK HttpClient configuration
    public JdkHttpClientBuilder httpClientBuilder(java.net.http.HttpClient.Builder httpClientBuilder);
    public java.net.http.HttpClient.Builder httpClientBuilder();

    // Build configured client
    public JdkHttpClient build();
}

Default Values:

  • connectTimeout: No default (uses JDK default, typically system-dependent)
  • readTimeout: No default (uses JDK default, typically unlimited)
  • httpClientBuilder: If not set, uses HttpClient.newBuilder() with defaults

Thread Safety: Not thread-safe. Use from single thread during construction.

Validation:

  • connectTimeout and readTimeout must not be null or negative if set
  • Throws IllegalArgumentException for invalid configurations

JdkHttpClientBuilderFactory

Service Provider Interface factory for framework integration.

package dev.langchain4j.http.client.jdk;

public class JdkHttpClientBuilderFactory implements dev.langchain4j.http.client.HttpClientBuilderFactory {
    public JdkHttpClientBuilder create();
}

SPI Registration: Registered in META-INF/services/dev.langchain4j.http.client.HttpClientBuilderFactory.

Usage: Automatically discovered by LangChain4j framework. Can also be instantiated directly.

Client Configuration

Timeout Configuration

Configure connection establishment and response read timeouts.

JdkHttpClient client = JdkHttpClient.builder()
    .connectTimeout(Duration.ofSeconds(30))  // Connection establishment timeout
    .readTimeout(Duration.ofSeconds(60))      // Response read timeout (per request)
    .build();

Connect Timeout: Maximum time to establish TCP connection and TLS handshake.

  • Applied once per request
  • null = use JDK default (system-dependent, typically OS TCP timeout)
  • Throws TimeoutException on expiry

Read Timeout: Maximum time to receive response after request sent.

  • Applied to response reading phase
  • null = use JDK default (typically unlimited)
  • Throws TimeoutException on expiry
  • For SSE streams, applies to entire stream duration

Edge Cases:

  • Timeout precision depends on OS scheduler (typically 100ms granularity on most systems)
  • Very short timeouts (<1s) may expire during DNS resolution on first request
  • SSE streams with long readTimeout may hold threads; use appropriate values

Advanced JDK HttpClient Configuration

Configure underlying JDK HttpClient with protocol version, SSL, proxy, redirects, authenticator, executor, and cookies.

import java.net.http.HttpClient;
import java.net.InetSocketAddress;
import java.net.ProxySelector;
import javax.net.ssl.SSLContext;
import java.util.concurrent.Executors;

JdkHttpClient client = JdkHttpClient.builder()
    .httpClientBuilder(HttpClient.newBuilder()
        // HTTP protocol version
        .version(HttpClient.Version.HTTP_2)

        // Redirect policy
        .followRedirects(HttpClient.Redirect.NORMAL)

        // Proxy configuration
        .proxy(ProxySelector.of(new InetSocketAddress("proxy.example.com", 8080)))

        // SSL/TLS configuration
        .sslContext(SSLContext.getDefault())

        // Custom executor for async operations
        .executor(Executors.newFixedThreadPool(4))

        // Authenticator for proxy/server authentication
        .authenticator(new java.net.Authenticator() {
            @Override
            protected java.net.PasswordAuthentication getPasswordAuthentication() {
                return new java.net.PasswordAuthentication("user", "pass".toCharArray());
            }
        })

        // Cookie handler
        .cookieHandler(new java.net.CookieManager())

        // Connection priority (for HTTP/2)
        .priority(1))  // 1 (highest) to 256 (lowest)
    .connectTimeout(Duration.ofSeconds(30))
    .readTimeout(Duration.ofSeconds(60))
    .build();

HTTP Version:

  • HttpClient.Version.HTTP_1_1: HTTP/1.1 only
  • HttpClient.Version.HTTP_2: HTTP/2 with fallback to HTTP/1.1
  • Default: Protocol negotiation (HTTP/2 preferred)

Redirect Policy:

  • HttpClient.Redirect.NEVER: Never follow redirects (301, 302, 303, 307, 308 return as-is)
  • HttpClient.Redirect.ALWAYS: Follow redirects (including HTTPS → HTTP, not recommended)
  • HttpClient.Redirect.NORMAL: Follow redirects except HTTPS → HTTP downgrade
  • Default: NEVER

Proxy:

  • ProxySelector.of(InetSocketAddress): Fixed proxy for all requests
  • ProxySelector.getDefault(): System default proxy
  • Default: ProxySelector.getDefault()

SSL/TLS:

  • sslContext(SSLContext): Custom SSL context for certificate validation, cipher suites
  • sslParameters(SSLParameters): Fine-grained SSL control (protocols, cipher suites, SNI)
  • Default: System default SSL context

Executor:

  • Custom Executor for async operations (SSE streaming, async requests)
  • Default: Internal thread pool managed by JDK

Authenticator:

  • Used for HTTP authentication (Basic, Digest) and proxy authentication
  • Callbacks may occur on any thread
  • Default: No authentication

Cookie Handler:

  • CookieManager for automatic cookie management
  • Default: No cookie handling

Connection Priority (HTTP/2 only):

  • Range: 1-256 (1 = highest priority)
  • Affects stream prioritization in HTTP/2 multiplexing
  • Default: No priority set

Important Notes:

  • httpClientBuilder() settings override timeout settings if conflicting
  • Connection pooling configured via HttpClient.Builder (not directly exposed)
  • Keep-alive settings controlled by JDK (typically enabled by default)

Builder Pattern Usage

// Minimal configuration
JdkHttpClient client1 = JdkHttpClient.builder()
    .build();

// Custom timeouts
JdkHttpClient client2 = JdkHttpClient.builder()
    .connectTimeout(Duration.ofSeconds(10))
    .readTimeout(Duration.ofSeconds(30))
    .build();

// HTTP/2 with timeouts
JdkHttpClient client3 = JdkHttpClient.builder()
    .httpClientBuilder(HttpClient.newBuilder()
        .version(HttpClient.Version.HTTP_2))
    .connectTimeout(Duration.ofSeconds(30))
    .readTimeout(Duration.ofSeconds(60))
    .build();

Factory Pattern Usage (SPI)

// Direct instantiation
JdkHttpClientBuilderFactory factory = new JdkHttpClientBuilderFactory();
JdkHttpClientBuilder builder = factory.create();
JdkHttpClient client = builder
    .connectTimeout(Duration.ofSeconds(30))
    .readTimeout(Duration.ofSeconds(60))
    .build();

// SPI discovery (automatic in LangChain4j framework)
ServiceLoader<HttpClientBuilderFactory> loader =
    ServiceLoader.load(HttpClientBuilderFactory.class);
HttpClientBuilderFactory factory = loader.findFirst().orElseThrow();
HttpClientBuilder builder = factory.create();

HTTP Request Construction

HttpRequest Builder

package dev.langchain4j.http.client;

public class HttpRequest {
    public static HttpRequest.Builder builder();

    // Accessors
    public String url();
    public HttpMethod method();
    public Map<String, List<String>> headers();
    public String body();
    public Map<String, String> formDataFields();
    public Map<String, FormDataFile> formDataFiles();
}

public static class HttpRequest.Builder {
    // URL configuration
    public HttpRequest.Builder url(String url);
    public HttpRequest.Builder url(String baseUrl, String path);

    // HTTP method (required)
    public HttpRequest.Builder method(HttpMethod method);

    // Headers
    public HttpRequest.Builder addHeader(String name, String... values);
    public HttpRequest.Builder addHeaders(Map<String, String> headers);
    public HttpRequest.Builder headers(Map<String, List<String>> headers);

    // Query parameters
    public HttpRequest.Builder addQueryParam(String name, String value);
    public HttpRequest.Builder addQueryParams(Map<String, String> queryParams);
    public HttpRequest.Builder queryParams(Map<String, String> queryParams);

    // Request body
    public HttpRequest.Builder body(String body);

    // Form data (multipart)
    public HttpRequest.Builder addFormDataField(String name, String value);
    public HttpRequest.Builder formDataFields(Map<String, String> formDataFields);
    public HttpRequest.Builder addFormDataFile(String name, String fileName, String contentType, byte[] content);
    public HttpRequest.Builder formDataFiles(Map<String, FormDataFile> formDataFiles);

    // Build request
    public HttpRequest build();
}

Required Fields:

  • url (String, must be valid HTTP/HTTPS URL)
  • method (HttpMethod enum: GET, POST, DELETE)

Optional Fields:

  • headers (default: empty, case-insensitive names)
  • body (default: null, UTF-8 encoded)
  • queryParams (default: empty, URL-encoded with UTF-8)
  • formDataFields / formDataFiles (default: empty, mutually exclusive with body)

Validation:

  • url must be non-null and valid HTTP/HTTPS URL
  • method must be non-null and one of GET, POST, DELETE
  • body and form data are mutually exclusive
  • Throws IllegalArgumentException for invalid configurations

URL Configuration

// Simple URL
HttpRequest request = HttpRequest.builder()
    .url("https://api.example.com/data")
    .method(HttpMethod.GET)
    .build();

// Base URL + path (joined with /)
HttpRequest request = HttpRequest.builder()
    .url("https://api.example.com", "/v1/data")
    .method(HttpMethod.GET)
    .build();
// Resulting URL: https://api.example.com/v1/data

URL Construction:

  • Single url(String): Used as-is
  • url(String baseUrl, String path): Joined with / separator (handles trailing/leading slashes)
  • Path segments not automatically encoded; encode manually if needed

Supported Schemes: http://, https://

Edge Cases:

  • Relative URLs not supported
  • IPv6 addresses supported: https://[::1]:8080/path
  • Internationalized domain names (IDN) supported via Java URL encoding
  • Fragment identifiers (#fragment) preserved but ignored by HTTP client

Query Parameters

// Add individual query parameters
HttpRequest request = HttpRequest.builder()
    .url("https://api.example.com/search")
    .method(HttpMethod.GET)
    .addQueryParam("q", "langchain")
    .addQueryParam("limit", "10")
    .addQueryParam("filter", "active")
    .build();
// Resulting URL: https://api.example.com/search?q=langchain&limit=10&filter=active

// Add multiple query parameters
Map<String, String> params = Map.of(
    "page", "1",
    "sort", "date",
    "order", "desc"
);
HttpRequest request = HttpRequest.builder()
    .url("https://api.example.com/items")
    .method(HttpMethod.GET)
    .addQueryParams(params)
    .build();

// Replace all query parameters (overwrites existing)
HttpRequest request = HttpRequest.builder()
    .url("https://api.example.com/data?old=param")
    .method(HttpMethod.GET)
    .queryParams(Map.of("new", "param"))
    .build();
// Resulting URL: https://api.example.com/data?new=param

URL Encoding:

  • Query parameter names and values automatically URL-encoded using UTF-8
  • Encoding per RFC 3986: alphanumeric + -_.~ unreserved, others %-encoded
  • Special characters: &= in values encoded to %26, %3D
  • Space encoded as %20 (not +)

Multiple Values:

  • Same parameter name can be added multiple times: ?key=val1&key=val2
  • Use addQueryParam() multiple times for same name

Edge Cases:

  • Empty parameter name: Not validated, may cause server-side errors
  • Null values: Throws NullPointerException
  • Empty string values: Encoded as param=
  • Very long query strings (>2000 chars): May exceed server/proxy limits

Headers

// Add individual headers
HttpRequest request = HttpRequest.builder()
    .url("https://api.example.com/data")
    .method(HttpMethod.POST)
    .addHeader("Content-Type", "application/json")
    .addHeader("Authorization", "Bearer token123")
    .addHeader("X-Custom-Header", "value1", "value2")  // Multiple values
    .build();

// Add multiple headers from map
Map<String, String> headers = Map.of(
    "Accept", "application/json",
    "User-Agent", "MyApp/1.0"
);
HttpRequest request = HttpRequest.builder()
    .url("https://api.example.com/data")
    .method(HttpMethod.GET)
    .addHeaders(headers)
    .build();

// Replace all headers
Map<String, List<String>> headerMap = Map.of(
    "Content-Type", List.of("application/json"),
    "Accept", List.of("application/json", "text/plain")
);
HttpRequest request = HttpRequest.builder()
    .url("https://api.example.com/data")
    .method(HttpMethod.POST)
    .headers(headerMap)
    .build();

Header Handling:

  • Header names are case-insensitive per HTTP spec
  • addHeader(name, value...): Appends values to existing header
  • addHeaders(Map): Appends headers from map (single value per name)
  • headers(Map<String, List<String>>): Replaces all headers (multiple values per name)
  • Multiple values for same header joined with , in HTTP message

Standard Headers:

  • Content-Type: Required for POST with body (e.g., application/json, text/plain)
  • Authorization: For authentication (e.g., Bearer token, Basic base64)
  • Accept: Response content type preference (e.g., application/json, text/event-stream for SSE)
  • User-Agent: Client identification (default: JDK HttpClient default)
  • Content-Length: Automatically set by JDK HttpClient (cannot override)
  • Host: Automatically set from URL (cannot override)
  • Connection: Managed by JDK HttpClient (cannot override)

Restricted Headers (cannot be set manually, controlled by JDK):

  • Content-Length, Host, Connection, Upgrade, Expect, Via
  • Attempting to set these may be silently ignored or throw exception

Edge Cases:

  • Null header name/value: Throws NullPointerException
  • Empty header value: Allowed, sent as Header-Name:
  • Header name with invalid characters: May throw exception during request execution
  • Very large header values (>8KB): May exceed server limits

Request Body

// POST with JSON body
HttpRequest request = HttpRequest.builder()
    .url("https://api.example.com/data")
    .method(HttpMethod.POST)
    .addHeader("Content-Type", "application/json")
    .body("{\"key\": \"value\", \"count\": 42}")
    .build();

// POST with plain text body
HttpRequest request = HttpRequest.builder()
    .url("https://api.example.com/submit")
    .method(HttpMethod.POST)
    .addHeader("Content-Type", "text/plain; charset=utf-8")
    .body("Plain text content")
    .build();

// POST with XML body
HttpRequest request = HttpRequest.builder()
    .url("https://api.example.com/soap")
    .method(HttpMethod.POST)
    .addHeader("Content-Type", "application/xml")
    .body("<?xml version=\"1.0\"?><root><item>value</item></root>")
    .build();

Body Encoding:

  • Body string encoded as UTF-8 bytes
  • Content-Type header should specify charset if not UTF-8
  • No automatic serialization; caller must serialize objects to string

Body with HTTP Methods:

  • GET: Body typically ignored by servers (not recommended)
  • POST: Body commonly used for request payload
  • DELETE: Body allowed but uncommon (some servers may ignore)

Edge Cases:

  • Null body: Sent as empty body (Content-Length: 0)
  • Empty string body: Sent as empty body (Content-Length: 0)
  • Very large bodies (>100MB): Consider streaming alternatives
  • Binary data: Convert to Base64 or use multipart form data

Mutually Exclusive:

  • Cannot use both body() and form data (formDataFields / formDataFiles)
  • Using both throws IllegalArgumentException on build()

Multipart Form Data

import dev.langchain4j.http.client.FormDataFile;

// Upload file with form fields
byte[] fileBytes = Files.readAllBytes(Path.of("document.pdf"));
FormDataFile file = new FormDataFile("document.pdf", "application/pdf", fileBytes);

HttpRequest request = HttpRequest.builder()
    .url("https://api.example.com/upload")
    .method(HttpMethod.POST)
    .addHeader("Content-Type", "multipart/form-data; boundary=----LangChain4j")
    .addFormDataField("description", "Annual report")
    .addFormDataField("category", "financial")
    .addFormDataFile("file", file.fileName(), file.contentType(), file.content())
    .build();

// Upload multiple files
byte[] file1Bytes = Files.readAllBytes(Path.of("doc1.pdf"));
byte[] file2Bytes = Files.readAllBytes(Path.of("doc2.pdf"));

HttpRequest request = HttpRequest.builder()
    .url("https://api.example.com/upload-multiple")
    .method(HttpMethod.POST)
    .addHeader("Content-Type", "multipart/form-data; boundary=----LangChain4j")
    .addFormDataFile("files", "doc1.pdf", "application/pdf", file1Bytes)
    .addFormDataFile("files", "doc2.pdf", "application/pdf", file2Bytes)
    .build();

// Using FormDataFile constructor
FormDataFile imageFile = new FormDataFile(
    "screenshot.png",
    "image/png",
    imageBytes
);

HttpRequest request = HttpRequest.builder()
    .url("https://api.example.com/upload-image")
    .method(HttpMethod.POST)
    .addHeader("Content-Type", "multipart/form-data; boundary=----LangChain4j")
    .addFormDataFile("image", imageFile.fileName(), imageFile.contentType(), imageFile.content())
    .build();

Multipart Boundary:

  • Fixed boundary: ----LangChain4j
  • Must include in Content-Type header: multipart/form-data; boundary=----LangChain4j
  • Boundary not configurable

FormDataFile:

package dev.langchain4j.http.client;

public class FormDataFile {
    public FormDataFile(String fileName, String contentType, byte[] content);

    public String fileName();
    public String contentType();
    public byte[] content();
}

Form Data Methods:

  • addFormDataField(name, value): Add text field
  • formDataFields(Map): Replace all text fields
  • addFormDataFile(name, fileName, contentType, bytes): Add file
  • formDataFiles(Map): Replace all files

Content-Disposition Headers:

  • Text fields: Content-Disposition: form-data; name="fieldName"
  • Files: Content-Disposition: form-data; name="fieldName"; filename="file.pdf"

Edge Cases:

  • Empty file (0 bytes): Allowed, sent as empty part
  • Very large files (>100MB): Entire content loaded into memory (consider chunking)
  • File name with special characters: Should be ASCII (non-ASCII may cause issues)
  • Missing Content-Type header or wrong boundary: Server will reject request
  • Same field name multiple times: Creates multiple parts (allowed per RFC 2388)

Memory Considerations:

  • All form data (fields + files) held in memory before sending
  • Large files may cause OutOfMemoryError
  • For large uploads, consider streaming alternatives outside LangChain4j framework

Synchronous HTTP Execution

execute() Method

public SuccessfulHttpResponse execute(HttpRequest request)
    throws HttpException, TimeoutException;

Executes HTTP request synchronously, blocking until response received or error occurs.

Parameters:

  • request: HttpRequest object built with HttpRequest.builder()

Returns:

  • SuccessfulHttpResponse for status codes 200-299 (inclusive)

Throws:

  • dev.langchain4j.exception.HttpException: Status code not in 200-299 range
    • Contains statusCode() and response body via getMessage()
  • dev.langchain4j.exception.TimeoutException: Request timed out
    • Wraps java.net.http.HttpTimeoutException
    • Occurs on connect timeout or read timeout expiry
  • RuntimeException: IO errors, interrupted thread, or other failures

Example:

import dev.langchain4j.exception.HttpException;
import dev.langchain4j.exception.TimeoutException;

JdkHttpClient client = JdkHttpClient.builder()
    .connectTimeout(Duration.ofSeconds(30))
    .readTimeout(Duration.ofSeconds(60))
    .build();

try {
    HttpRequest request = HttpRequest.builder()
        .url("https://api.example.com/data")
        .method(HttpMethod.POST)
        .addHeader("Content-Type", "application/json")
        .addHeader("Authorization", "Bearer token123")
        .body("{\"query\": \"example\"}")
        .build();

    SuccessfulHttpResponse response = client.execute(request);

    int statusCode = response.statusCode();  // 200-299
    String body = response.body();           // Response body as UTF-8 string
    Map<String, List<String>> headers = response.headers();  // Response headers

    System.out.println("Status: " + statusCode);
    System.out.println("Body: " + body);

} catch (HttpException e) {
    // HTTP error (4xx, 5xx)
    System.err.println("HTTP " + e.statusCode() + ": " + e.getMessage());

} catch (TimeoutException e) {
    // Timeout (connect or read)
    System.err.println("Timeout: " + e.getMessage());

} catch (RuntimeException e) {
    // IO error, interrupted thread, etc.
    System.err.println("Error: " + e.getMessage());
}

Status Code Handling

Success (200-299):

  • Returns SuccessfulHttpResponse
  • Includes: 200 OK, 201 Created, 202 Accepted, 204 No Content, etc.

Client Errors (400-499):

  • Throws HttpException with status code and body
  • Common: 400 Bad Request, 401 Unauthorized, 403 Forbidden, 404 Not Found, 429 Too Many Requests

Server Errors (500-599):

  • Throws HttpException with status code and body
  • Common: 500 Internal Server Error, 502 Bad Gateway, 503 Service Unavailable, 504 Gateway Timeout

Redirects (300-399):

  • Followed automatically if HttpClient.Builder.followRedirects() configured
  • Otherwise throws HttpException with redirect status code and Location header in body

Response Body Encoding

  • Response body decoded as UTF-8 string by default
  • If response has Content-Type: ...; charset=<encoding>, still decoded as UTF-8 (charset ignored)
  • For non-UTF-8 responses, body may contain invalid characters
  • Binary responses: Body contains binary data as string (may be corrupted)

Thread Interruption

  • If calling thread interrupted during execute(), throws RuntimeException wrapping InterruptedException
  • Thread interrupt status preserved

Connection Pooling

  • Connections automatically pooled and reused by underlying JDK HttpClient
  • Keep-alive enabled by default (HTTP/1.1 persistent connections, HTTP/2 multiplexing)
  • Pool size and idle timeout controlled by JDK implementation (not configurable)

Asynchronous HTTP Execution (SSE)

execute() Method (SSE)

public void execute(
    HttpRequest request,
    ServerSentEventParser parser,
    ServerSentEventListener listener
);

Executes HTTP request asynchronously for Server-Sent Events (SSE) streaming.

Parameters:

  • request: HttpRequest object (typically with Accept: text/event-stream header)
  • parser: ServerSentEventParser implementation for parsing SSE format
  • listener: ServerSentEventListener for event callbacks

Returns: void (asynchronous execution via internal CompletableFuture)

Execution Flow:

  1. Request sent asynchronously
  2. On success (status 200-299): listener.onOpen(response) called
  3. Response stream parsed by parser, events delivered to listener.onEvent()
  4. On stream end: listener.onClose() called
  5. On error: listener.onError(throwable) called

Error Handling:

  • HTTP errors (status not 200-299): onError() with HttpException
  • Timeouts: onError() with TimeoutException
  • IO errors: onError() with RuntimeException
  • Parser errors: onError() with parsing exception
  • Callback exceptions: Silently caught to prevent stream termination

Example:

import dev.langchain4j.http.client.sse.ServerSentEventListener;
import dev.langchain4j.http.client.sse.ServerSentEventParser;
import dev.langchain4j.http.client.sse.ServerSentEvent;

JdkHttpClient client = JdkHttpClient.builder()
    .readTimeout(Duration.ofMinutes(5))  // Long timeout for streaming
    .build();

ServerSentEventListener listener = new ServerSentEventListener() {
    @Override
    public void onOpen(SuccessfulHttpResponse response) {
        System.out.println("SSE stream opened: " + response.statusCode());
    }

    @Override
    public void onEvent(ServerSentEvent event) {
        System.out.println("Event: " + event.event());
        System.out.println("Data: " + event.data());
    }

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

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

// Custom parser (typically provided by LangChain4j framework)
ServerSentEventParser parser = new ServerSentEventParser() {
    @Override
    public void parse(InputStream inputStream, ServerSentEventListener listener) {
        // Parse SSE format from inputStream, call listener.onEvent() for each event
    }
};

HttpRequest request = HttpRequest.builder()
    .url("https://api.example.com/stream")
    .method(HttpMethod.POST)
    .addHeader("Accept", "text/event-stream")
    .addHeader("Content-Type", "application/json")
    .body("{\"prompt\": \"Hello\"}")
    .build();

// Execute asynchronously (returns immediately)
client.execute(request, parser, listener);

// Main thread continues; listener callbacks invoked on background threads

execute() Method (SSE with Default Parser)

default void execute(
    HttpRequest request,
    ServerSentEventListener listener
);

Convenience method using default ServerSentEventParser implementation from LangChain4j framework.

Example:

import dev.langchain4j.http.client.sse.ServerSentEventListener;
import dev.langchain4j.http.client.sse.ServerSentEvent;

JdkHttpClient client = JdkHttpClient.builder()
    .readTimeout(Duration.ofMinutes(5))
    .build();

ServerSentEventListener listener = new ServerSentEventListener() {
    @Override
    public void onOpen(SuccessfulHttpResponse response) {
        System.out.println("Connected");
    }

    @Override
    public void onEvent(ServerSentEvent event) {
        System.out.println(event.data());
    }

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

    @Override
    public void onError(Throwable error) {
        error.printStackTrace();
    }
};

HttpRequest request = HttpRequest.builder()
    .url("https://api.example.com/stream")
    .method(HttpMethod.POST)
    .addHeader("Accept", "text/event-stream")
    .body("{\"prompt\": \"Hello\"}")
    .build();

// Simplified execution with default parser
client.execute(request, listener);

ServerSentEventListener Interface

package dev.langchain4j.http.client.sse;

public interface ServerSentEventListener {
    // Lifecycle callbacks
    default void onOpen(SuccessfulHttpResponse response) {}
    default void onClose() {}
    void onError(Throwable error);  // Required implementation

    // Event handling (with context for cancellation, since 1.8.0)
    default void onEvent(ServerSentEvent event, ServerSentEventContext context) {
        onEvent(event);  // Delegate to legacy method
    }

    // Event handling (legacy, called by default implementation above)
    default void onEvent(ServerSentEvent event) {}
}

Lifecycle Callbacks:

  • onOpen(response): Stream connected, status 200-299
  • onEvent(event) or onEvent(event, context): Each SSE event received
  • onClose(): Stream ended normally
  • onError(throwable): Error occurred (HTTP error, timeout, IO error, parse error)

Callback Threading:

  • Callbacks invoked on background threads managed by JDK HttpClient
  • May be different threads for different callbacks
  • Callbacks should be thread-safe if accessing shared state

Callback Error Handling:

  • Exceptions thrown in callbacks are caught and ignored
  • Prevents callback errors from terminating stream
  • Log errors within callbacks if needed

Context-Aware Event Handling (since 1.8.0):

@Override
public void onEvent(ServerSentEvent event, ServerSentEventContext context) {
    System.out.println(event.data());

    // Cancel stream if done
    if (event.data().contains("DONE")) {
        context.parsingHandle().cancel();
    }
}

ServerSentEvent Structure

package dev.langchain4j.http.client.sse;

public class ServerSentEvent {
    public ServerSentEvent(String event, String data);

    public String event();  // Event type (null if not specified)
    public String data();   // Event data payload
}

SSE Format:

event: message
data: {"text": "Hello"}

event: done
data: [DONE]

Fields:

  • event(): Event type from event: field (null if not present)
  • data(): Event data from data: field (may span multiple lines)

ServerSentEventContext

package dev.langchain4j.http.client.sse;

// Experimental (since 1.8.0)
public class ServerSentEventContext {
    public ServerSentEventContext(ServerSentEventParsingHandle parsingHandle);

    public ServerSentEventParsingHandle parsingHandle();
}

Provides stream control context to event handlers.

ServerSentEventParsingHandle

package dev.langchain4j.http.client.sse;

// Experimental (since 1.8.0)
public interface ServerSentEventParsingHandle {
    void cancel();           // Cancel SSE stream parsing
    boolean isCancelled();   // Check if stream was cancelled
}

Usage:

@Override
public void onEvent(ServerSentEvent event, ServerSentEventContext context) {
    if (shouldStop(event)) {
        context.parsingHandle().cancel();
    }
}

Behavior:

  • cancel() stops parsing, closes stream, triggers onClose()
  • Thread-safe (can be called from any thread)
  • No-op if already cancelled or closed

SSE Read Timeout Behavior

  • readTimeout applies to entire SSE stream duration, not individual events
  • Long-running streams (minutes/hours) require appropriate readTimeout setting
  • Timeout expiry triggers onError(TimeoutException) and closes stream

Example:

// For long-running SSE streams
JdkHttpClient client = JdkHttpClient.builder()
    .connectTimeout(Duration.ofSeconds(30))
    .readTimeout(Duration.ofHours(1))  // Allow 1-hour stream
    .build();

Common SSE Use Cases

LLM Streaming Responses:

HttpRequest request = HttpRequest.builder()
    .url("https://api.openai.com/v1/chat/completions")
    .method(HttpMethod.POST)
    .addHeader("Authorization", "Bearer " + apiKey)
    .addHeader("Content-Type", "application/json")
    .addHeader("Accept", "text/event-stream")
    .body("{\"model\":\"gpt-4\",\"messages\":[{\"role\":\"user\",\"content\":\"Hello\"}],\"stream\":true}")
    .build();

client.execute(request, new ServerSentEventListener() {
    @Override
    public void onEvent(ServerSentEvent event) {
        if (!"[DONE]".equals(event.data())) {
            System.out.print(parseChunk(event.data()));
        }
    }

    @Override
    public void onClose() {
        System.out.println("\nStream complete");
    }

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

Response Handling

SuccessfulHttpResponse

package dev.langchain4j.http.client;

public class SuccessfulHttpResponse {
    public static SuccessfulHttpResponse.Builder builder();

    public int statusCode();                           // HTTP status code (200-299)
    public Map<String, List<String>> headers();        // Response headers (case-insensitive keys)
    public String body();                              // Response body (UTF-8 string)
}

Fields:

  • statusCode(): HTTP status code (200-299 inclusive)
  • headers(): Response headers as multi-value map
    • Header names are case-insensitive
    • Multiple values for same header returned as list
    • Common headers: Content-Type, Content-Length, Date, Server, Set-Cookie
  • body(): Response body decoded as UTF-8 string
    • Empty for 204 No Content responses
    • May be empty string for other 2xx responses

Example:

SuccessfulHttpResponse response = client.execute(request);

// Status code
System.out.println("Status: " + response.statusCode());  // 200, 201, etc.

// Response body
String json = response.body();
System.out.println("Body: " + json);

// Response headers
String contentType = response.headers().get("Content-Type").get(0);
System.out.println("Content-Type: " + contentType);

// Multiple header values
List<String> setCookies = response.headers().get("Set-Cookie");
if (setCookies != null) {
    setCookies.forEach(cookie -> System.out.println("Cookie: " + cookie));
}

// Case-insensitive header access
String server1 = response.headers().get("Server").get(0);
String server2 = response.headers().get("server").get(0);
// server1.equals(server2) == true

Response Body Encoding

  • Body always decoded as UTF-8, regardless of Content-Type charset
  • For non-UTF-8 responses, characters may be corrupted
  • Binary responses: Body contains binary data decoded as UTF-8 (likely corrupted)

Workaround for Non-UTF-8:

  • LangChain4j framework does not support custom charset
  • Use alternative HTTP client for non-UTF-8 responses

Empty Response Bodies

  • 204 No Content: body() returns empty string ""
  • Other 2xx with no body: body() returns empty string ""
  • Cannot distinguish between empty response and no response

Exception Handling

HttpException

package dev.langchain4j.exception;

public class HttpException extends RuntimeException {
    public HttpException(int statusCode, String message);

    public int statusCode();      // HTTP status code (400-599 typically)
    public String getMessage();   // Response body
}

Thrown When:

  • HTTP response status code not in 200-299 range
  • Includes client errors (4xx) and server errors (5xx)
  • Redirects (3xx) if not following redirects

Contains:

  • statusCode(): HTTP status code (e.g., 404, 500)
  • getMessage(): Full response body as string

Example:

try {
    SuccessfulHttpResponse response = client.execute(request);
} catch (HttpException e) {
    System.err.println("HTTP " + e.statusCode());
    System.err.println("Response: " + e.getMessage());

    if (e.statusCode() == 401) {
        System.err.println("Unauthorized - check credentials");
    } else if (e.statusCode() == 429) {
        System.err.println("Rate limited - retry after delay");
    } else if (e.statusCode() >= 500) {
        System.err.println("Server error - retry or contact support");
    }
}

TimeoutException

package dev.langchain4j.exception;

public class TimeoutException extends RuntimeException {
    public TimeoutException(Throwable cause);  // Wraps HttpTimeoutException
}

Thrown When:

  • Connect timeout expires during connection establishment
  • Read timeout expires during response reading
  • Read timeout expires during SSE streaming

Wraps: java.net.http.HttpTimeoutException from JDK

Example:

try {
    SuccessfulHttpResponse response = client.execute(request);
} catch (TimeoutException e) {
    System.err.println("Request timed out");
    System.err.println("Cause: " + e.getCause().getMessage());

    // Retry with exponential backoff or increase timeout
}

Other RuntimeExceptions

IO Errors:

  • Network failures: java.io.IOException wrapped in RuntimeException
  • DNS resolution failures: UnknownHostException wrapped in RuntimeException
  • SSL/TLS errors: SSLException wrapped in RuntimeException

Thread Interruption:

  • If thread interrupted during request: InterruptedException wrapped in RuntimeException
  • Thread interrupt status preserved

Example:

try {
    SuccessfulHttpResponse response = client.execute(request);
} catch (HttpException e) {
    // HTTP error
} catch (TimeoutException e) {
    // Timeout
} catch (RuntimeException e) {
    // IO error, interrupted, or other failure
    System.err.println("Request failed: " + e.getMessage());

    if (e.getCause() instanceof InterruptedException) {
        Thread.currentThread().interrupt();  // Restore interrupt status
    }
}

Exception Hierarchy

Throwable
└── RuntimeException
    ├── HttpException (dev.langchain4j.exception)
    │   └── statusCode: int
    │   └── message: String (response body)
    ├── TimeoutException (dev.langchain4j.exception)
    │   └── cause: HttpTimeoutException
    └── RuntimeException (other errors)
        └── cause: IOException, InterruptedException, etc.

Thread Safety

JdkHttpClient: Immutable and thread-safe after construction.

  • Single instance can be shared across threads
  • Internal state never modified after construction
  • Underlying JDK HttpClient is thread-safe

JdkHttpClientBuilder: Not thread-safe.

  • Use from single thread during construction
  • Do not share builder across threads

HttpRequest: Immutable and thread-safe.

  • Can be reused for multiple requests
  • Can be shared across threads

Recommendation:

// Singleton HTTP client (shared across application)
public class HttpClientProvider {
    private static final JdkHttpClient INSTANCE = JdkHttpClient.builder()
        .connectTimeout(Duration.ofSeconds(30))
        .readTimeout(Duration.ofSeconds(60))
        .build();

    public static JdkHttpClient getInstance() {
        return INSTANCE;
    }
}

// Usage from multiple threads
JdkHttpClient client = HttpClientProvider.getInstance();
SuccessfulHttpResponse response = client.execute(request);  // Thread-safe

Performance Considerations

Connection Pooling:

  • Managed automatically by JDK HttpClient
  • Keep-alive connections reused for same host
  • HTTP/2: Multiple streams over single connection
  • Pool size and idle timeout: JDK implementation-dependent (not configurable)

HTTP/2 Benefits:

  • Header compression (HPACK)
  • Multiplexing (multiple concurrent requests without head-of-line blocking)
  • Server push (not used in typical LangChain4j scenarios)

Resource Management:

  • No explicit cleanup required for JdkHttpClient
  • Connections closed automatically when idle
  • InputStreams closed in try-with-resources blocks

Memory Considerations:

  • Request/response bodies held in memory as strings
  • Multipart uploads: All files loaded into memory
  • Large payloads (>100MB): Risk of OutOfMemoryError
  • SSE streams: Events processed incrementally (low memory)

Optimization Recommendations:

  1. Reuse single JdkHttpClient instance across application
  2. Use HTTP/2 for high-throughput scenarios:
    JdkHttpClient client = JdkHttpClient.builder()
        .httpClientBuilder(HttpClient.newBuilder()
            .version(HttpClient.Version.HTTP_2))
        .build();
  3. Set appropriate timeouts to prevent resource exhaustion
  4. For large file uploads, consider streaming alternatives
  5. For SSE streams, set readTimeout appropriate to expected stream duration

Benchmarks (approximate, system-dependent):

  • Connection establishment (HTTPS): 50-200ms
  • HTTP/1.1 request throughput: 100-500 req/s per client
  • HTTP/2 request throughput: 500-2000 req/s per client
  • Memory per request: 1-10 KB (excluding body)

Edge Cases and Limitations

URL Limitations:

  • Maximum URL length: ~2000 chars (browser/proxy limits, not JDK limit)
  • Relative URLs: Not supported
  • IPv6: Supported with bracket notation: https://[::1]:8080
  • Internationalized domain names (IDN): Supported via Java URL encoding

Header Limitations:

  • Restricted headers: Cannot set Content-Length, Host, Connection (managed by JDK)
  • Header size limits: Typically 8KB total (server-dependent)
  • Cookie handling: Manual via Cookie header (or use CookieManager in HttpClient.Builder)

Body Limitations:

  • Maximum body size: Limited by available heap memory
  • Large bodies (>100MB): May cause OutOfMemoryError
  • Binary responses: Decoded as UTF-8 string (may corrupt data)
  • Non-UTF-8 responses: Characters may be corrupted

Timeout Precision:

  • Timeout precision: ~100ms (OS scheduler-dependent)
  • Very short timeouts (<1s): May expire during DNS resolution
  • Timeout of 0 or negative: Invalid, throws IllegalArgumentException

Redirect Handling:

  • Maximum redirects: Configured by JDK (typically 5-20)
  • Redirect loops: Detected and cause failure
  • POST to GET redirect: Handled per HTTP spec (303, 301, 302)

SSE Limitations:

  • Parser implementation: Provided by LangChain4j framework or custom
  • Event size: No limit (parsed incrementally)
  • Reconnection: Not automatic (must retry manually)
  • Last-Event-ID: Not automatically included (add manually to headers)

Concurrency Limitations:

  • Concurrent requests per client: No limit (managed by JDK)
  • Concurrent connections per host: Managed by JDK (typically 5-10 for HTTP/1.1)
  • HTTP/2: Single connection per host with stream multiplexing

Platform-Specific:

  • macOS: Uses native HTTP/2 implementation (fast)
  • Linux: Pure Java implementation (slightly slower)
  • Windows: Uses native WinHTTP (fast)
  • Android: Not supported (use alternative HTTP client)

Complete Examples

Simple GET Request

import dev.langchain4j.http.client.jdk.JdkHttpClient;
import dev.langchain4j.http.client.HttpRequest;
import dev.langchain4j.http.client.HttpMethod;
import dev.langchain4j.http.client.SuccessfulHttpResponse;
import java.time.Duration;

JdkHttpClient client = JdkHttpClient.builder()
    .connectTimeout(Duration.ofSeconds(10))
    .readTimeout(Duration.ofSeconds(30))
    .build();

HttpRequest request = HttpRequest.builder()
    .url("https://api.example.com/users/123")
    .method(HttpMethod.GET)
    .addHeader("Accept", "application/json")
    .build();

SuccessfulHttpResponse response = client.execute(request);
System.out.println(response.body());

POST with JSON Body

import dev.langchain4j.http.client.jdk.JdkHttpClient;
import dev.langchain4j.http.client.HttpRequest;
import dev.langchain4j.http.client.HttpMethod;
import dev.langchain4j.http.client.SuccessfulHttpResponse;
import dev.langchain4j.exception.HttpException;
import java.time.Duration;

JdkHttpClient client = JdkHttpClient.builder()
    .connectTimeout(Duration.ofSeconds(30))
    .readTimeout(Duration.ofSeconds(60))
    .build();

HttpRequest request = HttpRequest.builder()
    .url("https://api.example.com/users")
    .method(HttpMethod.POST)
    .addHeader("Content-Type", "application/json")
    .addHeader("Authorization", "Bearer token123")
    .body("{\"name\": \"John\", \"email\": \"john@example.com\"}")
    .build();

try {
    SuccessfulHttpResponse response = client.execute(request);
    System.out.println("Created: " + response.body());
} catch (HttpException e) {
    System.err.println("Failed: " + e.statusCode() + " - " + e.getMessage());
}

File Upload (Multipart)

import dev.langchain4j.http.client.jdk.JdkHttpClient;
import dev.langchain4j.http.client.HttpRequest;
import dev.langchain4j.http.client.HttpMethod;
import dev.langchain4j.http.client.FormDataFile;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Duration;

JdkHttpClient client = JdkHttpClient.builder()
    .connectTimeout(Duration.ofSeconds(30))
    .readTimeout(Duration.ofMinutes(5))  // Long timeout for large uploads
    .build();

byte[] fileBytes = Files.readAllBytes(Path.of("document.pdf"));

HttpRequest request = HttpRequest.builder()
    .url("https://api.example.com/upload")
    .method(HttpMethod.POST)
    .addHeader("Content-Type", "multipart/form-data; boundary=----LangChain4j")
    .addHeader("Authorization", "Bearer token123")
    .addFormDataField("title", "My Document")
    .addFormDataFile("file", "document.pdf", "application/pdf", fileBytes)
    .build();

SuccessfulHttpResponse response = client.execute(request);
System.out.println("Uploaded: " + response.body());

SSE Streaming (LLM Response)

import dev.langchain4j.http.client.jdk.JdkHttpClient;
import dev.langchain4j.http.client.HttpRequest;
import dev.langchain4j.http.client.HttpMethod;
import dev.langchain4j.http.client.sse.ServerSentEventListener;
import dev.langchain4j.http.client.sse.ServerSentEvent;
import java.time.Duration;
import java.util.concurrent.CountDownLatch;

JdkHttpClient client = JdkHttpClient.builder()
    .connectTimeout(Duration.ofSeconds(30))
    .readTimeout(Duration.ofMinutes(5))
    .build();

CountDownLatch latch = new CountDownLatch(1);

ServerSentEventListener listener = new ServerSentEventListener() {
    @Override
    public void onOpen(SuccessfulHttpResponse response) {
        System.out.println("Stream opened");
    }

    @Override
    public void onEvent(ServerSentEvent event) {
        if (!"[DONE]".equals(event.data())) {
            System.out.print(extractText(event.data()));
        }
    }

    @Override
    public void onClose() {
        System.out.println("\nStream closed");
        latch.countDown();
    }

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

    private String extractText(String json) {
        // Parse JSON and extract text (simplified)
        return json;
    }
};

HttpRequest request = HttpRequest.builder()
    .url("https://api.openai.com/v1/chat/completions")
    .method(HttpMethod.POST)
    .addHeader("Authorization", "Bearer " + System.getenv("OPENAI_API_KEY"))
    .addHeader("Content-Type", "application/json")
    .addHeader("Accept", "text/event-stream")
    .body("{\"model\":\"gpt-4\",\"messages\":[{\"role\":\"user\",\"content\":\"Hello\"}],\"stream\":true}")
    .build();

client.execute(request, listener);

// Wait for stream to complete
latch.await();

HTTP/2 with Advanced Configuration

import dev.langchain4j.http.client.jdk.JdkHttpClient;
import dev.langchain4j.http.client.HttpRequest;
import dev.langchain4j.http.client.HttpMethod;
import java.net.http.HttpClient;
import java.time.Duration;
import java.util.concurrent.Executors;

JdkHttpClient client = JdkHttpClient.builder()
    .httpClientBuilder(HttpClient.newBuilder()
        .version(HttpClient.Version.HTTP_2)
        .followRedirects(HttpClient.Redirect.NORMAL)
        .executor(Executors.newFixedThreadPool(4)))
    .connectTimeout(Duration.ofSeconds(30))
    .readTimeout(Duration.ofSeconds(60))
    .build();

HttpRequest request = HttpRequest.builder()
    .url("https://api.example.com/data")
    .method(HttpMethod.GET)
    .build();

SuccessfulHttpResponse response = client.execute(request);
System.out.println(response.body());

Error Handling (Comprehensive)

import dev.langchain4j.http.client.jdk.JdkHttpClient;
import dev.langchain4j.http.client.HttpRequest;
import dev.langchain4j.http.client.HttpMethod;
import dev.langchain4j.http.client.SuccessfulHttpResponse;
import dev.langchain4j.exception.HttpException;
import dev.langchain4j.exception.TimeoutException;
import java.time.Duration;

JdkHttpClient client = JdkHttpClient.builder()
    .connectTimeout(Duration.ofSeconds(10))
    .readTimeout(Duration.ofSeconds(30))
    .build();

HttpRequest request = HttpRequest.builder()
    .url("https://api.example.com/data")
    .method(HttpMethod.GET)
    .addHeader("Authorization", "Bearer token123")
    .build();

try {
    SuccessfulHttpResponse response = client.execute(request);
    System.out.println("Success: " + response.body());

} catch (HttpException e) {
    int status = e.statusCode();

    if (status == 400) {
        System.err.println("Bad request: " + e.getMessage());
    } else if (status == 401) {
        System.err.println("Unauthorized: Check credentials");
    } else if (status == 403) {
        System.err.println("Forbidden: Insufficient permissions");
    } else if (status == 404) {
        System.err.println("Not found: Resource does not exist");
    } else if (status == 429) {
        System.err.println("Rate limited: Retry after delay");
    } else if (status >= 500) {
        System.err.println("Server error: " + e.getMessage());
    } else {
        System.err.println("HTTP error " + status + ": " + e.getMessage());
    }

} catch (TimeoutException e) {
    System.err.println("Timeout: Request took too long");
    System.err.println("Consider increasing timeout or checking network");

} catch (RuntimeException e) {
    Throwable cause = e.getCause();

    if (cause instanceof java.net.UnknownHostException) {
        System.err.println("DNS error: Host not found");
    } else if (cause instanceof javax.net.ssl.SSLException) {
        System.err.println("SSL error: Certificate validation failed");
    } else if (cause instanceof java.io.IOException) {
        System.err.println("IO error: Network failure");
    } else if (cause instanceof InterruptedException) {
        System.err.println("Interrupted: Request cancelled");
        Thread.currentThread().interrupt();
    } else {
        System.err.println("Unexpected error: " + e.getMessage());
        e.printStackTrace();
    }
}

Migration Guide

From OkHttp to JDK HttpClient

OkHttp:

OkHttpClient client = new OkHttpClient.Builder()
    .connectTimeout(30, TimeUnit.SECONDS)
    .readTimeout(60, TimeUnit.SECONDS)
    .build();

Request request = new Request.Builder()
    .url("https://api.example.com/data")
    .header("Authorization", "Bearer token")
    .build();

Response response = client.newCall(request).execute();
String body = response.body().string();

JDK HttpClient (LangChain4j):

JdkHttpClient client = JdkHttpClient.builder()
    .connectTimeout(Duration.ofSeconds(30))
    .readTimeout(Duration.ofSeconds(60))
    .build();

HttpRequest request = HttpRequest.builder()
    .url("https://api.example.com/data")
    .method(HttpMethod.GET)
    .addHeader("Authorization", "Bearer token")
    .build();

SuccessfulHttpResponse response = client.execute(request);
String body = response.body();

Key Differences:

  • No external dependencies (JDK only vs OkHttp library)
  • Different exception types (HttpException vs IOException)
  • Method enum required (HttpMethod.GET vs inferred from builder)
  • No response body closing required (string already extracted)

From Apache HttpClient to JDK HttpClient

Apache HttpClient:

CloseableHttpClient client = HttpClients.custom()
    .setDefaultRequestConfig(RequestConfig.custom()
        .setConnectTimeout(30000)
        .setSocketTimeout(60000)
        .build())
    .build();

HttpPost post = new HttpPost("https://api.example.com/data");
post.setHeader("Content-Type", "application/json");
post.setEntity(new StringEntity("{\"key\":\"value\"}"));

CloseableHttpResponse response = client.execute(post);
String body = EntityUtils.toString(response.getEntity());
response.close();

JDK HttpClient (LangChain4j):

JdkHttpClient client = JdkHttpClient.builder()
    .connectTimeout(Duration.ofSeconds(30))
    .readTimeout(Duration.ofSeconds(60))
    .build();

HttpRequest request = HttpRequest.builder()
    .url("https://api.example.com/data")
    .method(HttpMethod.POST)
    .addHeader("Content-Type", "application/json")
    .body("{\"key\":\"value\"}")
    .build();

SuccessfulHttpResponse response = client.execute(request);
String body = response.body();

Key Differences:

  • No explicit resource closing required
  • Simpler builder API
  • Automatic UTF-8 encoding
  • Different exception types
Workspace
tessl
Visibility
Public
Created
Last updated
Describes
mavenpkg:maven/dev.langchain4j/langchain4j-http-client-jdk@1.11.x
Publish Source
CLI
Badge
tessl/maven-dev-langchain4j--langchain4j-http-client-jdk badge