JDK HttpClient implementation for LangChain4j HTTP client interface
JDK HttpClient implementation for LangChain4j HTTP client interface. Wraps java.net.http.HttpClient (Java 11+) with zero external dependencies.
<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'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;// 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);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.
JdkHttpClient implements dev.langchain4j.http.client.HttpClientJdkHttpClientBuilder implements dev.langchain4j.http.client.HttpClientBuilderJdkHttpClientBuilderFactory implements dev.langchain4j.http.client.HttpClientBuilderFactoryJdkHttpClient instances are immutable and thread-safe after construction. Can be shared across threads.HttpClient. Pool size and behavior controlled via HttpClient.Builder.HttpClient.Builder).HttpException, TimeoutException).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.
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 defaultsThread Safety: Not thread-safe. Use from single thread during construction.
Validation:
connectTimeout and readTimeout must not be null or negative if setIllegalArgumentException for invalid configurationsService 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.
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.
null = use JDK default (system-dependent, typically OS TCP timeout)TimeoutException on expiryRead Timeout: Maximum time to receive response after request sent.
null = use JDK default (typically unlimited)TimeoutException on expiryEdge Cases:
readTimeout may hold threads; use appropriate valuesConfigure 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 onlyHttpClient.Version.HTTP_2: HTTP/2 with fallback to HTTP/1.1Redirect 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 downgradeNEVERProxy:
ProxySelector.of(InetSocketAddress): Fixed proxy for all requestsProxySelector.getDefault(): System default proxyProxySelector.getDefault()SSL/TLS:
sslContext(SSLContext): Custom SSL context for certificate validation, cipher suitessslParameters(SSLParameters): Fine-grained SSL control (protocols, cipher suites, SNI)Executor:
Executor for async operations (SSE streaming, async requests)Authenticator:
Cookie Handler:
CookieManager for automatic cookie managementConnection Priority (HTTP/2 only):
Important Notes:
httpClientBuilder() settings override timeout settings if conflictingHttpClient.Builder (not directly exposed)// 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();// 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();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 URLmethod must be non-null and one of GET, POST, DELETEbody and form data are mutually exclusiveIllegalArgumentException for invalid configurations// 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/dataURL Construction:
url(String): Used as-isurl(String baseUrl, String path): Joined with / separator (handles trailing/leading slashes)Supported Schemes: http://, https://
Edge Cases:
https://[::1]:8080/path#fragment) preserved but ignored by HTTP client// 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=paramURL Encoding:
-_.~ unreserved, others %-encoded&= in values encoded to %26, %3D%20 (not +)Multiple Values:
?key=val1&key=val2addQueryParam() multiple times for same nameEdge Cases:
NullPointerExceptionparam=// 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:
addHeader(name, value...): Appends values to existing headeraddHeaders(Map): Appends headers from map (single value per name)headers(Map<String, List<String>>): Replaces all headers (multiple values per name), in HTTP messageStandard 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, ViaEdge Cases:
NullPointerExceptionHeader-Name: // 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:
Content-Type header should specify charset if not UTF-8Body with HTTP Methods:
Edge Cases:
Mutually Exclusive:
body() and form data (formDataFields / formDataFiles)IllegalArgumentException on build()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:
----LangChain4jContent-Type header: multipart/form-data; boundary=----LangChain4jFormDataFile:
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 fieldformDataFields(Map): Replace all text fieldsaddFormDataFile(name, fileName, contentType, bytes): Add fileformDataFiles(Map): Replace all filesContent-Disposition Headers:
Content-Disposition: form-data; name="fieldName"Content-Disposition: form-data; name="fieldName"; filename="file.pdf"Edge Cases:
Memory Considerations:
OutOfMemoryErrorpublic 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
statusCode() and response body via getMessage()dev.langchain4j.exception.TimeoutException: Request timed out
java.net.http.HttpTimeoutExceptionRuntimeException: IO errors, interrupted thread, or other failuresExample:
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());
}Success (200-299):
SuccessfulHttpResponseClient Errors (400-499):
HttpException with status code and bodyServer Errors (500-599):
HttpException with status code and bodyRedirects (300-399):
HttpClient.Builder.followRedirects() configuredHttpException with redirect status code and Location header in bodyContent-Type: ...; charset=<encoding>, still decoded as UTF-8 (charset ignored)execute(), throws RuntimeException wrapping InterruptedExceptionHttpClientpublic 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 formatlistener: ServerSentEventListener for event callbacksReturns: void (asynchronous execution via internal CompletableFuture)
Execution Flow:
listener.onOpen(response) calledparser, events delivered to listener.onEvent()listener.onClose() calledlistener.onError(throwable) calledError Handling:
onError() with HttpExceptiononError() with TimeoutExceptiononError() with RuntimeExceptiononError() with parsing exceptionExample:
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 threadsdefault 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);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-299onEvent(event) or onEvent(event, context): Each SSE event receivedonClose(): Stream ended normallyonError(throwable): Error occurred (HTTP error, timeout, IO error, parse error)Callback Threading:
HttpClientCallback Error Handling:
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();
}
}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)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.
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()readTimeout applies to entire SSE stream duration, not individual eventsreadTimeout settingonError(TimeoutException) and closes streamExample:
// For long-running SSE streams
JdkHttpClient client = JdkHttpClient.builder()
.connectTimeout(Duration.ofSeconds(30))
.readTimeout(Duration.ofHours(1)) // Allow 1-hour stream
.build();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());
}
});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
Content-Type, Content-Length, Date, Server, Set-Cookiebody(): Response body decoded as UTF-8 string
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) == trueContent-Type charsetWorkaround for Non-UTF-8:
body() returns empty string ""body() returns empty string ""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:
Contains:
statusCode(): HTTP status code (e.g., 404, 500)getMessage(): Full response body as stringExample:
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");
}
}package dev.langchain4j.exception;
public class TimeoutException extends RuntimeException {
public TimeoutException(Throwable cause); // Wraps HttpTimeoutException
}Thrown When:
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
}IO Errors:
java.io.IOException wrapped in RuntimeExceptionUnknownHostException wrapped in RuntimeExceptionSSLException wrapped in RuntimeExceptionThread Interruption:
InterruptedException wrapped in RuntimeExceptionExample:
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
}
}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.JdkHttpClient: Immutable and thread-safe after construction.
HttpClient is thread-safeJdkHttpClientBuilder: Not thread-safe.
HttpRequest: Immutable and thread-safe.
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-safeConnection Pooling:
HttpClientHTTP/2 Benefits:
Resource Management:
JdkHttpClientMemory Considerations:
OutOfMemoryErrorOptimization Recommendations:
JdkHttpClient instance across applicationJdkHttpClient client = JdkHttpClient.builder()
.httpClientBuilder(HttpClient.newBuilder()
.version(HttpClient.Version.HTTP_2))
.build();readTimeout appropriate to expected stream durationBenchmarks (approximate, system-dependent):
URL Limitations:
https://[::1]:8080Header Limitations:
Content-Length, Host, Connection (managed by JDK)Cookie header (or use CookieManager in HttpClient.Builder)Body Limitations:
OutOfMemoryErrorTimeout Precision:
IllegalArgumentExceptionRedirect Handling:
SSE Limitations:
Concurrency Limitations:
Platform-Specific:
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());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());
}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());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();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());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();
}
}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:
HttpException vs IOException)HttpMethod.GET vs inferred from builder)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:
Install with Tessl CLI
npx tessl i tessl/maven-dev-langchain4j--langchain4j-http-client-jdk