CtrlK
BlogDocsLog inGet started
Tessl Logo

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

JDK HttpClient implementation for LangChain4j HTTP client interface

Overview
Eval results
Files

index.mddocs/

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

Install with Tessl CLI

npx tessl i tessl/maven-dev-langchain4j--langchain4j-http-client-jdk@1.11.0

docs

index.md

tile.json