JDK HttpClient implementation for LangChain4j HTTP client interface
—
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Pending
The risk profile of this skill
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: