The filter automatically releases permits when requests complete (either successfully or with an error),
+ * using the {@link ReleasePermitOnComplete} wrapper.
+ *
+ * {
private volatile Response response;
private volatile Throwable throwable;
+ /**
+ * Creates a new BodyDeferringAsyncHandler with the specified output stream.
+ *
+ * @param os the output stream where the response body will be written
+ */
public BodyDeferringAsyncHandler(final OutputStream os) {
this.output = os;
this.responseSet = false;
}
+ /**
+ * Handles exceptions that occur during the request processing.
+ * This method ensures the latch is released and the output stream is closed
+ * to prevent blocking threads waiting on {@link #getResponse()}.
+ *
+ * @param t the exception that occurred during request processing
+ */
@Override
public void onThrowable(Throwable t) {
this.throwable = t;
@@ -121,30 +133,69 @@ public void onThrowable(Throwable t) {
}
}
+ /**
+ * Processes the HTTP response status line.
+ * Resets and begins building the response object.
+ *
+ * @param responseStatus the HTTP response status received from the server
+ * @return {@link State#CONTINUE} to proceed with the request processing
+ * @throws Exception if an error occurs while processing the status
+ */
@Override
- public State onStatusReceived(HttpResponseStatus responseStatus) {
+ public State onStatusReceived(HttpResponseStatus responseStatus) throws Exception {
responseBuilder.reset();
responseBuilder.accumulate(responseStatus);
return State.CONTINUE;
}
+ /**
+ * Processes the HTTP response headers.
+ * Accumulates headers for the response that will be available via {@link #getResponse()}.
+ *
+ * @param headers the HTTP response headers received from the server
+ * @return {@link State#CONTINUE} to proceed with the request processing
+ * @throws Exception if an error occurs while processing the headers
+ */
@Override
- public State onHeadersReceived(HttpHeaders headers) {
+ public State onHeadersReceived(HttpHeaders headers) throws Exception {
responseBuilder.accumulate(headers);
return State.CONTINUE;
}
+ /**
+ * Processes trailing HTTP headers (HTTP/2 and chunked transfer encoding).
+ * Accumulates trailing headers for the final response.
+ *
+ * @param headers the trailing HTTP headers received from the server
+ * @return {@link State#CONTINUE} to proceed with the request processing
+ * @throws Exception if an error occurs while processing the trailing headers
+ */
@Override
- public State onTrailingHeadersReceived(HttpHeaders headers) {
+ public State onTrailingHeadersReceived(HttpHeaders headers) throws Exception {
responseBuilder.accumulate(headers);
return State.CONTINUE;
}
+ /**
+ * Invoked when the request is being retried.
+ * This handler does not support retries due to the streaming nature of body handling.
+ *
+ * @throws UnsupportedOperationException always thrown as retries are not supported
+ */
@Override
public void onRetry() {
throw new UnsupportedOperationException(this.getClass().getSimpleName() + " cannot retry a request.");
}
+ /**
+ * Processes a chunk of the response body.
+ * On receiving the first body chunk, the response headers are finalized and made available
+ * via {@link #getResponse()}. The body bytes are immediately written to the output stream.
+ *
+ * @param bodyPart the chunk of response body received from the server
+ * @return {@link State#CONTINUE} to proceed with the request processing
+ * @throws Exception if an error occurs while processing or writing the body part
+ */
@Override
public State onBodyPartReceived(HttpResponseBodyPart bodyPart) throws Exception {
// body arrived, flush headers
@@ -158,6 +209,11 @@ public State onBodyPartReceived(HttpResponseBodyPart bodyPart) throws Exception
return State.CONTINUE;
}
+ /**
+ * Flushes and closes the output stream.
+ *
+ * @throws IOException if an I/O error occurs while flushing or closing the stream
+ */
protected void closeOut() throws IOException {
try {
output.flush();
@@ -166,6 +222,14 @@ protected void closeOut() throws IOException {
}
}
+ /**
+ * Completes the request processing.
+ * Ensures the response is finalized, releases waiting threads, closes the output stream,
+ * and returns the complete response.
+ *
+ * @return the complete {@link Response} object
+ * @throws IOException if an error occurs while completing the request or an exception was thrown earlier
+ */
@Override
public Response onCompleted() throws IOException {
@@ -200,23 +264,24 @@ public Response onCompleted() throws IOException {
}
/**
- * This method -- unlike Future<Reponse>.get() -- will block only as long,
- * as headers arrive. This is useful for large transfers, to examine headers
- * ASAP, and defer body streaming to it's fine destination and prevent
- * unneeded bandwidth consumption. The response here will contain the very
- * 1st response from server, so status code and headers, but it might be
- * incomplete in case of broken servers sending trailing headers. In that
- * case, the "usual" Future<Response>.get() method will return complete
- * headers, but multiple invocations of getResponse() will always return the
- * 1st cached, probably incomplete one. Note: the response returned by this
- * method will contain everything except the response body itself,
- * so invoking any method like Response.getResponseBodyXXX() will result in
- * error! Also, please not that this method might return null
- * in case of some errors.
+ * Retrieves the response as soon as headers are available, without waiting for the body.
+ *
+ * Unlike {@code Future.get()}, this method blocks only until headers arrive,
+ * making it useful for large transfers where you need to examine headers immediately
+ * and defer body streaming to prevent unnecessary bandwidth consumption.
+ *
+ * The returned response contains the initial response from the server (status code and headers),
+ * but may be incomplete if the server sends trailing headers. The complete headers can be
+ * obtained via {@code Future.get()}, but multiple invocations of this method
+ * will always return the same cached, potentially incomplete response.
+ *
+ * Important: The response returned by this method does not contain the response body.
+ * Invoking any {@code Response.getResponseBodyXXX()} method will result in an error.
+ * This method may return {@code null} in case of errors.
*
- * @return a {@link Response}
- * @throws InterruptedException if the latch is interrupted
- * @throws IOException if the handler completed with an exception
+ * @return a {@link Response} containing status and headers (but no body), or {@code null} on error
+ * @throws InterruptedException if the current thread is interrupted while waiting for headers
+ * @throws IOException if the handler completed with an exception
*/
public Response getResponse() throws InterruptedException, IOException {
// block here as long as headers arrive
@@ -237,14 +302,24 @@ public Response getResponse() throws InterruptedException, IOException {
// ==
/**
- * A simple helper class that is used to perform automatic "join" for async
- * download and the error checking of the Future of the request.
+ * A helper input stream that automatically "joins" (waits for completion of) the async
+ * download and performs error checking on the request's Future.
+ *
+ * This class wraps an input stream connected to the response body and ensures proper
+ * cleanup and error handling when the stream is closed.
*/
public static class BodyDeferringInputStream extends FilterInputStream {
private final Future future;
private final BodyDeferringAsyncHandler bdah;
+ /**
+ * Creates a new BodyDeferringInputStream.
+ *
+ * @param future the future representing the async request
+ * @param bdah the handler managing the request
+ * @param in the input stream containing the response body
+ */
public BodyDeferringInputStream(final Future future, final BodyDeferringAsyncHandler bdah, final InputStream in) {
super(in);
this.future = future;
@@ -252,8 +327,12 @@ public BodyDeferringInputStream(final Future future, final BodyDeferri
}
/**
- * Closes the input stream, and "joins" (wait for complete execution
- * together with potential exception thrown) of the async request.
+ * Closes the input stream and waits for the async request to complete.
+ *
+ * This method ensures the async request finishes completely and
+ * propagates any exceptions that occurred during the request.
+ *
+ * @throws IOException if an I/O error occurs or if the request completed with an exception
*/
@Override
public void close() throws IOException {
@@ -270,26 +349,29 @@ public void close() throws IOException {
}
/**
- * Delegates to {@link BodyDeferringAsyncHandler#getResponse()}. Will
- * blocks as long as headers arrives only. Might return
- * null. See
- * {@link BodyDeferringAsyncHandler#getResponse()} method for details.
+ * Retrieves the response as soon as headers are available.
+ *
+ * Delegates to {@link BodyDeferringAsyncHandler#getResponse()}.
+ * Blocks only until headers arrive. May return {@code null} in case of errors.
+ * See {@link BodyDeferringAsyncHandler#getResponse()} for details.
*
- * @return a {@link Response}
- * @throws InterruptedException if the latch is interrupted
- * @throws IOException if the handler completed with an exception
+ * @return a {@link Response} containing status and headers (but no body), or {@code null} on error
+ * @throws InterruptedException if the current thread is interrupted while waiting for headers
+ * @throws IOException if the handler completed with an exception
*/
public Response getAsapResponse() throws InterruptedException, IOException {
return bdah.getResponse();
}
/**
- * Delegates to Future$lt;Response>#get() method. Will block
- * as long as complete response arrives.
+ * Retrieves the complete response after it has fully arrived.
+ *
+ * Delegates to {@code Future#get()}. Blocks until the complete response
+ * (including body) has been received.
*
- * @return a {@link Response}
- * @throws ExecutionException if the computation threw an exception
- * @throws InterruptedException if the current thread was interrupted
+ * @return the complete {@link Response}
+ * @throws ExecutionException if the request threw an exception
+ * @throws InterruptedException if the current thread was interrupted while waiting
*/
public Response getLastResponse() throws InterruptedException, ExecutionException {
return future.get();
diff --git a/client/src/main/java/org/asynchttpclient/handler/MaxRedirectException.java b/client/src/main/java/org/asynchttpclient/handler/MaxRedirectException.java
index 9deb452ef8..ba305ecd87 100644
--- a/client/src/main/java/org/asynchttpclient/handler/MaxRedirectException.java
+++ b/client/src/main/java/org/asynchttpclient/handler/MaxRedirectException.java
@@ -14,11 +14,43 @@
package org.asynchttpclient.handler;
/**
- * Thrown when the {@link org.asynchttpclient.DefaultAsyncHttpClientConfig#getMaxRedirects()} has been reached.
+ * Exception thrown when the maximum number of redirects has been exceeded.
+ *
+ * This exception is thrown when a request follows more HTTP redirects than the limit
+ * configured via {@link org.asynchttpclient.DefaultAsyncHttpClientConfig#getMaxRedirects()}.
+ * The default maximum is typically 5 redirects, but can be configured per client.
+ *
+ * This prevents infinite redirect loops and excessive redirect chains that could
+ * indicate a misconfigured server or a malicious redirect attack.
+ *
+ *
Example:
+ * {@code
+ * AsyncHttpClient client = new DefaultAsyncHttpClient(
+ * new DefaultAsyncHttpClientConfig.Builder()
+ * .setMaxRedirects(3)
+ * .build()
+ * );
+ *
+ * try {
+ * client.prepareGet("http://example.com/redirect-loop").execute().get();
+ * } catch (ExecutionException e) {
+ * if (e.getCause() instanceof MaxRedirectException) {
+ * // Handle too many redirects
+ * }
+ * }
+ * }
*/
public class MaxRedirectException extends Exception {
private static final long serialVersionUID = 1L;
+ /**
+ * Creates a new MaxRedirectException with the specified message.
+ *
+ * The exception is created with suppression and stack trace writing disabled
+ * for performance reasons, as this is typically a well-understood control flow exception.
+ *
+ * @param msg the detail message explaining why the maximum redirect limit was exceeded
+ */
public MaxRedirectException(String msg) {
super(msg, null, true, false);
}
diff --git a/client/src/main/java/org/asynchttpclient/handler/ProgressAsyncHandler.java b/client/src/main/java/org/asynchttpclient/handler/ProgressAsyncHandler.java
index 556ce30065..9e087cef27 100644
--- a/client/src/main/java/org/asynchttpclient/handler/ProgressAsyncHandler.java
+++ b/client/src/main/java/org/asynchttpclient/handler/ProgressAsyncHandler.java
@@ -16,35 +16,80 @@
import org.asynchttpclient.Request;
/**
- * An extended {@link AsyncHandler} with two extra callback who get invoked during the content upload to a remote server.
- * This {@link AsyncHandler} must be used only with PUT and POST request.
+ * An extended {@link AsyncHandler} with additional callbacks invoked during content upload to a remote server.
+ *
+ * This interface provides progress tracking for request body uploads, making it useful for
+ * monitoring file uploads or large POST/PUT requests. It adds three methods to the standard
+ * {@link AsyncHandler} that are called at different stages of the upload process.
+ *
+ * This handler should be used with PUT and POST requests that include a request body.
+ *
+ *
Usage Example:
+ * {@code
+ * ProgressAsyncHandler handler = new AsyncCompletionHandlerBase() {
+ * @Override
+ * public State onHeadersWritten() {
+ * System.out.println("Request headers sent");
+ * return State.CONTINUE;
+ * }
+ *
+ * @Override
+ * public State onContentWriteProgress(long amount, long current, long total) {
+ * int percent = (int) ((current * 100.0) / total);
+ * System.out.println("Upload progress: " + percent + "%");
+ * return State.CONTINUE;
+ * }
+ *
+ * @Override
+ * public State onContentWritten() {
+ * System.out.println("Request body fully sent");
+ * return State.CONTINUE;
+ * }
+ * };
+ *
+ * client.preparePost("http://example.com/upload")
+ * .setBody(largeFile)
+ * .execute(handler);
+ * }
+ *
+ * @param the response type
*/
public interface ProgressAsyncHandler extends AsyncHandler {
/**
- * Invoked when the content (a {@link java.io.File}, {@link String} or {@link java.io.FileInputStream} has been fully
- * written on the I/O socket.
+ * Invoked when the request headers have been fully written to the I/O socket.
+ *
+ * This callback is triggered after all HTTP headers have been sent to the server,
+ * but before the request body (if any) is transmitted.
*
- * @return a {@link AsyncHandler.State} telling to CONTINUE or ABORT the current processing.
+ * @return {@link State#CONTINUE} to proceed with sending the request body, or
+ * {@link State#ABORT} to cancel the request
*/
State onHeadersWritten();
/**
- * Invoked when the content (a {@link java.io.File}, {@link String} or {@link java.io.FileInputStream} has been fully
- * written on the I/O socket.
+ * Invoked when the request body has been fully written to the I/O socket.
+ *
+ * This callback is triggered after the entire request body (such as a {@link java.io.File},
+ * {@link String}, or {@link java.io.InputStream}) has been completely transmitted to the server.
*
- * @return a {@link AsyncHandler.State} telling to CONTINUE or ABORT the current processing.
+ * @return {@link State#CONTINUE} to proceed with receiving the response, or
+ * {@link State#ABORT} to cancel the request
*/
State onContentWritten();
/**
- * Invoked when the I/O operation associated with the {@link Request} body wasn't fully written in a single I/O write
- * operation. This method is never invoked if the write operation complete in a sinfle I/O write.
+ * Invoked periodically during the request body upload to report progress.
+ *
+ * This callback is triggered when the I/O operation associated with the {@link Request} body
+ * requires multiple write operations. It is never invoked if the entire body is written
+ * in a single I/O operation. This allows tracking upload progress for large request bodies.
*
- * @param amount The amount of bytes to transfer.
- * @param current The amount of bytes transferred
- * @param total The total number of bytes transferred
- * @return a {@link AsyncHandler.State} telling to CONTINUE or ABORT the current processing.
+ * @param amount the number of bytes written in the current write operation
+ * @param current the cumulative number of bytes written so far
+ * @param total the total number of bytes to be written
+ * @return {@link State#CONTINUE} to proceed with the upload, or
+ * {@link State#ABORT} to cancel the request
*/
State onContentWriteProgress(long amount, long current, long total);
}
diff --git a/client/src/main/java/org/asynchttpclient/handler/StreamedAsyncHandler.java b/client/src/main/java/org/asynchttpclient/handler/StreamedAsyncHandler.java
index 2438cd0e71..e9ae765d61 100644
--- a/client/src/main/java/org/asynchttpclient/handler/StreamedAsyncHandler.java
+++ b/client/src/main/java/org/asynchttpclient/handler/StreamedAsyncHandler.java
@@ -17,15 +17,69 @@
import org.reactivestreams.Publisher;
/**
- * AsyncHandler that uses reactive streams to handle the request.
+ * An {@link AsyncHandler} that uses reactive streams to handle HTTP response bodies.
+ *
+ * This interface extends {@link AsyncHandler} to provide streaming capabilities through
+ * the Reactive Streams API. Instead of receiving body parts one at a time through
+ * {@link AsyncHandler#onBodyPartReceived}, implementations receive a {@link Publisher}
+ * that produces body parts, enabling backpressure-aware consumption of the response body.
+ *
+ * The {@link #onStream} method is invoked once when the response body begins streaming.
+ * It may not be called if the response has no body.
+ *
+ *
Usage Example:
+ * {@code
+ * StreamedAsyncHandler handler = new StreamedAsyncHandler() {
+ * @Override
+ * public State onStream(Publisher publisher) {
+ * publisher.subscribe(new Subscriber() {
+ * private Subscription subscription;
+ *
+ * @Override
+ * public void onSubscribe(Subscription s) {
+ * this.subscription = s;
+ * s.request(1); // Request first item
+ * }
+ *
+ * @Override
+ * public void onNext(HttpResponseBodyPart bodyPart) {
+ * // Process body part
+ * subscription.request(1); // Request next item
+ * }
+ *
+ * @Override
+ * public void onError(Throwable t) {
+ * // Handle error
+ * }
+ *
+ * @Override
+ * public void onComplete() {
+ * // Body streaming complete
+ * }
+ * });
+ * return State.CONTINUE;
+ * }
+ *
+ * // Implement other AsyncHandler methods...
+ * };
+ * }
+ *
+ * @param the response type
*/
public interface StreamedAsyncHandler extends AsyncHandler {
/**
- * Called when the body is received. May not be called if there's no body.
+ * Invoked when the response body begins streaming.
+ *
+ * This method provides a {@link Publisher} of {@link HttpResponseBodyPart} objects
+ * that can be consumed using the Reactive Streams API. The publisher will emit body
+ * parts as they arrive from the server, allowing for backpressure-aware processing.
+ *
+ * This method may not be called if the response has no body (e.g., HEAD requests
+ * or 204 No Content responses).
*
- * @param publisher The publisher of response body parts.
- * @return Whether to continue or abort.
+ * @param publisher the publisher that will emit response body parts
+ * @return {@link State#CONTINUE} to proceed with processing, or {@link State#ABORT} to cancel the request
*/
State onStream(Publisher publisher);
}
diff --git a/client/src/main/java/org/asynchttpclient/handler/TransferCompletionHandler.java b/client/src/main/java/org/asynchttpclient/handler/TransferCompletionHandler.java
index d3baff0ca0..97a46a8a43 100644
--- a/client/src/main/java/org/asynchttpclient/handler/TransferCompletionHandler.java
+++ b/client/src/main/java/org/asynchttpclient/handler/TransferCompletionHandler.java
@@ -22,36 +22,61 @@
import java.util.concurrent.ConcurrentLinkedQueue;
/**
- * A {@link org.asynchttpclient.AsyncHandler} that can be used to notify a set of {@link TransferListener}
- *
- *
- *
- * AsyncHttpClient client = new AsyncHttpClient();
- * TransferCompletionHandler tl = new TransferCompletionHandler();
- * tl.addTransferListener(new TransferListener() {
+ * An {@link org.asynchttpclient.AsyncHandler} that notifies registered {@link TransferListener}s
+ * of various transfer events during request and response processing.
+ *
+ * This handler extends {@link AsyncCompletionHandlerBase} and provides a mechanism to track
+ * the progress of HTTP requests and responses by notifying listeners at key points:
+ *
+ * Request headers sent
+ * Response headers received
+ * Bytes sent (upload progress)
+ * Bytes received (download progress)
+ * Request/response completion
+ * Exceptions
+ *
+ *
+ * By default, this handler does not accumulate response bytes in memory. To access the
+ * response body via {@link org.asynchttpclient.Response#getResponseBody()}, construct
+ * the handler with {@code accumulateResponseBytes = true}.
*
- * public void onRequestHeadersSent(HttpHeaders headers) {
- * }
+ *
Usage Example:
+ * {@code
+ * TransferCompletionHandler handler = new TransferCompletionHandler();
+ * handler.addTransferListener(new TransferListener() {
+ * @Override
+ * public void onRequestHeadersSent(HttpHeaders headers) {
+ * System.out.println("Request headers sent");
+ * }
*
- * public void onResponseHeadersReceived(HttpHeaders headers) {
- * }
+ * @Override
+ * public void onResponseHeadersReceived(HttpHeaders headers) {
+ * System.out.println("Response headers received");
+ * }
*
- * public void onBytesReceived(ByteBuffer buffer) {
- * }
+ * @Override
+ * public void onBytesReceived(byte[] bytes) {
+ * System.out.println("Received " + bytes.length + " bytes");
+ * }
*
- * public void onBytesSent(long amount, long current, long total) {
- * }
+ * @Override
+ * public void onBytesSent(long amount, long current, long total) {
+ * System.out.println("Sent " + current + " of " + total + " bytes");
+ * }
*
- * public void onRequestResponseCompleted() {
- * }
+ * @Override
+ * public void onRequestResponseCompleted() {
+ * System.out.println("Transfer completed");
+ * }
*
- * public void onThrowable(Throwable t) {
- * }
+ * @Override
+ * public void onThrowable(Throwable t) {
+ * System.err.println("Error: " + t.getMessage());
+ * }
* });
*
- * Response response = httpClient.prepareGet("http://...").execute(tl).get();
- *
- *
+ * Response response = client.prepareGet("http://example.com").execute(handler).get();
+ * }
*/
public class TransferCompletionHandler extends AsyncCompletionHandlerBase {
private final static Logger logger = LoggerFactory.getLogger(TransferCompletionHandler.class);
@@ -60,49 +85,108 @@ public class TransferCompletionHandler extends AsyncCompletionHandlerBase {
private HttpHeaders headers;
/**
- * Create a TransferCompletionHandler that will not accumulate bytes. The resulting {@link org.asynchttpclient.Response#getResponseBody()},
- * {@link org.asynchttpclient.Response#getResponseBodyAsStream()} will throw an IllegalStateException if called.
+ * Creates a TransferCompletionHandler that does not accumulate response bytes.
+ *
+ * When using this constructor, the response body is not stored in memory.
+ * Attempting to call {@link org.asynchttpclient.Response#getResponseBody()} or
+ * {@link org.asynchttpclient.Response#getResponseBodyAsStream()} will throw an
+ * {@link IllegalStateException}.
+ *
+ * This is useful when you only need to track transfer progress through listeners
+ * and don't need to access the response body through the Response object.
*/
public TransferCompletionHandler() {
this(false);
}
/**
- * Create a TransferCompletionHandler that can or cannot accumulate bytes and make it available when {@link org.asynchttpclient.Response#getResponseBody()} get called. The
- * default is false.
+ * Creates a TransferCompletionHandler with optional response byte accumulation.
+ *
+ * When {@code accumulateResponseBytes} is {@code true}, the response body is stored
+ * in memory and can be accessed via {@link org.asynchttpclient.Response#getResponseBody()}.
+ * When {@code false}, response body methods will throw an {@link IllegalStateException}.
*
- * @param accumulateResponseBytes true to accumulates bytes in memory.
+ * @param accumulateResponseBytes {@code true} to accumulate response bytes in memory,
+ * {@code false} to skip accumulation
*/
public TransferCompletionHandler(boolean accumulateResponseBytes) {
this.accumulateResponseBytes = accumulateResponseBytes;
}
+ /**
+ * Adds a listener to be notified of transfer events.
+ *
+ * Multiple listeners can be added. All registered listeners will be notified
+ * of transfer events in the order they were added.
+ *
+ * @param t the listener to add
+ * @return this handler for method chaining
+ */
public TransferCompletionHandler addTransferListener(TransferListener t) {
listeners.offer(t);
return this;
}
+ /**
+ * Removes a previously registered listener.
+ *
+ * If the listener was added multiple times, only the first occurrence is removed.
+ *
+ * @param t the listener to remove
+ * @return this handler for method chaining
+ */
public TransferCompletionHandler removeTransferListener(TransferListener t) {
listeners.remove(t);
return this;
}
+ /**
+ * Sets the request headers that will be sent to listeners when {@link #onHeadersWritten()} is called.
+ *
+ * This method should be called before the request is executed to ensure listeners
+ * are notified with the correct headers.
+ *
+ * @param headers the request headers to be sent to listeners
+ */
public void headers(HttpHeaders headers) {
this.headers = headers;
}
+ /**
+ * Processes response headers and notifies all registered listeners.
+ *
+ * @param headers the HTTP response headers received from the server
+ * @return the state returned by the superclass method
+ * @throws Exception if an error occurs during processing
+ */
@Override
public State onHeadersReceived(final HttpHeaders headers) throws Exception {
fireOnHeaderReceived(headers);
return super.onHeadersReceived(headers);
}
+ /**
+ * Processes trailing response headers and notifies all registered listeners.
+ *
+ * @param headers the trailing HTTP response headers received from the server
+ * @return the state returned by the superclass method
+ * @throws Exception if an error occurs during processing
+ */
@Override
public State onTrailingHeadersReceived(HttpHeaders headers) throws Exception {
fireOnHeaderReceived(headers);
return super.onHeadersReceived(headers);
}
+ /**
+ * Processes a response body chunk and notifies all registered listeners.
+ *
+ * If byte accumulation is enabled, delegates to the superclass to store the bytes.
+ *
+ * @param content the chunk of response body received from the server
+ * @return {@link State#CONTINUE} to proceed with processing
+ * @throws Exception if an error occurs during processing
+ */
@Override
public State onBodyPartReceived(final HttpResponseBodyPart content) throws Exception {
State s = State.CONTINUE;
@@ -113,12 +197,24 @@ public State onBodyPartReceived(final HttpResponseBodyPart content) throws Excep
return s;
}
+ /**
+ * Completes the request and notifies all registered listeners.
+ *
+ * @param response the complete response received from the server
+ * @return the response object
+ * @throws Exception if an error occurs during completion
+ */
@Override
public Response onCompleted(Response response) throws Exception {
fireOnEnd();
return response;
}
+ /**
+ * Invoked when request headers have been written, notifies all registered listeners.
+ *
+ * @return {@link State#CONTINUE} to proceed with the request
+ */
@Override
public State onHeadersWritten() {
if (headers != null) {
@@ -127,12 +223,25 @@ public State onHeadersWritten() {
return State.CONTINUE;
}
+ /**
+ * Invoked during request body upload to report progress, notifies all registered listeners.
+ *
+ * @param amount the number of bytes written in the current write operation
+ * @param current the cumulative number of bytes written so far
+ * @param total the total number of bytes to be written
+ * @return {@link State#CONTINUE} to proceed with the upload
+ */
@Override
public State onContentWriteProgress(long amount, long current, long total) {
fireOnBytesSent(amount, current, total);
return State.CONTINUE;
}
+ /**
+ * Handles exceptions that occur during request processing, notifies all registered listeners.
+ *
+ * @param t the exception that occurred
+ */
@Override
public void onThrowable(Throwable t) {
fireOnThrowable(t);
diff --git a/client/src/main/java/org/asynchttpclient/handler/TransferListener.java b/client/src/main/java/org/asynchttpclient/handler/TransferListener.java
index b733d5d40d..028fe377c8 100644
--- a/client/src/main/java/org/asynchttpclient/handler/TransferListener.java
+++ b/client/src/main/java/org/asynchttpclient/handler/TransferListener.java
@@ -15,49 +15,120 @@
import io.netty.handler.codec.http.HttpHeaders;
/**
- * A simple interface an application can implements in order to received byte transfer information.
+ * A listener interface for receiving notifications about HTTP request and response transfer events.
+ *
+ * Implementations of this interface can be registered with a {@link TransferCompletionHandler}
+ * to track the progress of HTTP requests and responses, including header transmission,
+ * byte transfer progress, completion, and error handling.
+ *
+ * This interface is particularly useful for:
+ *
+ * Monitoring upload and download progress
+ * Implementing custom logging or metrics collection
+ * Building progress bars or status indicators
+ * Debugging network communication
+ *
+ *
+ * Usage Example:
+ * {@code
+ * TransferListener listener = new TransferListener() {
+ * private long totalBytesReceived = 0;
+ *
+ * @Override
+ * public void onRequestHeadersSent(HttpHeaders headers) {
+ * System.out.println("Sent headers: " + headers.size());
+ * }
+ *
+ * @Override
+ * public void onResponseHeadersReceived(HttpHeaders headers) {
+ * System.out.println("Received headers: " + headers.size());
+ * }
+ *
+ * @Override
+ * public void onBytesReceived(byte[] bytes) {
+ * totalBytesReceived += bytes.length;
+ * System.out.println("Downloaded: " + totalBytesReceived + " bytes");
+ * }
+ *
+ * @Override
+ * public void onBytesSent(long amount, long current, long total) {
+ * int percent = (int) ((current * 100) / total);
+ * System.out.println("Upload: " + percent + "%");
+ * }
+ *
+ * @Override
+ * public void onRequestResponseCompleted() {
+ * System.out.println("Transfer complete");
+ * }
+ *
+ * @Override
+ * public void onThrowable(Throwable t) {
+ * System.err.println("Error: " + t.getMessage());
+ * }
+ * };
+ * }
*/
public interface TransferListener {
/**
- * Invoked when the request bytes are starting to get send.
+ * Invoked when the request headers begin to be sent to the server.
+ *
+ * This is called after the connection is established but before the request body
+ * (if any) is transmitted.
*
- * @param headers the headers
+ * @param headers the HTTP request headers being sent
*/
void onRequestHeadersSent(HttpHeaders headers);
/**
- * Invoked when the response bytes are starting to get received.
+ * Invoked when the response headers are received from the server.
+ *
+ * This is called after the server responds with headers but before the response
+ * body (if any) is received.
*
- * @param headers the headers
+ * @param headers the HTTP response headers received
*/
void onResponseHeadersReceived(HttpHeaders headers);
/**
- * Invoked every time response's chunk are received.
+ * Invoked each time a chunk of the response body is received.
+ *
+ * This method may be called multiple times for a single response as data arrives
+ * in chunks from the server. The frequency depends on network conditions and
+ * the size of the response.
*
- * @param bytes a {@link byte[]}
+ * @param bytes the chunk of response body bytes received
*/
void onBytesReceived(byte[] bytes);
/**
- * Invoked every time request's chunk are sent.
+ * Invoked periodically during the request body upload to report progress.
+ *
+ * This method is called multiple times as the request body is sent to the server,
+ * allowing tracking of upload progress. It is only invoked for requests that have
+ * a body (POST, PUT, etc.) and when the body requires multiple write operations.
*
- * @param amount The amount of bytes to transfer
- * @param current The amount of bytes transferred
- * @param total The total number of bytes transferred
+ * @param amount the number of bytes written in the current write operation
+ * @param current the cumulative number of bytes written so far
+ * @param total the total number of bytes to be written
*/
void onBytesSent(long amount, long current, long total);
/**
- * Invoked when the response bytes are been fully received.
+ * Invoked when the request and response transfer has been fully completed.
+ *
+ * This is called after all request bytes have been sent and all response bytes
+ * have been received, indicating successful completion of the HTTP transaction.
*/
void onRequestResponseCompleted();
/**
- * Invoked when there is an unexpected issue.
+ * Invoked when an error or exception occurs during the request or response processing.
+ *
+ * This method is called for any unexpected issues that occur during the HTTP
+ * transaction, including network errors, timeouts, or protocol violations.
*
- * @param t a {@link Throwable}
+ * @param t the exception or error that occurred
*/
void onThrowable(Throwable t);
}
diff --git a/client/src/main/java/org/asynchttpclient/handler/resumable/PropertiesBasedResumableProcessor.java b/client/src/main/java/org/asynchttpclient/handler/resumable/PropertiesBasedResumableProcessor.java
index 1eb99f11ba..b4014b9583 100644
--- a/client/src/main/java/org/asynchttpclient/handler/resumable/PropertiesBasedResumableProcessor.java
+++ b/client/src/main/java/org/asynchttpclient/handler/resumable/PropertiesBasedResumableProcessor.java
@@ -27,8 +27,28 @@
import static org.asynchttpclient.util.MiscUtils.closeSilently;
/**
- * A {@link org.asynchttpclient.handler.resumable.ResumableAsyncHandler.ResumableProcessor} which use a properties file
- * to store the download index information.
+ * A {@link ResumableAsyncHandler.ResumableProcessor} implementation that persists download progress
+ * to a properties file in the system temporary directory.
+ *
+ * This processor stores URL-to-byte-position mappings in a properties file, enabling
+ * downloads to resume after application restarts or crashes. The properties file is
+ * stored at {@code ${java.io.tmpdir}/ahc/ResumableAsyncHandler.properties}.
+ *
+ * The processor maintains an in-memory cache of the download state and persists it to
+ * disk when {@link #save(Map)} is called (typically during JVM shutdown via a shutdown hook).
+ *
+ *
Usage Example:
+ * {@code
+ * PropertiesBasedResumableProcessor processor = new PropertiesBasedResumableProcessor();
+ * ResumableAsyncHandler handler = new ResumableAsyncHandler(processor);
+ *
+ * // The processor automatically loads previous download state
+ * client.prepareGet("http://example.com/largefile.zip")
+ * .execute(handler)
+ * .get();
+ *
+ * // Download progress is automatically saved on JVM shutdown
+ * }
*/
public class PropertiesBasedResumableProcessor implements ResumableAsyncHandler.ResumableProcessor {
private final static Logger log = LoggerFactory.getLogger(PropertiesBasedResumableProcessor.class);
@@ -41,7 +61,13 @@ private static String append(Map.Entry e) {
}
/**
- * {@inheritDoc}
+ * Stores the number of bytes transferred for the specified URL.
+ *
+ * Updates the in-memory cache with the current download position. This information
+ * will be persisted to disk when {@link #save(Map)} is called.
+ *
+ * @param url the URL being downloaded (used as the key)
+ * @param transferredBytes the number of bytes successfully transferred so far
*/
@Override
public void put(String url, long transferredBytes) {
@@ -49,7 +75,12 @@ public void put(String url, long transferredBytes) {
}
/**
- * {@inheritDoc}
+ * Removes the download state for the specified URL.
+ *
+ * This is typically called when a download completes successfully, removing the
+ * entry from both the in-memory cache and (on next save) the persistent storage.
+ *
+ * @param uri the URL whose download state should be removed
*/
@Override
public void remove(String uri) {
@@ -59,7 +90,16 @@ public void remove(String uri) {
}
/**
- * {@inheritDoc}
+ * Persists the download state to a properties file.
+ *
+ * This method is typically invoked during JVM shutdown via a shutdown hook registered
+ * by {@link ResumableAsyncHandler}. It writes all URL-to-byte-position mappings to
+ * {@code ${java.io.tmpdir}/ahc/ResumableAsyncHandler.properties}.
+ *
+ * The method creates the necessary directories if they don't exist and handles
+ * errors gracefully by logging warnings rather than throwing exceptions.
+ *
+ * @param map the complete download state map (not used; this implementation uses internal state)
*/
@Override
public void save(Map map) {
@@ -91,7 +131,16 @@ public void save(Map map) {
}
/**
- * {@inheritDoc}
+ * Loads the previously saved download state from the properties file.
+ *
+ * This method is called when a {@link ResumableAsyncHandler} is created. It reads
+ * the properties file from {@code ${java.io.tmpdir}/ahc/ResumableAsyncHandler.properties}
+ * and loads all URL-to-byte-position mappings into memory.
+ *
+ * If the file doesn't exist or cannot be read, the method returns an empty map
+ * and logs appropriate messages.
+ *
+ * @return a map containing URL-to-byte-position mappings from the previous session
*/
@Override
public Map load() {
diff --git a/client/src/main/java/org/asynchttpclient/handler/resumable/ResumableAsyncHandler.java b/client/src/main/java/org/asynchttpclient/handler/resumable/ResumableAsyncHandler.java
index d6b671a270..46b47e2578 100644
--- a/client/src/main/java/org/asynchttpclient/handler/resumable/ResumableAsyncHandler.java
+++ b/client/src/main/java/org/asynchttpclient/handler/resumable/ResumableAsyncHandler.java
@@ -68,34 +68,95 @@ private ResumableAsyncHandler(long byteTransferred, ResumableProcessor resumable
this.accumulateBody = accumulateBody;
}
+ /**
+ * Creates a ResumableAsyncHandler starting from the specified byte position.
+ *
+ * Uses a NULL processor (no persistence) and does not accumulate response bytes.
+ *
+ * @param byteTransferred the number of bytes already transferred (for manual resume)
+ */
public ResumableAsyncHandler(long byteTransferred) {
this(byteTransferred, null, null, false);
}
+ /**
+ * Creates a ResumableAsyncHandler with optional response body accumulation.
+ *
+ * Uses a NULL processor (no persistence) and starts from byte 0.
+ *
+ * @param accumulateBody {@code true} to accumulate response bytes in memory for later access
+ */
public ResumableAsyncHandler(boolean accumulateBody) {
this(0, null, null, accumulateBody);
}
+ /**
+ * Creates a ResumableAsyncHandler with default settings.
+ *
+ * Uses a NULL processor (no persistence), starts from byte 0, and does not accumulate response bytes.
+ */
public ResumableAsyncHandler() {
this(0, null, null, false);
}
+ /**
+ * Creates a ResumableAsyncHandler that decorates another handler.
+ *
+ * Uses a {@link PropertiesBasedResumableProcessor} for persistence, starts from byte 0,
+ * and does not accumulate response bytes. The decorated handler will receive all
+ * handler callbacks in addition to the resume functionality.
+ *
+ * @param decoratedAsyncHandler the handler to decorate with resume capability
+ */
public ResumableAsyncHandler(AsyncHandler decoratedAsyncHandler) {
this(0, new PropertiesBasedResumableProcessor(), decoratedAsyncHandler, false);
}
+ /**
+ * Creates a ResumableAsyncHandler that decorates another handler with a specified starting position.
+ *
+ * Uses a {@link PropertiesBasedResumableProcessor} for persistence and does not accumulate response bytes.
+ *
+ * @param byteTransferred the number of bytes already transferred (for manual resume)
+ * @param decoratedAsyncHandler the handler to decorate with resume capability
+ */
public ResumableAsyncHandler(long byteTransferred, AsyncHandler decoratedAsyncHandler) {
this(byteTransferred, new PropertiesBasedResumableProcessor(), decoratedAsyncHandler, false);
}
+ /**
+ * Creates a ResumableAsyncHandler with a custom processor.
+ *
+ * Starts from byte 0 and does not accumulate response bytes.
+ *
+ * @param resumableProcessor the processor to use for persisting download state
+ */
public ResumableAsyncHandler(ResumableProcessor resumableProcessor) {
this(0, resumableProcessor, null, false);
}
+ /**
+ * Creates a ResumableAsyncHandler with a custom processor and optional response body accumulation.
+ *
+ * Starts from byte 0.
+ *
+ * @param resumableProcessor the processor to use for persisting download state
+ * @param accumulateBody {@code true} to accumulate response bytes in memory for later access
+ */
public ResumableAsyncHandler(ResumableProcessor resumableProcessor, boolean accumulateBody) {
this(0, resumableProcessor, null, accumulateBody);
}
+ /**
+ * Processes the HTTP response status.
+ *
+ * Accepts status codes 200 (OK) and 206 (Partial Content). Other status codes
+ * cause the request to be aborted. Delegates to the decorated handler if present.
+ *
+ * @param status the HTTP response status
+ * @return {@link State#CONTINUE} if the status is acceptable, {@link State#ABORT} otherwise
+ * @throws Exception if an error occurs during processing
+ */
@Override
public State onStatusReceived(final HttpResponseStatus status) throws Exception {
responseBuilder.accumulate(status);
@@ -112,6 +173,13 @@ public State onStatusReceived(final HttpResponseStatus status) throws Exception
return AsyncHandler.State.CONTINUE;
}
+ /**
+ * Handles exceptions that occur during request processing.
+ *
+ * Delegates to the decorated handler if present, otherwise logs the exception.
+ *
+ * @param t the exception that occurred
+ */
@Override
public void onThrowable(Throwable t) {
if (decoratedAsyncHandler != null) {
@@ -121,6 +189,16 @@ public void onThrowable(Throwable t) {
}
}
+ /**
+ * Processes a chunk of the response body.
+ *
+ * Optionally accumulates the bytes if configured to do so, notifies the resumable listener,
+ * delegates to the decorated handler if present, and updates the transfer position in the processor.
+ *
+ * @param bodyPart the chunk of response body
+ * @return the state returned by the decorated handler, or {@link State#CONTINUE}
+ * @throws Exception if an error occurs during processing
+ */
@Override
public State onBodyPartReceived(HttpResponseBodyPart bodyPart) throws Exception {
@@ -145,6 +223,15 @@ public State onBodyPartReceived(HttpResponseBodyPart bodyPart) throws Exception
return state;
}
+ /**
+ * Completes the request processing.
+ *
+ * Removes the URL from the processor (as download is complete), notifies the resumable listener,
+ * and delegates to the decorated handler if present.
+ *
+ * @return the complete response
+ * @throws Exception if an error occurs during completion
+ */
@Override
public Response onCompleted() throws Exception {
resumableProcessor.remove(url);
@@ -157,6 +244,16 @@ public Response onCompleted() throws Exception {
return responseBuilder.build();
}
+ /**
+ * Processes the HTTP response headers.
+ *
+ * Checks for invalid Content-Length values and aborts if -1 is encountered.
+ * Delegates to the decorated handler if present.
+ *
+ * @param headers the HTTP response headers
+ * @return the state returned by the decorated handler, or {@link State#CONTINUE}
+ * @throws Exception if an error occurs during processing
+ */
@Override
public State onHeadersReceived(HttpHeaders headers) throws Exception {
responseBuilder.accumulate(headers);
@@ -173,6 +270,14 @@ public State onHeadersReceived(HttpHeaders headers) throws Exception {
return State.CONTINUE;
}
+ /**
+ * Processes trailing HTTP headers.
+ *
+ * Accumulates trailing headers in the response builder.
+ *
+ * @param headers the trailing HTTP headers
+ * @return {@link State#CONTINUE} to proceed with processing
+ */
@Override
public State onTrailingHeadersReceived(HttpHeaders headers) {
responseBuilder.accumulate(headers);
@@ -180,11 +285,17 @@ public State onTrailingHeadersReceived(HttpHeaders headers) {
}
/**
- * Invoke this API if you want to set the Range header on your {@link Request} based on the last valid bytes
- * position.
+ * Adjusts the request to include a Range header for resuming the download.
+ *
+ * This method determines the last successfully transferred byte position from the processor
+ * and the resumable listener, then sets the appropriate Range header on the request to
+ * resume from that position. If the request already has a Range header, it is not modified.
+ *
+ * This method is typically called by {@link ResumableIOExceptionFilter} when retrying
+ * a failed request.
*
- * @param request {@link Request}
- * @return a {@link Request} with the Range header properly set.
+ * @param request the original request
+ * @return a new request with the Range header set to resume from the last valid position
*/
public Request adjustRequestRange(Request request) {
@@ -206,10 +317,14 @@ public Request adjustRequestRange(Request request) {
}
/**
- * Set a {@link ResumableListener}
+ * Sets the listener that will receive and process response body bytes.
+ *
+ * The resumable listener is responsible for writing bytes to the final destination
+ * (e.g., a file) and tracking the current download position. Common implementations
+ * include {@link ResumableRandomAccessFileListener}.
*
- * @param resumableListener a {@link ResumableListener}
- * @return this
+ * @param resumableListener the listener that will handle response body bytes
+ * @return this handler for method chaining
*/
public ResumableAsyncHandler setResumableListener(ResumableListener resumableListener) {
this.resumableListener = resumableListener;
diff --git a/client/src/main/java/org/asynchttpclient/handler/resumable/ResumableIOExceptionFilter.java b/client/src/main/java/org/asynchttpclient/handler/resumable/ResumableIOExceptionFilter.java
index 948bb6649c..778cfe1b6b 100644
--- a/client/src/main/java/org/asynchttpclient/handler/resumable/ResumableIOExceptionFilter.java
+++ b/client/src/main/java/org/asynchttpclient/handler/resumable/ResumableIOExceptionFilter.java
@@ -17,9 +17,49 @@
import org.asynchttpclient.filter.IOExceptionFilter;
/**
- * Simple {@link org.asynchttpclient.filter.IOExceptionFilter} that replay the current {@link org.asynchttpclient.Request} using a {@link ResumableAsyncHandler}
+ * An {@link IOExceptionFilter} that enables automatic retry with resume capability for failed downloads.
+ *
+ * This filter detects I/O exceptions during request processing and, if the handler is a
+ * {@link ResumableAsyncHandler}, automatically adjusts the request to resume from where it failed
+ * by setting the appropriate Range header. This allows downloads to continue from the last
+ * successfully received byte rather than starting over.
+ *
+ * The filter works in conjunction with {@link ResumableAsyncHandler} to implement resumable
+ * downloads that can recover from network interruptions, timeouts, or other I/O errors.
+ *
+ *
Usage Example:
+ * {@code
+ * AsyncHttpClient client = new DefaultAsyncHttpClient(
+ * new DefaultAsyncHttpClientConfig.Builder()
+ * .addIOExceptionFilter(new ResumableIOExceptionFilter())
+ * .build()
+ * );
+ *
+ * ResumableAsyncHandler handler = new ResumableAsyncHandler();
+ * handler.setResumableListener(new ResumableRandomAccessFileListener(outputFile));
+ *
+ * // The filter will automatically retry with Range header if I/O exceptions occur
+ * client.prepareGet("http://example.com/largefile.zip")
+ * .execute(handler)
+ * .get();
+ * }
*/
public class ResumableIOExceptionFilter implements IOExceptionFilter {
+
+ /**
+ * Filters I/O exceptions and enables request replay with resume capability.
+ *
+ * If an I/O exception occurs and the handler is a {@link ResumableAsyncHandler},
+ * this method adjusts the request to include a Range header starting from the
+ * last successfully transferred byte and marks the request for replay.
+ *
+ * For other handlers or if no exception occurred, the context is returned unchanged.
+ *
+ * @param ctx the filter context containing the exception, request, and handler
+ * @param the response type
+ * @return a new filter context with an adjusted request if resumption is possible,
+ * or the original context otherwise
+ */
public FilterContext filter(FilterContext ctx) {
if (ctx.getIOException() != null && ctx.getAsyncHandler() instanceof ResumableAsyncHandler) {
diff --git a/client/src/main/java/org/asynchttpclient/handler/resumable/ResumableListener.java b/client/src/main/java/org/asynchttpclient/handler/resumable/ResumableListener.java
index 03472bd085..b993410dec 100644
--- a/client/src/main/java/org/asynchttpclient/handler/resumable/ResumableListener.java
+++ b/client/src/main/java/org/asynchttpclient/handler/resumable/ResumableListener.java
@@ -16,27 +16,84 @@
import java.nio.ByteBuffer;
/**
- * A listener class that can be used to digest the bytes from an {@link ResumableAsyncHandler}
+ * A listener interface for processing response body bytes in a resumable download.
+ *
+ * Implementations of this interface are used by {@link ResumableAsyncHandler} to
+ * receive and store response body bytes. The listener is responsible for:
+ *
+ * Writing received bytes to their final destination (e.g., a file)
+ * Tracking the current download position
+ * Providing the length of previously downloaded bytes for resume operations
+ *
+ *
+ * The most common implementation is {@link ResumableRandomAccessFileListener}, which
+ * writes bytes to a file using {@link java.io.RandomAccessFile} for seek support.
+ *
+ *
Usage Example:
+ * {@code
+ * // Custom implementation example
+ * public class CustomResumableListener implements ResumableListener {
+ * private final OutputStream output;
+ * private long bytesWritten = 0;
+ *
+ * public CustomResumableListener(OutputStream output) {
+ * this.output = output;
+ * }
+ *
+ * @Override
+ * public void onBytesReceived(ByteBuffer buffer) throws IOException {
+ * byte[] bytes = new byte[buffer.remaining()];
+ * buffer.get(bytes);
+ * output.write(bytes);
+ * bytesWritten += bytes.length;
+ * }
+ *
+ * @Override
+ * public void onAllBytesReceived() {
+ * try {
+ * output.close();
+ * } catch (IOException e) {
+ * // Handle exception
+ * }
+ * }
+ *
+ * @Override
+ * public long length() {
+ * return bytesWritten;
+ * }
+ * }
+ * }
*/
public interface ResumableListener {
/**
- * Invoked when some bytes are available to digest.
+ * Invoked when a chunk of response body bytes is available.
+ *
+ * Implementations should write the bytes to their destination (e.g., file, stream)
+ * and update their internal state to track the download progress. This method
+ * may be called multiple times for a single response as data arrives in chunks.
*
- * @param byteBuffer the current bytes
- * @throws IOException exception while writing the byteBuffer
+ * @param byteBuffer the ByteBuffer containing the response body chunk
+ * @throws IOException if an I/O error occurs while processing the bytes
*/
void onBytesReceived(ByteBuffer byteBuffer) throws IOException;
/**
- * Invoked when all the bytes has been successfully transferred.
+ * Invoked when all response body bytes have been successfully received.
+ *
+ * Implementations should perform cleanup operations such as closing files
+ * or streams. This method is called once at the end of a successful download.
*/
void onAllBytesReceived();
/**
- * Return the length of previously downloaded bytes.
+ * Returns the number of bytes that have been previously downloaded.
+ *
+ * This value is used to determine the starting position when resuming an
+ * interrupted download. It should return the actual number of bytes successfully
+ * written to the destination.
*
- * @return the length of previously downloaded bytes
+ * @return the number of bytes previously downloaded, or 0 if starting fresh
*/
long length();
}
diff --git a/client/src/main/java/org/asynchttpclient/handler/resumable/ResumableRandomAccessFileListener.java b/client/src/main/java/org/asynchttpclient/handler/resumable/ResumableRandomAccessFileListener.java
index 918a2b9382..e8799c763b 100644
--- a/client/src/main/java/org/asynchttpclient/handler/resumable/ResumableRandomAccessFileListener.java
+++ b/client/src/main/java/org/asynchttpclient/handler/resumable/ResumableRandomAccessFileListener.java
@@ -19,21 +19,50 @@
import static org.asynchttpclient.util.MiscUtils.closeSilently;
/**
- * A {@link org.asynchttpclient.handler.resumable.ResumableListener} which use a {@link RandomAccessFile} for storing the received bytes.
+ * A {@link ResumableListener} implementation that writes response body bytes to a {@link RandomAccessFile}.
+ *
+ * This listener enables resumable file downloads by using a RandomAccessFile that can seek to
+ * specific positions. When resuming a download, the file pointer is positioned at the end of
+ * the existing file content, allowing new bytes to be appended seamlessly.
+ *
+ * The listener automatically seeks to the end of the file before each write operation,
+ * ensuring proper positioning even when resuming from a previous download attempt.
+ *
+ *
Usage Example:
+ * {@code
+ * File outputFile = new File("largefile.zip");
+ * RandomAccessFile raf = new RandomAccessFile(outputFile, "rw");
+ *
+ * ResumableRandomAccessFileListener listener = new ResumableRandomAccessFileListener(raf);
+ * ResumableAsyncHandler handler = new ResumableAsyncHandler();
+ * handler.setResumableListener(listener);
+ *
+ * // If the download is interrupted and resumed, the file will continue from where it left off
+ * client.prepareGet("http://example.com/largefile.zip")
+ * .execute(handler)
+ * .get();
+ * }
*/
public class ResumableRandomAccessFileListener implements ResumableListener {
private final RandomAccessFile file;
+ /**
+ * Creates a new listener that writes to the specified RandomAccessFile.
+ *
+ * @param file the RandomAccessFile where response body bytes will be written
+ */
public ResumableRandomAccessFileListener(RandomAccessFile file) {
this.file = file;
}
/**
- * This method uses the last valid bytes written on disk to position a {@link RandomAccessFile}, allowing
- * resumable file download.
+ * Writes a chunk of response body bytes to the file.
+ *
+ * This method positions the file pointer at the end of the file before writing,
+ * enabling resumable downloads. The method handles both heap and direct ByteBuffers.
*
- * @param buffer a {@link ByteBuffer}
- * @throws IOException exception while writing into the file
+ * @param buffer the ByteBuffer containing response body bytes
+ * @throws IOException if an I/O error occurs while writing to the file
*/
public void onBytesReceived(ByteBuffer buffer) throws IOException {
file.seek(file.length());
@@ -49,14 +78,20 @@ public void onBytesReceived(ByteBuffer buffer) throws IOException {
}
/**
- * {@inheritDoc}
+ * Invoked when all response bytes have been received.
+ *
+ * Closes the RandomAccessFile to release system resources.
*/
public void onAllBytesReceived() {
closeSilently(file);
}
/**
- * {@inheritDoc}
+ * Returns the current length of the file in bytes.
+ *
+ * This is used to determine the resume position when continuing an interrupted download.
+ *
+ * @return the current file length in bytes, or 0 if an error occurs
*/
public long length() {
try {
diff --git a/client/src/main/java/org/asynchttpclient/netty/EagerResponseBodyPart.java b/client/src/main/java/org/asynchttpclient/netty/EagerResponseBodyPart.java
index 8f2b189616..6b790f0181 100755
--- a/client/src/main/java/org/asynchttpclient/netty/EagerResponseBodyPart.java
+++ b/client/src/main/java/org/asynchttpclient/netty/EagerResponseBodyPart.java
@@ -20,13 +20,27 @@
import static org.asynchttpclient.netty.util.ByteBufUtils.byteBuf2Bytes;
/**
- * A callback class used when an HTTP response body is received.
- * Bytes are eagerly fetched from the ByteBuf
+ * Response body part that eagerly copies bytes from the Netty ByteBuf.
+ *
+ * This implementation immediately extracts bytes from the ByteBuf upon construction,
+ * allowing the original buffer to be released quickly. This is the default strategy
+ * and is suitable for most use cases where response bodies are small to medium sized.
+ *
+ *
+ * The eager strategy trades memory (for the byte copy) for simplified lifecycle management
+ * and avoids the need to manually manage ByteBuf reference counts.
+ *
*/
public class EagerResponseBodyPart extends HttpResponseBodyPart {
private final byte[] bytes;
+ /**
+ * Constructs an eager response body part.
+ *
+ * @param buf the Netty ByteBuf containing the response body chunk
+ * @param last whether this is the final body part
+ */
public EagerResponseBodyPart(ByteBuf buf, boolean last) {
super(last);
bytes = byteBuf2Bytes(buf);
diff --git a/client/src/main/java/org/asynchttpclient/netty/LazyResponseBodyPart.java b/client/src/main/java/org/asynchttpclient/netty/LazyResponseBodyPart.java
index 1abe8ce11e..023cbfab09 100755
--- a/client/src/main/java/org/asynchttpclient/netty/LazyResponseBodyPart.java
+++ b/client/src/main/java/org/asynchttpclient/netty/LazyResponseBodyPart.java
@@ -19,17 +19,45 @@
import java.nio.ByteBuffer;
/**
- * A callback class used when an HTTP response body is received.
+ * Response body part that lazily accesses bytes from the Netty ByteBuf.
+ *
+ * This implementation retains a reference to the original ByteBuf without copying its contents,
+ * allowing for zero-copy access to response data. This is more memory efficient but requires
+ * careful ByteBuf lifecycle management to avoid use-after-release bugs.
+ *
+ *
+ * The lazy strategy is suitable for advanced use cases where:
+ *
+ * Response bodies are very large and copying would be expensive
+ * The application can properly manage ByteBuf reference counts
+ * Zero-copy semantics are desired (e.g., writing directly to a file or network)
+ *
+ *
*/
public class LazyResponseBodyPart extends HttpResponseBodyPart {
private final ByteBuf buf;
+ /**
+ * Constructs a lazy response body part.
+ *
+ * @param buf the Netty ByteBuf containing the response body chunk
+ * @param last whether this is the final body part
+ */
public LazyResponseBodyPart(ByteBuf buf, boolean last) {
super(last);
this.buf = buf;
}
+ /**
+ * Returns the underlying Netty ByteBuf.
+ *
+ * Warning: The returned ByteBuf must be properly released when no longer needed
+ * to avoid memory leaks. Callers are responsible for reference count management.
+ *
+ *
+ * @return the Netty ByteBuf containing the body data
+ */
public ByteBuf getBuf() {
return buf;
}
diff --git a/client/src/main/java/org/asynchttpclient/netty/NettyResponse.java b/client/src/main/java/org/asynchttpclient/netty/NettyResponse.java
index a923c321f3..c45dd3f755 100755
--- a/client/src/main/java/org/asynchttpclient/netty/NettyResponse.java
+++ b/client/src/main/java/org/asynchttpclient/netty/NettyResponse.java
@@ -39,7 +39,13 @@
import static org.asynchttpclient.util.MiscUtils.withDefault;
/**
- * Wrapper around the {@link org.asynchttpclient.Response} API.
+ * Netty-based implementation of the {@link Response} interface.
+ *
+ * This class aggregates the HTTP response status, headers, and body parts collected
+ * during the request-response cycle. It provides convenient methods for accessing
+ * response data in various formats (bytes, String, ByteBuffer, InputStream) and
+ * handles cookie parsing from Set-Cookie headers.
+ *
*/
public class NettyResponse implements Response {
@@ -48,6 +54,13 @@ public class NettyResponse implements Response {
private final HttpResponseStatus status;
private List cookies;
+ /**
+ * Constructs a new NettyResponse.
+ *
+ * @param status the HTTP response status
+ * @param headers the HTTP response headers
+ * @param bodyParts the list of body parts received
+ */
public NettyResponse(HttpResponseStatus status,
HttpHeaders headers,
List bodyParts) {
diff --git a/client/src/main/java/org/asynchttpclient/netty/NettyResponseStatus.java b/client/src/main/java/org/asynchttpclient/netty/NettyResponseStatus.java
index bd5bce1f60..bc9911c4b7 100755
--- a/client/src/main/java/org/asynchttpclient/netty/NettyResponseStatus.java
+++ b/client/src/main/java/org/asynchttpclient/netty/NettyResponseStatus.java
@@ -21,7 +21,12 @@
import java.net.SocketAddress;
/**
- * A class that represent the HTTP response' status line (code + text)
+ * Netty-based implementation of HTTP response status information.
+ *
+ * This class wraps a Netty {@link HttpResponse} and extracts status code, status text,
+ * protocol version, and socket address information. It provides access to the complete
+ * HTTP status line and connection details.
+ *
*/
public class NettyResponseStatus extends HttpResponseStatus {
@@ -29,6 +34,13 @@ public class NettyResponseStatus extends HttpResponseStatus {
private final SocketAddress remoteAddress;
private final SocketAddress localAddress;
+ /**
+ * Constructs a new NettyResponseStatus.
+ *
+ * @param uri the request URI
+ * @param response the Netty HTTP response
+ * @param channel the channel the response was received on (may be null)
+ */
public NettyResponseStatus(Uri uri, HttpResponse response, Channel channel) {
super(uri);
this.response = response;
diff --git a/client/src/main/java/org/asynchttpclient/netty/channel/ChannelManager.java b/client/src/main/java/org/asynchttpclient/netty/channel/ChannelManager.java
index b93dfb380e..9a0d569902 100755
--- a/client/src/main/java/org/asynchttpclient/netty/channel/ChannelManager.java
+++ b/client/src/main/java/org/asynchttpclient/netty/channel/ChannelManager.java
@@ -67,6 +67,23 @@
import java.util.function.Function;
import java.util.stream.Collectors;
+/**
+ * Central manager for Netty channel lifecycle and pipeline configuration.
+ *
+ * This class is responsible for:
+ *
+ * Creating and configuring Netty bootstraps for HTTP and WebSocket connections
+ * Managing channel pools for connection reuse
+ * Configuring SSL/TLS handlers
+ * Setting up protocol-specific pipeline handlers
+ * Tracking open channels and connection statistics
+ *
+ *
+ *
+ * The ChannelManager supports multiple transport implementations (NIO, Epoll, KQueue)
+ * and handles both HTTP and WebSocket protocols with automatic pipeline reconfiguration.
+ *
+ */
public class ChannelManager {
public static final String HTTP_CLIENT_CODEC = "http";
@@ -95,6 +112,19 @@ public class ChannelManager {
private AsyncHttpClientHandler wsHandler;
+ /**
+ * Constructs a new ChannelManager with the specified configuration.
+ *
+ * This constructor initializes the event loop group, channel pool, SSL engine factory,
+ * and bootstraps for HTTP and WebSocket connections. It automatically selects the
+ * appropriate transport implementation (NIO, Epoll, or KQueue) based on the platform
+ * and configuration.
+ *
+ *
+ * @param config the async HTTP client configuration
+ * @param nettyTimer the timer for scheduling timeouts and periodic tasks
+ * @throws RuntimeException if SSL engine factory initialization fails
+ */
public ChannelManager(final AsyncHttpClientConfig config, Timer nettyTimer) {
this.config = config;
@@ -153,6 +183,12 @@ public ChannelManager(final AsyncHttpClientConfig config, Timer nettyTimer) {
httpBootstrap.option(ChannelOption.AUTO_READ, false);
}
+ /**
+ * Checks if an SSL handler is configured in the pipeline.
+ *
+ * @param pipeline the channel pipeline to check
+ * @return true if an SSL handler is present in the pipeline
+ */
public static boolean isSslHandlerConfigured(ChannelPipeline pipeline) {
return pipeline.get(SSL_HANDLER) != null;
}
@@ -207,6 +243,16 @@ private Bootstrap newBootstrap(ChannelFactory extends Channel> channelFactory,
throw new IllegalArgumentException("No suitable native transport (epoll or kqueue) available");
}
+ /**
+ * Configures the HTTP and WebSocket bootstraps with protocol handlers.
+ *
+ * This method sets up the channel initializers that install the appropriate
+ * handlers in the pipeline for HTTP and WebSocket connections. It must be
+ * called after construction and before any connections are made.
+ *
+ *
+ * @param requestSender the request sender for handling outgoing requests
+ */
public void configureBootstraps(NettyRequestSender requestSender) {
final AsyncHttpClientHandler httpHandler = new HttpHandler(config, this, requestSender);
@@ -265,6 +311,19 @@ protected String getTargetContentEncoding(String contentEncoding) {
return new HttpContentDecompressor();
}
+ /**
+ * Attempts to return a channel to the connection pool for reuse.
+ *
+ * This method first notifies the async handler via {@link AsyncHandler#onConnectionOffer(Channel)},
+ * then offers the channel to the pool if it's active and keepAlive is true. If the pool
+ * rejects the channel or keepAlive is false, the channel is closed.
+ *
+ *
+ * @param channel the channel to offer to the pool
+ * @param asyncHandler the async handler to notify of the connection offer
+ * @param keepAlive whether to keep the connection alive
+ * @param partitionKey the pool partition key (typically based on host/port)
+ */
public final void tryToOfferChannelToPool(Channel channel, AsyncHandler> asyncHandler, boolean keepAlive, Object partitionKey) {
if (channel.isActive() && keepAlive) {
LOGGER.debug("Adding key: {} for channel {}", partitionKey, channel);
@@ -286,11 +345,33 @@ public final void tryToOfferChannelToPool(Channel channel, AsyncHandler> async
}
}
+ /**
+ * Polls an idle channel from the connection pool.
+ *
+ * This method attempts to retrieve an existing connection from the pool based on
+ * the partition key determined by the URI, virtual host, and proxy settings.
+ *
+ *
+ * @param uri the target URI
+ * @param virtualHost the virtual host header value, or null
+ * @param proxy the proxy server configuration, or null
+ * @param connectionPoolPartitioning the partitioning strategy for the pool
+ * @return a channel from the pool, or null if none available
+ */
public Channel poll(Uri uri, String virtualHost, ProxyServer proxy, ChannelPoolPartitioning connectionPoolPartitioning) {
Object partitionKey = connectionPoolPartitioning.getPartitionKey(uri, virtualHost, proxy);
return channelPool.poll(partitionKey);
}
+ /**
+ * Removes all instances of a channel from the connection pool.
+ *
+ * This method is called when a channel becomes unusable and should be
+ * removed from all pool partitions where it might be stored.
+ *
+ *
+ * @param connection the channel to remove from the pool
+ */
public void removeAll(Channel connection) {
channelPool.removeAll(connection);
}
@@ -311,6 +392,15 @@ public void close() {
}
}
+ /**
+ * Closes a channel and removes it from the pool and open channels group.
+ *
+ * This method marks the channel for discard, removes it from the pool,
+ * and performs a silent close operation.
+ *
+ *
+ * @param channel the channel to close
+ */
public void closeChannel(Channel channel) {
LOGGER.debug("Closing Channel {} ", channel);
Channels.setDiscard(channel);
@@ -318,6 +408,15 @@ public void closeChannel(Channel channel) {
Channels.silentlyCloseChannel(channel);
}
+ /**
+ * Registers a newly opened channel with the manager.
+ *
+ * This method adds the channel to the tracked open channels group,
+ * allowing it to be managed and closed during shutdown.
+ *
+ *
+ * @param channel the channel to register
+ */
public void registerOpenChannel(Channel channel) {
openChannels.add(channel);
}
@@ -340,6 +439,18 @@ private SslHandler createSslHandler(String peerHost, int peerPort) {
return sslHandler;
}
+ /**
+ * Updates the pipeline for HTTP tunneling through a proxy (HTTP CONNECT).
+ *
+ * This method reconfigures the pipeline after a successful CONNECT request,
+ * adding SSL if needed and reinstalling the HTTP codec. For WebSocket upgrades
+ * through tunnels, it also switches to the WebSocket handler.
+ *
+ *
+ * @param pipeline the channel pipeline to update
+ * @param requestUri the target URI after tunneling
+ * @return a Future that completes when SSL handshake finishes (if SSL), or null
+ */
public Future updatePipelineForHttpTunneling(ChannelPipeline pipeline, Uri requestUri) {
Future whenHandshaked = null;
@@ -371,6 +482,19 @@ public Future updatePipelineForHttpTunneling(ChannelPipeline pipeline,
return whenHandshaked;
}
+ /**
+ * Adds an SSL handler to the pipeline for secure connections.
+ *
+ * This method creates and installs an SSL handler configured for the target
+ * host and port. The virtualHost parameter is used for SNI (Server Name Indication).
+ *
+ *
+ * @param pipeline the channel pipeline to modify
+ * @param uri the target URI
+ * @param virtualHost the virtual host for SNI, or null to use the URI host
+ * @param hasSocksProxyHandler whether a SOCKS proxy handler is already in the pipeline
+ * @return the created SslHandler
+ */
public SslHandler addSslHandler(ChannelPipeline pipeline, Uri uri, String virtualHost, boolean hasSocksProxyHandler) {
String peerHost;
int peerPort;
@@ -399,6 +523,19 @@ public SslHandler addSslHandler(ChannelPipeline pipeline, Uri uri, String virtua
return sslHandler;
}
+ /**
+ * Retrieves or creates an appropriate bootstrap for the connection.
+ *
+ * This method selects the correct bootstrap (HTTP or WebSocket) and configures
+ * it for SOCKS proxy if needed. For SOCKS proxies, it clones the HTTP bootstrap
+ * and adds the SOCKS handler after resolving the proxy address.
+ *
+ *
+ * @param uri the target URI
+ * @param nameResolver the name resolver for resolving proxy addresses
+ * @param proxy the proxy server configuration, or null for direct connections
+ * @return a Future containing the configured Bootstrap
+ */
public Future getBootstrap(Uri uri, NameResolver nameResolver, ProxyServer proxy) {
final Promise promise = ImmediateEventExecutor.INSTANCE.newPromise();
@@ -455,6 +592,16 @@ protected void initChannel(Channel channel) throws Exception {
return promise;
}
+ /**
+ * Upgrades the pipeline from HTTP to WebSocket protocol.
+ *
+ * This method replaces the HTTP codec with WebSocket frame encoder/decoder
+ * and optionally adds a frame aggregator for fragmented messages. It is called
+ * after a successful WebSocket handshake upgrade.
+ *
+ *
+ * @param pipeline the channel pipeline to upgrade
+ */
public void upgradePipelineForWebSockets(ChannelPipeline pipeline) {
pipeline.addAfter(HTTP_CLIENT_CODEC, WS_ENCODER_HANDLER, new WebSocket08FrameEncoder(true));
pipeline.addAfter(WS_ENCODER_HANDLER, WS_DECODER_HANDLER, new WebSocket08FrameDecoder(false, config.isEnableWebSocketCompression(), config.getWebSocketMaxFrameSize()));
@@ -482,14 +629,33 @@ public void drainChannelAndOffer(Channel channel, NettyResponseFuture> future,
Channels.setAttribute(channel, newDrainCallback(future, channel, keepAlive, partitionKey));
}
+ /**
+ * Returns the channel pool used by this manager.
+ *
+ * @return the ChannelPool instance
+ */
public ChannelPool getChannelPool() {
return channelPool;
}
+ /**
+ * Returns the event loop group used by this manager.
+ *
+ * @return the EventLoopGroup instance
+ */
public EventLoopGroup getEventLoopGroup() {
return eventLoopGroup;
}
+ /**
+ * Retrieves statistics about open and idle connections.
+ *
+ * This method aggregates connection statistics per host, including total
+ * connections, idle connections in the pool, and active connections.
+ *
+ *
+ * @return ClientStats containing per-host connection statistics
+ */
public ClientStats getClientStats() {
Map totalConnectionsPerHost = openChannels.stream().map(Channel::remoteAddress).filter(a -> a instanceof InetSocketAddress)
.map(a -> (InetSocketAddress) a).map(InetSocketAddress::getHostString).collect(Collectors.groupingBy(Function.identity(), Collectors.counting()));
@@ -503,6 +669,11 @@ public ClientStats getClientStats() {
return new ClientStats(statsPerHost);
}
+ /**
+ * Checks if the channel manager is open and accepting connections.
+ *
+ * @return true if the manager and its channel pool are open
+ */
public boolean isOpen() {
return channelPool.isOpen();
}
diff --git a/client/src/main/java/org/asynchttpclient/netty/channel/ChannelState.java b/client/src/main/java/org/asynchttpclient/netty/channel/ChannelState.java
index d4439f6825..ea48ec87e2 100644
--- a/client/src/main/java/org/asynchttpclient/netty/channel/ChannelState.java
+++ b/client/src/main/java/org/asynchttpclient/netty/channel/ChannelState.java
@@ -13,6 +13,34 @@
*/
package org.asynchttpclient.netty.channel;
+/**
+ * Represents the lifecycle state of a Netty channel in the connection pool.
+ *
+ * Channels transition through various states from creation to closure, with
+ * pooling and reconnection states in between. This enum tracks these states
+ * for connection management and debugging purposes.
+ *
+ */
public enum ChannelState {
- NEW, POOLED, RECONNECTED, CLOSED,
+ /**
+ * Indicates a newly created channel that has not been used yet.
+ */
+ NEW,
+
+ /**
+ * Indicates a channel that has been returned to the connection pool
+ * and is available for reuse.
+ */
+ POOLED,
+
+ /**
+ * Indicates a channel that was taken from the pool and reconnected
+ * for a new request.
+ */
+ RECONNECTED,
+
+ /**
+ * Indicates a channel that has been closed and is no longer usable.
+ */
+ CLOSED,
}
\ No newline at end of file
diff --git a/client/src/main/java/org/asynchttpclient/netty/channel/Channels.java b/client/src/main/java/org/asynchttpclient/netty/channel/Channels.java
index 1ddfda1e50..8ca3e58026 100755
--- a/client/src/main/java/org/asynchttpclient/netty/channel/Channels.java
+++ b/client/src/main/java/org/asynchttpclient/netty/channel/Channels.java
@@ -20,6 +20,14 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+/**
+ * Utility class for managing Netty channel attributes and lifecycle operations.
+ *
+ * This class provides helper methods for attaching state to channels, checking
+ * channel activity, and performing safe channel operations. It uses Netty's
+ * attribute system to store per-channel state information.
+ *
+ */
public class Channels {
private static final Logger LOGGER = LoggerFactory.getLogger(Channels.class);
@@ -27,31 +35,95 @@ public class Channels {
private static final AttributeKey DEFAULT_ATTRIBUTE = AttributeKey.valueOf("default");
private static final AttributeKey ACTIVE_TOKEN_ATTRIBUTE = AttributeKey.valueOf("activeToken");
+ /**
+ * Retrieves the attribute attached to the channel.
+ *
+ * The attribute typically contains state information such as a {@link org.asynchttpclient.netty.NettyResponseFuture},
+ * {@link org.asynchttpclient.netty.handler.StreamedResponsePublisher}, or {@link org.asynchttpclient.netty.DiscardEvent}.
+ *
+ *
+ * @param channel the channel to query
+ * @return the attribute object, or null if no attribute is set
+ */
public static Object getAttribute(Channel channel) {
Attribute attr = channel.attr(DEFAULT_ATTRIBUTE);
return attr != null ? attr.get() : null;
}
+ /**
+ * Sets an attribute on the channel.
+ *
+ * This method attaches state information to the channel for use by handlers
+ * in the pipeline. Common attributes include response futures and callbacks.
+ *
+ *
+ * @param channel the channel to modify
+ * @param o the attribute object to attach
+ */
public static void setAttribute(Channel channel, Object o) {
channel.attr(DEFAULT_ATTRIBUTE).set(o);
}
+ /**
+ * Marks the channel for discard by setting the DISCARD attribute.
+ *
+ * When a channel is marked for discard, handlers will ignore incoming
+ * messages and the channel will be closed rather than returned to the pool.
+ *
+ *
+ * @param channel the channel to mark for discard
+ */
public static void setDiscard(Channel channel) {
setAttribute(channel, DiscardEvent.DISCARD);
}
+ /**
+ * Checks if a channel is active and usable.
+ *
+ * @param channel the channel to check
+ * @return true if the channel is non-null and active
+ */
public static boolean isChannelActive(Channel channel) {
return channel != null && channel.isActive();
}
+ /**
+ * Sets an active token on the channel.
+ *
+ * This token is used to track channel activation state and prevent
+ * duplicate connection attempts or race conditions.
+ *
+ *
+ * @param channel the channel to mark as active
+ */
public static void setActiveToken(Channel channel) {
channel.attr(ACTIVE_TOKEN_ATTRIBUTE).set(Active.INSTANCE);
}
+ /**
+ * Checks and removes the active token from the channel atomically.
+ *
+ * This method returns true if the token was present (indicating the channel
+ * was properly activated) and removes it in a single atomic operation.
+ *
+ *
+ * @param channel the channel to check
+ * @return true if the active token was present and has been removed
+ */
public static boolean isActiveTokenSet(Channel channel) {
return channel != null && channel.attr(ACTIVE_TOKEN_ATTRIBUTE).getAndSet(null) != null;
}
+ /**
+ * Closes a channel while suppressing any exceptions.
+ *
+ * This method attempts to close the channel if it is active, logging
+ * any errors that occur during closure. It is safe to call on null
+ * or already-closed channels.
+ *
+ *
+ * @param channel the channel to close (may be null)
+ */
public static void silentlyCloseChannel(Channel channel) {
try {
if (channel != null && channel.isActive())
diff --git a/client/src/main/java/org/asynchttpclient/netty/channel/ConnectionSemaphore.java b/client/src/main/java/org/asynchttpclient/netty/channel/ConnectionSemaphore.java
index dc37d2d0b0..9c9ee7089f 100644
--- a/client/src/main/java/org/asynchttpclient/netty/channel/ConnectionSemaphore.java
+++ b/client/src/main/java/org/asynchttpclient/netty/channel/ConnectionSemaphore.java
@@ -16,12 +16,37 @@
import java.io.IOException;
/**
- * Connections limiter.
+ * Semaphore for limiting concurrent connections.
+ *
+ * This interface provides a mechanism to control the maximum number of concurrent
+ * connections that can be established, either globally or per-host. Implementations
+ * enforce connection limits to prevent resource exhaustion.
+ *
*/
public interface ConnectionSemaphore {
+ /**
+ * Acquires a lock to allow a new channel connection.
+ *
+ * This method blocks or throws an exception if the connection limit has been reached.
+ * The lock must be released by calling {@link #releaseChannelLock(Object)} when the
+ * connection is no longer needed.
+ *
+ *
+ * @param partitionKey the key identifying the connection partition (e.g., host)
+ * @throws IOException if the lock cannot be acquired due to connection limits
+ */
void acquireChannelLock(Object partitionKey) throws IOException;
+ /**
+ * Releases a previously acquired channel lock.
+ *
+ * This method should be called when a connection is closed or returned to the pool,
+ * allowing other pending requests to proceed.
+ *
+ *
+ * @param partitionKey the key identifying the connection partition (e.g., host)
+ */
void releaseChannelLock(Object partitionKey);
}
diff --git a/client/src/main/java/org/asynchttpclient/netty/channel/ConnectionSemaphoreFactory.java b/client/src/main/java/org/asynchttpclient/netty/channel/ConnectionSemaphoreFactory.java
index b94e336390..8aab4c175f 100644
--- a/client/src/main/java/org/asynchttpclient/netty/channel/ConnectionSemaphoreFactory.java
+++ b/client/src/main/java/org/asynchttpclient/netty/channel/ConnectionSemaphoreFactory.java
@@ -15,8 +15,25 @@
import org.asynchttpclient.AsyncHttpClientConfig;
+/**
+ * Factory for creating ConnectionSemaphore instances.
+ *
+ * This factory creates connection limiters based on the client configuration,
+ * allowing different semaphore strategies (per-host, global, or no limits).
+ *
+ */
public interface ConnectionSemaphoreFactory {
+ /**
+ * Creates a new ConnectionSemaphore based on the provided configuration.
+ *
+ * The returned semaphore enforces connection limits according to the
+ * configuration settings such as maxConnections and maxConnectionsPerHost.
+ *
+ *
+ * @param config the async HTTP client configuration
+ * @return a new ConnectionSemaphore instance
+ */
ConnectionSemaphore newConnectionSemaphore(AsyncHttpClientConfig config);
}
diff --git a/client/src/main/java/org/asynchttpclient/netty/channel/DefaultChannelPool.java b/client/src/main/java/org/asynchttpclient/netty/channel/DefaultChannelPool.java
index f9c08b973b..7c6f9cf6d7 100755
--- a/client/src/main/java/org/asynchttpclient/netty/channel/DefaultChannelPool.java
+++ b/client/src/main/java/org/asynchttpclient/netty/channel/DefaultChannelPool.java
@@ -38,7 +38,19 @@
import static org.asynchttpclient.util.DateUtils.unpreciseMillisTime;
/**
- * A simple implementation of {@link ChannelPool} based on a {@link java.util.concurrent.ConcurrentHashMap}
+ * Default implementation of {@link ChannelPool} based on a {@link java.util.concurrent.ConcurrentHashMap}.
+ *
+ * This pool manages idle connections organized by partition keys (typically host:port),
+ * supporting configurable connection TTL and idle timeouts. It uses a timer-based cleaner
+ * to remove expired or remotely-closed connections.
+ *
+ *
+ * Supports two lease strategies:
+ *
+ * LIFO (Last-In-First-Out) - reuses most recently returned connections (default)
+ * FIFO (First-In-First-Out) - reuses oldest connections first
+ *
+ *
*/
public final class DefaultChannelPool implements ChannelPool {
@@ -55,6 +67,12 @@ public final class DefaultChannelPool implements ChannelPool {
private final long cleanerPeriod;
private final PoolLeaseStrategy poolLeaseStrategy;
+ /**
+ * Constructs a pool using configuration settings.
+ *
+ * @param config the client configuration
+ * @param hashedWheelTimer the timer for scheduling cleanup tasks
+ */
public DefaultChannelPool(AsyncHttpClientConfig config, Timer hashedWheelTimer) {
this(config.getPooledConnectionIdleTimeout(),
config.getConnectionTtl(),
diff --git a/client/src/main/java/org/asynchttpclient/netty/channel/NettyChannelConnector.java b/client/src/main/java/org/asynchttpclient/netty/channel/NettyChannelConnector.java
index 8951bd062e..6f01fce2e9 100644
--- a/client/src/main/java/org/asynchttpclient/netty/channel/NettyChannelConnector.java
+++ b/client/src/main/java/org/asynchttpclient/netty/channel/NettyChannelConnector.java
@@ -26,6 +26,14 @@
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
+/**
+ * Manages connection attempts to multiple resolved addresses with automatic failover.
+ *
+ * This class iterates through a list of resolved IP addresses, attempting to connect
+ * to each in sequence until one succeeds. It notifies the AsyncHandler of connection
+ * attempts and outcomes, supporting transparent failover for multi-homed hosts.
+ *
+ */
public class NettyChannelConnector {
private static final Logger LOGGER = LoggerFactory.getLogger(NettyChannelConnector.class);
@@ -39,6 +47,14 @@ public class NettyChannelConnector {
private final AsyncHttpClientState clientState;
private volatile int i = 0;
+ /**
+ * Constructs a new NettyChannelConnector.
+ *
+ * @param localAddress the local address to bind to, or null for automatic
+ * @param remoteAddresses the list of resolved remote addresses to try
+ * @param asyncHandler the async handler for connection notifications
+ * @param clientState the client state for shutdown checks
+ */
public NettyChannelConnector(InetAddress localAddress,
List remoteAddresses,
AsyncHandler> asyncHandler,
@@ -54,6 +70,17 @@ private boolean pickNextRemoteAddress() {
return i < remoteAddresses.size();
}
+ /**
+ * Initiates a connection attempt to the current remote address.
+ *
+ * This method notifies the async handler of the connection attempt and
+ * initiates the asynchronous connect operation. On failure, it automatically
+ * tries the next address if available.
+ *
+ *
+ * @param bootstrap the Netty bootstrap to use for connecting
+ * @param connectListener the listener for connection outcome
+ */
public void connect(final Bootstrap bootstrap, final NettyConnectListener> connectListener) {
final InetSocketAddress remoteAddress = remoteAddresses.get(i);
diff --git a/client/src/main/java/org/asynchttpclient/netty/channel/NettyConnectListener.java b/client/src/main/java/org/asynchttpclient/netty/channel/NettyConnectListener.java
index 4a6f4dce20..c70e561069 100755
--- a/client/src/main/java/org/asynchttpclient/netty/channel/NettyConnectListener.java
+++ b/client/src/main/java/org/asynchttpclient/netty/channel/NettyConnectListener.java
@@ -32,7 +32,14 @@
import java.net.InetSocketAddress;
/**
- * Non Blocking connect.
+ * Listener for handling non-blocking channel connection events.
+ *
+ * This class manages the connection lifecycle from channel creation through
+ * SSL/TLS handshake (if needed) to request writing. It handles connection
+ * failures with retry logic and manages connection semaphore locks.
+ *
+ *
+ * @param the response type
*/
public final class NettyConnectListener {
@@ -43,6 +50,14 @@ public final class NettyConnectListener {
private final ChannelManager channelManager;
private final ConnectionSemaphore connectionSemaphore;
+ /**
+ * Constructs a new NettyConnectListener.
+ *
+ * @param future the response future for this connection
+ * @param requestSender the request sender for writing requests
+ * @param channelManager the channel manager for pipeline configuration
+ * @param connectionSemaphore the semaphore for connection limiting, or null
+ */
public NettyConnectListener(NettyResponseFuture future,
NettyRequestSender requestSender,
ChannelManager channelManager,
@@ -80,6 +95,17 @@ private void writeRequest(Channel channel) {
requestSender.writeRequest(future, channel);
}
+ /**
+ * Handles successful channel connection.
+ *
+ * This method transfers the connection semaphore lock to the channel,
+ * sets up SSL/TLS if needed, and initiates request writing. For SSL connections,
+ * it waits for the handshake to complete before writing the request.
+ *
+ *
+ * @param channel the successfully connected channel
+ * @param remoteAddress the remote address that was connected to
+ */
public void onSuccess(Channel channel, InetSocketAddress remoteAddress) {
if (connectionSemaphore != null) {
@@ -157,6 +183,17 @@ protected void onFailure(Throwable cause) {
}
}
+ /**
+ * Handles channel connection failure.
+ *
+ * This method attempts to retry the connection if retries are available
+ * and the failure is recoverable. If retry fails or is not applicable,
+ * it aborts the future with a ConnectException.
+ *
+ *
+ * @param channel the failed channel (may be null)
+ * @param cause the cause of the failure
+ */
public void onFailure(Channel channel, Throwable cause) {
// beware, channel can be null
diff --git a/client/src/main/java/org/asynchttpclient/netty/channel/TransportFactory.java b/client/src/main/java/org/asynchttpclient/netty/channel/TransportFactory.java
index 76f45c2d28..1a0c6ba147 100644
--- a/client/src/main/java/org/asynchttpclient/netty/channel/TransportFactory.java
+++ b/client/src/main/java/org/asynchttpclient/netty/channel/TransportFactory.java
@@ -19,8 +19,26 @@
import java.util.concurrent.ThreadFactory;
+/**
+ * Factory for creating transport-specific channel and event loop group instances.
+ *
+ * This interface abstracts the creation of Netty transport components, allowing
+ * support for multiple transport implementations (NIO, Epoll, KQueue). Each implementation
+ * provides optimized channel and event loop classes for their respective platform.
+ *
+ *
+ * @param the channel type (e.g., NioSocketChannel, EpollSocketChannel)
+ * @param the event loop group type (e.g., NioEventLoopGroup, EpollEventLoopGroup)
+ */
public interface TransportFactory extends ChannelFactory {
+ /**
+ * Creates a new event loop group for this transport.
+ *
+ * @param ioThreadsCount the number of I/O threads to create
+ * @param threadFactory the factory for creating threads
+ * @return a new event loop group instance
+ */
L newEventLoopGroup(int ioThreadsCount, ThreadFactory threadFactory);
}
diff --git a/client/src/main/java/org/asynchttpclient/netty/handler/AsyncHttpClientHandler.java b/client/src/main/java/org/asynchttpclient/netty/handler/AsyncHttpClientHandler.java
index ec158673f0..3f36d9ee4d 100755
--- a/client/src/main/java/org/asynchttpclient/netty/handler/AsyncHttpClientHandler.java
+++ b/client/src/main/java/org/asynchttpclient/netty/handler/AsyncHttpClientHandler.java
@@ -40,6 +40,15 @@
import static org.asynchttpclient.util.MiscUtils.getCause;
+/**
+ * Base handler for processing async HTTP client channel events in Netty pipeline.
+ *
+ * This abstract handler manages the lifecycle of HTTP requests and responses, handling
+ * channel read operations, exceptions, and connection state transitions. It provides
+ * the foundation for specific protocol handlers (HTTP, WebSocket) by defining common
+ * error handling and channel management logic.
+ *
+ */
public abstract class AsyncHttpClientHandler extends ChannelInboundHandlerAdapter {
protected final Logger logger = LoggerFactory.getLogger(getClass());
@@ -50,6 +59,13 @@ public abstract class AsyncHttpClientHandler extends ChannelInboundHandlerAdapte
final Interceptors interceptors;
final boolean hasIOExceptionFilters;
+ /**
+ * Constructs a new AsyncHttpClientHandler.
+ *
+ * @param config the async HTTP client configuration
+ * @param channelManager the channel manager for managing channel lifecycle
+ * @param requestSender the request sender for sending HTTP requests
+ */
AsyncHttpClientHandler(AsyncHttpClientConfig config,
ChannelManager channelManager,
NettyRequestSender requestSender) {
@@ -60,6 +76,18 @@ public abstract class AsyncHttpClientHandler extends ChannelInboundHandlerAdapte
hasIOExceptionFilters = !config.getIoExceptionFilters().isEmpty();
}
+ /**
+ * Processes incoming channel messages and routes them to the appropriate handler.
+ *
+ * This method handles different types of channel attributes including {@link NettyResponseFuture},
+ * {@link StreamedResponsePublisher}, and {@link OnLastHttpContentCallback}. It ensures proper
+ * message routing and resource cleanup via reference counting.
+ *
+ *
+ * @param ctx the channel handler context
+ * @param msg the message to process
+ * @throws Exception if an error occurs during message processing
+ */
@Override
public void channelRead(final ChannelHandlerContext ctx, Object msg) throws Exception {
@@ -114,6 +142,17 @@ public void channelRead(final ChannelHandlerContext ctx, Object msg) throws Exce
}
}
+ /**
+ * Handles channel inactivation by cleaning up resources and managing connection state.
+ *
+ * When a channel becomes inactive, this method removes it from the channel manager,
+ * applies IO exception filters if configured, and handles unexpected closures. It
+ * ensures proper cleanup of streaming publishers and callbacks.
+ *
+ *
+ * @param ctx the channel handler context
+ * @throws Exception if an error occurs during channel cleanup
+ */
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
if (requestSender.isClosed())
@@ -146,6 +185,17 @@ public void channelInactive(ChannelHandlerContext ctx) throws Exception {
}
}
+ /**
+ * Handles exceptions caught during channel operations.
+ *
+ * This method processes exceptions by attempting recovery through IO exception filters,
+ * and if recovery fails, aborts the request and closes the channel. It ignores expected
+ * exceptions like {@link PrematureChannelClosureException} and {@link ClosedChannelException}.
+ *
+ *
+ * @param ctx the channel handler context
+ * @param e the caught exception
+ */
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable e) {
Throwable cause = getCause(e);
@@ -210,11 +260,26 @@ public void exceptionCaught(ChannelHandlerContext ctx, Throwable e) {
Channels.silentlyCloseChannel(channel);
}
+ /**
+ * Handles channel activation by triggering an initial read operation.
+ *
+ * @param ctx the channel handler context
+ */
@Override
public void channelActive(ChannelHandlerContext ctx) {
ctx.read();
}
+ /**
+ * Handles channel read completion by triggering the next read if not using reactive streams.
+ *
+ * When reactive streams (StreamedResponsePublisher) is active, read management is
+ * delegated to the reactive streams implementation. Otherwise, this method triggers
+ * the next read operation to continue processing.
+ *
+ *
+ * @param ctx the channel handler context
+ */
@Override
public void channelReadComplete(ChannelHandlerContext ctx) {
if (!isHandledByReactiveStreams(ctx)) {
@@ -228,6 +293,17 @@ private boolean isHandledByReactiveStreams(ChannelHandlerContext ctx) {
return Channels.getAttribute(ctx.channel()) instanceof StreamedResponsePublisher;
}
+ /**
+ * Completes the processing of a request-response cycle.
+ *
+ * This method cancels any active timeouts, optionally closes the channel or returns it
+ * to the connection pool, and marks the future as done.
+ *
+ *
+ * @param future the response future to complete
+ * @param channel the channel used for the request
+ * @param close whether to close the channel (true) or return it to the pool (false)
+ */
void finishUpdate(NettyResponseFuture> future, Channel channel, boolean close) {
future.cancelTimeouts();
@@ -245,9 +321,40 @@ void finishUpdate(NettyResponseFuture> future, Channel channel, boolean close)
}
}
+ /**
+ * Processes a message read from the channel for a specific protocol.
+ *
+ * This method must be implemented by protocol-specific handlers to process
+ * incoming messages such as HTTP responses, WebSocket frames, etc.
+ *
+ *
+ * @param channel the channel the message was read from
+ * @param future the response future associated with the request
+ * @param message the message to handle
+ * @throws Exception if an error occurs during message handling
+ */
public abstract void handleRead(Channel channel, NettyResponseFuture> future, Object message) throws Exception;
+ /**
+ * Handles exceptions specific to the protocol implementation.
+ *
+ * This method is called when an exception occurs that requires protocol-specific
+ * handling, such as notifying WebSocket listeners or cleaning up HTTP state.
+ *
+ *
+ * @param future the response future associated with the request
+ * @param error the exception to handle
+ */
public abstract void handleException(NettyResponseFuture> future, Throwable error);
+ /**
+ * Handles channel inactivation specific to the protocol implementation.
+ *
+ * This method is called when a channel becomes inactive and requires protocol-specific
+ * cleanup, such as notifying WebSocket close handlers or handling unexpected HTTP disconnections.
+ *
+ *
+ * @param future the response future associated with the request
+ */
public abstract void handleChannelInactive(NettyResponseFuture> future);
}
diff --git a/client/src/main/java/org/asynchttpclient/netty/handler/HttpHandler.java b/client/src/main/java/org/asynchttpclient/netty/handler/HttpHandler.java
index dddaeb34cb..557ce14eb1 100755
--- a/client/src/main/java/org/asynchttpclient/netty/handler/HttpHandler.java
+++ b/client/src/main/java/org/asynchttpclient/netty/handler/HttpHandler.java
@@ -32,9 +32,37 @@
import java.io.IOException;
import java.net.InetSocketAddress;
+/**
+ * HTTP protocol handler for processing HTTP request-response cycles.
+ *
+ * This handler manages HTTP response processing including status lines, headers,
+ * body chunks, and trailer headers. It supports both buffered and streaming response
+ * handling through the AsyncHandler and StreamedAsyncHandler interfaces.
+ *
+ * Usage Examples:
+ * {@code
+ * // The HttpHandler is automatically installed in the pipeline
+ * AsyncHttpClient client = new DefaultAsyncHttpClient();
+ * client.prepareGet("http://example.com")
+ * .execute(new AsyncCompletionHandler() {
+ * @Override
+ * public Response onCompleted(Response response) {
+ * // HttpHandler processed the complete response
+ * return response;
+ * }
+ * });
+ * }
+ */
@Sharable
public final class HttpHandler extends AsyncHttpClientHandler {
+ /**
+ * Constructs a new HttpHandler.
+ *
+ * @param config the async HTTP client configuration
+ * @param channelManager the channel manager for managing channel lifecycle
+ * @param requestSender the request sender for sending HTTP requests
+ */
public HttpHandler(AsyncHttpClientConfig config, ChannelManager channelManager, NettyRequestSender requestSender) {
super(config, channelManager, requestSender);
}
@@ -114,6 +142,23 @@ private void handleChunk(HttpContent chunk,
}
}
+ /**
+ * Processes HTTP protocol messages including responses and body chunks.
+ *
+ * This method handles:
+ *
+ * HttpResponse - status line and headers
+ * HttpContent - response body chunks and trailer headers
+ * DecoderResult errors from the HTTP codec
+ *
+ * It also applies IO exception filters and retry logic when appropriate.
+ *
+ *
+ * @param channel the channel the message was read from
+ * @param future the response future associated with the request
+ * @param e the message to handle (HttpResponse or HttpContent)
+ * @throws Exception if an error occurs during message handling
+ */
@Override
public void handleRead(final Channel channel, final NettyResponseFuture> future, final Object e) throws Exception {
@@ -165,10 +210,29 @@ private void readFailed(Channel channel, NettyResponseFuture> future, Throwabl
}
}
+ /**
+ * Handles exceptions for HTTP protocol operations.
+ *
+ * This method provides no additional exception handling beyond the base class,
+ * as HTTP-specific error handling is performed in {@link #handleRead}.
+ *
+ *
+ * @param future the response future associated with the request
+ * @param error the exception that occurred
+ */
@Override
public void handleException(NettyResponseFuture> future, Throwable error) {
}
+ /**
+ * Handles channel inactivation for HTTP protocol operations.
+ *
+ * This method provides no additional handling beyond the base class,
+ * as HTTP channel lifecycle management is performed in the base handler.
+ *
+ *
+ * @param future the response future associated with the request
+ */
@Override
public void handleChannelInactive(NettyResponseFuture> future) {
}
diff --git a/client/src/main/java/org/asynchttpclient/netty/handler/StreamedResponsePublisher.java b/client/src/main/java/org/asynchttpclient/netty/handler/StreamedResponsePublisher.java
index 4fb24dbd1a..c1e77536b0 100644
--- a/client/src/main/java/org/asynchttpclient/netty/handler/StreamedResponsePublisher.java
+++ b/client/src/main/java/org/asynchttpclient/netty/handler/StreamedResponsePublisher.java
@@ -24,6 +24,30 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+/**
+ * Reactive Streams publisher for streaming HTTP response body parts.
+ *
+ * This publisher implements backpressure-aware streaming of HTTP response body parts
+ * using the Reactive Streams API. It extends Netty's HandlerPublisher to integrate
+ * with the Netty pipeline and manages demand-driven read operations.
+ *
+ * Usage Examples:
+ * {@code
+ * // Used internally with StreamedAsyncHandler
+ * StreamedAsyncHandler handler = new StreamedAsyncHandler() {
+ * @Override
+ * public State onStream(Publisher publisher) {
+ * publisher.subscribe(new Subscriber() {
+ * public void onNext(HttpResponseBodyPart part) {
+ * // Process body part with backpressure
+ * subscription.request(1);
+ * }
+ * });
+ * return State.CONTINUE;
+ * }
+ * };
+ * }
+ */
public class StreamedResponsePublisher extends HandlerPublisher {
protected final Logger logger = LoggerFactory.getLogger(getClass());
@@ -34,6 +58,14 @@ public class StreamedResponsePublisher extends HandlerPublisher future, Channel channel) {
super(executor, HttpResponseBodyPart.class);
this.channelManager = channelManager;
@@ -41,6 +73,13 @@ public class StreamedResponsePublisher extends HandlerPublisher
+ * When the subscriber cancels the subscription, this method marks the future as done
+ * and closes the channel since the remaining response body will not be consumed.
+ *
+ */
@Override
protected void cancelled() {
logger.debug("Subscriber cancelled, ignoring the rest of the body");
@@ -56,31 +95,78 @@ protected void cancelled() {
channelManager.closeChannel(channel);
}
+ /**
+ * Handles demand requests from the subscriber.
+ *
+ * This method is called when the subscriber requests more items. It sets the
+ * outstanding request flag to indicate that the subscriber is ready to receive data.
+ *
+ */
@Override
protected void requestDemand() {
hasOutstandingRequest = true;
super.requestDemand();
}
+ /**
+ * Handles channel read completion events.
+ *
+ * This method clears the outstanding request flag when a read completes, indicating
+ * that the current demand has been satisfied. It then delegates to the parent
+ * implementation to trigger backpressure handling.
+ *
+ *
+ * @param ctx the channel handler context
+ * @throws Exception if an error occurs during processing
+ */
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
hasOutstandingRequest = false;
super.channelReadComplete(ctx);
}
+ /**
+ * Subscribes a subscriber to this publisher.
+ *
+ * This method wraps the provided subscriber with an ErrorReplacingSubscriber
+ * to handle error replacement during completion, allowing deferred errors
+ * to be signaled at the appropriate time.
+ *
+ *
+ * @param subscriber the subscriber to receive body parts
+ */
@Override
public void subscribe(Subscriber super HttpResponseBodyPart> subscriber) {
super.subscribe(new ErrorReplacingSubscriber(subscriber));
}
+ /**
+ * Checks if there is an outstanding demand request from the subscriber.
+ *
+ * @return true if the subscriber has requested data that hasn't been fulfilled yet
+ */
public boolean hasOutstandingRequest() {
return hasOutstandingRequest;
}
+ /**
+ * Returns the response future associated with this publisher.
+ *
+ * @return the NettyResponseFuture for this streaming response
+ */
NettyResponseFuture> future() {
return future;
}
+ /**
+ * Sets an error that should be delivered to the subscriber on completion.
+ *
+ * This allows deferring error signaling until the natural completion point
+ * of the stream, converting a successful completion into an error signal.
+ *
+ *
+ * @param t the error to deliver on completion
+ */
public void setError(Throwable t) {
this.error = t;
}
diff --git a/client/src/main/java/org/asynchttpclient/netty/handler/WebSocketHandler.java b/client/src/main/java/org/asynchttpclient/netty/handler/WebSocketHandler.java
index 533322f4bd..32c2362d7e 100755
--- a/client/src/main/java/org/asynchttpclient/netty/handler/WebSocketHandler.java
+++ b/client/src/main/java/org/asynchttpclient/netty/handler/WebSocketHandler.java
@@ -34,9 +34,24 @@
import static io.netty.handler.codec.http.HttpResponseStatus.SWITCHING_PROTOCOLS;
import static org.asynchttpclient.ws.WebSocketUtils.getAcceptKey;
+/**
+ * WebSocket protocol handler for managing WebSocket connections and frame processing.
+ *
+ * This handler manages the WebSocket handshake upgrade process, validates the upgrade
+ * response, and processes WebSocket frames. It implements the WebSocket protocol as
+ * specified in RFC 6455.
+ *
+ */
@Sharable
public final class WebSocketHandler extends AsyncHttpClientHandler {
+ /**
+ * Constructs a new WebSocketHandler.
+ *
+ * @param config the async HTTP client configuration
+ * @param channelManager the channel manager for managing channel lifecycle
+ * @param requestSender the request sender for sending HTTP requests
+ */
public WebSocketHandler(AsyncHttpClientConfig config,
ChannelManager channelManager,
NettyRequestSender requestSender) {
@@ -95,6 +110,22 @@ private void abort(Channel channel, NettyResponseFuture> future, WebSocketUpgr
}
}
+ /**
+ * Processes WebSocket protocol messages including handshake responses and frames.
+ *
+ * This method handles:
+ *
+ * HTTP upgrade responses for WebSocket handshake validation
+ * WebSocket frames for established connections
+ * Final handshake cleanup with LastHttpContent
+ *
+ *
+ *
+ * @param channel the channel the message was read from
+ * @param future the response future associated with the WebSocket upgrade request
+ * @param e the message to handle (HttpResponse, WebSocketFrame, or LastHttpContent)
+ * @throws Exception if an error occurs during message handling
+ */
@Override
public void handleRead(Channel channel, NettyResponseFuture> future, Object e) throws Exception {
@@ -137,6 +168,16 @@ public void handleRead(Channel channel, NettyResponseFuture> future, Object e)
}
}
+ /**
+ * Handles exceptions during WebSocket operations by notifying the WebSocket listener.
+ *
+ * This method propagates the exception to the WebSocket's error handler and
+ * sends a close frame to gracefully terminate the connection.
+ *
+ *
+ * @param future the response future associated with the WebSocket
+ * @param e the exception that occurred
+ */
@Override
public void handleException(NettyResponseFuture> future, Throwable e) {
logger.warn("onError", e);
@@ -152,6 +193,16 @@ public void handleException(NettyResponseFuture> future, Throwable e) {
}
}
+ /**
+ * Handles WebSocket connection closure when the channel becomes inactive.
+ *
+ * This method is called when the connection is closed without receiving a proper
+ * WebSocket close frame. It notifies the WebSocket listener with status code 1006
+ * (abnormal closure) as per RFC 6455.
+ *
+ *
+ * @param future the response future associated with the WebSocket
+ */
@Override
public void handleChannelInactive(NettyResponseFuture> future) {
logger.trace("Connection was closed abnormally (that is, with no close frame being received).");
diff --git a/client/src/main/java/org/asynchttpclient/netty/request/NettyRequest.java b/client/src/main/java/org/asynchttpclient/netty/request/NettyRequest.java
index 1863501574..91ceb982fb 100755
--- a/client/src/main/java/org/asynchttpclient/netty/request/NettyRequest.java
+++ b/client/src/main/java/org/asynchttpclient/netty/request/NettyRequest.java
@@ -16,20 +16,43 @@
import io.netty.handler.codec.http.HttpRequest;
import org.asynchttpclient.netty.request.body.NettyBody;
+/**
+ * Represents a complete Netty HTTP request with optional body.
+ *
+ * This class wraps a Netty {@link HttpRequest} and its associated {@link NettyBody},
+ * providing a unified representation of the request to be written to the channel.
+ *
+ */
public final class NettyRequest {
private final HttpRequest httpRequest;
private final NettyBody body;
+ /**
+ * Constructs a new NettyRequest.
+ *
+ * @param httpRequest the Netty HTTP request
+ * @param body the request body, or null for requests without a body
+ */
NettyRequest(HttpRequest httpRequest, NettyBody body) {
this.httpRequest = httpRequest;
this.body = body;
}
+ /**
+ * Returns the Netty HTTP request.
+ *
+ * @return the HTTP request containing headers and metadata
+ */
public HttpRequest getHttpRequest() {
return httpRequest;
}
+ /**
+ * Returns the request body.
+ *
+ * @return the body to be written, or null if no body
+ */
public NettyBody getBody() {
return body;
}
diff --git a/client/src/main/java/org/asynchttpclient/netty/request/NettyRequestFactory.java b/client/src/main/java/org/asynchttpclient/netty/request/NettyRequestFactory.java
index 4cfee06cd6..c3dad92404 100755
--- a/client/src/main/java/org/asynchttpclient/netty/request/NettyRequestFactory.java
+++ b/client/src/main/java/org/asynchttpclient/netty/request/NettyRequestFactory.java
@@ -37,6 +37,20 @@
import static org.asynchttpclient.util.MiscUtils.isNonEmpty;
import static org.asynchttpclient.ws.WebSocketUtils.getWebSocketKey;
+/**
+ * Factory for creating Netty HTTP requests from async-http-client Request objects.
+ *
+ * This factory handles the transformation of high-level Request objects into Netty-specific
+ * HttpRequest instances, including:
+ *
+ * Header configuration (User-Agent, Accept, Content-Type, etc.)
+ * Body encoding (form params, multipart, files, streams, etc.)
+ * Authentication headers (Basic, Digest, NTLM, OAuth)
+ * Proxy configuration and CONNECT requests
+ * WebSocket upgrade headers
+ *
+ *
+ */
public final class NettyRequestFactory {
private static final Integer ZERO_CONTENT_LENGTH = 0;
@@ -44,6 +58,11 @@ public final class NettyRequestFactory {
private final AsyncHttpClientConfig config;
private final ClientCookieEncoder cookieEncoder;
+ /**
+ * Constructs a new NettyRequestFactory.
+ *
+ * @param config the async HTTP client configuration
+ */
NettyRequestFactory(AsyncHttpClientConfig config) {
this.config = config;
cookieEncoder = config.isUseLaxCookieEncoder() ? ClientCookieEncoder.LAX : ClientCookieEncoder.STRICT;
@@ -97,17 +116,48 @@ private NettyBody body(Request request) {
return nettyBody;
}
+ /**
+ * Adds an authorization header to the request.
+ *
+ * This method appends (not replaces) the authorization header, allowing
+ * multiple authorization schemes to be present.
+ *
+ *
+ * @param headers the HTTP headers to modify
+ * @param authorizationHeader the authorization header value, or null
+ */
public void addAuthorizationHeader(HttpHeaders headers, String authorizationHeader) {
if (authorizationHeader != null)
// don't override authorization but append
headers.add(AUTHORIZATION, authorizationHeader);
}
+ /**
+ * Sets the proxy authorization header for the request.
+ *
+ * @param headers the HTTP headers to modify
+ * @param proxyAuthorizationHeader the proxy authorization header value, or null
+ */
public void setProxyAuthorizationHeader(HttpHeaders headers, String proxyAuthorizationHeader) {
if (proxyAuthorizationHeader != null)
headers.set(PROXY_AUTHORIZATION, proxyAuthorizationHeader);
}
+ /**
+ * Creates a new NettyRequest from a high-level Request object.
+ *
+ * This method handles all aspects of request construction including method selection,
+ * URI formatting (for proxies and CONNECT requests), body encoding, header configuration,
+ * authentication, and WebSocket upgrade headers.
+ *
+ *
+ * @param request the async-http-client request
+ * @param performConnectRequest whether to create an HTTP CONNECT request for tunneling
+ * @param proxyServer the proxy server configuration, or null
+ * @param realm the authentication realm, or null
+ * @param proxyRealm the proxy authentication realm, or null
+ * @return a new NettyRequest ready to be written to a channel
+ */
public NettyRequest newNettyRequest(Request request, boolean performConnectRequest, ProxyServer proxyServer, Realm realm, Realm proxyRealm) {
Uri uri = request.getUri();
diff --git a/client/src/main/java/org/asynchttpclient/netty/request/NettyRequestSender.java b/client/src/main/java/org/asynchttpclient/netty/request/NettyRequestSender.java
index 4fa0589a84..7f58b72b9b 100755
--- a/client/src/main/java/org/asynchttpclient/netty/request/NettyRequestSender.java
+++ b/client/src/main/java/org/asynchttpclient/netty/request/NettyRequestSender.java
@@ -59,6 +59,21 @@
import static org.asynchttpclient.util.MiscUtils.getCause;
import static org.asynchttpclient.util.ProxyUtils.getProxyServer;
+/**
+ * Central orchestrator for sending HTTP requests through Netty channels.
+ *
+ * This class manages the complete request lifecycle including:
+ *
+ * Channel acquisition from pool or creation of new connections
+ * Address resolution and connection establishment
+ * SSL/TLS handshake coordination
+ * Proxy tunneling via HTTP CONNECT
+ * Request writing and timeout management
+ * Retry logic and error handling
+ * IO exception filter application
+ *
+ *
+ */
public final class NettyRequestSender {
private static final Logger LOGGER = LoggerFactory.getLogger(NettyRequestSender.class);
@@ -70,6 +85,14 @@ public final class NettyRequestSender {
private final AsyncHttpClientState clientState;
private final NettyRequestFactory requestFactory;
+ /**
+ * Constructs a new NettyRequestSender.
+ *
+ * @param config the async HTTP client configuration
+ * @param channelManager the channel manager for connection lifecycle
+ * @param nettyTimer the timer for scheduling timeouts
+ * @param clientState the client state for shutdown checks
+ */
public NettyRequestSender(AsyncHttpClientConfig config,
ChannelManager channelManager,
Timer nettyTimer,
@@ -84,6 +107,22 @@ public NettyRequestSender(AsyncHttpClientConfig config,
requestFactory = new NettyRequestFactory(config);
}
+ /**
+ * Sends an HTTP request and returns a future for the response.
+ *
+ * This method is the main entry point for request execution. It handles proxy
+ * configuration, channel pooling, connection establishment, and request writing.
+ * For HTTPS or WebSocket connections through HTTP proxies, it automatically
+ * performs CONNECT tunneling.
+ *
+ *
+ * @param request the request to send
+ * @param asyncHandler the handler for processing the response
+ * @param future an existing future to reuse (for redirects/retries), or null
+ * @param the response type
+ * @return a ListenableFuture that will complete with the response
+ * @throws IllegalStateException if the client is closed
+ */
public ListenableFuture sendRequest(final Request request,
final AsyncHandler asyncHandler,
NettyResponseFuture future) {
@@ -380,6 +419,18 @@ private NettyResponseFuture newNettyResponseFuture(Request request,
return future;
}
+ /**
+ * Writes an HTTP request to a channel.
+ *
+ * This method writes the HTTP request headers and optionally the body,
+ * handling Expect: 100-continue logic, progress tracking, and write
+ * completion listeners. It also schedules the read timeout.
+ *
+ *
+ * @param future the response future containing the request
+ * @param channel the channel to write to
+ * @param the response type
+ */
public void writeRequest(NettyResponseFuture future, Channel channel) {
NettyRequest nettyRequest = future.getNettyRequest();
@@ -461,6 +512,18 @@ private void scheduleReadTimeout(NettyResponseFuture> nettyResponseFuture) {
}
}
+ /**
+ * Aborts a request by closing the channel and completing the future exceptionally.
+ *
+ * This method is called when an error occurs that makes it impossible to continue
+ * the request. It ensures the channel is closed and the future is completed with
+ * the exception.
+ *
+ *
+ * @param channel the channel to close (may be null)
+ * @param future the future to abort
+ * @param t the exception that caused the abort
+ */
public void abort(Channel channel, NettyResponseFuture> future, Throwable t) {
if (channel != null) {
@@ -495,6 +558,17 @@ public void handleUnexpectedClosedChannel(Channel channel, NettyResponseFuture
}
}
+ /**
+ * Attempts to retry a failed request.
+ *
+ * This method checks if retry is possible based on the request's retry count
+ * and replayability. If retry is allowed, it resets the channel state and
+ * sends the request again.
+ *
+ *
+ * @param future the future to retry
+ * @return true if retry was initiated, false otherwise
+ */
public boolean retry(NettyResponseFuture> future) {
if (isClosed()) {
@@ -527,6 +601,19 @@ public boolean retry(NettyResponseFuture> future) {
}
}
+ /**
+ * Applies IO exception filters and potentially replays the request.
+ *
+ * This method runs all configured IO exception filters, allowing them to
+ * inspect the exception and decide whether to retry the request. If any
+ * filter requests a replay and retry is possible, the request is resent.
+ *
+ *
+ * @param future the response future
+ * @param e the IOException that occurred
+ * @param channel the channel where the exception occurred
+ * @return true if the request was replayed, false otherwise
+ */
public boolean applyIoExceptionFiltersAndReplayRequest(NettyResponseFuture> future, IOException e,
Channel channel) {
diff --git a/client/src/main/java/org/asynchttpclient/netty/ws/NettyWebSocket.java b/client/src/main/java/org/asynchttpclient/netty/ws/NettyWebSocket.java
index f6ab4ae2f3..07c374c9bb 100755
--- a/client/src/main/java/org/asynchttpclient/netty/ws/NettyWebSocket.java
+++ b/client/src/main/java/org/asynchttpclient/netty/ws/NettyWebSocket.java
@@ -35,6 +35,23 @@
import static io.netty.buffer.Unpooled.wrappedBuffer;
import static org.asynchttpclient.netty.util.ByteBufUtils.byteBuf2Bytes;
+/**
+ * Netty-based WebSocket implementation.
+ *
+ * This class provides a complete WebSocket client implementation supporting:
+ *
+ * Text and binary message frames
+ * Fragmented messages via continuation frames
+ * Ping/pong frames for keep-alive
+ * Close frames with status codes and reason text
+ * Multiple listener registration for event notification
+ *
+ *
+ *
+ * The implementation handles frame buffering during the upgrade handshake and
+ * correctly manages fragmented message reassembly according to RFC 6455.
+ *
+ */
public final class NettyWebSocket implements WebSocket {
private static final Logger LOGGER = LoggerFactory.getLogger(NettyWebSocket.class);
@@ -47,6 +64,12 @@ public final class NettyWebSocket implements WebSocket {
private boolean ready;
private List bufferedFrames;
+ /**
+ * Constructs a new NettyWebSocket.
+ *
+ * @param channel the Netty channel for this WebSocket connection
+ * @param upgradeHeaders the HTTP headers from the upgrade response
+ */
public NettyWebSocket(Channel channel, HttpHeaders upgradeHeaders) {
this(channel, upgradeHeaders, new ConcurrentLinkedQueue<>());
}
@@ -179,10 +202,29 @@ public WebSocket removeWebSocketListener(WebSocketListener l) {
// INTERNAL, NOT FOR PUBLIC USAGE!!!
+ /**
+ * Checks if the WebSocket is ready to process frames.
+ *
+ * Internal method used to determine if the upgrade handshake has completed
+ * and listeners have been notified.
+ *
+ *
+ * @return true if ready to process frames
+ */
public boolean isReady() {
return ready;
}
+ /**
+ * Buffers a frame received before the WebSocket was fully initialized.
+ *
+ * During the upgrade handshake, frames may arrive before the WebSocket
+ * has been fully set up. These frames are buffered and replayed once
+ * initialization completes.
+ *
+ *
+ * @param frame the frame to buffer
+ */
public void bufferFrame(WebSocketFrame frame) {
if (bufferedFrames == null) {
bufferedFrames = new ArrayList<>(1);
@@ -200,6 +242,13 @@ private void releaseBufferedFrames() {
}
}
+ /**
+ * Processes all buffered frames and marks the WebSocket as ready.
+ *
+ * This method is called after the upgrade handshake completes and listeners
+ * are notified. It replays any frames that arrived during initialization.
+ *
+ */
public void processBufferedFrames() {
ready = true;
if (bufferedFrames != null) {
@@ -214,6 +263,16 @@ public void processBufferedFrames() {
}
}
+ /**
+ * Handles an incoming WebSocket frame by dispatching to the appropriate handler.
+ *
+ * This method routes frames based on their type (text, binary, close, ping, pong,
+ * continuation) and notifies registered listeners. For close frames, it also
+ * initiates channel closure.
+ *
+ *
+ * @param frame the WebSocket frame to handle
+ */
public void handleFrame(WebSocketFrame frame) {
if (frame instanceof TextWebSocketFrame) {
onTextFrame((TextWebSocketFrame) frame);
@@ -238,6 +297,15 @@ public void handleFrame(WebSocketFrame frame) {
}
}
+ /**
+ * Notifies all listeners of an error.
+ *
+ * This method is called when an exception occurs during WebSocket operations.
+ * It ensures all buffered frames are released to prevent memory leaks.
+ *
+ *
+ * @param t the error that occurred
+ */
public void onError(Throwable t) {
try {
for (WebSocketListener listener : listeners) {
@@ -252,6 +320,16 @@ public void onError(Throwable t) {
}
}
+ /**
+ * Notifies all listeners of WebSocket closure.
+ *
+ * This method is called when the WebSocket connection closes, either normally
+ * or abnormally. It clears all listeners and releases buffered frames.
+ *
+ *
+ * @param code the closure status code (as defined in RFC 6455)
+ * @param reason the closure reason text
+ */
public void onClose(int code, String reason) {
try {
for (WebSocketListener l : listeners) {
diff --git a/client/src/main/java/org/asynchttpclient/ntlm/NtlmEngine.java b/client/src/main/java/org/asynchttpclient/ntlm/NtlmEngine.java
index 06a70b9182..c999679096 100644
--- a/client/src/main/java/org/asynchttpclient/ntlm/NtlmEngine.java
+++ b/client/src/main/java/org/asynchttpclient/ntlm/NtlmEngine.java
@@ -43,12 +43,47 @@
/**
* Provides an implementation for NTLMv1, NTLMv2, and NTLM2 Session forms of the NTLM
* authentication protocol.
+ *
+ * This class implements the Windows NT LAN Manager (NTLM) authentication protocol,
+ * which is used for authentication in Windows-based networks. It supports multiple
+ * versions of the protocol including NTLMv1, NTLMv2, and NTLM2 Session.
+ *
+ *
+ * This implementation is based on Apache HttpComponents and supports HMAC-SHA1
+ * signature calculation and header inclusion methods. The protocol involves a
+ * three-way handshake:
+ *
+ *
+ * Type 1 message: Client sends authentication request
+ * Type 2 message: Server responds with challenge
+ * Type 3 message: Client sends encrypted response
+ *
+ *
+ * Usage Examples:
+ * {@code
+ * NtlmEngine engine = NtlmEngine.INSTANCE;
+ *
+ * // Generate Type 1 message (initial request)
+ * String type1Msg = engine.generateType1Msg();
+ *
+ * // After receiving Type 2 message from server, generate Type 3 response
+ * String type3Msg = engine.generateType3Msg(
+ * "username",
+ * "password",
+ * "DOMAIN",
+ * "WORKSTATION",
+ * serverChallenge
+ * );
+ * }
*
* @since 4.1
*/
@SuppressWarnings("unused")
public final class NtlmEngine {
+ /**
+ * Singleton instance of the NTLM engine.
+ */
public static final NtlmEngine INSTANCE = new NtlmEngine();
/** Unicode encoding */
@@ -1511,16 +1546,44 @@ void update(final byte[] input) {
}
/**
- * Creates the first message (type 1 message) in the NTLM authentication
- * sequence. This message includes the user name, domain and host for the
- * authentication session.
+ * Creates the first message (Type 1 message) in the NTLM authentication sequence.
+ *
+ * This message is the initial authentication request sent to the server. It includes
+ * protocol version and supported flags but does not include credentials. The server
+ * will respond with a Type 2 message containing a challenge.
+ *
+ *
+ * The message is Base64-encoded and ready to be added to the HTTP Authorization header.
+ *
*
- * @return String the message to add to the HTTP request header.
- */
+ * @return the Base64-encoded Type 1 message to add to the HTTP Authorization header
+ */
public String generateType1Msg() {
return TYPE_1_MESSAGE;
}
+ /**
+ * Creates the third message (Type 3 message) in the NTLM authentication sequence.
+ *
+ * This message is the authentication response that proves the client knows the password
+ * without transmitting it. It includes the username, domain, workstation name, and
+ * encrypted responses to the server's challenge.
+ *
+ *
+ * The method decodes the server's Type 2 challenge message, extracts the server nonce
+ * and flags, and generates appropriate responses based on the protocol version supported
+ * by the server (NTLMv1, NTLMv2, or NTLM2 Session).
+ *
+ *
+ * @param username the username for authentication (without domain prefix)
+ * @param password the user's password
+ * @param domain the Windows domain name (can be null)
+ * @param workstation the client workstation name (can be null)
+ * @param challenge the Base64-encoded Type 2 message received from the server
+ * @return the Base64-encoded Type 3 message to add to the HTTP Authorization header
+ * @throws NtlmEngineException if the challenge message is invalid, encryption fails,
+ * or required cryptographic algorithms are unavailable
+ */
public String generateType3Msg(final String username, final String password, final String domain, final String workstation,
final String challenge) throws NtlmEngineException {
final Type2Message t2m = new Type2Message(challenge);
diff --git a/client/src/main/java/org/asynchttpclient/ntlm/NtlmEngineException.java b/client/src/main/java/org/asynchttpclient/ntlm/NtlmEngineException.java
index fe15cffd33..3231062651 100644
--- a/client/src/main/java/org/asynchttpclient/ntlm/NtlmEngineException.java
+++ b/client/src/main/java/org/asynchttpclient/ntlm/NtlmEngineException.java
@@ -29,13 +29,18 @@
/**
* Signals NTLM protocol failure.
+ *
+ * This exception is thrown when an error occurs during NTLM authentication processing,
+ * such as when message decoding fails, required cryptographic algorithms are unavailable,
+ * or the authentication challenge is malformed.
+ *
*/
class NtlmEngineException extends RuntimeException {
private static final long serialVersionUID = 6027981323731768824L;
/**
- * Creates a new NTLMEngineException with the specified message.
+ * Creates a new NtlmEngineException with the specified message.
*
* @param message the exception detail message
*/
@@ -44,11 +49,11 @@ class NtlmEngineException extends RuntimeException {
}
/**
- * Creates a new NTLMEngineException with the specified detail message and cause.
+ * Creates a new NtlmEngineException with the specified detail message and cause.
*
* @param message the exception detail message
- * @param cause the Throwable that caused this exception, or null
- * if the cause is unavailable, unknown, or not a Throwable
+ * @param cause the {@code Throwable} that caused this exception, or {@code null}
+ * if the cause is unavailable, unknown, or not a {@code Throwable}
*/
NtlmEngineException(String message, Throwable cause) {
super(message, cause);
diff --git a/client/src/main/java/org/asynchttpclient/oauth/ConsumerKey.java b/client/src/main/java/org/asynchttpclient/oauth/ConsumerKey.java
index dd193daf4e..7718730377 100644
--- a/client/src/main/java/org/asynchttpclient/oauth/ConsumerKey.java
+++ b/client/src/main/java/org/asynchttpclient/oauth/ConsumerKey.java
@@ -17,26 +17,64 @@
/**
* Value class for OAuth consumer keys.
+ *
+ * Represents the consumer credentials used in OAuth 1.0 authentication, consisting of a key and a secret.
+ * The key is automatically percent-encoded for use in OAuth signatures.
+ *
+ *
+ * Usage Examples:
+ * {@code
+ * ConsumerKey consumerKey = new ConsumerKey("my-consumer-key", "my-consumer-secret");
+ * String key = consumerKey.getKey();
+ * String encodedKey = consumerKey.getPercentEncodedKey();
+ * }
*/
public class ConsumerKey {
private final String key;
private final String secret;
private final String percentEncodedKey;
+ /**
+ * Creates a new consumer key with the specified key and secret.
+ *
+ * The key is automatically percent-encoded using UTF-8 URL encoding for use in OAuth signatures.
+ *
+ *
+ * @param key the consumer key (public identifier)
+ * @param secret the consumer secret (confidential part)
+ */
public ConsumerKey(String key, String secret) {
this.key = key;
this.secret = secret;
this.percentEncodedKey = Utf8UrlEncoder.percentEncodeQueryElement(key);
}
+ /**
+ * Returns the consumer key (public identifier).
+ *
+ * @return the consumer key
+ */
public String getKey() {
return key;
}
+ /**
+ * Returns the consumer secret (confidential part).
+ *
+ * @return the consumer secret
+ */
public String getSecret() {
return secret;
}
+ /**
+ * Returns the percent-encoded consumer key.
+ *
+ * This is the URL-encoded version of the key, ready for use in OAuth signature calculations.
+ *
+ *
+ * @return the percent-encoded consumer key
+ */
public String getPercentEncodedKey() {
return percentEncodedKey;
}
diff --git a/client/src/main/java/org/asynchttpclient/oauth/OAuthSignatureCalculator.java b/client/src/main/java/org/asynchttpclient/oauth/OAuthSignatureCalculator.java
index a0235bb5af..39728b7c5b 100644
--- a/client/src/main/java/org/asynchttpclient/oauth/OAuthSignatureCalculator.java
+++ b/client/src/main/java/org/asynchttpclient/oauth/OAuthSignatureCalculator.java
@@ -23,6 +23,23 @@
/**
* OAuth {@link SignatureCalculator} that delegates to {@link OAuthSignatureCalculatorInstance}s.
+ *
+ * This class provides a thread-safe OAuth 1.0 signature calculator by maintaining a pool of
+ * {@link OAuthSignatureCalculatorInstance} objects using ThreadLocal storage. Each thread gets
+ * its own instance, avoiding synchronization overhead.
+ *
+ *
+ * Usage Examples:
+ * {@code
+ * ConsumerKey consumerKey = new ConsumerKey("consumer-key", "consumer-secret");
+ * RequestToken requestToken = new RequestToken("request-key", "request-secret");
+ * OAuthSignatureCalculator calculator = new OAuthSignatureCalculator(consumerKey, requestToken);
+ *
+ * AsyncHttpClient client = Dsl.asyncHttpClient();
+ * client.prepareGet("https://api.example.com/protected")
+ * .setSignatureCalculator(calculator)
+ * .execute();
+ * }
*/
public class OAuthSignatureCalculator implements SignatureCalculator {
@@ -39,14 +56,28 @@ public class OAuthSignatureCalculator implements SignatureCalculator {
private final RequestToken userAuth;
/**
+ * Creates a new OAuth signature calculator with the specified credentials.
+ *
* @param consumerAuth Consumer key to use for signature calculation
- * @param userAuth Request/access token to use for signature calculation
+ * @param userAuth Request/access token to use for signature calculation
*/
public OAuthSignatureCalculator(ConsumerKey consumerAuth, RequestToken userAuth) {
this.consumerAuth = consumerAuth;
this.userAuth = userAuth;
}
+ /**
+ * Calculates the OAuth signature and adds it to the request as an Authorization header.
+ *
+ * This method computes the OAuth 1.0 signature based on the request details and the
+ * configured consumer key and request token, then sets the Authorization header on the
+ * request builder.
+ *
+ *
+ * @param request the request to sign
+ * @param requestBuilder the request builder to add the signature to
+ * @throws IllegalArgumentException if the signature cannot be computed due to invalid keys
+ */
@Override
public void calculateAndAddSignature(Request request, RequestBuilderBase> requestBuilder) {
try {
diff --git a/client/src/main/java/org/asynchttpclient/oauth/OAuthSignatureCalculatorInstance.java b/client/src/main/java/org/asynchttpclient/oauth/OAuthSignatureCalculatorInstance.java
index aa92c5aaa1..545995f500 100644
--- a/client/src/main/java/org/asynchttpclient/oauth/OAuthSignatureCalculatorInstance.java
+++ b/client/src/main/java/org/asynchttpclient/oauth/OAuthSignatureCalculatorInstance.java
@@ -35,8 +35,14 @@
/**
* Non thread-safe {@link SignatureCalculator} for OAuth1.
*
- * Supports most common signature inclusion and calculation methods: HMAC-SHA1 for calculation, and Header inclusion as inclusion method. Nonce generation uses simple random
- * numbers with base64 encoding.
+ * Supports most common signature inclusion and calculation methods: HMAC-SHA1 for calculation,
+ * and Header inclusion as inclusion method. Nonce generation uses simple random numbers with
+ * base64 encoding.
+ *
+ *
+ * This class is not thread-safe and should be used through the thread-local instance pool
+ * provided by {@link OAuthSignatureCalculator}.
+ *
*/
public class OAuthSignatureCalculatorInstance {
@@ -58,10 +64,34 @@ public class OAuthSignatureCalculatorInstance {
private final byte[] nonceBuffer = new byte[16];
private final Parameters parameters = new Parameters();
+ /**
+ * Creates a new OAuth signature calculator instance.
+ *
+ * Initializes the HMAC-SHA1 MAC instance used for signature generation.
+ *
+ *
+ * @throws NoSuchAlgorithmException if HMAC-SHA1 algorithm is not available
+ */
public OAuthSignatureCalculatorInstance() throws NoSuchAlgorithmException {
mac = Mac.getInstance(HMAC_SHA1_ALGORITHM);
}
+ /**
+ * Computes the OAuth authorization header for the given request.
+ *
+ * This method generates a nonce and timestamp, then computes the OAuth signature
+ * and constructs the complete Authorization header value.
+ *
+ *
+ * @param consumerAuth the consumer key credentials
+ * @param userAuth the request/access token credentials
+ * @param uri the request URI
+ * @param method the HTTP method (e.g., GET, POST)
+ * @param formParams the form parameters (can be null)
+ * @param queryParams the query parameters (can be null)
+ * @return the OAuth Authorization header value
+ * @throws InvalidKeyException if the consumer or user secrets are invalid
+ */
public String computeAuthorizationHeader(ConsumerKey consumerAuth,
RequestToken userAuth,
Uri uri,
diff --git a/client/src/main/java/org/asynchttpclient/oauth/Parameter.java b/client/src/main/java/org/asynchttpclient/oauth/Parameter.java
index bc4734ea29..1de39f16ca 100644
--- a/client/src/main/java/org/asynchttpclient/oauth/Parameter.java
+++ b/client/src/main/java/org/asynchttpclient/oauth/Parameter.java
@@ -14,28 +14,63 @@
package org.asynchttpclient.oauth;
/**
- * Helper class for sorting query and form parameters that we need
+ * Helper class for sorting query and form parameters used in OAuth signature calculation.
+ *
+ * This class represents a key-value pair that can be sorted according to OAuth specifications.
+ * Parameters are first sorted by key, then by value if keys are equal.
+ *
*/
final class Parameter implements Comparable {
final String key, value;
+ /**
+ * Creates a new parameter with the specified key and value.
+ *
+ * @param key the parameter key
+ * @param value the parameter value
+ */
public Parameter(String key, String value) {
this.key = key;
this.value = value;
}
+ /**
+ * Compares this parameter to another for sorting purposes.
+ *
+ * Parameters are compared first by key, then by value if keys are equal.
+ * This ordering is required for OAuth signature calculation.
+ *
+ *
+ * @param other the parameter to compare to
+ * @return a negative integer, zero, or a positive integer as this parameter
+ * is less than, equal to, or greater than the specified parameter
+ */
@Override
public int compareTo(Parameter other) {
int keyDiff = key.compareTo(other.key);
return keyDiff == 0 ? value.compareTo(other.value) : keyDiff;
}
+ /**
+ * Returns a string representation of this parameter in the form "key=value".
+ *
+ * @return a string representation of this parameter
+ */
@Override
public String toString() {
return key + "=" + value;
}
+ /**
+ * Indicates whether some other object is equal to this parameter.
+ *
+ * Two parameters are considered equal if both their keys and values are equal.
+ *
+ *
+ * @param o the reference object with which to compare
+ * @return {@code true} if this parameter is equal to the argument; {@code false} otherwise
+ */
@Override
public boolean equals(Object o) {
if (this == o)
@@ -47,6 +82,11 @@ public boolean equals(Object o) {
return key.equals(parameter.key) && value.equals(parameter.value);
}
+ /**
+ * Returns a hash code value for this parameter.
+ *
+ * @return a hash code value for this parameter
+ */
@Override
public int hashCode() {
int result = key.hashCode();
diff --git a/client/src/main/java/org/asynchttpclient/oauth/Parameters.java b/client/src/main/java/org/asynchttpclient/oauth/Parameters.java
index b0c533ac25..d257e3cd1e 100644
--- a/client/src/main/java/org/asynchttpclient/oauth/Parameters.java
+++ b/client/src/main/java/org/asynchttpclient/oauth/Parameters.java
@@ -19,19 +19,50 @@
import java.util.Collections;
import java.util.List;
+/**
+ * Collection of OAuth parameters used for signature calculation.
+ *
+ * This class maintains a list of parameters that can be sorted and concatenated
+ * according to OAuth 1.0 specifications. Parameters should be added in their
+ * percent-encoded form.
+ *
+ */
final class Parameters {
private List parameters = new ArrayList<>();
+ /**
+ * Adds a parameter to the collection.
+ *
+ * @param key the parameter key (should be percent-encoded)
+ * @param value the parameter value (should be percent-encoded)
+ * @return this Parameters instance for method chaining
+ */
public Parameters add(String key, String value) {
parameters.add(new Parameter(key, value));
return this;
}
+ /**
+ * Clears all parameters from the collection.
+ *
+ * This method allows the Parameters instance to be reused for multiple signature calculations.
+ *
+ */
public void reset() {
parameters.clear();
}
+ /**
+ * Sorts the parameters and concatenates them into a query string.
+ *
+ * Parameters are sorted according to OAuth specifications (first by key, then by value),
+ * then concatenated with '&' separators. The result is used in OAuth signature base string
+ * construction.
+ *
+ *
+ * @return the sorted and concatenated parameter string
+ */
String sortAndConcat() {
// then sort them (AFTER encoding, important)
Collections.sort(parameters);
diff --git a/client/src/main/java/org/asynchttpclient/oauth/RequestToken.java b/client/src/main/java/org/asynchttpclient/oauth/RequestToken.java
index 883eb3bcab..bbf61357fb 100644
--- a/client/src/main/java/org/asynchttpclient/oauth/RequestToken.java
+++ b/client/src/main/java/org/asynchttpclient/oauth/RequestToken.java
@@ -16,29 +16,66 @@
import org.asynchttpclient.util.Utf8UrlEncoder;
/**
- * Value class used for OAuth tokens (request secret, access secret);
- * simple container with two parts, public id part ("key") and
- * confidential ("secret") part.
+ * Value class used for OAuth tokens (request token or access token).
+ *
+ * This is a simple container with two parts: a public identifier ("key") and a
+ * confidential ("secret") part. The key is automatically percent-encoded for use
+ * in OAuth signatures.
+ *
+ *
+ * Usage Examples:
+ * {@code
+ * RequestToken requestToken = new RequestToken("request-key", "request-secret");
+ * String key = requestToken.getKey();
+ * String encodedKey = requestToken.getPercentEncodedKey();
+ * }
*/
public class RequestToken {
private final String key;
private final String secret;
private final String percentEncodedKey;
+ /**
+ * Creates a new request token with the specified key and secret.
+ *
+ * The key is automatically percent-encoded using UTF-8 URL encoding for use in OAuth signatures.
+ *
+ *
+ * @param key the token key (public identifier)
+ * @param token the token secret (confidential part)
+ */
public RequestToken(String key, String token) {
this.key = key;
this.secret = token;
this.percentEncodedKey = Utf8UrlEncoder.percentEncodeQueryElement(key);
}
+ /**
+ * Returns the token key (public identifier).
+ *
+ * @return the token key
+ */
public String getKey() {
return key;
}
+ /**
+ * Returns the token secret (confidential part).
+ *
+ * @return the token secret
+ */
public String getSecret() {
return secret;
}
+ /**
+ * Returns the percent-encoded token key.
+ *
+ * This is the URL-encoded version of the key, ready for use in OAuth signature calculations.
+ *
+ *
+ * @return the percent-encoded token key
+ */
public String getPercentEncodedKey() {
return percentEncodedKey;
}
diff --git a/client/src/main/java/org/asynchttpclient/proxy/ProxyServer.java b/client/src/main/java/org/asynchttpclient/proxy/ProxyServer.java
index 13c33590be..bb2fdf9b00 100644
--- a/client/src/main/java/org/asynchttpclient/proxy/ProxyServer.java
+++ b/client/src/main/java/org/asynchttpclient/proxy/ProxyServer.java
@@ -26,7 +26,33 @@
import static org.asynchttpclient.util.MiscUtils.isNonEmpty;
/**
- * Represents a proxy server.
+ * Represents a proxy server configuration.
+ *
+ * This class encapsulates all the information needed to connect through a proxy server,
+ * including the host, port, authentication realm, and patterns for hosts that should
+ * bypass the proxy. Both HTTP and SOCKS proxy types are supported.
+ *
+ *
+ * Usage Examples:
+ * {@code
+ * // Simple HTTP proxy
+ * ProxyServer proxy = new ProxyServer.Builder("proxy.example.com", 8080)
+ * .build();
+ *
+ * // Proxy with authentication
+ * Realm realm = new Realm.Builder("username", "password")
+ * .setScheme(Realm.AuthScheme.BASIC)
+ * .build();
+ * ProxyServer proxy = new ProxyServer.Builder("proxy.example.com", 8080)
+ * .setRealm(realm)
+ * .build();
+ *
+ * // Proxy with non-proxy hosts
+ * ProxyServer proxy = new ProxyServer.Builder("proxy.example.com", 8080)
+ * .setNonProxyHost("*.internal.com")
+ * .setNonProxyHost("localhost")
+ * .build();
+ * }
*/
public class ProxyServer {
@@ -37,6 +63,16 @@ public class ProxyServer {
private final List nonProxyHosts;
private final ProxyType proxyType;
+ /**
+ * Creates a new proxy server with the specified configuration.
+ *
+ * @param host the proxy server hostname
+ * @param port the proxy server port for non-SSL connections
+ * @param securedPort the proxy server port for SSL connections
+ * @param realm the authentication realm (can be null)
+ * @param nonProxyHosts list of host patterns that should bypass the proxy (can be null)
+ * @param proxyType the type of proxy (HTTP or SOCKS)
+ */
public ProxyServer(String host, int port, int securedPort, Realm realm, List nonProxyHosts,
ProxyType proxyType) {
this.host = host;
@@ -47,26 +83,56 @@ public ProxyServer(String host, int port, int securedPort, Realm realm, List getNonProxyHosts() {
return nonProxyHosts;
}
+ /**
+ * Returns the authentication realm for the proxy server.
+ *
+ * @return the authentication realm, or null if no authentication is configured
+ */
public Realm getRealm() {
return realm;
}
+ /**
+ * Returns the type of proxy (HTTP or SOCKS).
+ *
+ * @return the proxy type
+ */
public ProxyType getProxyType() {
return proxyType;
}
@@ -110,6 +176,9 @@ private boolean matchNonProxyHost(String targetHost, String nonProxyHost) {
return nonProxyHost.equalsIgnoreCase(targetHost);
}
+ /**
+ * Builder for creating ProxyServer instances.
+ */
public static class Builder {
private String host;
@@ -119,27 +188,68 @@ public static class Builder {
private List nonProxyHosts;
private ProxyType proxyType;
+ /**
+ * Creates a new proxy server builder with the specified host and port.
+ *
+ * The secured port is initially set to the same value as the port.
+ *
+ *
+ * @param host the proxy server hostname
+ * @param port the proxy server port
+ */
public Builder(String host, int port) {
this.host = host;
this.port = port;
this.securedPort = port;
}
+ /**
+ * Sets the proxy server port for SSL connections.
+ *
+ * @param securedPort the secured port number
+ * @return this builder for method chaining
+ */
public Builder setSecuredPort(int securedPort) {
this.securedPort = securedPort;
return this;
}
+ /**
+ * Sets the authentication realm for the proxy server.
+ *
+ * @param realm the authentication realm
+ * @return this builder for method chaining
+ */
public Builder setRealm(Realm realm) {
this.realm = realm;
return this;
}
+ /**
+ * Sets the authentication realm using a realm builder.
+ *
+ * @param realm the realm builder
+ * @return this builder for method chaining
+ */
public Builder setRealm(Realm.Builder realm) {
this.realm = realm.build();
return this;
}
+ /**
+ * Adds a single host pattern that should bypass the proxy.
+ *
+ * Patterns can use wildcards (*) as prefixes or suffixes. For example:
+ *
+ *
+ * *.example.com - matches any subdomain of example.com
+ * 192.168.* - matches any IP starting with 192.168.
+ * localhost - exact match
+ *
+ *
+ * @param nonProxyHost the host pattern to bypass the proxy
+ * @return this builder for method chaining
+ */
public Builder setNonProxyHost(String nonProxyHost) {
if (nonProxyHosts == null)
nonProxyHosts = new ArrayList<>(1);
@@ -147,16 +257,37 @@ public Builder setNonProxyHost(String nonProxyHost) {
return this;
}
+ /**
+ * Sets the list of host patterns that should bypass the proxy.
+ *
+ * @param nonProxyHosts the list of host patterns
+ * @return this builder for method chaining
+ */
public Builder setNonProxyHosts(List nonProxyHosts) {
this.nonProxyHosts = nonProxyHosts;
return this;
}
+ /**
+ * Sets the proxy type (HTTP or SOCKS).
+ *
+ * @param proxyType the proxy type
+ * @return this builder for method chaining
+ */
public Builder setProxyType(ProxyType proxyType) {
this.proxyType = proxyType;
return this;
}
+ /**
+ * Builds a new ProxyServer instance with the configured settings.
+ *
+ * If no proxy type is set, defaults to HTTP. If no non-proxy hosts are set,
+ * uses an empty list.
+ *
+ *
+ * @return a new ProxyServer instance
+ */
public ProxyServer build() {
List nonProxyHosts = this.nonProxyHosts != null ? Collections.unmodifiableList(this.nonProxyHosts)
: Collections.emptyList();
diff --git a/client/src/main/java/org/asynchttpclient/proxy/ProxyServerSelector.java b/client/src/main/java/org/asynchttpclient/proxy/ProxyServerSelector.java
index c3381005aa..15882c47b2 100644
--- a/client/src/main/java/org/asynchttpclient/proxy/ProxyServerSelector.java
+++ b/client/src/main/java/org/asynchttpclient/proxy/ProxyServerSelector.java
@@ -3,20 +3,54 @@
import org.asynchttpclient.uri.Uri;
/**
- * Selector for a proxy server
+ * Selector for choosing a proxy server based on the target URI.
+ *
+ * This interface allows for flexible proxy selection strategies, such as selecting
+ * different proxies based on the protocol, host, or other URI attributes. It can
+ * also be used to implement proxy auto-configuration (PAC) or other dynamic proxy
+ * selection mechanisms.
+ *
+ *
+ * Usage Examples:
+ * {@code
+ * // Always use the same proxy
+ * ProxyServer proxy = new ProxyServer.Builder("proxy.example.com", 8080).build();
+ * ProxyServerSelector selector = uri -> proxy;
+ *
+ * // Select proxy based on URI
+ * ProxyServerSelector selector = uri -> {
+ * if (uri.isSecured()) {
+ * return secureProxy;
+ * } else {
+ * return standardProxy;
+ * }
+ * };
+ *
+ * // No proxy
+ * ProxyServerSelector selector = ProxyServerSelector.NO_PROXY_SELECTOR;
+ * }
*/
public interface ProxyServerSelector {
/**
* A selector that always selects no proxy.
+ *
+ * Use this selector when you want to bypass proxy configuration and connect
+ * directly to all destinations.
+ *
*/
ProxyServerSelector NO_PROXY_SELECTOR = uri -> null;
/**
- * Select a proxy server to use for the given URI.
+ * Selects a proxy server to use for the given URI.
+ *
+ * Implementations can inspect the URI's protocol, host, port, or any other
+ * attributes to determine which proxy to use, or whether to bypass the proxy
+ * entirely.
+ *
*
- * @param uri The URI to select a proxy server for.
- * @return The proxy server to use, if any. May return null.
+ * @param uri the URI to select a proxy server for
+ * @return the proxy server to use, or null to connect directly without a proxy
*/
ProxyServer select(Uri uri);
}
diff --git a/client/src/main/java/org/asynchttpclient/proxy/ProxyType.java b/client/src/main/java/org/asynchttpclient/proxy/ProxyType.java
index bf680018a7..1b9bafe1f7 100644
--- a/client/src/main/java/org/asynchttpclient/proxy/ProxyType.java
+++ b/client/src/main/java/org/asynchttpclient/proxy/ProxyType.java
@@ -13,19 +13,69 @@
*/
package org.asynchttpclient.proxy;
+/**
+ * Enumeration of supported proxy server types.
+ *
+ * This enum defines the types of proxy protocols that can be used for routing
+ * HTTP requests through a proxy server. Each type has different characteristics
+ * and capabilities.
+ *
+ */
public enum ProxyType {
- HTTP(true), SOCKS_V4(false), SOCKS_V5(false);
+ /**
+ * HTTP proxy type.
+ *
+ * HTTP proxies operate at the application layer (Layer 7) and understand HTTP protocol.
+ * They can cache responses, modify headers, and provide content filtering.
+ * This is the most common proxy type for web traffic.
+ *
+ */
+ HTTP(true),
+
+ /**
+ * SOCKS version 4 proxy type.
+ *
+ * SOCKS v4 is a circuit-level proxy that operates at the session layer (Layer 5).
+ * It supports TCP connections but does not support authentication or UDP.
+ *
+ */
+ SOCKS_V4(false),
+
+ /**
+ * SOCKS version 5 proxy type.
+ *
+ * SOCKS v5 is an enhanced version of SOCKS that supports authentication,
+ * UDP traffic, and IPv6. It provides more features than SOCKS v4 while
+ * still operating at the session layer.
+ *
+ */
+ SOCKS_V5(false);
private final boolean http;
+ /**
+ * Creates a proxy type with the specified HTTP flag.
+ *
+ * @param http true if this is an HTTP proxy type, false for SOCKS
+ */
ProxyType(boolean http) {
this.http = http;
}
+ /**
+ * Checks whether this is an HTTP proxy type.
+ *
+ * @return true if this is an HTTP proxy, false otherwise
+ */
public boolean isHttp() {
return http;
}
+ /**
+ * Checks whether this is a SOCKS proxy type.
+ *
+ * @return true if this is a SOCKS proxy (v4 or v5), false otherwise
+ */
public boolean isSocks() {
return !isHttp();
}
diff --git a/client/src/main/java/org/asynchttpclient/request/body/Body.java b/client/src/main/java/org/asynchttpclient/request/body/Body.java
index 80e7e1c6b5..7ef2385925 100644
--- a/client/src/main/java/org/asynchttpclient/request/body/Body.java
+++ b/client/src/main/java/org/asynchttpclient/request/body/Body.java
@@ -19,40 +19,81 @@
import java.io.IOException;
/**
- * A request body.
+ * Represents a request body that can be transferred to a target buffer.
+ *
+ * This interface provides methods to retrieve the content length and transfer
+ * body content in chunks to a ByteBuf. Implementations should be closeable to
+ * release any resources held by the body.
+ *
+ *
+ * Usage Examples:
+ * {@code
+ * Body body = bodyGenerator.createBody();
+ * try {
+ * long length = body.getContentLength();
+ * ByteBuf buffer = Unpooled.buffer();
+ * BodyState state = body.transferTo(buffer);
+ * while (state == BodyState.CONTINUE) {
+ * // Process buffer content
+ * state = body.transferTo(buffer);
+ * }
+ * } finally {
+ * body.close();
+ * }
+ * }
*/
public interface Body extends Closeable {
/**
- * Gets the length of the body.
+ * Gets the content length of the body.
*
- * @return The length of the body in bytes, or negative if unknown.
+ * @return the length of the body in bytes, or a negative value if the length is unknown
*/
long getContentLength();
/**
- * Reads the next chunk of bytes from the body.
+ * Transfers the next chunk of bytes from the body to the target buffer.
+ *
+ * This method reads available bytes from the body and writes them to the provided
+ * target buffer. The transfer continues until the buffer is full or no more data
+ * is immediately available.
+ *
*
- * @param target The buffer to store the chunk in, must not be {@code null}.
- * @return The state.
- * @throws IOException If the chunk could not be read.
+ * @param target the buffer to store the chunk in, must not be {@code null}
+ * @return the current state of the body transfer indicating whether to continue,
+ * suspend, or stop reading
+ * @throws IOException if the chunk could not be read due to an I/O error
*/
BodyState transferTo(ByteBuf target) throws IOException;
+ /**
+ * Represents the state of a body transfer operation.
+ *
+ * This enum is used to control the flow of data transfer from a body to a target buffer,
+ * indicating whether more data is available, the operation should pause, or the transfer
+ * is complete.
+ *
+ */
enum BodyState {
/**
- * There's something to read
+ * More data is available and the transfer should continue.
+ * This state indicates that the body has successfully written data to the buffer
+ * and additional data may be available for reading.
*/
CONTINUE,
/**
- * There's nothing to read and input has to suspend
+ * No data is currently available and the transfer should be suspended.
+ * This state indicates that the transfer should pause temporarily and may be
+ * resumed later when more data becomes available.
*/
SUSPEND,
/**
- * There's nothing to read and input has to stop
+ * No more data is available and the transfer should stop.
+ * This state indicates that the body has been completely transferred and
+ * no further reading is necessary.
*/
STOP
}
diff --git a/client/src/main/java/org/asynchttpclient/request/body/RandomAccessBody.java b/client/src/main/java/org/asynchttpclient/request/body/RandomAccessBody.java
index 2d706fa636..d3959d319c 100644
--- a/client/src/main/java/org/asynchttpclient/request/body/RandomAccessBody.java
+++ b/client/src/main/java/org/asynchttpclient/request/body/RandomAccessBody.java
@@ -17,16 +17,39 @@
import java.nio.channels.WritableByteChannel;
/**
- * A request body which supports random access to its contents.
+ * A request body that supports random access to its contents via channel-based transfer.
+ *
+ * This interface extends {@link Body} to provide an additional transfer method using
+ * {@link WritableByteChannel}, which enables efficient zero-copy transfer for certain
+ * types of bodies (e.g., file-based bodies). This is particularly useful for HTTP
+ * connections where zero-copy optimizations can be applied.
+ *
+ *
+ * Usage Examples:
+ * {@code
+ * RandomAccessBody body = new FileBodyGenerator(new File("data.bin")).createBody();
+ * try (FileChannel channel = FileChannel.open(outputPath, StandardOpenOption.WRITE)) {
+ * long transferred = body.transferTo(channel);
+ * System.out.println("Transferred " + transferred + " bytes");
+ * } finally {
+ * body.close();
+ * }
+ * }
*/
public interface RandomAccessBody extends Body {
/**
- * Transfers the specified chunk of bytes from this body to the specified channel.
+ * Transfers bytes from this body to the specified writable channel.
+ *
+ * This method performs an efficient transfer of body content to the target channel,
+ * potentially using zero-copy optimizations when supported by the underlying
+ * implementation. The transfer is typically more efficient than buffer-based
+ * transfers for file-based bodies.
+ *
*
- * @param target The destination channel to transfer the body chunk to, must not be {@code null}.
- * @return The non-negative number of bytes actually transferred.
- * @throws IOException If the body chunk could not be transferred.
+ * @param target the destination channel to transfer the body chunk to, must not be {@code null}
+ * @return the non-negative number of bytes actually transferred
+ * @throws IOException if the body chunk could not be transferred due to an I/O error
*/
long transferTo(WritableByteChannel target) throws IOException;
}
diff --git a/client/src/main/java/org/asynchttpclient/request/body/generator/BodyChunk.java b/client/src/main/java/org/asynchttpclient/request/body/generator/BodyChunk.java
index c4e01fbff0..85220bef15 100644
--- a/client/src/main/java/org/asynchttpclient/request/body/generator/BodyChunk.java
+++ b/client/src/main/java/org/asynchttpclient/request/body/generator/BodyChunk.java
@@ -15,10 +15,34 @@
import io.netty.buffer.ByteBuf;
+/**
+ * Represents a chunk of body data for feedable body generators.
+ *
+ * A body chunk contains a buffer with the actual data and a flag indicating
+ * whether this is the last chunk in the sequence. This class is used by
+ * {@link FeedableBodyGenerator} implementations to queue and transfer body
+ * data incrementally.
+ *
+ */
public final class BodyChunk {
+ /**
+ * Indicates whether this is the last chunk in the body.
+ * When {@code true}, no more chunks will follow.
+ */
public final boolean last;
+
+ /**
+ * The buffer containing the chunk data.
+ * This buffer holds the actual bytes to be transferred.
+ */
public final ByteBuf buffer;
+ /**
+ * Constructs a new body chunk.
+ *
+ * @param buffer the buffer containing the chunk data
+ * @param last {@code true} if this is the last chunk, {@code false} otherwise
+ */
BodyChunk(ByteBuf buffer, boolean last) {
this.buffer = buffer;
this.last = last;
diff --git a/client/src/main/java/org/asynchttpclient/request/body/generator/BodyGenerator.java b/client/src/main/java/org/asynchttpclient/request/body/generator/BodyGenerator.java
index be44180f06..e73ff49d63 100644
--- a/client/src/main/java/org/asynchttpclient/request/body/generator/BodyGenerator.java
+++ b/client/src/main/java/org/asynchttpclient/request/body/generator/BodyGenerator.java
@@ -16,16 +16,36 @@
import org.asynchttpclient.request.body.Body;
/**
- * Creates a request body.
+ * A factory for creating request bodies.
+ *
+ * Implementations of this interface are responsible for creating {@link Body} instances
+ * that provide the actual data to be sent in HTTP requests. The generator pattern allows
+ * for creating multiple body instances with the same content, which is necessary for
+ * scenarios like authentication challenges and redirects where the request needs to be resent.
+ *
+ *
+ * Usage Examples:
+ * {@code
+ * // Create a body generator from a byte array
+ * BodyGenerator generator = new ByteArrayBodyGenerator("Hello World".getBytes());
+ * Body body = generator.createBody();
+ *
+ * // Create a body generator from a file
+ * BodyGenerator fileGenerator = new FileBodyGenerator(new File("data.txt"));
+ * Body fileBody = fileGenerator.createBody();
+ * }
*/
public interface BodyGenerator {
/**
- * Creates a new instance of the request body to be read. While each invocation of this method is supposed to create
- * a fresh instance of the body, the actual contents of all these body instances is the same. For example, the body
- * needs to be resend after an authentication challenge of a redirect.
+ * Creates a new instance of the request body to be read.
+ *
+ * Each invocation of this method creates a fresh instance of the body, but the actual
+ * contents of all these body instances is the same. This is necessary for scenarios where
+ * the body needs to be resent, such as after an authentication challenge or redirect.
+ *
*
- * @return The request body, never {@code null}.
+ * @return the request body, never {@code null}
*/
Body createBody();
}
diff --git a/client/src/main/java/org/asynchttpclient/request/body/generator/BoundedQueueFeedableBodyGenerator.java b/client/src/main/java/org/asynchttpclient/request/body/generator/BoundedQueueFeedableBodyGenerator.java
index b19590c54e..8e90f4276d 100644
--- a/client/src/main/java/org/asynchttpclient/request/body/generator/BoundedQueueFeedableBodyGenerator.java
+++ b/client/src/main/java/org/asynchttpclient/request/body/generator/BoundedQueueFeedableBodyGenerator.java
@@ -16,12 +16,57 @@
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
+/**
+ * A feedable body generator backed by a bounded blocking queue.
+ *
+ * This implementation uses an {@link ArrayBlockingQueue} with a fixed capacity to store
+ * body chunks. When the queue reaches its capacity, attempts to feed additional chunks
+ * will fail (return {@code false}) until space becomes available. This provides backpressure
+ * to prevent unbounded memory consumption.
+ *
+ *
+ * Usage Examples:
+ * {@code
+ * // Create a bounded feedable body generator with capacity of 100 chunks
+ * BoundedQueueFeedableBodyGenerator generator = new BoundedQueueFeedableBodyGenerator(100);
+ * generator.setListener(new FeedListener() {
+ * public void onContentAdded() {
+ * System.out.println("Content added to queue");
+ * }
+ * public void onError(Throwable t) {
+ * System.err.println("Error: " + t.getMessage());
+ * }
+ * });
+ *
+ * // Feed data
+ * ByteBuf buffer = Unpooled.wrappedBuffer("data".getBytes());
+ * boolean accepted = generator.feed(buffer, false);
+ * if (!accepted) {
+ * System.out.println("Queue is full");
+ * }
+ * }
+ */
public final class BoundedQueueFeedableBodyGenerator extends QueueBasedFeedableBodyGenerator> {
+ /**
+ * Constructs a bounded queue feedable body generator with the specified capacity.
+ *
+ * @param capacity the maximum number of chunks that can be queued
+ */
public BoundedQueueFeedableBodyGenerator(int capacity) {
super(new ArrayBlockingQueue<>(capacity, true));
}
+ /**
+ * Attempts to add a chunk to the queue.
+ *
+ * If the queue is full, this method returns {@code false} immediately without blocking.
+ *
+ *
+ * @param chunk the body chunk to add to the queue
+ * @return {@code true} if the chunk was added, {@code false} if the queue is full
+ * @throws InterruptedException if interrupted while attempting to add the chunk
+ */
@Override
protected boolean offer(BodyChunk chunk) throws InterruptedException {
return queue.offer(chunk);
diff --git a/client/src/main/java/org/asynchttpclient/request/body/generator/ByteArrayBodyGenerator.java b/client/src/main/java/org/asynchttpclient/request/body/generator/ByteArrayBodyGenerator.java
index ccbfd86fb4..1dbbc22c55 100644
--- a/client/src/main/java/org/asynchttpclient/request/body/generator/ByteArrayBodyGenerator.java
+++ b/client/src/main/java/org/asynchttpclient/request/body/generator/ByteArrayBodyGenerator.java
@@ -17,31 +17,81 @@
/**
* A {@link BodyGenerator} backed by a byte array.
+ *
+ * This implementation creates bodies that read from an in-memory byte array.
+ * The byte array is shared across all body instances created by this generator,
+ * but each body maintains its own read position to support multiple reads
+ * (e.g., for retries and redirects).
+ *
+ *
+ * Usage Examples:
+ * {@code
+ * // Create a body generator from a byte array
+ * byte[] data = "Request body content".getBytes(StandardCharsets.UTF_8);
+ * BodyGenerator generator = new ByteArrayBodyGenerator(data);
+ *
+ * // Use with AsyncHttpClient
+ * AsyncHttpClient client = asyncHttpClient();
+ * client.preparePost("http://example.com/api")
+ * .setBody(generator)
+ * .execute();
+ * }
*/
public final class ByteArrayBodyGenerator implements BodyGenerator {
private final byte[] bytes;
+ /**
+ * Constructs a byte array body generator.
+ *
+ * @param bytes the byte array to use as the request body
+ */
public ByteArrayBodyGenerator(byte[] bytes) {
this.bytes = bytes;
}
/**
- * {@inheritDoc}
+ * Creates a new body instance that reads from the byte array.
+ *
+ * @return a new body instance
*/
@Override
public Body createBody() {
return new ByteBody();
}
+ /**
+ * A body implementation that reads from a byte array.
+ *
+ * This class maintains state for reading from the byte array and can be reset
+ * to support multiple reads of the same content.
+ *
+ */
protected final class ByteBody implements Body {
private boolean eof = false;
private int lastPosition = 0;
+ /**
+ * Returns the content length of this body.
+ *
+ * @return the length of the byte array in bytes
+ */
public long getContentLength() {
return bytes.length;
}
+ /**
+ * Transfers bytes from the byte array to the target buffer.
+ *
+ * This method transfers as many bytes as possible to the target buffer,
+ * limited by either the remaining bytes in the array or the available
+ * space in the target buffer.
+ *
+ *
+ * @param target the buffer to write bytes to
+ * @return {@link BodyState#STOP} if all bytes have been transferred,
+ * {@link BodyState#CONTINUE} otherwise
+ */
public BodyState transferTo(ByteBuf target) {
if (eof) {
@@ -60,6 +110,13 @@ public BodyState transferTo(ByteBuf target) {
return BodyState.CONTINUE;
}
+ /**
+ * Resets the body state to allow re-reading from the beginning.
+ *
+ * This method resets the read position and end-of-file flag, allowing
+ * the body to be read again from the start.
+ *
+ */
public void close() {
lastPosition = 0;
eof = false;
diff --git a/client/src/main/java/org/asynchttpclient/request/body/generator/FeedListener.java b/client/src/main/java/org/asynchttpclient/request/body/generator/FeedListener.java
index 3ca74f5622..ec89f3869b 100644
--- a/client/src/main/java/org/asynchttpclient/request/body/generator/FeedListener.java
+++ b/client/src/main/java/org/asynchttpclient/request/body/generator/FeedListener.java
@@ -13,8 +13,50 @@
*/
package org.asynchttpclient.request.body.generator;
+/**
+ * A listener interface for receiving notifications from feedable body generators.
+ *
+ * Implementations of this interface can be registered with {@link FeedableBodyGenerator}
+ * instances to be notified when content is added to the generator or when errors occur.
+ * This allows for reactive processing of body data as it becomes available.
+ *
+ *
+ * Usage Examples:
+ * {@code
+ * FeedableBodyGenerator generator = new UnboundedQueueFeedableBodyGenerator();
+ * generator.setListener(new FeedListener() {
+ * @Override
+ * public void onContentAdded() {
+ * System.out.println("New content is available");
+ * // Resume request processing
+ * }
+ *
+ * @Override
+ * public void onError(Throwable t) {
+ * System.err.println("Error feeding content: " + t.getMessage());
+ * // Handle error
+ * }
+ * });
+ * }
+ */
public interface FeedListener {
+ /**
+ * Called when new content has been added to the feedable body generator.
+ *
+ * This notification indicates that data is available for transfer and any
+ * suspended operations may be resumed.
+ *
+ */
void onContentAdded();
+ /**
+ * Called when an error occurs while feeding content to the generator.
+ *
+ * This notification allows the listener to handle errors that occur during
+ * the content feeding process.
+ *
+ *
+ * @param t the error that occurred, never {@code null}
+ */
void onError(Throwable t);
}
diff --git a/client/src/main/java/org/asynchttpclient/request/body/generator/FeedableBodyGenerator.java b/client/src/main/java/org/asynchttpclient/request/body/generator/FeedableBodyGenerator.java
index 9016cdcd31..95ed77844f 100644
--- a/client/src/main/java/org/asynchttpclient/request/body/generator/FeedableBodyGenerator.java
+++ b/client/src/main/java/org/asynchttpclient/request/body/generator/FeedableBodyGenerator.java
@@ -16,12 +16,69 @@
import io.netty.buffer.ByteBuf;
/**
- * {@link BodyGenerator} which may return just part of the payload at the time handler is requesting it.
- * If it happens, client becomes responsible for providing the rest of the chunks.
+ * A {@link BodyGenerator} that supports incremental feeding of body content.
+ *
+ * Unlike regular body generators where all content is available upfront, feedable
+ * body generators allow content to be provided incrementally over time. This is useful
+ * for scenarios where the full request body is not immediately available, such as
+ * streaming uploads or reactive data sources.
+ *
+ *
+ * When the body generator returns only part of the payload, the client becomes
+ * responsible for feeding the remaining chunks through the {@link #feed} method.
+ * A {@link FeedListener} can be registered to receive notifications when content
+ * is added or errors occur.
+ *
+ *
+ * Usage Examples:
+ * {@code
+ * // Create a feedable body generator
+ * FeedableBodyGenerator generator = new UnboundedQueueFeedableBodyGenerator();
+ *
+ * // Set up a listener for notifications
+ * generator.setListener(new FeedListener() {
+ * public void onContentAdded() {
+ * System.out.println("Content available");
+ * }
+ * public void onError(Throwable t) {
+ * System.err.println("Error: " + t);
+ * }
+ * });
+ *
+ * // Feed data incrementally
+ * ByteBuf chunk1 = Unpooled.wrappedBuffer("Hello ".getBytes());
+ * generator.feed(chunk1, false);
+ *
+ * ByteBuf chunk2 = Unpooled.wrappedBuffer("World!".getBytes());
+ * generator.feed(chunk2, true); // Mark as last chunk
+ * }
*/
public interface FeedableBodyGenerator extends BodyGenerator {
+ /**
+ * Feeds a chunk of data to the body generator.
+ *
+ * This method adds a chunk of body content to the generator. The chunk is queued
+ * for transfer to the target. If this is the last chunk, the {@code isLast} parameter
+ * should be {@code true} to signal completion.
+ *
+ *
+ * @param buffer the buffer containing the chunk data to feed
+ * @param isLast {@code true} if this is the last chunk, {@code false} otherwise
+ * @return {@code true} if the chunk was accepted, {@code false} if it could not be queued
+ * (e.g., queue is full in bounded implementations)
+ * @throws Exception if an error occurs while feeding the chunk
+ */
boolean feed(ByteBuf buffer, boolean isLast) throws Exception;
+ /**
+ * Sets the listener to be notified of feed events.
+ *
+ * The listener will be called when content is successfully added to the generator
+ * or when errors occur during the feeding process.
+ *
+ *
+ * @param listener the listener to notify, or {@code null} to remove the current listener
+ */
void setListener(FeedListener listener);
}
diff --git a/client/src/main/java/org/asynchttpclient/request/body/generator/FileBodyGenerator.java b/client/src/main/java/org/asynchttpclient/request/body/generator/FileBodyGenerator.java
index 1b260ee514..7d3b74c7b5 100644
--- a/client/src/main/java/org/asynchttpclient/request/body/generator/FileBodyGenerator.java
+++ b/client/src/main/java/org/asynchttpclient/request/body/generator/FileBodyGenerator.java
@@ -20,6 +20,32 @@
/**
* Creates a request body from the contents of a file.
+ *
+ * This body generator supports reading the entire file or a specific region within
+ * the file. It implements {@link BodyGenerator} and returns a {@link RandomAccessBody}
+ * to enable efficient zero-copy file transfers using Netty's file region support.
+ *
+ *
+ * Note: The {@link #createBody()} method is not actually invoked during request
+ * processing. Instead, Netty directly sends the file using its optimized file
+ * transfer mechanisms.
+ *
+ *
+ * Usage Examples:
+ * {@code
+ * // Create a body generator for an entire file
+ * File file = new File("upload.dat");
+ * BodyGenerator generator = new FileBodyGenerator(file);
+ *
+ * // Create a body generator for a specific region of a file
+ * BodyGenerator regionGenerator = new FileBodyGenerator(file, 1024, 2048);
+ *
+ * // Use with AsyncHttpClient
+ * AsyncHttpClient client = asyncHttpClient();
+ * client.preparePost("http://example.com/upload")
+ * .setBody(generator)
+ * .execute();
+ * }
*/
public final class FileBodyGenerator implements BodyGenerator {
@@ -27,33 +53,68 @@ public final class FileBodyGenerator implements BodyGenerator {
private final long regionSeek;
private final long regionLength;
+ /**
+ * Constructs a file body generator for the entire file.
+ *
+ * @param file the file to read from
+ */
public FileBodyGenerator(File file) {
this(file, 0L, file.length());
}
+ /**
+ * Constructs a file body generator for a specific region of the file.
+ *
+ * @param file the file to read from
+ * @param regionSeek the offset in bytes from the start of the file
+ * @param regionLength the number of bytes to read from the file
+ */
public FileBodyGenerator(File file, long regionSeek, long regionLength) {
this.file = assertNotNull(file, "file");
this.regionLength = regionLength;
this.regionSeek = regionSeek;
}
+ /**
+ * Gets the file that this generator reads from.
+ *
+ * @return the file
+ */
public File getFile() {
return file;
}
+ /**
+ * Gets the length of the region to read from the file.
+ *
+ * @return the region length in bytes
+ */
public long getRegionLength() {
return regionLength;
}
+ /**
+ * Gets the offset within the file where reading should start.
+ *
+ * @return the region seek offset in bytes
+ */
public long getRegionSeek() {
return regionSeek;
}
/**
- * {@inheritDoc}
+ * Creates a body instance.
+ *
+ * Note: This method is not actually used during request processing. Netty directly
+ * sends the file using its optimized file transfer mechanisms, so calling this
+ * method will throw an exception.
+ *
+ *
+ * @return never returns normally
+ * @throws UnsupportedOperationException always thrown as this method is not used
*/
@Override
public RandomAccessBody createBody() {
- throw new UnsupportedOperationException("FileBodyGenerator.createBody isn't used, Netty direclt sends the file");
+ throw new UnsupportedOperationException("FileBodyGenerator.createBody isn't used, Netty directly sends the file");
}
}
diff --git a/client/src/main/java/org/asynchttpclient/request/body/generator/InputStreamBodyGenerator.java b/client/src/main/java/org/asynchttpclient/request/body/generator/InputStreamBodyGenerator.java
index b69e6b1eb1..4595784641 100644
--- a/client/src/main/java/org/asynchttpclient/request/body/generator/InputStreamBodyGenerator.java
+++ b/client/src/main/java/org/asynchttpclient/request/body/generator/InputStreamBodyGenerator.java
@@ -22,10 +22,35 @@
import java.io.InputStream;
/**
- * A {@link BodyGenerator} which use an {@link InputStream} for reading bytes, without having to read the entire stream in memory.
- *
- * NOTE: The {@link InputStream} must support the {@link InputStream#mark} and {@link java.io.InputStream#reset()} operation. If not, mechanisms like authentication, redirect, or
- * resumable download will not works.
+ * A {@link BodyGenerator} that reads from an {@link InputStream} without loading the entire stream into memory.
+ *
+ * This implementation allows streaming of request bodies from input streams, which is useful
+ * for large payloads that should not be fully buffered in memory. The content is read
+ * incrementally as needed during the request transfer.
+ *
+ *
+ * Important: The {@link InputStream} must support the {@link InputStream#mark(int)} and
+ * {@link InputStream#reset()} operations for proper functionality. If these operations are not
+ * supported, mechanisms like authentication challenges, redirects, or resumable transfers will
+ * not work correctly.
+ *
+ *
+ * Usage Examples:
+ * {@code
+ * // Create from an input stream with unknown length
+ * InputStream stream = new FileInputStream("data.bin");
+ * BodyGenerator generator = new InputStreamBodyGenerator(stream);
+ *
+ * // Create from an input stream with known length
+ * InputStream stream2 = new ByteArrayInputStream(data);
+ * BodyGenerator generator2 = new InputStreamBodyGenerator(stream2, data.length);
+ *
+ * // Use with AsyncHttpClient
+ * AsyncHttpClient client = asyncHttpClient();
+ * client.preparePost("http://example.com/upload")
+ * .setBody(generator)
+ * .execute();
+ * }
*/
public final class InputStreamBodyGenerator implements BodyGenerator {
@@ -33,25 +58,48 @@ public final class InputStreamBodyGenerator implements BodyGenerator {
private final InputStream inputStream;
private final long contentLength;
+ /**
+ * Constructs an input stream body generator with unknown content length.
+ *
+ * @param inputStream the input stream to read from
+ */
public InputStreamBodyGenerator(InputStream inputStream) {
this(inputStream, -1L);
}
+ /**
+ * Constructs an input stream body generator with specified content length.
+ *
+ * @param inputStream the input stream to read from
+ * @param contentLength the total number of bytes to read, or -1 if unknown
+ */
public InputStreamBodyGenerator(InputStream inputStream, long contentLength) {
this.inputStream = inputStream;
this.contentLength = contentLength;
}
+ /**
+ * Gets the input stream that this generator reads from.
+ *
+ * @return the input stream
+ */
public InputStream getInputStream() {
return inputStream;
}
+ /**
+ * Gets the content length of this body.
+ *
+ * @return the content length in bytes, or -1 if unknown
+ */
public long getContentLength() {
return contentLength;
}
/**
- * {@inheritDoc}
+ * Creates a new body instance that reads from the input stream.
+ *
+ * @return a new body instance
*/
@Override
public Body createBody() {
diff --git a/client/src/main/java/org/asynchttpclient/request/body/generator/PushBody.java b/client/src/main/java/org/asynchttpclient/request/body/generator/PushBody.java
index 180108de54..30dad80b5e 100644
--- a/client/src/main/java/org/asynchttpclient/request/body/generator/PushBody.java
+++ b/client/src/main/java/org/asynchttpclient/request/body/generator/PushBody.java
@@ -18,20 +18,60 @@
import java.util.Queue;
+/**
+ * A body implementation that reads from a queue of chunks.
+ *
+ * This class is used by {@link QueueBasedFeedableBodyGenerator} implementations to provide
+ * a body that pulls data from a queue as it becomes available. The body maintains state
+ * to track whether more data is expected, the transfer should be suspended, or the
+ * body is complete.
+ *
+ *
+ * The content length is unknown (-1) since data is fed incrementally to the queue.
+ * The body continues reading chunks from the queue until it receives a chunk marked
+ * as the last chunk.
+ *
+ */
public final class PushBody implements Body {
private final Queue queue;
private BodyState state = BodyState.CONTINUE;
+ /**
+ * Constructs a push body that reads from the specified queue.
+ *
+ * @param queue the queue containing body chunks
+ */
public PushBody(Queue queue) {
this.queue = queue;
}
+ /**
+ * Returns the content length of this body.
+ *
+ * Since the content is fed incrementally, the total length is not known in advance.
+ *
+ *
+ * @return -1 indicating unknown length
+ */
@Override
public long getContentLength() {
return -1;
}
+ /**
+ * Transfers available chunks from the queue to the target buffer.
+ *
+ * This method reads chunks from the queue and writes them to the target buffer
+ * until the buffer is full, no more chunks are available, or the last chunk
+ * has been processed.
+ *
+ *
+ * @param target the buffer to write chunks to
+ * @return the current body state: {@link BodyState#CONTINUE} if more data may be available,
+ * {@link BodyState#SUSPEND} if the queue is empty and more data is expected,
+ * or {@link BodyState#STOP} if the last chunk has been processed
+ */
@Override
public BodyState transferTo(final ByteBuf target) {
switch (state) {
diff --git a/client/src/main/java/org/asynchttpclient/request/body/generator/QueueBasedFeedableBodyGenerator.java b/client/src/main/java/org/asynchttpclient/request/body/generator/QueueBasedFeedableBodyGenerator.java
index d0f3878219..58299739ca 100644
--- a/client/src/main/java/org/asynchttpclient/request/body/generator/QueueBasedFeedableBodyGenerator.java
+++ b/client/src/main/java/org/asynchttpclient/request/body/generator/QueueBasedFeedableBodyGenerator.java
@@ -18,22 +18,70 @@
import java.util.Queue;
+/**
+ * An abstract base class for feedable body generators backed by a queue.
+ *
+ * This class provides a common implementation for feedable body generators that use
+ * a queue to store {@link BodyChunk}s. Subclasses must implement the {@link #offer(BodyChunk)}
+ * method to define how chunks are added to their specific queue implementation (bounded
+ * or unbounded).
+ *
+ *
+ * The generator creates {@link PushBody} instances that read from the queue as data
+ * becomes available. When chunks are successfully added, registered {@link FeedListener}s
+ * are notified.
+ *
+ */
public abstract class QueueBasedFeedableBodyGenerator> implements FeedableBodyGenerator {
protected final T queue;
private FeedListener listener;
+ /**
+ * Constructs a queue-based feedable body generator.
+ *
+ * @param queue the queue to use for storing body chunks
+ */
public QueueBasedFeedableBodyGenerator(T queue) {
this.queue = queue;
}
+ /**
+ * Creates a new body instance that reads from the queue.
+ *
+ * @return a new {@link PushBody} instance backed by the queue
+ */
@Override
public Body createBody() {
return new PushBody(queue);
}
+ /**
+ * Attempts to add a chunk to the queue.
+ *
+ * Subclasses must implement this method to define the specific queuing behavior,
+ * such as blocking until space is available or returning immediately if the queue
+ * is full.
+ *
+ *
+ * @param chunk the body chunk to add to the queue
+ * @return {@code true} if the chunk was successfully added, {@code false} otherwise
+ * @throws Exception if an error occurs while adding the chunk
+ */
protected abstract boolean offer(BodyChunk chunk) throws Exception;
+ /**
+ * Feeds a chunk of data to the body generator.
+ *
+ * This method wraps the buffer in a {@link BodyChunk}, attempts to add it to the queue,
+ * and notifies the listener if the chunk was successfully added.
+ *
+ *
+ * @param buffer the buffer containing the chunk data
+ * @param isLast {@code true} if this is the last chunk, {@code false} otherwise
+ * @return {@code true} if the chunk was accepted, {@code false} otherwise
+ * @throws Exception if an error occurs while feeding the chunk
+ */
@Override
public boolean feed(final ByteBuf buffer, final boolean isLast) throws Exception {
boolean offered = offer(new BodyChunk(buffer, isLast));
@@ -43,6 +91,11 @@ public boolean feed(final ByteBuf buffer, final boolean isLast) throws Exception
return offered;
}
+ /**
+ * Sets the listener to be notified when content is added.
+ *
+ * @param listener the listener to notify, or {@code null} to remove the current listener
+ */
@Override
public void setListener(FeedListener listener) {
this.listener = listener;
diff --git a/client/src/main/java/org/asynchttpclient/request/body/generator/ReactiveStreamsBodyGenerator.java b/client/src/main/java/org/asynchttpclient/request/body/generator/ReactiveStreamsBodyGenerator.java
index 7cf1c14fd4..bbfa0f3e48 100644
--- a/client/src/main/java/org/asynchttpclient/request/body/generator/ReactiveStreamsBodyGenerator.java
+++ b/client/src/main/java/org/asynchttpclient/request/body/generator/ReactiveStreamsBodyGenerator.java
@@ -26,6 +26,38 @@
import static org.asynchttpclient.util.Assertions.assertNotNull;
+/**
+ * A {@link BodyGenerator} that integrates with Reactive Streams publishers.
+ *
+ * This implementation bridges the Reactive Streams specification with the async-http-client
+ * body generator API. It subscribes to a {@link Publisher} of {@link ByteBuf} and feeds
+ * the emitted buffers to an underlying {@link FeedableBodyGenerator}.
+ *
+ *
+ * The publisher is subscribed to lazily when the body transfer begins. Data from the
+ * publisher is automatically fed to the request as it becomes available, following
+ * the Reactive Streams backpressure protocol.
+ *
+ *
+ * Usage Examples:
+ * {@code
+ * // Create a publisher that emits ByteBuf chunks
+ * Publisher publisher = Flux.just(
+ * Unpooled.wrappedBuffer("Hello ".getBytes()),
+ * Unpooled.wrappedBuffer("World!".getBytes())
+ * );
+ *
+ * // Create a reactive streams body generator
+ * // Use -1 for chunked transfer encoding, or specify actual length
+ * ReactiveStreamsBodyGenerator generator = new ReactiveStreamsBodyGenerator(publisher, -1L);
+ *
+ * // Use with AsyncHttpClient
+ * AsyncHttpClient client = asyncHttpClient();
+ * client.preparePost("http://example.com/stream")
+ * .setBody(generator)
+ * .execute();
+ * }
+ */
public class ReactiveStreamsBodyGenerator implements FeedableBodyGenerator {
private final Publisher publisher;
@@ -34,12 +66,14 @@ public class ReactiveStreamsBodyGenerator implements FeedableBodyGenerator {
private volatile FeedListener feedListener;
/**
- * Creates a Streamable Body which takes a Content-Length.
- * If the contentLength parameter is -1L a Http Header of Transfer-Encoding: chunked will be set.
- * Otherwise it will set the Content-Length header to the value provided
+ * Constructs a reactive streams body generator.
+ *
+ * If the content length is -1, the HTTP request will use Transfer-Encoding: chunked.
+ * Otherwise, the Content-Length header will be set to the specified value.
+ *
*
- * @param publisher Body as a Publisher
- * @param contentLength Content-Length of the Body
+ * @param publisher the reactive streams publisher that will emit body chunks
+ * @param contentLength the total content length in bytes, or -1 for chunked encoding
*/
public ReactiveStreamsBodyGenerator(Publisher publisher, long contentLength) {
this.publisher = publisher;
@@ -47,25 +81,61 @@ public ReactiveStreamsBodyGenerator(Publisher publisher, long contentLe
this.contentLength = contentLength;
}
+ /**
+ * Gets the publisher that this generator uses as a data source.
+ *
+ * @return the reactive streams publisher
+ */
public Publisher getPublisher() {
return this.publisher;
}
+ /**
+ * Feeds a chunk of data to the underlying body generator.
+ *
+ * This method is called by the Reactive Streams subscriber to feed data from
+ * the publisher into the request body.
+ *
+ *
+ * @param buffer the buffer containing the chunk data
+ * @param isLast {@code true} if this is the last chunk, {@code false} otherwise
+ * @return {@code true} if the chunk was accepted, {@code false} otherwise
+ * @throws Exception if an error occurs while feeding the chunk
+ */
@Override
public boolean feed(ByteBuf buffer, boolean isLast) throws Exception {
return feedableBodyGenerator.feed(buffer, isLast);
}
+ /**
+ * Sets the listener to be notified of feed events.
+ *
+ * @param listener the listener to notify, or {@code null} to remove the current listener
+ */
@Override
public void setListener(FeedListener listener) {
feedListener = listener;
feedableBodyGenerator.setListener(listener);
}
+ /**
+ * Gets the content length of the body.
+ *
+ * @return the content length in bytes, or -1 if unknown (chunked encoding)
+ */
public long getContentLength() {
return contentLength;
}
+ /**
+ * Creates a new body instance that subscribes to the publisher.
+ *
+ * The publisher subscription is initiated lazily when the first transfer
+ * operation begins.
+ *
+ *
+ * @return a new streamed body instance
+ */
@Override
public Body createBody() {
return new StreamedBody(feedableBodyGenerator, contentLength);
diff --git a/client/src/main/java/org/asynchttpclient/request/body/generator/UnboundedQueueFeedableBodyGenerator.java b/client/src/main/java/org/asynchttpclient/request/body/generator/UnboundedQueueFeedableBodyGenerator.java
index b74319506a..610fe37747 100755
--- a/client/src/main/java/org/asynchttpclient/request/body/generator/UnboundedQueueFeedableBodyGenerator.java
+++ b/client/src/main/java/org/asynchttpclient/request/body/generator/UnboundedQueueFeedableBodyGenerator.java
@@ -15,12 +15,61 @@
import java.util.concurrent.ConcurrentLinkedQueue;
+/**
+ * A feedable body generator backed by an unbounded concurrent queue.
+ *
+ * This implementation uses a {@link ConcurrentLinkedQueue} to store body chunks.
+ * Unlike {@link BoundedQueueFeedableBodyGenerator}, this generator will always accept
+ * new chunks regardless of queue size, which means it does not provide backpressure.
+ * This can lead to unbounded memory consumption if chunks are fed faster than they
+ * can be transferred.
+ *
+ *
+ * Use this generator when you need a simple feedable body without backpressure concerns,
+ * or when the data source naturally limits the rate of chunk production.
+ *
+ *
+ * Usage Examples:
+ * {@code
+ * // Create an unbounded feedable body generator
+ * UnboundedQueueFeedableBodyGenerator generator = new UnboundedQueueFeedableBodyGenerator();
+ *
+ * // Set up listener for notifications
+ * generator.setListener(new FeedListener() {
+ * public void onContentAdded() {
+ * System.out.println("Content available");
+ * }
+ * public void onError(Throwable t) {
+ * System.err.println("Error: " + t.getMessage());
+ * }
+ * });
+ *
+ * // Feed data - always succeeds
+ * ByteBuf buffer1 = Unpooled.wrappedBuffer("chunk1".getBytes());
+ * generator.feed(buffer1, false);
+ *
+ * ByteBuf buffer2 = Unpooled.wrappedBuffer("chunk2".getBytes());
+ * generator.feed(buffer2, true);
+ * }
+ */
public final class UnboundedQueueFeedableBodyGenerator extends QueueBasedFeedableBodyGenerator> {
+ /**
+ * Constructs an unbounded queue feedable body generator.
+ */
public UnboundedQueueFeedableBodyGenerator() {
super(new ConcurrentLinkedQueue<>());
}
+ /**
+ * Adds a chunk to the unbounded queue.
+ *
+ * This method always succeeds and returns {@code true}, as the queue has no capacity limit.
+ *
+ *
+ * @param chunk the body chunk to add to the queue
+ * @return {@code true} always, as the chunk is always added
+ */
@Override
protected boolean offer(BodyChunk chunk) {
return queue.offer(chunk);
diff --git a/client/src/main/java/org/asynchttpclient/request/body/multipart/ByteArrayPart.java b/client/src/main/java/org/asynchttpclient/request/body/multipart/ByteArrayPart.java
index 9a2200e428..99d3819f2c 100644
--- a/client/src/main/java/org/asynchttpclient/request/body/multipart/ByteArrayPart.java
+++ b/client/src/main/java/org/asynchttpclient/request/body/multipart/ByteArrayPart.java
@@ -16,35 +16,122 @@
import static org.asynchttpclient.util.Assertions.assertNotNull;
+/**
+ * A multipart part representing binary data from a byte array.
+ *
+ * This class is used for in-memory binary data in multipart/form-data requests.
+ * It provides a convenient way to upload binary content without requiring file I/O.
+ * The content type is automatically determined from the file name if not explicitly
+ * specified.
+ *
+ *
+ * Usage Examples:
+ * {@code
+ * // Simple byte array part with auto-detected content type
+ * byte[] imageData = Files.readAllBytes(Paths.get("image.png"));
+ * Part imagePart = new ByteArrayPart("image", imageData);
+ *
+ * // Byte array part with explicit content type
+ * byte[] jsonData = "{\"key\":\"value\"}".getBytes(StandardCharsets.UTF_8);
+ * Part jsonPart = new ByteArrayPart("data", jsonData, "application/json");
+ *
+ * // Byte array part with file name for content type detection
+ * Part filePart = new ByteArrayPart("document", pdfBytes,
+ * null, null, "document.pdf");
+ *
+ * // Use in a multipart request
+ * AsyncHttpClient client = asyncHttpClient();
+ * client.preparePost("http://example.com/upload")
+ * .addBodyPart(imagePart)
+ * .addBodyPart(jsonPart)
+ * .execute();
+ * }
+ */
public class ByteArrayPart extends FileLikePart {
private final byte[] bytes;
+ /**
+ * Constructs a byte array part with name and data.
+ *
+ * @param name the name of the form field
+ * @param bytes the binary data
+ */
public ByteArrayPart(String name, byte[] bytes) {
this(name, bytes, null);
}
+ /**
+ * Constructs a byte array part with name, data, and content type.
+ *
+ * @param name the name of the form field
+ * @param bytes the binary data
+ * @param contentType the content type, or {@code null} to auto-detect
+ */
public ByteArrayPart(String name, byte[] bytes, String contentType) {
this(name, bytes, contentType, null);
}
+ /**
+ * Constructs a byte array part with name, data, content type, and charset.
+ *
+ * @param name the name of the form field
+ * @param bytes the binary data
+ * @param contentType the content type, or {@code null} to auto-detect
+ * @param charset the character encoding, or {@code null} for default
+ */
public ByteArrayPart(String name, byte[] bytes, String contentType, Charset charset) {
this(name, bytes, contentType, charset, null);
}
+ /**
+ * Constructs a byte array part with name, data, content type, charset, and file name.
+ *
+ * @param name the name of the form field
+ * @param bytes the binary data
+ * @param contentType the content type, or {@code null} to auto-detect from fileName
+ * @param charset the character encoding, or {@code null} for default
+ * @param fileName the file name for content type detection and disposition header
+ */
public ByteArrayPart(String name, byte[] bytes, String contentType, Charset charset, String fileName) {
this(name, bytes, contentType, charset, fileName, null);
}
+ /**
+ * Constructs a byte array part with name, data, content type, charset, file name, and content ID.
+ *
+ * @param name the name of the form field
+ * @param bytes the binary data
+ * @param contentType the content type, or {@code null} to auto-detect from fileName
+ * @param charset the character encoding, or {@code null} for default
+ * @param fileName the file name for content type detection and disposition header
+ * @param contentId the content ID, or {@code null} if not needed
+ */
public ByteArrayPart(String name, byte[] bytes, String contentType, Charset charset, String fileName, String contentId) {
this(name, bytes, contentType, charset, fileName, contentId, null);
}
+ /**
+ * Constructs a byte array part with all optional parameters.
+ *
+ * @param name the name of the form field
+ * @param bytes the binary data
+ * @param contentType the content type, or {@code null} to auto-detect from fileName
+ * @param charset the character encoding, or {@code null} for default
+ * @param fileName the file name for content type detection and disposition header
+ * @param contentId the content ID, or {@code null} if not needed
+ * @param transferEncoding the transfer encoding, or {@code null} for default
+ */
public ByteArrayPart(String name, byte[] bytes, String contentType, Charset charset, String fileName, String contentId, String transferEncoding) {
super(name, contentType, charset, fileName, contentId, transferEncoding);
this.bytes = assertNotNull(bytes, "bytes");
}
+ /**
+ * Returns the binary data of this part.
+ *
+ * @return the byte array containing the data
+ */
public byte[] getBytes() {
return bytes;
}
diff --git a/client/src/main/java/org/asynchttpclient/request/body/multipart/FileLikePart.java b/client/src/main/java/org/asynchttpclient/request/body/multipart/FileLikePart.java
index 03de497867..35cddbf5d3 100644
--- a/client/src/main/java/org/asynchttpclient/request/body/multipart/FileLikePart.java
+++ b/client/src/main/java/org/asynchttpclient/request/body/multipart/FileLikePart.java
@@ -20,7 +20,17 @@
import static org.asynchttpclient.util.MiscUtils.withDefault;
/**
- * This class is an adaptation of the Apache HttpClient implementation
+ * Abstract base class for file-like multipart parts.
+ *
+ * This class provides common functionality for parts that represent file-like content,
+ * including file uploads ({@link FilePart}), byte arrays ({@link ByteArrayPart}), and
+ * input streams ({@link InputStreamPart}). It handles automatic content type detection
+ * based on file name extensions using a MIME types mapping.
+ *
+ *
+ * This class is an adaptation of the Apache HttpClient implementation and includes
+ * a built-in MIME types file (ahc-mime.types) for content type detection.
+ *
*/
public abstract class FileLikePart extends PartBase {
@@ -35,19 +45,23 @@ public abstract class FileLikePart extends PartBase {
}
/**
- * Default content encoding of file attachments.
+ * The file name associated with this part.
*/
private String fileName;
/**
- * FilePart Constructor.
+ * Constructs a file-like part with the specified parameters.
+ *
+ * If no content type is provided, it will be automatically determined from the
+ * file name extension using the built-in MIME types mapping.
+ *
*
- * @param name the name for this part
- * @param contentType the content type for this part, if null try to figure out from the fileName mime type
- * @param charset the charset encoding for this part
- * @param fileName the fileName
- * @param contentId the content id
- * @param transferEncoding the transfer encoding
+ * @param name the name of the form field
+ * @param contentType the content type, or {@code null} to auto-detect from fileName
+ * @param charset the character encoding, or {@code null} for default
+ * @param fileName the file name for content type detection and disposition header
+ * @param contentId the content ID, or {@code null} if not needed
+ * @param transferEncoding the transfer encoding, or {@code null} for default
*/
public FileLikePart(String name, String contentType, Charset charset, String fileName, String contentId, String transferEncoding) {
super(name,
@@ -58,14 +72,35 @@ public FileLikePart(String name, String contentType, Charset charset, String fil
this.fileName = fileName;
}
+ /**
+ * Computes the content type based on the provided type or file name.
+ *
+ * If a content type is explicitly provided, it is used. Otherwise, the content type
+ * is determined from the file name extension using the MIME types mapping.
+ *
+ *
+ * @param contentType the explicit content type, or {@code null}
+ * @param fileName the file name for type detection, or {@code null}
+ * @return the computed content type
+ */
private static String computeContentType(String contentType, String fileName) {
return contentType != null ? contentType : MIME_TYPES_FILE_TYPE_MAP.getContentType(withDefault(fileName, ""));
}
+ /**
+ * Returns the file name associated with this part.
+ *
+ * @return the file name, or {@code null} if not set
+ */
public String getFileName() {
return fileName;
}
+ /**
+ * Returns a string representation of this part including the file name.
+ *
+ * @return a string representation of this part
+ */
@Override
public String toString() {
return super.toString() + " filename=" + fileName;
diff --git a/client/src/main/java/org/asynchttpclient/request/body/multipart/FilePart.java b/client/src/main/java/org/asynchttpclient/request/body/multipart/FilePart.java
index b164fbc2bc..f4f2af7513 100644
--- a/client/src/main/java/org/asynchttpclient/request/body/multipart/FilePart.java
+++ b/client/src/main/java/org/asynchttpclient/request/body/multipart/FilePart.java
@@ -17,30 +17,119 @@
import static org.asynchttpclient.util.Assertions.assertNotNull;
+/**
+ * A multipart part representing a file upload.
+ *
+ * This class is used for file uploads in multipart/form-data requests. The file must
+ * be a regular file (not a directory) and must be readable. The content type is
+ * automatically determined from the file name if not explicitly specified. If no file
+ * name is provided, the actual file name is used.
+ *
+ *
+ * Usage Examples:
+ * {@code
+ * // Simple file upload
+ * File document = new File("/path/to/document.pdf");
+ * Part filePart = new FilePart("document", document);
+ *
+ * // File upload with explicit content type
+ * File image = new File("/path/to/photo.jpg");
+ * Part imagePart = new FilePart("photo", image, "image/jpeg");
+ *
+ * // File upload with custom file name in request
+ * File data = new File("/path/to/data.bin");
+ * Part dataPart = new FilePart("data", data, "application/octet-stream",
+ * null, "custom-name.bin");
+ *
+ * // Use in a multipart request
+ * AsyncHttpClient client = asyncHttpClient();
+ * client.preparePost("http://example.com/upload")
+ * .addBodyPart(filePart)
+ * .addBodyPart(imagePart)
+ * .execute();
+ * }
+ */
public class FilePart extends FileLikePart {
private final File file;
+ /**
+ * Constructs a file part with name and file.
+ *
+ * @param name the name of the form field
+ * @param file the file to upload
+ * @throws IllegalArgumentException if the file is not a regular file or is not readable
+ */
public FilePart(String name, File file) {
this(name, file, null);
}
+ /**
+ * Constructs a file part with name, file, and content type.
+ *
+ * @param name the name of the form field
+ * @param file the file to upload
+ * @param contentType the content type, or {@code null} to auto-detect from file name
+ * @throws IllegalArgumentException if the file is not a regular file or is not readable
+ */
public FilePart(String name, File file, String contentType) {
this(name, file, contentType, null);
}
+ /**
+ * Constructs a file part with name, file, content type, and charset.
+ *
+ * @param name the name of the form field
+ * @param file the file to upload
+ * @param contentType the content type, or {@code null} to auto-detect from file name
+ * @param charset the character encoding, or {@code null} for default
+ * @throws IllegalArgumentException if the file is not a regular file or is not readable
+ */
public FilePart(String name, File file, String contentType, Charset charset) {
this(name, file, contentType, charset, null);
}
+ /**
+ * Constructs a file part with name, file, content type, charset, and custom file name.
+ *
+ * @param name the name of the form field
+ * @param file the file to upload
+ * @param contentType the content type, or {@code null} to auto-detect from fileName
+ * @param charset the character encoding, or {@code null} for default
+ * @param fileName the file name to use in the request, or {@code null} to use actual file name
+ * @throws IllegalArgumentException if the file is not a regular file or is not readable
+ */
public FilePart(String name, File file, String contentType, Charset charset, String fileName) {
this(name, file, contentType, charset, fileName, null);
}
+ /**
+ * Constructs a file part with name, file, content type, charset, file name, and content ID.
+ *
+ * @param name the name of the form field
+ * @param file the file to upload
+ * @param contentType the content type, or {@code null} to auto-detect from fileName
+ * @param charset the character encoding, or {@code null} for default
+ * @param fileName the file name to use in the request, or {@code null} to use actual file name
+ * @param contentId the content ID, or {@code null} if not needed
+ * @throws IllegalArgumentException if the file is not a regular file or is not readable
+ */
public FilePart(String name, File file, String contentType, Charset charset, String fileName, String contentId) {
this(name, file, contentType, charset, fileName, contentId, null);
}
+ /**
+ * Constructs a file part with all optional parameters.
+ *
+ * @param name the name of the form field
+ * @param file the file to upload
+ * @param contentType the content type, or {@code null} to auto-detect from fileName
+ * @param charset the character encoding, or {@code null} for default
+ * @param fileName the file name to use in the request, or {@code null} to use actual file name
+ * @param contentId the content ID, or {@code null} if not needed
+ * @param transferEncoding the transfer encoding, or {@code null} for default
+ * @throws IllegalArgumentException if the file is not a regular file or is not readable
+ */
public FilePart(String name, File file, String contentType, Charset charset, String fileName, String contentId, String transferEncoding) {
super(name,
contentType,
@@ -55,6 +144,11 @@ public FilePart(String name, File file, String contentType, Charset charset, Str
this.file = file;
}
+ /**
+ * Returns the file being uploaded.
+ *
+ * @return the file
+ */
public File getFile() {
return file;
}
diff --git a/client/src/main/java/org/asynchttpclient/request/body/multipart/InputStreamPart.java b/client/src/main/java/org/asynchttpclient/request/body/multipart/InputStreamPart.java
index ca7d0db367..e839f32bf4 100644
--- a/client/src/main/java/org/asynchttpclient/request/body/multipart/InputStreamPart.java
+++ b/client/src/main/java/org/asynchttpclient/request/body/multipart/InputStreamPart.java
@@ -18,32 +18,122 @@
import static org.asynchttpclient.util.Assertions.assertNotNull;
+/**
+ * A multipart part representing data from an input stream.
+ *
+ * This class is used for streaming data uploads in multipart/form-data requests.
+ * It allows uploading data from an {@link InputStream} without loading the entire
+ * content into memory. The content length can be specified if known, or -1 if unknown.
+ * The content type is automatically determined from the file name if not explicitly
+ * specified.
+ *
+ *
+ * Usage Examples:
+ * {@code
+ * // Stream upload with known length
+ * InputStream stream = new FileInputStream("data.bin");
+ * long length = new File("data.bin").length();
+ * Part streamPart = new InputStreamPart("file", stream, "data.bin", length);
+ *
+ * // Stream upload with unknown length
+ * InputStream urlStream = new URL("http://example.com/data").openStream();
+ * Part urlPart = new InputStreamPart("download", urlStream, "download.dat", -1);
+ *
+ * // Stream upload with explicit content type
+ * InputStream jsonStream = new ByteArrayInputStream(jsonBytes);
+ * Part jsonPart = new InputStreamPart("json", jsonStream, "data.json",
+ * jsonBytes.length, "application/json");
+ *
+ * // Use in a multipart request
+ * AsyncHttpClient client = asyncHttpClient();
+ * client.preparePost("http://example.com/upload")
+ * .addBodyPart(streamPart)
+ * .execute();
+ * }
+ */
public class InputStreamPart extends FileLikePart {
private final InputStream inputStream;
private final long contentLength;
+ /**
+ * Constructs an input stream part with name, stream, and file name.
+ *
+ * @param name the name of the form field
+ * @param inputStream the input stream to read from
+ * @param fileName the file name for content type detection and disposition header
+ */
public InputStreamPart(String name, InputStream inputStream, String fileName) {
this(name, inputStream, fileName, -1);
}
+ /**
+ * Constructs an input stream part with name, stream, file name, and content length.
+ *
+ * @param name the name of the form field
+ * @param inputStream the input stream to read from
+ * @param fileName the file name for content type detection and disposition header
+ * @param contentLength the total number of bytes to read, or -1 if unknown
+ */
public InputStreamPart(String name, InputStream inputStream, String fileName, long contentLength) {
this(name, inputStream, fileName, contentLength, null);
}
+ /**
+ * Constructs an input stream part with name, stream, file name, content length, and content type.
+ *
+ * @param name the name of the form field
+ * @param inputStream the input stream to read from
+ * @param fileName the file name for content type detection and disposition header
+ * @param contentLength the total number of bytes to read, or -1 if unknown
+ * @param contentType the content type, or {@code null} to auto-detect from fileName
+ */
public InputStreamPart(String name, InputStream inputStream, String fileName, long contentLength, String contentType) {
this(name, inputStream, fileName, contentLength, contentType, null);
}
+ /**
+ * Constructs an input stream part with name, stream, file name, content length, content type, and charset.
+ *
+ * @param name the name of the form field
+ * @param inputStream the input stream to read from
+ * @param fileName the file name for content type detection and disposition header
+ * @param contentLength the total number of bytes to read, or -1 if unknown
+ * @param contentType the content type, or {@code null} to auto-detect from fileName
+ * @param charset the character encoding, or {@code null} for default
+ */
public InputStreamPart(String name, InputStream inputStream, String fileName, long contentLength, String contentType, Charset charset) {
this(name, inputStream, fileName, contentLength, contentType, charset, null);
}
+ /**
+ * Constructs an input stream part with name, stream, file name, content length, content type, charset, and content ID.
+ *
+ * @param name the name of the form field
+ * @param inputStream the input stream to read from
+ * @param fileName the file name for content type detection and disposition header
+ * @param contentLength the total number of bytes to read, or -1 if unknown
+ * @param contentType the content type, or {@code null} to auto-detect from fileName
+ * @param charset the character encoding, or {@code null} for default
+ * @param contentId the content ID, or {@code null} if not needed
+ */
public InputStreamPart(String name, InputStream inputStream, String fileName, long contentLength, String contentType, Charset charset,
String contentId) {
this(name, inputStream, fileName, contentLength, contentType, charset, contentId, null);
}
+ /**
+ * Constructs an input stream part with all optional parameters.
+ *
+ * @param name the name of the form field
+ * @param inputStream the input stream to read from
+ * @param fileName the file name for content type detection and disposition header
+ * @param contentLength the total number of bytes to read, or -1 if unknown
+ * @param contentType the content type, or {@code null} to auto-detect from fileName
+ * @param charset the character encoding, or {@code null} for default
+ * @param contentId the content ID, or {@code null} if not needed
+ * @param transferEncoding the transfer encoding, or {@code null} for default
+ */
public InputStreamPart(String name, InputStream inputStream, String fileName, long contentLength, String contentType, Charset charset,
String contentId, String transferEncoding) {
super(name,
@@ -56,10 +146,20 @@ public InputStreamPart(String name, InputStream inputStream, String fileName, lo
this.contentLength = contentLength;
}
+ /**
+ * Returns the input stream that provides the part's data.
+ *
+ * @return the input stream
+ */
public InputStream getInputStream() {
return inputStream;
}
+ /**
+ * Returns the content length of this part.
+ *
+ * @return the content length in bytes, or -1 if unknown
+ */
public long getContentLength() {
return contentLength;
}
diff --git a/client/src/main/java/org/asynchttpclient/request/body/multipart/MultipartBody.java b/client/src/main/java/org/asynchttpclient/request/body/multipart/MultipartBody.java
index f2d8eb9598..a2bee14002 100644
--- a/client/src/main/java/org/asynchttpclient/request/body/multipart/MultipartBody.java
+++ b/client/src/main/java/org/asynchttpclient/request/body/multipart/MultipartBody.java
@@ -29,6 +29,20 @@
import static org.asynchttpclient.util.Assertions.assertNotNull;
import static org.asynchttpclient.util.MiscUtils.closeSilently;
+/**
+ * A body implementation for multipart/form-data requests.
+ *
+ * This class implements {@link RandomAccessBody} to provide both buffer-based and
+ * channel-based transfer of multipart data. It manages a list of {@link MultipartPart}s
+ * and transfers them sequentially, handling proper encoding according to the
+ * multipart/form-data specification.
+ *
+ *
+ * The body computes the total content length if all parts have known lengths, or
+ * returns -1 for chunked transfer encoding if any part has unknown length. Transfer
+ * operations are stateful and track the current part being transferred.
+ *
+ */
public class MultipartBody implements RandomAccessBody {
private final static Logger LOGGER = LoggerFactory.getLogger(MultipartBody.class);
@@ -41,6 +55,13 @@ public class MultipartBody implements RandomAccessBody {
private boolean done = false;
private AtomicBoolean closed = new AtomicBoolean();
+ /**
+ * Constructs a multipart body with the specified parts, content type, and boundary.
+ *
+ * @param parts the list of multipart parts to include
+ * @param contentType the Content-Type header value (typically includes the boundary)
+ * @param boundary the multipart boundary bytes
+ */
public MultipartBody(List> parts, String contentType, byte[] boundary) {
this.boundary = boundary;
this.contentType = contentType;
diff --git a/client/src/main/java/org/asynchttpclient/request/body/multipart/MultipartUtils.java b/client/src/main/java/org/asynchttpclient/request/body/multipart/MultipartUtils.java
index 78e2d130a4..65dd17648b 100644
--- a/client/src/main/java/org/asynchttpclient/request/body/multipart/MultipartUtils.java
+++ b/client/src/main/java/org/asynchttpclient/request/body/multipart/MultipartUtils.java
@@ -26,14 +26,29 @@
import static org.asynchttpclient.util.HttpUtils.*;
import static org.asynchttpclient.util.MiscUtils.isNonEmpty;
+/**
+ * Utility class for creating multipart request bodies.
+ *
+ * This class provides static methods for constructing {@link MultipartBody} instances
+ * from a list of {@link Part}s. It handles boundary generation, Content-Type header
+ * construction, and conversion of parts to their multipart representation.
+ *
+ */
public class MultipartUtils {
/**
- * Creates a new multipart entity containing the given parts.
+ * Creates a new multipart body containing the specified parts.
+ *
+ * This method generates a multipart boundary, constructs the appropriate Content-Type
+ * header, and creates a {@link MultipartBody} that encodes all the parts according to
+ * the multipart/form-data specification. If a Content-Type header with a boundary is
+ * already present in the request headers, that boundary is used; otherwise, a new
+ * boundary is generated.
+ *
*
- * @param parts the parts to include.
- * @param requestHeaders the request headers
- * @return a MultipartBody
+ * @param parts the parts to include in the multipart body
+ * @param requestHeaders the request headers, used to check for existing boundary
+ * @return a new multipart body containing the specified parts
*/
public static MultipartBody newMultipartBody(List parts, HttpHeaders requestHeaders) {
assertNotNull(parts, "parts");
@@ -63,6 +78,20 @@ public static MultipartBody newMultipartBody(List parts, HttpHeaders reque
return new MultipartBody(multipartParts, contentType, boundary);
}
+ /**
+ * Generates multipart part representations from a list of parts.
+ *
+ * This method converts high-level {@link Part} objects into their corresponding
+ * {@link MultipartPart} implementations that handle the actual encoding and transfer
+ * of data. Each part type (FilePart, ByteArrayPart, etc.) is mapped to its specific
+ * multipart implementation. A message end part is automatically appended to properly
+ * terminate the multipart message.
+ *
+ *
+ * @param parts the list of parts to convert
+ * @param boundary the multipart boundary bytes
+ * @return a list of multipart part implementations, including a terminating message end part
+ */
public static List> generateMultipartParts(List parts, byte[] boundary) {
List> multipartParts = new ArrayList<>(parts.size());
for (Part part : parts) {
diff --git a/client/src/main/java/org/asynchttpclient/request/body/multipart/Part.java b/client/src/main/java/org/asynchttpclient/request/body/multipart/Part.java
index 77c4a0f07a..04f5bb0dfb 100644
--- a/client/src/main/java/org/asynchttpclient/request/body/multipart/Part.java
+++ b/client/src/main/java/org/asynchttpclient/request/body/multipart/Part.java
@@ -17,53 +17,113 @@
import java.nio.charset.Charset;
import java.util.List;
+/**
+ * Represents a part in a multipart HTTP request.
+ *
+ * A part is a component of a multipart/form-data request, which allows combining
+ * multiple pieces of data (text fields, files, etc.) in a single HTTP request body.
+ * Each part has a name, optional content type, charset, transfer encoding, and other
+ * metadata that controls how it is encoded in the request.
+ *
+ *
+ * Common implementations include {@link StringPart} for text fields, {@link FilePart}
+ * for file uploads, {@link ByteArrayPart} for in-memory binary data, and
+ * {@link InputStreamPart} for streaming data.
+ *
+ *
+ * Usage Examples:
+ * {@code
+ * // Create different types of parts
+ * Part textField = new StringPart("username", "john_doe");
+ * Part fileUpload = new FilePart("avatar", new File("photo.jpg"), "image/jpeg");
+ * Part binaryData = new ByteArrayPart("data", bytes, "application/octet-stream");
+ *
+ * // Use parts in a multipart request
+ * List parts = Arrays.asList(textField, fileUpload, binaryData);
+ * AsyncHttpClient client = asyncHttpClient();
+ * client.preparePost("http://example.com/upload")
+ * .setBodyParts(parts)
+ * .execute();
+ * }
+ */
public interface Part {
/**
- * Return the name of this part.
+ * Returns the name of this part.
+ *
+ * The name is used in the Content-Disposition header and corresponds to the
+ * form field name in HTML form submissions.
+ *
*
- * @return The name.
+ * @return the part name, or {@code null} if no name is set
*/
String getName();
/**
* Returns the content type of this part.
+ *
+ * The content type specifies the MIME type of the part's content and is included
+ * in the Content-Type header. If {@code null}, the Content-Type header may be
+ * omitted or a default type may be used.
+ *
*
- * @return the content type, or null to exclude the content
- * type header
+ * @return the content type, or {@code null} to exclude the Content-Type header
*/
String getContentType();
/**
- * Return the character encoding of this part.
+ * Returns the character encoding of this part.
+ *
+ * The charset is appended to the Content-Type header and specifies how text
+ * content should be decoded. This is primarily used for text-based parts.
+ *
*
- * @return the character encoding, or null to exclude the
- * character encoding header
+ * @return the character encoding, or {@code null} to exclude the charset parameter
*/
Charset getCharset();
/**
- * Return the transfer encoding of this part.
+ * Returns the transfer encoding of this part.
+ *
+ * The transfer encoding specifies how the part's content is encoded for transmission
+ * and is included in the Content-Transfer-Encoding header.
+ *
*
- * @return the transfer encoding, or null to exclude the
- * transfer encoding header
+ * @return the transfer encoding, or {@code null} to exclude the Content-Transfer-Encoding header
*/
String getTransferEncoding();
/**
- * Return the content ID of this part.
+ * Returns the content ID of this part.
+ *
+ * The content ID provides a unique identifier for the part and is included
+ * in the Content-ID header. This is useful for referencing parts within
+ * multipart messages.
+ *
*
- * @return the content ID, or null to exclude the content ID
- * header
+ * @return the content ID, or {@code null} to exclude the Content-ID header
*/
String getContentId();
/**
- * Gets the disposition-type to be used in Content-Disposition header
+ * Returns the disposition type to be used in the Content-Disposition header.
+ *
+ * The disposition type is typically "form-data" for multipart/form-data requests.
+ * Other values like "attachment" may be used in different contexts.
+ *
*
- * @return the disposition-type
+ * @return the disposition type, or {@code null} to use the default
*/
String getDispositionType();
+ /**
+ * Returns the list of custom headers for this part.
+ *
+ * Custom headers allow adding arbitrary HTTP headers to the part beyond the
+ * standard Content-Disposition, Content-Type, and other predefined headers.
+ *
+ *
+ * @return the list of custom headers, or {@code null} if no custom headers are set
+ */
List getCustomHeaders();
}
diff --git a/client/src/main/java/org/asynchttpclient/request/body/multipart/PartBase.java b/client/src/main/java/org/asynchttpclient/request/body/multipart/PartBase.java
index 94003a3ea7..b32507fb4a 100644
--- a/client/src/main/java/org/asynchttpclient/request/body/multipart/PartBase.java
+++ b/client/src/main/java/org/asynchttpclient/request/body/multipart/PartBase.java
@@ -18,20 +18,29 @@
import java.util.ArrayList;
import java.util.List;
+/**
+ * Abstract base class implementing common functionality for multipart parts.
+ *
+ * This class provides the core implementation of the {@link Part} interface,
+ * managing common metadata such as name, content type, charset, transfer encoding,
+ * disposition type, and custom headers. Concrete implementations extend this class
+ * to provide specific content handling.
+ *
+ */
public abstract class PartBase implements Part {
/**
- * The name of the form field, part of the Content-Disposition header
+ * The name of the form field, used in the Content-Disposition header.
*/
private final String name;
/**
- * The main part of the Content-Type header
+ * The MIME type of the part's content, used in the Content-Type header.
*/
private final String contentType;
/**
- * The charset (part of Content-Type header)
+ * The character encoding, appended to the Content-Type header.
*/
private final Charset charset;
@@ -41,28 +50,28 @@ public abstract class PartBase implements Part {
private final String transferEncoding;
/**
- * The Content-Id
+ * The Content-ID header value, used to uniquely identify the part.
*/
private final String contentId;
/**
- * The disposition type (part of Content-Disposition)
+ * The disposition type used in the Content-Disposition header (e.g., "form-data").
*/
private String dispositionType;
/**
- * Additional part headers
+ * Additional custom headers to be included with this part.
*/
private List customHeaders;
/**
- * Constructor.
+ * Constructs a part base with the specified metadata.
*
- * @param name The name of the part, or null
- * @param contentType The content type, or null
- * @param charset The character encoding, or null
- * @param contentId The content id, or null
- * @param transferEncoding The transfer encoding, or null
+ * @param name the name of the form field, or {@code null}
+ * @param contentType the content type, or {@code null}
+ * @param charset the character encoding, or {@code null}
+ * @param contentId the content ID, or {@code null}
+ * @param transferEncoding the transfer encoding, or {@code null}
*/
public PartBase(String name, String contentType, Charset charset, String contentId, String transferEncoding) {
this.name = name;
@@ -102,6 +111,11 @@ public String getDispositionType() {
return dispositionType;
}
+ /**
+ * Sets the disposition type for this part.
+ *
+ * @param dispositionType the disposition type (e.g., "form-data", "attachment")
+ */
public void setDispositionType(String dispositionType) {
this.dispositionType = dispositionType;
}
@@ -111,10 +125,24 @@ public List getCustomHeaders() {
return customHeaders;
}
+ /**
+ * Sets the custom headers for this part.
+ *
+ * @param customHeaders the list of custom headers, or {@code null} to clear
+ */
public void setCustomHeaders(List customHeaders) {
this.customHeaders = customHeaders;
}
+ /**
+ * Adds a custom header to this part.
+ *
+ * If no custom headers have been set yet, a new list is created.
+ *
+ *
+ * @param name the header name
+ * @param value the header value
+ */
public void addCustomHeader(String name, String value) {
if (customHeaders == null) {
customHeaders = new ArrayList<>(2);
@@ -122,6 +150,11 @@ public void addCustomHeader(String name, String value) {
customHeaders.add(new Param(name, value));
}
+ /**
+ * Returns a string representation of this part.
+ *
+ * @return a string containing the part's metadata
+ */
public String toString() {
return getClass().getSimpleName() +
" name=" + getName() +
diff --git a/client/src/main/java/org/asynchttpclient/request/body/multipart/StringPart.java b/client/src/main/java/org/asynchttpclient/request/body/multipart/StringPart.java
index 6d2e078cb1..bd5e02531c 100644
--- a/client/src/main/java/org/asynchttpclient/request/body/multipart/StringPart.java
+++ b/client/src/main/java/org/asynchttpclient/request/body/multipart/StringPart.java
@@ -18,34 +18,103 @@
import static org.asynchttpclient.util.Assertions.assertNotNull;
import static org.asynchttpclient.util.MiscUtils.withDefault;
+/**
+ * A multipart part representing a string value (text field).
+ *
+ * This class is used for text-based form fields in multipart/form-data requests.
+ * String parts default to UTF-8 encoding if no charset is specified. The string
+ * value must not contain NUL characters (U+0000) as per RFC 2048 section 2.8.
+ *
+ *
+ * Usage Examples:
+ * {@code
+ * // Simple text field
+ * Part username = new StringPart("username", "john_doe");
+ *
+ * // Text field with explicit content type
+ * Part description = new StringPart("description", "User profile", "text/plain");
+ *
+ * // Text field with custom charset
+ * Part comment = new StringPart("comment", "Comment text",
+ * "text/plain", StandardCharsets.ISO_8859_1);
+ *
+ * // Use in a multipart request
+ * AsyncHttpClient client = asyncHttpClient();
+ * client.preparePost("http://example.com/form")
+ * .addBodyPart(username)
+ * .addBodyPart(description)
+ * .execute();
+ * }
+ */
public class StringPart extends PartBase {
/**
- * Default charset of string parameters
+ * Default charset for string parameters (UTF-8).
*/
private static final Charset DEFAULT_CHARSET = UTF_8;
/**
- * Contents of this StringPart.
+ * The string value of this part.
*/
private final String value;
+ /**
+ * Constructs a string part with the specified name and value.
+ *
+ * @param name the name of the form field
+ * @param value the string value
+ */
public StringPart(String name, String value) {
this(name, value, null);
}
+ /**
+ * Constructs a string part with name, value, and content type.
+ *
+ * @param name the name of the form field
+ * @param value the string value
+ * @param contentType the content type, or {@code null} for default
+ */
public StringPart(String name, String value, String contentType) {
this(name, value, contentType, null);
}
+ /**
+ * Constructs a string part with name, value, content type, and charset.
+ *
+ * @param name the name of the form field
+ * @param value the string value
+ * @param contentType the content type, or {@code null} for default
+ * @param charset the character encoding, or {@code null} for UTF-8
+ */
public StringPart(String name, String value, String contentType, Charset charset) {
this(name, value, contentType, charset, null);
}
+ /**
+ * Constructs a string part with name, value, content type, charset, and content ID.
+ *
+ * @param name the name of the form field
+ * @param value the string value
+ * @param contentType the content type, or {@code null} for default
+ * @param charset the character encoding, or {@code null} for UTF-8
+ * @param contentId the content ID, or {@code null} if not needed
+ */
public StringPart(String name, String value, String contentType, Charset charset, String contentId) {
this(name, value, contentType, charset, contentId, null);
}
+ /**
+ * Constructs a string part with all optional parameters.
+ *
+ * @param name the name of the form field
+ * @param value the string value
+ * @param contentType the content type, or {@code null} for default
+ * @param charset the character encoding, or {@code null} for UTF-8
+ * @param contentId the content ID, or {@code null} if not needed
+ * @param transferEncoding the transfer encoding, or {@code null} for default
+ * @throws IllegalArgumentException if the value contains NUL characters
+ */
public StringPart(String name, String value, String contentType, Charset charset, String contentId, String transferEncoding) {
super(name, contentType, charsetOrDefault(charset), contentId, transferEncoding);
assertNotNull(value, "value");
@@ -61,6 +130,11 @@ private static Charset charsetOrDefault(Charset charset) {
return withDefault(charset, DEFAULT_CHARSET);
}
+ /**
+ * Returns the string value of this part.
+ *
+ * @return the string value
+ */
public String getValue() {
return value;
}
diff --git a/client/src/main/java/org/asynchttpclient/request/body/multipart/part/ByteArrayMultipartPart.java b/client/src/main/java/org/asynchttpclient/request/body/multipart/part/ByteArrayMultipartPart.java
index d545601072..fa83d226fc 100644
--- a/client/src/main/java/org/asynchttpclient/request/body/multipart/part/ByteArrayMultipartPart.java
+++ b/client/src/main/java/org/asynchttpclient/request/body/multipart/part/ByteArrayMultipartPart.java
@@ -20,10 +20,23 @@
import java.io.IOException;
import java.nio.channels.WritableByteChannel;
+/**
+ * Multipart part implementation for {@link ByteArrayPart}.
+ *
+ * This class handles encoding and transfer of in-memory byte array data as a multipart
+ * part. The byte array is wrapped in a ByteBuf for efficient transfer operations.
+ *
+ */
public class ByteArrayMultipartPart extends FileLikeMultipartPart {
private final ByteBuf contentBuffer;
+ /**
+ * Constructs a byte array multipart part.
+ *
+ * @param part the byte array part containing the data and metadata
+ * @param boundary the multipart boundary bytes
+ */
public ByteArrayMultipartPart(ByteArrayPart part, byte[] boundary) {
super(part, boundary);
contentBuffer = Unpooled.wrappedBuffer(part.getBytes());
diff --git a/client/src/main/java/org/asynchttpclient/request/body/multipart/part/FileLikeMultipartPart.java b/client/src/main/java/org/asynchttpclient/request/body/multipart/part/FileLikeMultipartPart.java
index e3023cc626..df6b63df5b 100644
--- a/client/src/main/java/org/asynchttpclient/request/body/multipart/part/FileLikeMultipartPart.java
+++ b/client/src/main/java/org/asynchttpclient/request/body/multipart/part/FileLikeMultipartPart.java
@@ -4,13 +4,33 @@
import static java.nio.charset.StandardCharsets.*;
+/**
+ * Abstract base class for multipart parts representing file-like content.
+ *
+ * This class extends {@link MultipartPart} to provide common functionality for parts
+ * that have file names, such as file uploads, byte arrays, and input streams. It adds
+ * the filename parameter to the Content-Disposition header when present.
+ *
+ *
+ * Concrete implementations include {@link FileMultipartPart}, {@link ByteArrayMultipartPart},
+ * and {@link InputStreamMultipartPart}.
+ *
+ *
+ * @param the type of FileLikePart this multipart part represents
+ */
public abstract class FileLikeMultipartPart extends MultipartPart {
/**
- * Attachment's file name as a byte array
+ * The filename parameter prefix for the Content-Disposition header.
*/
private static final byte[] FILE_NAME_BYTES = "; filename=".getBytes(US_ASCII);
+ /**
+ * Constructs a file-like multipart part.
+ *
+ * @param part the file-like part containing metadata
+ * @param boundary the multipart boundary bytes
+ */
FileLikeMultipartPart(T part, byte[] boundary) {
super(part, boundary);
}
diff --git a/client/src/main/java/org/asynchttpclient/request/body/multipart/part/FileMultipartPart.java b/client/src/main/java/org/asynchttpclient/request/body/multipart/part/FileMultipartPart.java
index 1b5caca7a7..fe1ceec61b 100644
--- a/client/src/main/java/org/asynchttpclient/request/body/multipart/part/FileMultipartPart.java
+++ b/client/src/main/java/org/asynchttpclient/request/body/multipart/part/FileMultipartPart.java
@@ -23,12 +23,28 @@
import static org.asynchttpclient.util.MiscUtils.closeSilently;
+/**
+ * Multipart part implementation for {@link FilePart}.
+ *
+ * This class handles encoding and transfer of file uploads as multipart parts. It uses
+ * {@link FileChannel} for efficient file reading and supports both buffer-based and
+ * channel-based transfer operations. The file is validated for existence and readability
+ * during construction.
+ *
+ */
public class FileMultipartPart extends FileLikeMultipartPart {
private final long length;
private FileChannel channel;
private long position = 0L;
+ /**
+ * Constructs a file multipart part.
+ *
+ * @param part the file part containing the file and metadata
+ * @param boundary the multipart boundary bytes
+ * @throws IllegalArgumentException if the file doesn't exist or can't be read
+ */
public FileMultipartPart(FilePart part, byte[] boundary) {
super(part, boundary);
File file = part.getFile();
diff --git a/client/src/main/java/org/asynchttpclient/request/body/multipart/part/InputStreamMultipartPart.java b/client/src/main/java/org/asynchttpclient/request/body/multipart/part/InputStreamMultipartPart.java
index 1c2ca251d3..c7e02cbdf3 100644
--- a/client/src/main/java/org/asynchttpclient/request/body/multipart/part/InputStreamMultipartPart.java
+++ b/client/src/main/java/org/asynchttpclient/request/body/multipart/part/InputStreamMultipartPart.java
@@ -26,12 +26,26 @@
import static org.asynchttpclient.util.MiscUtils.closeSilently;
+/**
+ * Multipart part implementation for {@link InputStreamPart}.
+ *
+ * This class handles encoding and transfer of streaming data as multipart parts. It wraps
+ * the input stream in a {@link ReadableByteChannel} for efficient transfer operations and
+ * tracks the position for content length validation when known.
+ *
+ */
public class InputStreamMultipartPart extends FileLikeMultipartPart {
private long position = 0L;
private ByteBuffer buffer;
private ReadableByteChannel channel;
+ /**
+ * Constructs an input stream multipart part.
+ *
+ * @param part the input stream part containing the stream and metadata
+ * @param boundary the multipart boundary bytes
+ */
public InputStreamMultipartPart(InputStreamPart part, byte[] boundary) {
super(part, boundary);
}
diff --git a/client/src/main/java/org/asynchttpclient/request/body/multipart/part/MessageEndMultipartPart.java b/client/src/main/java/org/asynchttpclient/request/body/multipart/part/MessageEndMultipartPart.java
index e81fb905f5..fc13046c54 100644
--- a/client/src/main/java/org/asynchttpclient/request/body/multipart/part/MessageEndMultipartPart.java
+++ b/client/src/main/java/org/asynchttpclient/request/body/multipart/part/MessageEndMultipartPart.java
@@ -21,11 +21,25 @@
import java.io.IOException;
import java.nio.channels.WritableByteChannel;
+/**
+ * Special multipart part that represents the end of a multipart message.
+ *
+ * This class generates the final boundary marker that terminates a multipart/form-data
+ * message. It consists of "--boundary--CRLF" according to the multipart specification.
+ * Unlike regular parts, it has no associated Part object and no separate pre-content,
+ * content, and post-content phases.
+ *
+ */
public class MessageEndMultipartPart extends MultipartPart {
// lazy
private ByteBuf contentBuffer;
+ /**
+ * Constructs a message end multipart part.
+ *
+ * @param boundary the multipart boundary bytes
+ */
public MessageEndMultipartPart(byte[] boundary) {
super(null, boundary);
state = MultipartState.PRE_CONTENT;
diff --git a/client/src/main/java/org/asynchttpclient/request/body/multipart/part/MultipartPart.java b/client/src/main/java/org/asynchttpclient/request/body/multipart/part/MultipartPart.java
index b8c8622680..77c90c558e 100644
--- a/client/src/main/java/org/asynchttpclient/request/body/multipart/part/MultipartPart.java
+++ b/client/src/main/java/org/asynchttpclient/request/body/multipart/part/MultipartPart.java
@@ -30,18 +30,34 @@
import static java.nio.charset.StandardCharsets.US_ASCII;
import static org.asynchttpclient.util.MiscUtils.isNonEmpty;
+/**
+ * Abstract base class for multipart part implementations that handle encoding and transfer.
+ *
+ * This class provides the core functionality for encoding and transferring multipart parts
+ * according to the multipart/form-data specification (RFC 2388). It manages the three-phase
+ * transfer process: pre-content (boundary and headers), content (actual data), and post-content
+ * (trailing CRLF).
+ *
+ *
+ * Subclasses must implement {@link #getContentLength()}, {@link #transferContentTo(ByteBuf)},
+ * and {@link #transferContentTo(WritableByteChannel)} to provide content-specific transfer logic.
+ * The class handles lazy loading of pre-content and post-content buffers to optimize memory usage.
+ *
+ *
+ * @param the type of Part this multipart part represents
+ */
public abstract class MultipartPart implements Closeable {
/**
- * Content disposition as a byte
+ * Quote character as a byte, used in headers.
*/
static final byte QUOTE_BYTE = '\"';
/**
- * Carriage return/linefeed as a byte array
+ * Carriage return/line feed sequence as bytes (CRLF).
*/
protected static final byte[] CRLF_BYTES = "\r\n".getBytes(US_ASCII);
/**
- * Extra characters as a byte array
+ * Boundary delimiter prefix ("--") as bytes.
*/
protected static final byte[] EXTRA_BYTES = "--".getBytes(US_ASCII);
diff --git a/client/src/main/java/org/asynchttpclient/request/body/multipart/part/MultipartState.java b/client/src/main/java/org/asynchttpclient/request/body/multipart/part/MultipartState.java
index 1d9f4b9de0..476ce3fd87 100644
--- a/client/src/main/java/org/asynchttpclient/request/body/multipart/part/MultipartState.java
+++ b/client/src/main/java/org/asynchttpclient/request/body/multipart/part/MultipartState.java
@@ -13,13 +13,38 @@
*/
package org.asynchttpclient.request.body.multipart.part;
+/**
+ * Represents the state of a multipart part during transfer.
+ *
+ * This enum tracks the progress of transferring a multipart part, which consists of
+ * three phases: pre-content (headers and boundary), content (actual data), and
+ * post-content (trailing boundary). The DONE state indicates the part has been
+ * completely transferred.
+ *
+ */
public enum MultipartState {
+ /**
+ * The part is transferring pre-content data (boundary, headers).
+ * This is the initial state when a part begins transfer.
+ */
PRE_CONTENT,
+ /**
+ * The part is transferring the actual content data.
+ * This state follows PRE_CONTENT once all headers have been written.
+ */
CONTENT,
+ /**
+ * The part is transferring post-content data (trailing CRLF).
+ * This state follows CONTENT once all data has been written.
+ */
POST_CONTENT,
+ /**
+ * The part transfer is complete.
+ * All data for this part has been successfully transferred.
+ */
DONE
}
diff --git a/client/src/main/java/org/asynchttpclient/request/body/multipart/part/PartVisitor.java b/client/src/main/java/org/asynchttpclient/request/body/multipart/part/PartVisitor.java
index 8f3abd221c..ab48c6aebc 100644
--- a/client/src/main/java/org/asynchttpclient/request/body/multipart/part/PartVisitor.java
+++ b/client/src/main/java/org/asynchttpclient/request/body/multipart/part/PartVisitor.java
@@ -14,43 +14,107 @@
import io.netty.buffer.ByteBuf;
+/**
+ * Visitor interface for building multipart part headers and boundaries.
+ *
+ * This interface implements the Visitor pattern for constructing multipart part data.
+ * It provides methods for appending bytes and is used by {@link MultipartPart} to
+ * build pre-content and post-content buffers. Two implementations are provided:
+ * {@link CounterPartVisitor} for computing sizes and {@link ByteBufVisitor} for
+ * writing actual data.
+ *
+ */
public interface PartVisitor {
+ /**
+ * Visits a byte array, incorporating it into the part being built.
+ *
+ * @param bytes the byte array to visit
+ */
void withBytes(byte[] bytes);
+ /**
+ * Visits a single byte, incorporating it into the part being built.
+ *
+ * @param b the byte to visit
+ */
void withByte(byte b);
+ /**
+ * A visitor implementation that counts the total number of bytes visited.
+ *
+ * This visitor is used to compute the size of pre-content and post-content sections
+ * before allocating buffers for them.
+ *
+ */
class CounterPartVisitor implements PartVisitor {
private int count = 0;
+ /**
+ * Increments the count by the length of the byte array.
+ *
+ * @param bytes the byte array being visited
+ */
@Override
public void withBytes(byte[] bytes) {
count += bytes.length;
}
+ /**
+ * Increments the count by one.
+ *
+ * @param b the byte being visited
+ */
@Override
public void withByte(byte b) {
count++;
}
+ /**
+ * Returns the total count of bytes visited.
+ *
+ * @return the total byte count
+ */
public int getCount() {
return count;
}
}
+ /**
+ * A visitor implementation that writes bytes to a ByteBuf.
+ *
+ * This visitor is used to actually write the pre-content and post-content data
+ * to allocated buffers that will be transferred.
+ *
+ */
class ByteBufVisitor implements PartVisitor {
private final ByteBuf target;
+ /**
+ * Constructs a ByteBuf visitor with the specified target buffer.
+ *
+ * @param target the buffer to write bytes to
+ */
public ByteBufVisitor(ByteBuf target) {
this.target = target;
}
+ /**
+ * Writes the byte array to the target buffer.
+ *
+ * @param bytes the byte array to write
+ */
@Override
public void withBytes(byte[] bytes) {
target.writeBytes(bytes);
}
+ /**
+ * Writes the single byte to the target buffer.
+ *
+ * @param b the byte to write
+ */
@Override
public void withByte(byte b) {
target.writeByte(b);
diff --git a/client/src/main/java/org/asynchttpclient/request/body/multipart/part/StringMultipartPart.java b/client/src/main/java/org/asynchttpclient/request/body/multipart/part/StringMultipartPart.java
index daf37a97c3..03f69b08a6 100644
--- a/client/src/main/java/org/asynchttpclient/request/body/multipart/part/StringMultipartPart.java
+++ b/client/src/main/java/org/asynchttpclient/request/body/multipart/part/StringMultipartPart.java
@@ -20,10 +20,23 @@
import java.io.IOException;
import java.nio.channels.WritableByteChannel;
+/**
+ * Multipart part implementation for {@link StringPart}.
+ *
+ * This class handles encoding and transfer of text field data as multipart parts. The
+ * string value is encoded using the part's charset and wrapped in a ByteBuf for transfer.
+ *
+ */
public class StringMultipartPart extends MultipartPart {
private final ByteBuf contentBuffer;
+ /**
+ * Constructs a string multipart part.
+ *
+ * @param part the string part containing the text and metadata
+ * @param boundary the multipart boundary bytes
+ */
public StringMultipartPart(StringPart part, byte[] boundary) {
super(part, boundary);
contentBuffer = Unpooled.wrappedBuffer(part.getValue().getBytes(part.getCharset()));
diff --git a/client/src/main/java/org/asynchttpclient/resolver/RequestHostnameResolver.java b/client/src/main/java/org/asynchttpclient/resolver/RequestHostnameResolver.java
index da42fcf660..155a08181f 100644
--- a/client/src/main/java/org/asynchttpclient/resolver/RequestHostnameResolver.java
+++ b/client/src/main/java/org/asynchttpclient/resolver/RequestHostnameResolver.java
@@ -27,12 +27,35 @@
import java.util.ArrayList;
import java.util.List;
+/**
+ * Hostname resolver that coordinates DNS resolution with AsyncHandler callbacks.
+ *
+ * This singleton resolver wraps Netty's NameResolver and provides hooks for
+ * AsyncHandlers to be notified of resolution attempts, successes, and failures.
+ * It converts resolved InetAddresses to InetSocketAddresses with the appropriate port.
+ *
+ */
public enum RequestHostnameResolver {
+ /** Singleton instance */
INSTANCE;
private static final Logger LOGGER = LoggerFactory.getLogger(RequestHostnameResolver.class);
+ /**
+ * Resolves a hostname to a list of socket addresses, notifying the handler of progress.
+ *
+ * This method performs asynchronous DNS resolution using the provided name resolver,
+ * and invokes the appropriate callbacks on the async handler for resolution attempts,
+ * successes, and failures.
+ *
+ *
+ * @param nameResolver the Netty name resolver to use for DNS resolution
+ * @param unresolvedAddress the unresolved socket address containing the hostname and port
+ * @param asyncHandler the handler to notify of resolution events
+ * @return a future that will be completed with the list of resolved socket addresses,
+ * or failed with an exception if resolution fails
+ */
public Future> resolve(NameResolver nameResolver, InetSocketAddress unresolvedAddress, AsyncHandler> asyncHandler) {
final String hostname = unresolvedAddress.getHostString();
diff --git a/client/src/main/java/org/asynchttpclient/spnego/NamePasswordCallbackHandler.java b/client/src/main/java/org/asynchttpclient/spnego/NamePasswordCallbackHandler.java
index 680cdcac43..3ee78dbe74 100644
--- a/client/src/main/java/org/asynchttpclient/spnego/NamePasswordCallbackHandler.java
+++ b/client/src/main/java/org/asynchttpclient/spnego/NamePasswordCallbackHandler.java
@@ -11,6 +11,22 @@
import java.io.IOException;
import java.lang.reflect.Method;
+/**
+ * Callback handler that provides username and password for JAAS authentication.
+ *
+ * This implementation handles standard {@link NameCallback} and {@link PasswordCallback}
+ * callbacks, as well as custom password callbacks through reflection. It is primarily
+ * used for Kerberos/SPNEGO authentication where credentials need to be provided
+ * programmatically.
+ *
+ *
+ * Usage Examples:
+ * {@code
+ * CallbackHandler handler = new NamePasswordCallbackHandler("username", "password");
+ * LoginContext loginContext = new LoginContext("MyContext", handler);
+ * loginContext.login();
+ * }
+ */
public class NamePasswordCallbackHandler implements CallbackHandler {
private final Logger log = LoggerFactory.getLogger(getClass());
private static final String PASSWORD_CALLBACK_NAME = "setObject";
@@ -22,16 +38,46 @@ public class NamePasswordCallbackHandler implements CallbackHandler {
private String passwordCallbackName;
+ /**
+ * Creates a new callback handler with the specified username and password.
+ *
+ * @param username the username to provide in callbacks
+ * @param password the password to provide in callbacks
+ */
public NamePasswordCallbackHandler(String username, String password) {
this(username, password, null);
}
+ /**
+ * Creates a new callback handler with the specified username, password, and custom
+ * password callback method name.
+ *
+ * @param username the username to provide in callbacks
+ * @param password the password to provide in callbacks
+ * @param passwordCallbackName the name of the method to invoke for custom password callbacks
+ * (can be null to use the default "setObject")
+ */
public NamePasswordCallbackHandler(String username, String password, String passwordCallbackName) {
this.username = username;
this.password = password;
this.passwordCallbackName = passwordCallbackName;
}
+ /**
+ * Handles the specified callbacks by providing username and password information.
+ *
+ * This method supports:
+ *
+ *
+ * {@link NameCallback} - provides the username
+ * {@link PasswordCallback} - provides the password as a character array
+ * Custom callbacks - invokes password setter methods through reflection
+ *
+ *
+ * @param callbacks the callbacks to handle
+ * @throws IOException if an I/O error occurs
+ * @throws UnsupportedCallbackException if a callback type is not supported
+ */
public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
for (int i = 0; i < callbacks.length; i++) {
Callback callback = callbacks[i];
@@ -50,16 +96,30 @@ public void handle(Callback[] callbacks) throws IOException, UnsupportedCallback
}
}
+ /**
+ * Extension point for subclasses to handle custom callback types.
+ *
+ * This method is called before standard callback handling. Subclasses can override
+ * this method to provide custom callback handling logic.
+ *
+ *
+ * @param callback the callback to handle
+ * @return {@code true} if the callback was handled, {@code false} otherwise
+ */
protected boolean handleCallback(Callback callback) {
return false;
}
- /*
- * This method is called from the handle(Callback[]) method when the specified callback
- * did not match any of the known callback classes. It looks for the callback method
- * having the specified method name with one of the supported parameter types.
- * If found, it invokes the callback method on the object and returns true.
- * If not, it returns false.
+ /**
+ * Invokes a password setter method on the callback object through reflection.
+ *
+ * This method is called when the callback doesn't match any of the standard types.
+ * It looks for a method with the configured name (or default "setObject") that accepts
+ * one of the supported parameter types (Object, char[], or String).
+ *
+ *
+ * @param callback the callback on which to invoke the password setter
+ * @return {@code true} if a matching method was found and invoked, {@code false} otherwise
*/
private boolean invokePasswordCallback(Callback callback) {
String cbname = passwordCallbackName == null
diff --git a/client/src/main/java/org/asynchttpclient/spnego/SpnegoEngine.java b/client/src/main/java/org/asynchttpclient/spnego/SpnegoEngine.java
index 515bf63184..0fca72ca78 100644
--- a/client/src/main/java/org/asynchttpclient/spnego/SpnegoEngine.java
+++ b/client/src/main/java/org/asynchttpclient/spnego/SpnegoEngine.java
@@ -62,6 +62,35 @@
/**
* SPNEGO (Simple and Protected GSSAPI Negotiation Mechanism) authentication scheme.
+ *
+ * This class implements the SPNEGO authentication protocol used primarily for Kerberos-based
+ * authentication in enterprise environments. SPNEGO allows HTTP clients to authenticate to
+ * servers using GSS-API tokens, typically wrapping Kerberos tickets.
+ *
+ *
+ * The implementation supports both SPNEGO (OID 1.3.6.1.5.5.2) and Kerberos v5
+ * (OID 1.2.840.113554.1.2.2) mechanisms, with automatic fallback to Kerberos if SPNEGO
+ * is not supported by the JRE.
+ *
+ *
+ * Usage Examples:
+ * {@code
+ * // Using default instance
+ * SpnegoEngine engine = new SpnegoEngine();
+ * String token = engine.generateToken("example.com");
+ *
+ * // Using configured instance with credentials
+ * SpnegoEngine engine = SpnegoEngine.instance(
+ * "username",
+ * "password",
+ * null, // servicePrincipalName
+ * "EXAMPLE.COM", // realmName
+ * true, // useCanonicalHostname
+ * null, // customLoginConfig
+ * "MyLoginContext" // loginContextName
+ * );
+ * String token = engine.generateToken("server.example.com");
+ * }
*
* @since 4.1
*/
@@ -80,6 +109,18 @@ public class SpnegoEngine {
private final String loginContextName;
private final Map customLoginConfig;
+ /**
+ * Creates a new SPNEGO engine with the specified configuration.
+ *
+ * @param username the username for authentication (can be null for default credentials)
+ * @param password the password for authentication (can be null for default credentials)
+ * @param servicePrincipalName the service principal name (can be null to use default HTTP@host)
+ * @param realmName the Kerberos realm name (can be null)
+ * @param useCanonicalHostname whether to use canonical hostname resolution
+ * @param customLoginConfig custom login configuration map for Krb5LoginModule (can be null)
+ * @param loginContextName the name of the login context (can be null)
+ * @param spnegoGenerator optional SPNEGO token generator for wrapping Kerberos tickets (can be null)
+ */
public SpnegoEngine(final String username,
final String password,
final String servicePrincipalName,
@@ -98,6 +139,13 @@ public SpnegoEngine(final String username,
this.loginContextName = loginContextName;
}
+ /**
+ * Creates a new SPNEGO engine with default configuration.
+ *
+ * This constructor initializes the engine to use canonical hostname resolution
+ * and default credentials from the system.
+ *
+ */
public SpnegoEngine() {
this(null,
null,
@@ -109,6 +157,23 @@ public SpnegoEngine() {
null);
}
+ /**
+ * Returns a cached SPNEGO engine instance for the given configuration.
+ *
+ * This factory method maintains a cache of engine instances keyed by the configuration
+ * parameters. If an engine with the same configuration already exists, it is returned;
+ * otherwise, a new instance is created and cached.
+ *
+ *
+ * @param username the username for authentication (can be null for default credentials)
+ * @param password the password for authentication (can be null for default credentials)
+ * @param servicePrincipalName the service principal name (can be null to use default HTTP@host)
+ * @param realmName the Kerberos realm name (can be null)
+ * @param useCanonicalHostname whether to use canonical hostname resolution
+ * @param customLoginConfig custom login configuration map for Krb5LoginModule (can be null)
+ * @param loginContextName the name of the login context (can be null)
+ * @return a cached or new SPNEGO engine instance
+ */
public static SpnegoEngine instance(final String username,
final String password,
final String servicePrincipalName,
@@ -144,6 +209,28 @@ public static SpnegoEngine instance(final String username,
return instances.get(key);
}
+ /**
+ * Generates a SPNEGO authentication token for the specified host.
+ *
+ * This method creates a GSS security context and generates a Base64-encoded authentication
+ * token that can be used in an HTTP Authorization header. The method handles:
+ *
+ *
+ * Creating a GSS context with SPNEGO or Kerberos OID
+ * Performing Kerberos login if credentials are configured
+ * Initializing the security context with the server name
+ * Wrapping Kerberos tickets in SPNEGO format if needed
+ *
+ *
+ * The method will attempt to use SPNEGO by default and fall back to Kerberos v5
+ * if SPNEGO is not supported by the JRE (typically JRE 1.5 and earlier).
+ *
+ *
+ * @param host the target server hostname for which to generate the token
+ * @return a Base64-encoded SPNEGO or Kerberos authentication token
+ * @throws SpnegoEngineException if token generation fails due to invalid credentials,
+ * GSS-API errors, or login failures
+ */
public String generateToken(String host) throws SpnegoEngineException {
GSSContext gssContext = null;
byte[] token = null; // base64 decoded challenge
@@ -256,6 +343,17 @@ public String generateToken(String host) throws SpnegoEngineException {
}
}
+ /**
+ * Constructs the complete service principal name (SPN) for the given host.
+ *
+ * If a custom service principal name is configured, it is used directly. Otherwise,
+ * the method constructs an SPN in the format "HTTP@hostname". If canonical hostname
+ * resolution is enabled, the hostname is resolved to its canonical form.
+ *
+ *
+ * @param host the hostname for which to construct the SPN
+ * @return the complete service principal name
+ */
String getCompleteServicePrincipalName(String host) {
String name;
if (servicePrincipalName == null) {
@@ -292,6 +390,16 @@ private CallbackHandler getUsernamePasswordHandler() {
return new NamePasswordCallbackHandler(username, password);
}
+ /**
+ * Returns the login configuration for Kerberos authentication.
+ *
+ * If custom login configuration is provided, returns a configuration that uses
+ * the Krb5LoginModule with the custom settings. Otherwise, returns null to use
+ * the default system configuration.
+ *
+ *
+ * @return the login configuration, or null to use system defaults
+ */
public Configuration getLoginConfiguration() {
if (customLoginConfig != null && !customLoginConfig.isEmpty()) {
return new Configuration() {
diff --git a/client/src/main/java/org/asynchttpclient/spnego/SpnegoEngineException.java b/client/src/main/java/org/asynchttpclient/spnego/SpnegoEngineException.java
index 5e55704299..1042c8207c 100644
--- a/client/src/main/java/org/asynchttpclient/spnego/SpnegoEngineException.java
+++ b/client/src/main/java/org/asynchttpclient/spnego/SpnegoEngineException.java
@@ -15,15 +15,31 @@
/**
* Signals SPNEGO protocol failure.
+ *
+ * This exception is thrown when an error occurs during SPNEGO authentication processing,
+ * such as when GSS context initialization fails, credentials are invalid or expired,
+ * or login failures occur.
+ *
*/
public class SpnegoEngineException extends Exception {
private static final long serialVersionUID = -3123799505052881438L;
+ /**
+ * Creates a new SpnegoEngineException with the specified message.
+ *
+ * @param message the exception detail message
+ */
public SpnegoEngineException(String message) {
super(message);
}
+ /**
+ * Creates a new SpnegoEngineException with the specified detail message and cause.
+ *
+ * @param message the exception detail message
+ * @param cause the {@code Throwable} that caused this exception
+ */
public SpnegoEngineException(String message, Throwable cause) {
super(message, cause);
}
diff --git a/client/src/main/java/org/asynchttpclient/spnego/SpnegoTokenGenerator.java b/client/src/main/java/org/asynchttpclient/spnego/SpnegoTokenGenerator.java
index 5db40b1848..3bee7a07db 100644
--- a/client/src/main/java/org/asynchttpclient/spnego/SpnegoTokenGenerator.java
+++ b/client/src/main/java/org/asynchttpclient/spnego/SpnegoTokenGenerator.java
@@ -41,14 +41,31 @@
import java.io.IOException;
/**
- * Abstract SPNEGO token generator. Implementations should take an Kerberos ticket and transform
- * into a SPNEGO token.
- *
+ * Interface for generating SPNEGO tokens from Kerberos tickets.
+ *
+ * Implementations of this interface transform Kerberos tickets into SPNEGO tokens
+ * by wrapping them in the appropriate DER-encoded SPNEGO structure. This is necessary
+ * for servers that only accept SPNEGO tokens but not raw Kerberos tickets.
+ *
+ *
* Implementations of this interface are expected to be thread-safe.
+ *
*
* @since 4.1
*/
public interface SpnegoTokenGenerator {
+ /**
+ * Generates a SPNEGO DER-encoded object from a Kerberos ticket.
+ *
+ * This method wraps the provided Kerberos ticket in a SPNEGO token structure
+ * according to RFC 4178. The resulting token can be sent to servers that require
+ * SPNEGO authentication.
+ *
+ *
+ * @param kerberosTicket the Kerberos ticket bytes to wrap
+ * @return the SPNEGO DER-encoded token bytes
+ * @throws IOException if an error occurs during token generation
+ */
byte[] generateSpnegoDERObject(byte[] kerberosTicket) throws IOException;
}
diff --git a/client/src/main/java/org/asynchttpclient/uri/Uri.java b/client/src/main/java/org/asynchttpclient/uri/Uri.java
index 19986dcfc3..62c81c2a5f 100644
--- a/client/src/main/java/org/asynchttpclient/uri/Uri.java
+++ b/client/src/main/java/org/asynchttpclient/uri/Uri.java
@@ -22,11 +22,40 @@
import static org.asynchttpclient.util.MiscUtils.isEmpty;
import static org.asynchttpclient.util.MiscUtils.isNonEmpty;
+/**
+ * Immutable URI representation optimized for HTTP client operations.
+ *
+ * This class provides a high-performance URI implementation that parses and validates
+ * URI components according to RFC 3986. It supports HTTP, HTTPS, WS (WebSocket), and
+ * WSS (secure WebSocket) schemes.
+ *
+ *
+ * Usage Examples:
+ * {@code
+ * // Parse a URI from string
+ * Uri uri = Uri.create("https://example.com:8443/path?query=value");
+ *
+ * // Access components
+ * String scheme = uri.getScheme(); // "https"
+ * String host = uri.getHost(); // "example.com"
+ * int port = uri.getPort(); // 8443
+ * String path = uri.getPath(); // "/path"
+ * String query = uri.getQuery(); // "query=value"
+ *
+ * // Create with context (relative URL resolution)
+ * Uri base = Uri.create("https://example.com/foo/bar");
+ * Uri relative = Uri.create(base, "../baz"); // resolves to https://example.com/foo/baz
+ * }
+ */
public class Uri {
+ /** HTTP scheme constant */
public static final String HTTP = "http";
+ /** HTTPS scheme constant */
public static final String HTTPS = "https";
+ /** WebSocket scheme constant */
public static final String WS = "ws";
+ /** Secure WebSocket scheme constant */
public static final String WSS = "wss";
private final String scheme;
private final String userInfo;
diff --git a/client/src/main/java/org/asynchttpclient/util/Assertions.java b/client/src/main/java/org/asynchttpclient/util/Assertions.java
index 6d7d8ad235..0c632f1add 100644
--- a/client/src/main/java/org/asynchttpclient/util/Assertions.java
+++ b/client/src/main/java/org/asynchttpclient/util/Assertions.java
@@ -13,11 +13,37 @@
*/
package org.asynchttpclient.util;
+/**
+ * Utility class for performing common assertion checks on method parameters.
+ *
+ * This class provides convenience methods to validate that parameters meet certain requirements
+ * such as being non-null or non-empty. These methods throw appropriate exceptions when assertions fail.
+ *
+ */
public final class Assertions {
private Assertions() {
}
+ /**
+ * Asserts that the specified value is not null.
+ *
+ * This method validates that the provided value is not null and returns the value if valid.
+ * If the value is null, a {@link NullPointerException} is thrown with the parameter name.
+ *
+ *
+ * Usage Examples:
+ * {@code
+ * String userName = assertNotNull(getUserName(), "userName");
+ * Request request = assertNotNull(buildRequest(), "request");
+ * }
+ *
+ * @param the type of the value being checked
+ * @param value the value to check for null
+ * @param name the name of the parameter (used in error message)
+ * @return the non-null value
+ * @throws NullPointerException if the value is null
+ */
public static T assertNotNull(T value, String name) {
if (value == null)
throw new NullPointerException(name);
@@ -25,6 +51,26 @@ public static T assertNotNull(T value, String name) {
}
+ /**
+ * Asserts that the specified string is not null and not empty.
+ *
+ * This method validates that the provided string is not null and has a length greater than zero.
+ * If the value is null, a {@link NullPointerException} is thrown. If the value is an empty string,
+ * an {@link IllegalArgumentException} is thrown.
+ *
+ *
+ * Usage Examples:
+ * {@code
+ * String host = assertNotEmpty(uri.getHost(), "host");
+ * String userName = assertNotEmpty(credentials.getUsername(), "userName");
+ * }
+ *
+ * @param value the string value to check
+ * @param name the name of the parameter (used in error messages)
+ * @return the non-empty string value
+ * @throws NullPointerException if the value is null
+ * @throws IllegalArgumentException if the value is an empty string
+ */
public static String assertNotEmpty(String value, String name) {
assertNotNull(value, name);
if (value.length() == 0)
diff --git a/client/src/main/java/org/asynchttpclient/util/AuthenticatorUtils.java b/client/src/main/java/org/asynchttpclient/util/AuthenticatorUtils.java
index 00d69af7d2..0d738fb304 100644
--- a/client/src/main/java/org/asynchttpclient/util/AuthenticatorUtils.java
+++ b/client/src/main/java/org/asynchttpclient/util/AuthenticatorUtils.java
@@ -29,10 +29,31 @@
import static org.asynchttpclient.Dsl.realm;
import static org.asynchttpclient.util.MiscUtils.isNonEmpty;
+/**
+ * Utility class for handling HTTP authentication mechanisms.
+ *
+ * This class provides methods to compute and generate authentication headers for various
+ * authentication schemes including Basic, Digest, NTLM, Kerberos, and SPNEGO. It supports
+ * both direct request authentication and proxy authentication.
+ *
+ */
public final class AuthenticatorUtils {
+ /**
+ * The "Negotiate" authentication scheme identifier used for Kerberos and SPNEGO.
+ */
public static final String NEGOTIATE = "Negotiate";
+ /**
+ * Searches for an authentication header that starts with the specified prefix.
+ *
+ * The comparison is case-insensitive.
+ *
+ *
+ * @param authenticateHeaders the list of authentication headers to search
+ * @param prefix the prefix to look for
+ * @return the first header starting with the prefix, or null if not found
+ */
public static String getHeaderWithPrefix(List authenticateHeaders, String prefix) {
if (authenticateHeaders != null) {
for (String authenticateHeader : authenticateHeaders) {
@@ -44,15 +65,44 @@ public static String getHeaderWithPrefix(List authenticateHeaders, Strin
return null;
}
+ /**
+ * Computes the Basic authentication header value from a realm.
+ *
+ * @param realm the realm containing authentication credentials
+ * @return the computed Basic authentication header value, or null if realm is null
+ */
private static String computeBasicAuthentication(Realm realm) {
return realm != null ? computeBasicAuthentication(realm.getPrincipal(), realm.getPassword(), realm.getCharset()) : null;
}
+ /**
+ * Computes the Basic authentication header value from credentials.
+ *
+ * Encodes the principal and password in the format "username:password" using Base64 encoding.
+ *
+ *
+ * @param principal the username
+ * @param password the password
+ * @param charset the character set to use for encoding
+ * @return the computed Basic authentication header value in the format "Basic [base64]"
+ */
private static String computeBasicAuthentication(String principal, String password, Charset charset) {
String s = principal + ":" + password;
return "Basic " + Base64.getEncoder().encodeToString(s.getBytes(charset));
}
+ /**
+ * Computes the URI to use for the realm based on authentication requirements.
+ *
+ * This method constructs the appropriate URI format based on whether an absolute URI
+ * is required and whether the query string should be included.
+ *
+ *
+ * @param uri the request URI
+ * @param useAbsoluteURI whether to use the absolute URI format
+ * @param omitQuery whether to omit the query string
+ * @return the computed realm URI string
+ */
public static String computeRealmURI(Uri uri, boolean useAbsoluteURI, boolean omitQuery) {
if (useAbsoluteURI) {
return omitQuery && MiscUtils.isNonEmpty(uri.getQuery()) ? uri.withNewQuery(null).toUrl() : uri.toUrl();
@@ -62,6 +112,17 @@ public static String computeRealmURI(Uri uri, boolean useAbsoluteURI, boolean om
}
}
+ /**
+ * Computes the Digest authentication header value from a realm.
+ *
+ * Constructs the Digest authentication header according to RFC 2617, including all
+ * required and optional directives such as username, realm, nonce, uri, response,
+ * algorithm, opaque, qop, nc, and cnonce.
+ *
+ *
+ * @param realm the realm containing authentication parameters
+ * @return the computed Digest authentication header value
+ */
private static String computeDigestAuthentication(Realm realm) {
String realmUri = computeRealmURI(realm.getUri(), realm.isUseAbsoluteURI(), realm.isOmitQuery());
@@ -91,6 +152,14 @@ private static String computeDigestAuthentication(Realm realm) {
return new String(StringUtils.charSequence2Bytes(builder, ISO_8859_1));
}
+ /**
+ * Appends a name-value pair to the StringBuilder for Digest authentication.
+ *
+ * @param builder the StringBuilder to append to
+ * @param name the directive name
+ * @param value the directive value
+ * @param quoted whether the value should be quoted
+ */
private static void append(StringBuilder builder, String name, String value, boolean quoted) {
builder.append(name).append('=');
if (quoted)
@@ -101,6 +170,18 @@ private static void append(StringBuilder builder, String name, String value, boo
builder.append(", ");
}
+ /**
+ * Computes the proxy authorization header for connection-level authentication schemes.
+ *
+ * This method handles authentication schemes that require connection-level negotiation,
+ * such as NTLM, Kerberos, and SPNEGO. It generates the Type 1 message for NTLM authentication
+ * if not already present in the request headers.
+ *
+ *
+ * @param request the HTTP request
+ * @param proxyRealm the proxy authentication realm
+ * @return the proxy authorization header value, or null if not applicable
+ */
public static String perConnectionProxyAuthorizationHeader(Request request, Realm proxyRealm) {
String proxyAuthorization = null;
if (proxyRealm != null && proxyRealm.isUsePreemptiveAuth()) {
@@ -122,6 +203,19 @@ public static String perConnectionProxyAuthorizationHeader(Request request, Real
return proxyAuthorization;
}
+ /**
+ * Computes the proxy authorization header for request-level authentication schemes.
+ *
+ * This method handles authentication schemes that can be sent with each request,
+ * such as Basic and Digest authentication. For connection-level schemes (NTLM, Kerberos, SPNEGO),
+ * this method returns null as they are handled by {@link #perConnectionProxyAuthorizationHeader}.
+ *
+ *
+ * @param request the HTTP request
+ * @param proxyRealm the proxy authentication realm
+ * @return the proxy authorization header value, or null if not applicable
+ * @throws IllegalStateException if an invalid authentication scheme is specified
+ */
public static String perRequestProxyAuthorizationHeader(Request request, Realm proxyRealm) {
String proxyAuthorization = null;
@@ -155,6 +249,20 @@ public static String perRequestProxyAuthorizationHeader(Request request, Realm p
return proxyAuthorization;
}
+ /**
+ * Computes the authorization header for connection-level authentication schemes.
+ *
+ * This method handles authentication schemes that require connection-level negotiation,
+ * including NTLM, Kerberos, and SPNEGO. For NTLM, it generates the Type 1 message.
+ * For Kerberos and SPNEGO, it generates the appropriate token using the SPNEGO engine.
+ *
+ *
+ * @param request the HTTP request
+ * @param proxyServer the proxy server, if any
+ * @param realm the authentication realm
+ * @return the authorization header value, or null if not applicable
+ * @throws RuntimeException if SPNEGO token generation fails
+ */
public static String perConnectionAuthorizationHeader(Request request, ProxyServer proxyServer, Realm realm) {
String authorizationHeader = null;
@@ -195,6 +303,19 @@ else if (request.getVirtualHost() != null)
return authorizationHeader;
}
+ /**
+ * Computes the authorization header for request-level authentication schemes.
+ *
+ * This method handles authentication schemes that can be sent with each request,
+ * such as Basic and Digest authentication. For connection-level schemes (NTLM, Kerberos, SPNEGO),
+ * this method returns null as they are handled by {@link #perConnectionAuthorizationHeader}.
+ *
+ *
+ * @param request the HTTP request
+ * @param realm the authentication realm
+ * @return the authorization header value, or null if not applicable
+ * @throws IllegalStateException if an invalid authentication scheme is specified
+ */
public static String perRequestAuthorizationHeader(Request request, Realm realm) {
String authorizationHeader = null;
diff --git a/client/src/main/java/org/asynchttpclient/util/Counted.java b/client/src/main/java/org/asynchttpclient/util/Counted.java
index b8791e2fea..bde23cc340 100644
--- a/client/src/main/java/org/asynchttpclient/util/Counted.java
+++ b/client/src/main/java/org/asynchttpclient/util/Counted.java
@@ -1,23 +1,38 @@
package org.asynchttpclient.util;
/**
- * An interface that defines useful methods to check how many {@linkplain org.asynchttpclient.AsyncHttpClient}
- * instances this particular implementation is shared with.
+ * An interface that defines useful methods to track how many {@linkplain org.asynchttpclient.AsyncHttpClient}
+ * instances share this particular implementation.
+ *
+ * This interface provides thread-safe counter operations for managing shared resources across
+ * multiple client instances. Implementations should ensure atomic operations for increment,
+ * decrement, and count retrieval.
+ *
*/
public interface Counted {
/**
- * Increment counter and return the incremented value
+ * Atomically increments the counter by one and returns the updated value.
+ *
+ * @return the updated counter value after incrementing
*/
int incrementAndGet();
/**
- * Decrement counter and return the decremented value
+ * Atomically decrements the counter by one and returns the updated value.
+ *
+ * @return the updated counter value after decrementing
*/
int decrementAndGet();
/**
- * Return the current counter
+ * Returns the current value of the counter.
+ *
+ * Note: The returned value is a snapshot and may change immediately after this method returns
+ * if other threads are concurrently modifying the counter.
+ *
+ *
+ * @return the current counter value
*/
int count();
}
diff --git a/client/src/main/java/org/asynchttpclient/util/DateUtils.java b/client/src/main/java/org/asynchttpclient/util/DateUtils.java
index b8bf42cd18..389fceca13 100644
--- a/client/src/main/java/org/asynchttpclient/util/DateUtils.java
+++ b/client/src/main/java/org/asynchttpclient/util/DateUtils.java
@@ -13,11 +13,36 @@
*/
package org.asynchttpclient.util;
+/**
+ * Utility class for date and time operations.
+ *
+ * This class provides convenience methods for working with time-related operations
+ * in the async-http-client library.
+ *
+ */
public final class DateUtils {
private DateUtils() {
}
+ /**
+ * Returns the current time in milliseconds.
+ *
+ * This method is a wrapper around {@link System#currentTimeMillis()} and provides
+ * the current time in milliseconds since the Unix epoch (January 1, 1970, 00:00:00 GMT).
+ * The name "unprecise" indicates that the precision may vary depending on the underlying
+ * operating system.
+ *
+ *
+ * Usage Examples:
+ * {@code
+ * long startTime = unpreciseMillisTime();
+ * // perform operation
+ * long elapsed = unpreciseMillisTime() - startTime;
+ * }
+ *
+ * @return the current time in milliseconds
+ */
public static long unpreciseMillisTime() {
return System.currentTimeMillis();
}
diff --git a/client/src/main/java/org/asynchttpclient/util/HttpConstants.java b/client/src/main/java/org/asynchttpclient/util/HttpConstants.java
index e17681e6dd..07fdff1802 100644
--- a/client/src/main/java/org/asynchttpclient/util/HttpConstants.java
+++ b/client/src/main/java/org/asynchttpclient/util/HttpConstants.java
@@ -16,36 +16,131 @@
import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.HttpResponseStatus;
+/**
+ * Constants for HTTP methods and response status codes.
+ *
+ * This class provides convenient access to commonly used HTTP method names and
+ * response status codes as defined in HTTP/1.1 specifications.
+ *
+ */
public final class HttpConstants {
private HttpConstants() {
}
+ /**
+ * HTTP method name constants.
+ *
+ * This class provides string constants for standard HTTP methods as defined in
+ * RFC 7231 and related specifications.
+ *
+ */
public static final class Methods {
+ /**
+ * The CONNECT method establishes a tunnel to the server identified by the target resource.
+ */
public static final String CONNECT = HttpMethod.CONNECT.name();
+
+ /**
+ * The DELETE method deletes the specified resource.
+ */
public static final String DELETE = HttpMethod.DELETE.name();
+
+ /**
+ * The GET method requests a representation of the specified resource.
+ */
public static final String GET = HttpMethod.GET.name();
+
+ /**
+ * The HEAD method asks for a response identical to a GET request, but without the response body.
+ */
public static final String HEAD = HttpMethod.HEAD.name();
+
+ /**
+ * The OPTIONS method describes the communication options for the target resource.
+ */
public static final String OPTIONS = HttpMethod.OPTIONS.name();
+
+ /**
+ * The PATCH method applies partial modifications to a resource.
+ */
public static final String PATCH = HttpMethod.PATCH.name();
+
+ /**
+ * The POST method submits an entity to the specified resource.
+ */
public static final String POST = HttpMethod.POST.name();
+
+ /**
+ * The PUT method replaces all current representations of the target resource with the request payload.
+ */
public static final String PUT = HttpMethod.PUT.name();
+
+ /**
+ * The TRACE method performs a message loop-back test along the path to the target resource.
+ */
public static final String TRACE = HttpMethod.TRACE.name();
private Methods() {
}
}
+ /**
+ * HTTP response status code constants.
+ *
+ * This class provides integer constants for commonly used HTTP response status codes
+ * as defined in RFC 7231 and related specifications.
+ *
+ */
public static final class ResponseStatusCodes {
+ /**
+ * 100 Continue - The server has received the request headers and the client should proceed to send the request body.
+ */
public static final int CONTINUE_100 = HttpResponseStatus.CONTINUE.code();
+
+ /**
+ * 101 Switching Protocols - The server is switching protocols as requested by the client.
+ */
public static final int SWITCHING_PROTOCOLS_101 = HttpResponseStatus.SWITCHING_PROTOCOLS.code();
+
+ /**
+ * 200 OK - The request has succeeded.
+ */
public static final int OK_200 = HttpResponseStatus.OK.code();
+
+ /**
+ * 301 Moved Permanently - The resource has been permanently moved to a new URI.
+ */
public static final int MOVED_PERMANENTLY_301 = HttpResponseStatus.MOVED_PERMANENTLY.code();
+
+ /**
+ * 302 Found - The resource temporarily resides under a different URI.
+ */
public static final int FOUND_302 = HttpResponseStatus.FOUND.code();
+
+ /**
+ * 303 See Other - The response can be found under a different URI using a GET method.
+ */
public static final int SEE_OTHER_303 = HttpResponseStatus.SEE_OTHER.code();
+
+ /**
+ * 307 Temporary Redirect - The resource temporarily resides under a different URI, and the request method should not change.
+ */
public static final int TEMPORARY_REDIRECT_307 = HttpResponseStatus.TEMPORARY_REDIRECT.code();
+
+ /**
+ * 308 Permanent Redirect - The resource has been permanently moved to a new URI, and the request method should not change.
+ */
public static final int PERMANENT_REDIRECT_308 = HttpResponseStatus.PERMANENT_REDIRECT.code();
+
+ /**
+ * 401 Unauthorized - The request requires user authentication.
+ */
public static final int UNAUTHORIZED_401 = HttpResponseStatus.UNAUTHORIZED.code();
+
+ /**
+ * 407 Proxy Authentication Required - The client must first authenticate itself with the proxy.
+ */
public static final int PROXY_AUTHENTICATION_REQUIRED_407 = HttpResponseStatus.PROXY_AUTHENTICATION_REQUIRED.code();
private ResponseStatusCodes() {
diff --git a/client/src/main/java/org/asynchttpclient/util/HttpUtils.java b/client/src/main/java/org/asynchttpclient/util/HttpUtils.java
index 779dba9c78..995ba54494 100644
--- a/client/src/main/java/org/asynchttpclient/util/HttpUtils.java
+++ b/client/src/main/java/org/asynchttpclient/util/HttpUtils.java
@@ -29,12 +29,23 @@
import static java.nio.charset.StandardCharsets.*;
/**
- * {@link org.asynchttpclient.AsyncHttpClient} common utilities.
+ * Common utility methods for HTTP operations.
+ *
+ * This class provides various helper methods for working with HTTP headers, URIs, encoding,
+ * and content types. It includes utilities for URL encoding, multipart boundaries, header
+ * parsing, and form parameter encoding.
+ *
*/
public class HttpUtils {
+ /**
+ * Header value for accepting all content types ("*/*").
+ */
public static final AsciiString ACCEPT_ALL_HEADER_VALUE = new AsciiString("*/*");
+ /**
+ * Header value for gzip and deflate compression encoding.
+ */
public static final AsciiString GZIP_DEFLATE = new AsciiString(HttpHeaderValues.GZIP + "," + HttpHeaderValues.DEFLATE);
private static final String CONTENT_TYPE_CHARSET_ATTRIBUTE = "charset=";
@@ -46,12 +57,32 @@ public class HttpUtils {
private HttpUtils() {
}
+ /**
+ * Computes the Host header value from a URI.
+ *
+ * If the port is the default port for the scheme or is not specified, only the host is returned.
+ * Otherwise, the host and port are returned in the format "host:port".
+ *
+ *
+ * @param uri the URI
+ * @return the Host header value
+ */
public static String hostHeader(Uri uri) {
String host = uri.getHost();
int port = uri.getPort();
return port == -1 || port == uri.getSchemeDefaultPort() ? host : host + ":" + port;
}
+ /**
+ * Computes the Origin header value from a URI.
+ *
+ * The Origin header includes the scheme, host, and port (if non-default).
+ * For secure URIs, "https://" is used; otherwise, "http://".
+ *
+ *
+ * @param uri the URI
+ * @return the Origin header value
+ */
public static String originHeader(Uri uri) {
StringBuilder sb = StringBuilderPool.DEFAULT.stringBuilder();
sb.append(uri.isSecured() ? "https://" : "http://").append(uri.getHost());
@@ -61,15 +92,44 @@ public static String originHeader(Uri uri) {
return sb.toString();
}
+ /**
+ * Extracts the charset attribute from a Content-Type header.
+ *
+ * Parses the Content-Type header and returns the Charset specified in the charset attribute.
+ *
+ *
+ * @param contentType the Content-Type header value
+ * @return the Charset, or null if not found or invalid
+ */
public static Charset extractContentTypeCharsetAttribute(String contentType) {
String charsetName = extractContentTypeAttribute(contentType, CONTENT_TYPE_CHARSET_ATTRIBUTE);
return charsetName != null ? Charset.forName(charsetName) : null;
}
+ /**
+ * Extracts the boundary attribute from a Content-Type header.
+ *
+ * Parses the Content-Type header and returns the boundary value used for multipart content.
+ *
+ *
+ * @param contentType the Content-Type header value
+ * @return the boundary value, or null if not found
+ */
public static String extractContentTypeBoundaryAttribute(String contentType) {
return extractContentTypeAttribute(contentType, CONTENT_TYPE_BOUNDARY_ATTRIBUTE);
}
+ /**
+ * Extracts a specific attribute value from a Content-Type header.
+ *
+ * Searches for the specified attribute name in the Content-Type header and returns
+ * its value, trimming any surrounding quotes or whitespace.
+ *
+ *
+ * @param contentType the Content-Type header value
+ * @param attribute the attribute name to extract (e.g., "charset=", "boundary=")
+ * @return the attribute value, or null if not found
+ */
private static String extractContentTypeAttribute(String contentType, String attribute) {
if (contentType == null) {
return null;
@@ -114,7 +174,15 @@ private static String extractContentTypeAttribute(String contentType, String att
// The pool of ASCII chars to be used for generating a multipart boundary.
private static byte[] MULTIPART_CHARS = "-_1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ".getBytes(US_ASCII);
- // a random size from 30 to 40
+ /**
+ * Computes a random multipart boundary.
+ *
+ * Generates a random boundary string of 30-40 characters using alphanumeric characters,
+ * hyphens, and underscores. This boundary is used to delimit parts in multipart/form-data requests.
+ *
+ *
+ * @return a byte array containing the random boundary
+ */
public static byte[] computeMultipartBoundary() {
ThreadLocalRandom random = ThreadLocalRandom.current();
byte[] bytes = new byte[random.nextInt(11) + 30];
@@ -124,6 +192,17 @@ public static byte[] computeMultipartBoundary() {
return bytes;
}
+ /**
+ * Adds a boundary attribute to a Content-Type header value.
+ *
+ * Appends the boundary attribute to the given Content-Type base value, properly
+ * handling semicolon separators.
+ *
+ *
+ * @param base the base Content-Type value
+ * @param boundary the boundary value to add
+ * @return the complete Content-Type header value with boundary
+ */
public static String patchContentTypeWithBoundaryAttribute(CharSequence base, byte[] boundary) {
StringBuilder sb = StringBuilderPool.DEFAULT.stringBuilder().append(base);
if (base.length() != 0 && base.charAt(base.length() - 1) != ';') {
@@ -132,14 +211,41 @@ public static String patchContentTypeWithBoundaryAttribute(CharSequence base, by
return sb.append(' ').append(CONTENT_TYPE_BOUNDARY_ATTRIBUTE).append(new String(boundary, US_ASCII)).toString();
}
+ /**
+ * Determines whether to follow redirects for a request.
+ *
+ * Returns the request-specific setting if present, otherwise falls back to the client configuration.
+ *
+ *
+ * @param config the client configuration
+ * @param request the request
+ * @return true if redirects should be followed, false otherwise
+ */
public static boolean followRedirect(AsyncHttpClientConfig config, Request request) {
return request.getFollowRedirect() != null ? request.getFollowRedirect() : config.isFollowRedirect();
}
+ /**
+ * URL-encodes form parameters into a ByteBuffer.
+ *
+ * Encodes the list of parameters in application/x-www-form-urlencoded format.
+ *
+ *
+ * @param params the list of parameters to encode
+ * @param charset the character set to use for encoding
+ * @return a ByteBuffer containing the encoded parameters
+ */
public static ByteBuffer urlEncodeFormParams(List params, Charset charset) {
return StringUtils.charSequence2ByteBuffer(urlEncodeFormParams0(params, charset), US_ASCII);
}
+ /**
+ * URL-encodes form parameters into a StringBuilder.
+ *
+ * @param params the list of parameters to encode
+ * @param charset the character set to use for encoding
+ * @return a StringBuilder containing the encoded parameters
+ */
private static StringBuilder urlEncodeFormParams0(List params, Charset charset) {
StringBuilder sb = StringBuilderPool.DEFAULT.stringBuilder();
for (Param param : params) {
@@ -149,6 +255,14 @@ private static StringBuilder urlEncodeFormParams0(List params, Charset ch
return sb;
}
+ /**
+ * Encodes and appends a single form parameter.
+ *
+ * @param sb the StringBuilder to append to
+ * @param name the parameter name
+ * @param value the parameter value (may be null)
+ * @param charset the character set to use for encoding
+ */
private static void encodeAndAppendFormParam(StringBuilder sb, String name, String value, Charset charset) {
encodeAndAppendFormField(sb, name, charset);
if (value != null) {
@@ -158,6 +272,13 @@ private static void encodeAndAppendFormParam(StringBuilder sb, String name, Stri
sb.append('&');
}
+ /**
+ * Encodes and appends a single form field (name or value).
+ *
+ * @param sb the StringBuilder to append to
+ * @param field the field to encode
+ * @param charset the character set to use for encoding
+ */
private static void encodeAndAppendFormField(StringBuilder sb, String field, Charset charset) {
if (charset.equals(UTF_8)) {
Utf8UrlEncoder.encodeAndAppendFormElement(sb, field);
@@ -171,6 +292,15 @@ private static void encodeAndAppendFormField(StringBuilder sb, String field, Cha
}
}
+ /**
+ * Filters out Brotli encoding from an Accept-Encoding header value.
+ *
+ * Removes the ", br" suffix if present, as Brotli compression is not currently supported.
+ *
+ *
+ * @param acceptEncoding the Accept-Encoding header value
+ * @return the filtered Accept-Encoding value
+ */
public static CharSequence filterOutBrotliFromAcceptEncoding(String acceptEncoding) {
// we don't support Brotly ATM
if (acceptEncoding.endsWith(BROTLY_ACCEPT_ENCODING_SUFFIX)) {
diff --git a/client/src/main/java/org/asynchttpclient/util/MessageDigestUtils.java b/client/src/main/java/org/asynchttpclient/util/MessageDigestUtils.java
index 3b5cfb4266..768d60af66 100644
--- a/client/src/main/java/org/asynchttpclient/util/MessageDigestUtils.java
+++ b/client/src/main/java/org/asynchttpclient/util/MessageDigestUtils.java
@@ -16,6 +16,14 @@
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
+/**
+ * Utility class for managing pooled MessageDigest instances.
+ *
+ * This class provides thread-local pools of MessageDigest instances for commonly used
+ * hashing algorithms (MD5 and SHA-1). Using pooled instances improves performance by
+ * avoiding the overhead of repeatedly creating new MessageDigest instances.
+ *
+ */
public final class MessageDigestUtils {
private static final ThreadLocal MD5_MESSAGE_DIGESTS = ThreadLocal.withInitial(() -> {
@@ -34,12 +42,46 @@ public final class MessageDigestUtils {
}
});
+ /**
+ * Returns a thread-local, reset MD5 MessageDigest instance.
+ *
+ * The returned MessageDigest is reset and ready for use. Each thread gets its own
+ * instance, making this method thread-safe without requiring synchronization.
+ *
+ *
+ * Usage Examples:
+ * {@code
+ * MessageDigest md5 = pooledMd5MessageDigest();
+ * md5.update(data);
+ * byte[] hash = md5.digest();
+ * }
+ *
+ * @return a reset MD5 MessageDigest instance
+ * @throws InternalError if MD5 algorithm is not supported on this platform
+ */
public static MessageDigest pooledMd5MessageDigest() {
MessageDigest md = MD5_MESSAGE_DIGESTS.get();
md.reset();
return md;
}
+ /**
+ * Returns a thread-local, reset SHA-1 MessageDigest instance.
+ *
+ * The returned MessageDigest is reset and ready for use. Each thread gets its own
+ * instance, making this method thread-safe without requiring synchronization.
+ *
+ *
+ * Usage Examples:
+ * {@code
+ * MessageDigest sha1 = pooledSha1MessageDigest();
+ * sha1.update(data);
+ * byte[] hash = sha1.digest();
+ * }
+ *
+ * @return a reset SHA-1 MessageDigest instance
+ * @throws InternalError if SHA-1 algorithm is not supported on this platform
+ */
public static MessageDigest pooledSha1MessageDigest() {
MessageDigest md = SHA1_MESSAGE_DIGESTS.get();
md.reset();
diff --git a/client/src/main/java/org/asynchttpclient/util/MiscUtils.java b/client/src/main/java/org/asynchttpclient/util/MiscUtils.java
index 26e98fe9d2..38ca843003 100644
--- a/client/src/main/java/org/asynchttpclient/util/MiscUtils.java
+++ b/client/src/main/java/org/asynchttpclient/util/MiscUtils.java
@@ -17,39 +17,105 @@
import java.util.Collection;
import java.util.Map;
+/**
+ * Miscellaneous utility methods for common operations.
+ *
+ * This class provides helper methods for checking emptiness of various data structures,
+ * handling default values, closing resources, and working with exceptions.
+ *
+ */
public class MiscUtils {
private MiscUtils() {
}
+ /**
+ * Checks if a string is non-empty (not null and has length greater than zero).
+ *
+ * @param string the string to check
+ * @return true if the string is non-empty, false otherwise
+ */
public static boolean isNonEmpty(String string) {
return !isEmpty(string);
}
+ /**
+ * Checks if a string is empty (null or has zero length).
+ *
+ * @param string the string to check
+ * @return true if the string is empty or null, false otherwise
+ */
public static boolean isEmpty(String string) {
return string == null || string.isEmpty();
}
+ /**
+ * Checks if an object array is non-empty (not null and has length greater than zero).
+ *
+ * @param array the array to check
+ * @return true if the array is non-empty, false otherwise
+ */
public static boolean isNonEmpty(Object[] array) {
return array != null && array.length != 0;
}
+ /**
+ * Checks if a byte array is non-empty (not null and has length greater than zero).
+ *
+ * @param array the array to check
+ * @return true if the array is non-empty, false otherwise
+ */
public static boolean isNonEmpty(byte[] array) {
return array != null && array.length != 0;
}
+ /**
+ * Checks if a collection is non-empty (not null and contains at least one element).
+ *
+ * @param collection the collection to check
+ * @return true if the collection is non-empty, false otherwise
+ */
public static boolean isNonEmpty(Collection> collection) {
return collection != null && !collection.isEmpty();
}
+ /**
+ * Checks if a map is non-empty (not null and contains at least one entry).
+ *
+ * @param map the map to check
+ * @return true if the map is non-empty, false otherwise
+ */
public static boolean isNonEmpty(Map, ?> map) {
return map != null && !map.isEmpty();
}
+ /**
+ * Returns the value if non-null, otherwise returns the default value.
+ *
+ * Usage Examples:
+ * {@code
+ * String result = withDefault(optionalValue, "defaultValue");
+ * Integer timeout = withDefault(config.getTimeout(), 30000);
+ * }
+ *
+ * @param the type of the value
+ * @param value the value to check
+ * @param def the default value to return if value is null
+ * @return the value if non-null, otherwise the default value
+ */
public static T withDefault(T value, T def) {
return value == null ? def : value;
}
+ /**
+ * Closes a Closeable resource silently, suppressing any IOException.
+ *
+ * This method is useful for cleanup operations where exceptions should not
+ * interrupt the flow of execution.
+ *
+ *
+ * @param closeable the resource to close (may be null)
+ */
public static void closeSilently(Closeable closeable) {
if (closeable != null)
try {
@@ -59,6 +125,16 @@ public static void closeSilently(Closeable closeable) {
}
}
+ /**
+ * Recursively retrieves the root cause of a throwable.
+ *
+ * Traverses the exception chain to find the deepest cause. If the throwable
+ * has no cause, returns the throwable itself.
+ *
+ *
+ * @param t the throwable to analyze
+ * @return the root cause throwable
+ */
public static Throwable getCause(Throwable t) {
Throwable cause = t.getCause();
return cause != null ? getCause(cause) : t;
diff --git a/client/src/main/java/org/asynchttpclient/util/ProxyUtils.java b/client/src/main/java/org/asynchttpclient/util/ProxyUtils.java
index 11d00c0572..2f1101c6b1 100644
--- a/client/src/main/java/org/asynchttpclient/util/ProxyUtils.java
+++ b/client/src/main/java/org/asynchttpclient/util/ProxyUtils.java
@@ -71,9 +71,16 @@ private ProxyUtils() {
}
/**
- * @param config the global config
- * @param request the request
- * @return the proxy server to be used for this request (can be null)
+ * Determines the proxy server to use for a given request.
+ *
+ * This method first checks if a proxy server is explicitly configured on the request.
+ * If not, it falls back to the proxy server selector configured on the client. The method
+ * also verifies that the determined proxy is not configured to be ignored for the request's host.
+ *
+ *
+ * @param config the global client configuration
+ * @param request the HTTP request
+ * @return the proxy server to be used for this request, or null if no proxy should be used
*/
public static ProxyServer getProxyServer(AsyncHttpClientConfig config, Request request) {
ProxyServer proxyServer = request.getProxyServer();
@@ -126,19 +133,28 @@ public static ProxyServerSelector createProxyServerSelector(Properties propertie
}
/**
- * Get a proxy server selector based on the JDK default proxy selector.
+ * Gets a proxy server selector based on the JDK default proxy selector.
+ *
+ * This method creates a selector that uses Java's default {@link ProxySelector} to
+ * determine proxy settings, which typically respects system proxy properties.
+ *
*
- * @return The proxy server selector.
+ * @return the proxy server selector based on JDK defaults
*/
public static ProxyServerSelector getJdkDefaultProxyServerSelector() {
return createProxyServerSelector(ProxySelector.getDefault());
}
/**
- * Create a proxy server selector based on the passed in JDK proxy selector.
+ * Creates a proxy server selector based on the specified JDK proxy selector.
+ *
+ * This method wraps a {@link ProxySelector} to provide proxy selection functionality
+ * compatible with the async-http-client API. It handles HTTP proxies and properly
+ * manages direct connections.
+ *
*
- * @param proxySelector The proxy selector to use. Must not be null.
- * @return The proxy server selector.
+ * @param proxySelector the JDK proxy selector to use (must not be null)
+ * @return the proxy server selector
*/
private static ProxyServerSelector createProxyServerSelector(final ProxySelector proxySelector) {
return uri -> {
diff --git a/client/src/main/java/org/asynchttpclient/util/StringBuilderPool.java b/client/src/main/java/org/asynchttpclient/util/StringBuilderPool.java
index 69ed426fed..c0244c3ae2 100644
--- a/client/src/main/java/org/asynchttpclient/util/StringBuilderPool.java
+++ b/client/src/main/java/org/asynchttpclient/util/StringBuilderPool.java
@@ -12,16 +12,42 @@
*/
package org.asynchttpclient.util;
+/**
+ * A thread-local pool of StringBuilder instances for efficient string building.
+ *
+ * This class maintains a thread-local pool of StringBuilder instances with an initial
+ * capacity of 512 characters. Using pooled StringBuilders reduces object allocation
+ * overhead for frequently performed string concatenation operations.
+ *
+ */
public class StringBuilderPool {
+ /**
+ * The default, shared StringBuilderPool instance.
+ */
public static final StringBuilderPool DEFAULT = new StringBuilderPool();
private final ThreadLocal pool = ThreadLocal.withInitial(() -> new StringBuilder(512));
/**
- * BEWARE: MUSTN'T APPEND TO ITSELF!
+ * Returns a reset, pooled StringBuilder ready for use.
+ *
+ * The returned StringBuilder has its length reset to zero but retains its underlying
+ * character array capacity. Each thread gets its own instance, making this method
+ * thread-safe without requiring synchronization.
+ *
+ * IMPORTANT: The returned StringBuilder must not be appended to itself,
+ * as it's reused across calls within the same thread.
+ *
+ *
+ * Usage Examples:
+ * {@code
+ * StringBuilder sb = StringBuilderPool.DEFAULT.stringBuilder();
+ * sb.append("hello").append(" ").append("world");
+ * String result = sb.toString();
+ * }
*
- * @return a pooled StringBuilder
+ * @return a reset, pooled StringBuilder
*/
public StringBuilder stringBuilder() {
StringBuilder sb = pool.get();
diff --git a/client/src/main/java/org/asynchttpclient/util/StringUtils.java b/client/src/main/java/org/asynchttpclient/util/StringUtils.java
index ef08f938d9..d4f4fddb3a 100644
--- a/client/src/main/java/org/asynchttpclient/util/StringUtils.java
+++ b/client/src/main/java/org/asynchttpclient/util/StringUtils.java
@@ -16,26 +16,79 @@
import java.nio.CharBuffer;
import java.nio.charset.Charset;
+/**
+ * Utility methods for string and byte conversions.
+ *
+ * This class provides helper methods for converting between CharSequences, ByteBuffers,
+ * and byte arrays, as well as encoding bytes to hexadecimal string representations.
+ *
+ */
public final class StringUtils {
private StringUtils() {
}
+ /**
+ * Converts a CharSequence to a ByteBuffer using the specified charset.
+ *
+ * This method encodes the character sequence into bytes according to the given charset.
+ *
+ *
+ * @param cs the CharSequence to convert
+ * @param charset the charset to use for encoding
+ * @return a ByteBuffer containing the encoded bytes
+ */
public static ByteBuffer charSequence2ByteBuffer(CharSequence cs, Charset charset) {
return charset.encode(CharBuffer.wrap(cs));
}
+ /**
+ * Converts a ByteBuffer to a byte array.
+ *
+ * This method extracts the remaining bytes from the ByteBuffer into a new byte array.
+ * The ByteBuffer's position is advanced by the number of bytes read.
+ *
+ *
+ * @param bb the ByteBuffer to convert
+ * @return a byte array containing the remaining bytes
+ */
public static byte[] byteBuffer2ByteArray(ByteBuffer bb) {
byte[] rawBase = new byte[bb.remaining()];
bb.get(rawBase);
return rawBase;
}
+ /**
+ * Converts a CharSequence to a byte array using the specified charset.
+ *
+ * This is a convenience method that combines {@link #charSequence2ByteBuffer(CharSequence, Charset)}
+ * and {@link #byteBuffer2ByteArray(ByteBuffer)}.
+ *
+ *
+ * @param sb the CharSequence to convert
+ * @param charset the charset to use for encoding
+ * @return a byte array containing the encoded bytes
+ */
public static byte[] charSequence2Bytes(CharSequence sb, Charset charset) {
ByteBuffer bb = charSequence2ByteBuffer(sb, charset);
return byteBuffer2ByteArray(bb);
}
+ /**
+ * Converts a byte array to a hexadecimal string representation.
+ *
+ * Each byte is represented as two hexadecimal digits (lowercase).
+ *
+ *
+ * Usage Examples:
+ * {@code
+ * byte[] data = {0x1A, 0x2B, 0x3C};
+ * String hex = toHexString(data); // Returns "1a2b3c"
+ * }
+ *
+ * @param data the byte array to convert
+ * @return a hexadecimal string representation
+ */
public static String toHexString(byte[] data) {
StringBuilder buffer = StringBuilderPool.DEFAULT.stringBuilder();
for (byte aData : data) {
@@ -45,6 +98,16 @@ public static String toHexString(byte[] data) {
return buffer.toString();
}
+ /**
+ * Appends the base-16 (hexadecimal) representation of bytes to a StringBuilder.
+ *
+ * Each byte is represented as two hexadecimal digits (lowercase). This method
+ * modifies the provided StringBuilder in place.
+ *
+ *
+ * @param buf the StringBuilder to append to
+ * @param bytes the byte array to encode
+ */
public static void appendBase16(StringBuilder buf, byte[] bytes) {
int base = 16;
for (byte b : bytes) {
diff --git a/client/src/main/java/org/asynchttpclient/util/ThrowableUtil.java b/client/src/main/java/org/asynchttpclient/util/ThrowableUtil.java
index dd586658ae..740b579147 100644
--- a/client/src/main/java/org/asynchttpclient/util/ThrowableUtil.java
+++ b/client/src/main/java/org/asynchttpclient/util/ThrowableUtil.java
@@ -12,17 +12,45 @@
*/
package org.asynchttpclient.util;
+/**
+ * Utility methods for working with Throwable instances.
+ *
+ * This class provides helper methods for manipulating exceptions and their stack traces,
+ * particularly useful for performance optimization in high-throughput scenarios.
+ *
+ */
public final class ThrowableUtil {
private ThrowableUtil() {
}
/**
+ * Replaces the stack trace of a throwable with a minimal, synthetic stack trace.
+ *
+ * This method is useful for pre-allocated or frequently thrown exceptions where
+ * the overhead of capturing a full stack trace is undesirable. The resulting
+ * throwable has a single stack trace element indicating the caller class and method.
+ *
+ *
+ * Note: Use this method with caution, as it removes valuable debugging information.
+ * It's primarily intended for performance-critical paths where exceptions are expected
+ * and stack traces are not needed.
+ *
+ *
+ * Usage Examples:
+ * {@code
+ * IOException exception = unknownStackTrace(
+ * new IOException("Connection closed"),
+ * MyClass.class,
+ * "processRequest"
+ * );
+ * }
+ *
* @param the Throwable type
- * @param t the throwable whose stacktrace we want to remove
- * @param clazz the caller class
- * @param method the caller method
- * @return the input throwable with removed stacktrace
+ * @param t the throwable whose stack trace should be replaced
+ * @param clazz the caller class to record in the synthetic stack trace
+ * @param method the caller method to record in the synthetic stack trace
+ * @return the input throwable with a minimal synthetic stack trace
*/
public static T unknownStackTrace(T t, Class> clazz, String method) {
t.setStackTrace(new StackTraceElement[]{new StackTraceElement(clazz.getName(), method, null, -1)});
diff --git a/client/src/main/java/org/asynchttpclient/util/UriEncoder.java b/client/src/main/java/org/asynchttpclient/util/UriEncoder.java
index 4349d0d316..c73fb9d170 100644
--- a/client/src/main/java/org/asynchttpclient/util/UriEncoder.java
+++ b/client/src/main/java/org/asynchttpclient/util/UriEncoder.java
@@ -20,9 +20,35 @@
import static org.asynchttpclient.util.MiscUtils.isNonEmpty;
import static org.asynchttpclient.util.Utf8UrlEncoder.encodeAndAppendQuery;
+/**
+ * Enumeration of URI encoding strategies for HTTP requests.
+ *
+ * This enum provides two strategies for encoding URIs:
+ *
+ *
+ * {@link #FIXING} - Properly encodes paths and query parameters according to RFC 3986
+ * {@link #RAW} - Leaves URIs unencoded, passing them through as-is
+ *
+ *
+ * The appropriate encoder can be selected using {@link #uriEncoder(boolean)}.
+ *
+ */
public enum UriEncoder {
+ /**
+ * Encoder that properly encodes paths and query parameters according to RFC 3986.
+ *
+ * This encoder ensures that URI components are properly percent-encoded, making them
+ * safe for transmission in HTTP requests.
+ *
+ */
FIXING {
+ /**
+ * Encodes a URI path according to RFC 3986.
+ *
+ * @param path the path to encode
+ * @return the encoded path
+ */
public String encodePath(String path) {
return Utf8UrlEncoder.encodePath(path);
}
@@ -67,7 +93,21 @@ protected String withoutQueryWithParams(final List queryParams) {
}
},
+ /**
+ * Encoder that leaves URI components unencoded.
+ *
+ * This encoder passes paths and query parameters through without any encoding.
+ * Use with caution, as it assumes the URI is already properly encoded or does not
+ * contain characters that require encoding.
+ *
+ */
RAW {
+ /**
+ * Returns the path without encoding.
+ *
+ * @param path the path
+ * @return the unmodified path
+ */
public String encodePath(String path) {
return path;
}
@@ -107,24 +147,74 @@ protected String withoutQueryWithParams(final List queryParams) {
}
};
+ /**
+ * Returns the appropriate URI encoder based on the encoding preference.
+ *
+ * @param disableUrlEncoding true to disable URL encoding (returns {@link #RAW}),
+ * false to enable encoding (returns {@link #FIXING})
+ * @return the appropriate URI encoder
+ */
public static UriEncoder uriEncoder(boolean disableUrlEncoding) {
return disableUrlEncoding ? RAW : FIXING;
}
+ /**
+ * Encodes a query string with additional query parameters.
+ *
+ * @param query the existing query string
+ * @param queryParams additional query parameters to append
+ * @return the complete encoded query string
+ */
protected abstract String withQueryWithParams(final String query, final List queryParams);
+ /**
+ * Encodes a query string without additional parameters.
+ *
+ * @param query the query string to encode
+ * @return the encoded query string
+ */
protected abstract String withQueryWithoutParams(final String query);
+ /**
+ * Encodes query parameters when there is no existing query string.
+ *
+ * @param queryParams the query parameters to encode
+ * @return the encoded query string
+ */
protected abstract String withoutQueryWithParams(final List queryParams);
+ /**
+ * Encodes a query string, optionally merging with additional query parameters.
+ *
+ * @param query the existing query string
+ * @param queryParams additional query parameters
+ * @return the complete encoded query string
+ */
private String withQuery(final String query, final List queryParams) {
return isNonEmpty(queryParams) ? withQueryWithParams(query, queryParams) : withQueryWithoutParams(query);
}
+ /**
+ * Encodes query parameters when there is no existing query string.
+ *
+ * @param queryParams the query parameters
+ * @return the encoded query string, or null if no parameters
+ */
private String withoutQuery(final List queryParams) {
return isNonEmpty(queryParams) ? withoutQueryWithParams(queryParams) : null;
}
+ /**
+ * Encodes a URI with optional additional query parameters.
+ *
+ * This method encodes the path and merges any additional query parameters with
+ * the existing query string, if present.
+ *
+ *
+ * @param uri the URI to encode
+ * @param queryParams additional query parameters to merge
+ * @return a new URI with encoded components
+ */
public Uri encode(Uri uri, List queryParams) {
String newPath = encodePath(uri.getPath());
String newQuery = encodeQuery(uri.getQuery(), queryParams);
@@ -137,8 +227,21 @@ public Uri encode(Uri uri, List queryParams) {
uri.getFragment());
}
+ /**
+ * Encodes a URI path.
+ *
+ * @param path the path to encode
+ * @return the encoded path
+ */
protected abstract String encodePath(String path);
+ /**
+ * Encodes a query string with optional additional parameters.
+ *
+ * @param query the existing query string
+ * @param queryParams additional query parameters
+ * @return the encoded query string
+ */
private String encodeQuery(final String query, final List queryParams) {
return isNonEmpty(query) ? withQuery(query, queryParams) : withoutQuery(queryParams);
}
diff --git a/client/src/main/java/org/asynchttpclient/util/Utf8UrlEncoder.java b/client/src/main/java/org/asynchttpclient/util/Utf8UrlEncoder.java
index cdac64e11d..20ca70d6a0 100644
--- a/client/src/main/java/org/asynchttpclient/util/Utf8UrlEncoder.java
+++ b/client/src/main/java/org/asynchttpclient/util/Utf8UrlEncoder.java
@@ -15,6 +15,23 @@
import java.util.BitSet;
+/**
+ * UTF-8 URL encoder that follows RFC 3986 specifications.
+ *
+ * This class provides methods to encode various URI components (path, query, form elements)
+ * according to RFC 3986 and HTML5 form encoding specifications. It efficiently handles
+ * UTF-8 multi-byte characters and uses predefined character sets to determine which
+ * characters should be percent-encoded.
+ *
+ *
+ * The encoder supports different encoding modes for different URI components:
+ *
+ *
+ * Path encoding - for URI paths
+ * Query encoding - for URI query strings
+ * Form encoding - for application/x-www-form-urlencoded data
+ *
+ */
public final class Utf8UrlEncoder {
// see http://tools.ietf.org/html/rfc3986#section-3.4
@@ -119,29 +136,88 @@ public final class Utf8UrlEncoder {
private Utf8UrlEncoder() {
}
+ /**
+ * Encodes a URI path according to RFC 3986.
+ *
+ * This method percent-encodes characters that are not allowed in URI paths,
+ * while leaving valid path characters (including '/' separators) unencoded.
+ * If no encoding is needed, returns the original input string.
+ *
+ *
+ * @param input the path to encode
+ * @return the encoded path
+ */
public static String encodePath(String input) {
StringBuilder sb = lazyAppendEncoded(null, input, BUILT_PATH_UNTOUCHED_CHARS, false);
return sb == null ? input : sb.toString();
}
+ /**
+ * Encodes and appends a query string to a StringBuilder.
+ *
+ * This method encodes the query string according to RFC 3986, preserving
+ * valid query characters including '?' and '&'.
+ *
+ *
+ * @param sb the StringBuilder to append to
+ * @param query the query string to encode
+ * @return the StringBuilder with the encoded query appended
+ */
public static StringBuilder encodeAndAppendQuery(StringBuilder sb, String query) {
return appendEncoded(sb, query, BUILT_QUERY_UNTOUCHED_CHARS, false);
}
+ /**
+ * Encodes a query parameter element (name or value).
+ *
+ * This method encodes the input according to form URL encoding rules,
+ * percent-encoding special characters but not spaces (which are encoded as '+').
+ *
+ *
+ * @param input the query element to encode
+ * @return the encoded query element
+ */
public static String encodeQueryElement(String input) {
StringBuilder sb = new StringBuilder(input.length() + 6);
encodeAndAppendQueryElement(sb, input);
return sb.toString();
}
+ /**
+ * Encodes and appends a query parameter element to a StringBuilder.
+ *
+ * @param sb the StringBuilder to append to
+ * @param input the query element to encode
+ * @return the StringBuilder with the encoded element appended
+ */
public static StringBuilder encodeAndAppendQueryElement(StringBuilder sb, CharSequence input) {
return appendEncoded(sb, input, FORM_URL_ENCODED_SAFE_CHARS, false);
}
+ /**
+ * Encodes and appends a form parameter element to a StringBuilder.
+ *
+ * This method uses HTML5 form encoding rules where spaces are encoded as '+'.
+ *
+ *
+ * @param sb the StringBuilder to append to
+ * @param input the form element to encode
+ * @return the StringBuilder with the encoded element appended
+ */
public static StringBuilder encodeAndAppendFormElement(StringBuilder sb, CharSequence input) {
return appendEncoded(sb, input, FORM_URL_ENCODED_SAFE_CHARS, true);
}
+ /**
+ * Percent-encodes a query element using only RFC 3986 unreserved characters.
+ *
+ * This is a stricter encoding that encodes all characters except alphanumerics
+ * and the unreserved set (- . _ ~).
+ *
+ *
+ * @param input the string to encode
+ * @return the percent-encoded string, or null if input is null
+ */
public static String percentEncodeQueryElement(String input) {
if (input == null) {
return null;
@@ -151,6 +227,13 @@ public static String percentEncodeQueryElement(String input) {
return sb.toString();
}
+ /**
+ * Encodes and appends a percent-encoded string to a StringBuilder.
+ *
+ * @param sb the StringBuilder to append to
+ * @param input the string to encode
+ * @return the StringBuilder with the encoded string appended
+ */
public static StringBuilder encodeAndAppendPercentEncoded(StringBuilder sb, CharSequence input) {
return appendEncoded(sb, input, RFC3986_UNRESERVED_CHARS, false);
}
diff --git a/client/src/main/java/org/asynchttpclient/webdav/WebDavResponse.java b/client/src/main/java/org/asynchttpclient/webdav/WebDavResponse.java
index b5c4e23ec5..ac40365169 100644
--- a/client/src/main/java/org/asynchttpclient/webdav/WebDavResponse.java
+++ b/client/src/main/java/org/asynchttpclient/webdav/WebDavResponse.java
@@ -25,13 +25,36 @@
import java.util.List;
/**
- * Customized {@link Response} which add support for getting the response's body as an XML document (@link WebDavResponse#getBodyAsXML}
+ * Customized {@link Response} that adds support for getting the response body as an XML document.
+ *
+ * This response wrapper is specifically designed for WebDAV operations, which typically return
+ * XML responses (especially for multi-status 207 responses). It provides access to the parsed
+ * XML document alongside all standard HTTP response properties.
+ *
+ *
+ * Usage Examples:
+ * {@code
+ * WebDavCompletionHandlerBase handler = new WebDavCompletionHandlerBase() {
+ * @Override
+ * public WebDavResponse onCompleted(WebDavResponse response) {
+ * Document doc = response.getBodyAsXML();
+ * // Process XML document
+ * return response;
+ * }
+ * };
+ * }
*/
public class WebDavResponse implements Response {
private final Response response;
private final Document document;
+ /**
+ * Creates a new WebDAV response wrapping an HTTP response and its parsed XML document.
+ *
+ * @param response the underlying HTTP response
+ * @param document the parsed XML document from the response body (can be null)
+ */
WebDavResponse(Response response, Document document) {
this.response = response;
this.document = document;
@@ -114,6 +137,16 @@ public SocketAddress getLocalAddress() {
return response.getLocalAddress();
}
+ /**
+ * Returns the response body as a parsed XML document.
+ *
+ * This method provides access to the DOM document parsed from the response body.
+ * For WebDAV multi-status responses (HTTP 207), this contains the structured XML
+ * data describing the results of the operation.
+ *
+ *
+ * @return the parsed XML document, or null if the response didn't contain XML
+ */
public Document getBodyAsXML() {
return document;
}
diff --git a/client/src/main/java/org/asynchttpclient/ws/WebSocket.java b/client/src/main/java/org/asynchttpclient/ws/WebSocket.java
index 7b64468ba4..58defc96b5 100644
--- a/client/src/main/java/org/asynchttpclient/ws/WebSocket.java
+++ b/client/src/main/java/org/asynchttpclient/ws/WebSocket.java
@@ -20,12 +20,45 @@
import java.net.SocketAddress;
/**
- * A WebSocket client
+ * A WebSocket client interface for sending and receiving WebSocket frames.
+ *
+ * This interface provides methods for sending various types of WebSocket frames
+ * (text, binary, ping, pong, close) and managing WebSocket listeners. It supports
+ * both simple and advanced use cases, including message fragmentation and WebSocket
+ * extensions through RSV bits.
+ *
+ *
+ * Usage Examples:
+ * {@code
+ * // Simple text message
+ * websocket.sendTextFrame("Hello, WebSocket!");
+ *
+ * // Binary message
+ * byte[] data = {1, 2, 3, 4};
+ * websocket.sendBinaryFrame(data);
+ *
+ * // Fragmented message (3 parts)
+ * websocket.sendTextFrame("First part ", false, 0);
+ * websocket.sendContinuationFrame("middle part ", false, 0);
+ * websocket.sendContinuationFrame("last part", true, 0);
+ *
+ * // Ping/Pong for keepalive
+ * websocket.sendPingFrame();
+ *
+ * // Close connection
+ * websocket.sendCloseFrame(1000, "Normal closure");
+ * }
*/
public interface WebSocket {
/**
- * @return the headers received in the Upgrade response
+ * Returns the HTTP headers received in the WebSocket upgrade response.
+ *
+ * These headers may include server information, protocol negotiation results,
+ * and custom headers sent by the server during the handshake.
+ *
+ *
+ * @return the headers received in the upgrade response
*/
HttpHeaders getUpgradeHeaders();
diff --git a/client/src/main/java/org/asynchttpclient/ws/WebSocketListener.java b/client/src/main/java/org/asynchttpclient/ws/WebSocketListener.java
index 3b37e74c57..ab863511ba 100644
--- a/client/src/main/java/org/asynchttpclient/ws/WebSocketListener.java
+++ b/client/src/main/java/org/asynchttpclient/ws/WebSocketListener.java
@@ -13,31 +13,76 @@
package org.asynchttpclient.ws;
/**
- * A generic {@link WebSocketListener} for WebSocket events. Use the appropriate listener for receiving message bytes.
+ * Listener interface for WebSocket events and frame notifications.
+ *
+ * This interface provides callbacks for WebSocket lifecycle events (open, close, error)
+ * and frame-level notifications (text, binary, ping, pong). Implementations can selectively
+ * override the frame methods they're interested in, as they provide default no-op implementations.
+ *
+ *
+ * Usage Examples:
+ * {@code
+ * WebSocketListener listener = new WebSocketListener() {
+ * @Override
+ * public void onOpen(WebSocket websocket) {
+ * System.out.println("WebSocket opened");
+ * websocket.sendTextFrame("Hello");
+ * }
+ *
+ * @Override
+ * public void onTextFrame(String payload, boolean finalFragment, int rsv) {
+ * System.out.println("Received: " + payload);
+ * }
+ *
+ * @Override
+ * public void onClose(WebSocket websocket, int code, String reason) {
+ * System.out.println("WebSocket closed: " + code + " " + reason);
+ * }
+ *
+ * @Override
+ * public void onError(Throwable t) {
+ * System.err.println("WebSocket error: " + t.getMessage());
+ * }
+ * };
+ * }
*/
public interface WebSocketListener {
/**
- * Invoked when the {@link WebSocket} is open.
+ * Invoked when the {@link WebSocket} connection is successfully established.
+ *
+ * This callback is called after the WebSocket handshake completes successfully.
+ * The connection is now ready to send and receive frames.
+ *
*
- * @param websocket the WebSocket
+ * @param websocket the WebSocket instance that was opened
*/
void onOpen(WebSocket websocket);
/**
- * Invoked when the {@link WebSocket} is closed.
+ * Invoked when the {@link WebSocket} connection is closed.
+ *
+ * This callback is triggered when a close frame is received or the connection
+ * is terminated. The status code and reason provide information about why
+ * the connection was closed.
+ *
*
- * @param websocket the WebSocket
- * @param code the status code
- * @param reason the reason message
- * @see "http://tools.ietf.org/html/rfc6455#section-5.5.1"
+ * @param websocket the WebSocket instance that was closed
+ * @param code the status code indicating the reason for closure (e.g., 1000 for normal closure)
+ * @param reason a textual description of the closure reason (may be empty)
+ * @see RFC 6455 Section 5.5.1
*/
void onClose(WebSocket websocket, int code, String reason);
/**
- * Invoked when the {@link WebSocket} crashes.
+ * Invoked when an error occurs on the {@link WebSocket} connection.
+ *
+ * This callback is triggered when an exception occurs during WebSocket
+ * communication, such as network errors, protocol violations, or application-level
+ * exceptions.
+ *
*
- * @param t a {@link Throwable}
+ * @param t the Throwable that caused the error
*/
void onError(Throwable t);
diff --git a/client/src/main/java/org/asynchttpclient/ws/WebSocketUpgradeHandler.java b/client/src/main/java/org/asynchttpclient/ws/WebSocketUpgradeHandler.java
index a4624d6336..3fcb9722de 100644
--- a/client/src/main/java/org/asynchttpclient/ws/WebSocketUpgradeHandler.java
+++ b/client/src/main/java/org/asynchttpclient/ws/WebSocketUpgradeHandler.java
@@ -25,35 +25,149 @@
import java.util.List;
/**
- * An {@link AsyncHandler} which is able to execute WebSocket upgrade. Use the Builder for configuring WebSocket options.
+ * An {@link AsyncHandler} that handles WebSocket protocol upgrade and connection management.
+ *
+ * This handler manages the HTTP-to-WebSocket upgrade process by:
+ *
+ *
+ * Validating the HTTP 101 Switching Protocols response
+ * Processing the upgrade headers
+ * Establishing the WebSocket connection
+ * Notifying registered listeners of connection events
+ *
+ *
+ * Subclasses can override the protected hook methods (*0) to add custom behavior
+ * at various stages of the upgrade process.
+ *
+ *
+ * Usage Examples:
+ * {@code
+ * WebSocketUpgradeHandler handler = new WebSocketUpgradeHandler.Builder()
+ * .addWebSocketListener(new WebSocketListener() {
+ * @Override
+ * public void onOpen(WebSocket websocket) {
+ * websocket.sendTextFrame("Hello!");
+ * }
+ *
+ * @Override
+ * public void onTextFrame(String payload, boolean finalFragment, int rsv) {
+ * System.out.println("Received: " + payload);
+ * }
+ *
+ * @Override
+ * public void onClose(WebSocket websocket, int code, String reason) {
+ * System.out.println("Connection closed");
+ * }
+ *
+ * @Override
+ * public void onError(Throwable t) {
+ * System.err.println("Error: " + t.getMessage());
+ * }
+ * })
+ * .build();
+ *
+ * WebSocket websocket = asyncHttpClient
+ * .prepareGet("ws://echo.websocket.org")
+ * .execute(handler)
+ * .get();
+ * }
*/
public class WebSocketUpgradeHandler implements AsyncHandler {
private final List listeners;
private NettyWebSocket webSocket;
+ /**
+ * Creates a new WebSocket upgrade handler with the specified listeners.
+ *
+ * @param listeners the list of WebSocket listeners to notify of events
+ */
public WebSocketUpgradeHandler(List listeners) {
this.listeners = listeners;
}
+ /**
+ * Extension point called when the WebSocket instance is set.
+ *
+ * Subclasses can override this method to perform additional initialization
+ * or configuration when the WebSocket connection is established.
+ *
+ *
+ * @param webSocket the WebSocket instance
+ */
protected void setWebSocket0(NettyWebSocket webSocket) {
}
+ /**
+ * Extension point called when the HTTP response status is received.
+ *
+ * Subclasses can override this method to inspect or validate the status
+ * before the upgrade proceeds.
+ *
+ *
+ * @param responseStatus the HTTP response status
+ * @throws Exception if an error occurs during status processing
+ */
protected void onStatusReceived0(HttpResponseStatus responseStatus) throws Exception {
}
+ /**
+ * Extension point called when the HTTP response headers are received.
+ *
+ * Subclasses can override this method to inspect or validate the upgrade
+ * headers before the connection is established.
+ *
+ *
+ * @param headers the HTTP response headers
+ * @throws Exception if an error occurs during header processing
+ */
protected void onHeadersReceived0(HttpHeaders headers) throws Exception {
}
+ /**
+ * Extension point called when the HTTP response body part is received.
+ *
+ * Subclasses can override this method to process any response body data
+ * received during the upgrade (though typically there isn't any).
+ *
+ *
+ * @param bodyPart the HTTP response body part
+ * @throws Exception if an error occurs during body part processing
+ */
protected void onBodyPartReceived0(HttpResponseBodyPart bodyPart) throws Exception {
}
+ /**
+ * Extension point called when the upgrade completes successfully.
+ *
+ * Subclasses can override this method to perform actions after the
+ * WebSocket connection is fully established.
+ *
+ *
+ * @throws Exception if an error occurs during completion processing
+ */
protected void onCompleted0() throws Exception {
}
+ /**
+ * Extension point called when an error occurs during the upgrade.
+ *
+ * Subclasses can override this method to add custom error handling
+ * logic before listeners are notified.
+ *
+ *
+ * @param t the Throwable that caused the error
+ */
protected void onThrowable0(Throwable t) {
}
+ /**
+ * Extension point called when the WebSocket connection is opened.
+ *
+ * Subclasses can override this method to perform actions when the
+ * connection is ready to send and receive frames.
+ *
+ */
protected void onOpen0() {
}
diff --git a/client/src/main/java/org/asynchttpclient/ws/WebSocketUtils.java b/client/src/main/java/org/asynchttpclient/ws/WebSocketUtils.java
index 31bd8f5c2a..f2699d7fac 100644
--- a/client/src/main/java/org/asynchttpclient/ws/WebSocketUtils.java
+++ b/client/src/main/java/org/asynchttpclient/ws/WebSocketUtils.java
@@ -20,9 +20,30 @@
import static java.nio.charset.StandardCharsets.US_ASCII;
import static org.asynchttpclient.util.MessageDigestUtils.pooledSha1MessageDigest;
+/**
+ * Utility class for WebSocket protocol operations.
+ *
+ * This class provides helper methods for WebSocket handshake key generation and validation
+ * according to RFC 6455 (The WebSocket Protocol). It handles the cryptographic operations
+ * required for the WebSocket opening handshake.
+ *
+ */
public final class WebSocketUtils {
+ /**
+ * Magic GUID defined in RFC 6455 for WebSocket handshake accept key calculation.
+ */
private static final String MAGIC_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
+ /**
+ * Generates a random WebSocket key for the client handshake.
+ *
+ * This method creates a Base64-encoded random 16-byte value to be sent in the
+ * Sec-WebSocket-Key header during the WebSocket opening handshake. The server
+ * will use this key to generate the Sec-WebSocket-Accept response header.
+ *
+ *
+ * @return a Base64-encoded random key suitable for WebSocket handshake
+ */
public static String getWebSocketKey() {
byte[] nonce = new byte[16];
ThreadLocalRandom random = ThreadLocalRandom.current();
@@ -32,6 +53,21 @@ public static String getWebSocketKey() {
return Base64.getEncoder().encodeToString(nonce);
}
+ /**
+ * Computes the WebSocket accept key from a client key.
+ *
+ * This method implements the algorithm defined in RFC 6455 for computing the
+ * Sec-WebSocket-Accept header value. It concatenates the client key with the
+ * magic GUID, computes SHA-1 hash, and Base64-encodes the result.
+ *
+ *
+ * This is used by clients to validate server responses and by servers to
+ * generate the accept header.
+ *
+ *
+ * @param key the Sec-WebSocket-Key value from the client handshake
+ * @return the Base64-encoded accept key for the Sec-WebSocket-Accept header
+ */
public static String getAcceptKey(String key) {
return Base64.getEncoder().encodeToString(pooledSha1MessageDigest().digest(
(key + MAGIC_GUID).getBytes(US_ASCII)));
diff --git a/extras/guava/src/main/java/org/asynchttpclient/extras/guava/ListenableFutureAdapter.java b/extras/guava/src/main/java/org/asynchttpclient/extras/guava/ListenableFutureAdapter.java
index 5138a224e9..760f0ac1ac 100644
--- a/extras/guava/src/main/java/org/asynchttpclient/extras/guava/ListenableFutureAdapter.java
+++ b/extras/guava/src/main/java/org/asynchttpclient/extras/guava/ListenableFutureAdapter.java
@@ -19,12 +19,41 @@
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
+/**
+ * Adapter utility to convert AsyncHttpClient's ListenableFuture to Guava's ListenableFuture.
+ *
+ * This adapter allows seamless integration between AsyncHttpClient and Guava's utilities
+ * for working with futures and callbacks.
+ */
public final class ListenableFutureAdapter {
/**
- * @param future an AHC ListenableFuture
- * @param the Future's value type
- * @return a Guava ListenableFuture
+ * Converts an AsyncHttpClient ListenableFuture to a Guava ListenableFuture.
+ *
+ * The returned future delegates all operations to the original AsyncHttpClient future,
+ * preserving cancellation, execution, and listener behavior.
+ *
+ *
Usage Examples:
+ * {@code
+ * AsyncHttpClient client = asyncHttpClient();
+ * ListenableFuture ahcFuture = client.prepareGet("http://www.example.com").execute();
+ * com.google.common.util.concurrent.ListenableFuture guavaFuture =
+ * ListenableFutureAdapter.asGuavaFuture(ahcFuture);
+ *
+ * // Now you can use Guava utilities
+ * Futures.addCallback(guavaFuture, new FutureCallback() {
+ * public void onSuccess(Response response) {
+ * System.out.println("Status: " + response.getStatusCode());
+ * }
+ * public void onFailure(Throwable thrown) {
+ * System.err.println("Request failed: " + thrown);
+ * }
+ * }, MoreExecutors.directExecutor());
+ * }
+ *
+ * @param future an AsyncHttpClient ListenableFuture to adapt
+ * @param the type of the future's result value
+ * @return a Guava ListenableFuture that delegates to the provided future
*/
public static com.google.common.util.concurrent.ListenableFuture asGuavaFuture(final ListenableFuture future) {
diff --git a/extras/guava/src/main/java/org/asynchttpclient/extras/guava/RateLimitedThrottleRequestFilter.java b/extras/guava/src/main/java/org/asynchttpclient/extras/guava/RateLimitedThrottleRequestFilter.java
index 6d74a08dbe..708e885c99 100644
--- a/extras/guava/src/main/java/org/asynchttpclient/extras/guava/RateLimitedThrottleRequestFilter.java
+++ b/extras/guava/src/main/java/org/asynchttpclient/extras/guava/RateLimitedThrottleRequestFilter.java
@@ -16,6 +16,21 @@
* The maxWaitMs argument is respected across both permit acquisitions. For
* example, if 1000 ms is given, and the filter spends 500 ms waiting for a connection,
* it will only spend another 500 ms waiting for the rate limiter.
+ *
+ * Usage Examples:
+ * {@code
+ * // Limit to 10 concurrent connections and 5 requests per second
+ * RateLimitedThrottleRequestFilter filter =
+ * new RateLimitedThrottleRequestFilter(10, 5.0);
+ *
+ * AsyncHttpClient client = asyncHttpClient(config()
+ * .addRequestFilter(filter)
+ * .build());
+ *
+ * // With custom max wait time of 2 seconds
+ * RateLimitedThrottleRequestFilter customFilter =
+ * new RateLimitedThrottleRequestFilter(10, 5.0, 2000);
+ * }
*/
public class RateLimitedThrottleRequestFilter implements RequestFilter {
private final static Logger logger = LoggerFactory.getLogger(RateLimitedThrottleRequestFilter.class);
@@ -23,10 +38,29 @@ public class RateLimitedThrottleRequestFilter implements RequestFilter {
private final int maxWaitMs;
private final RateLimiter rateLimiter;
+ /**
+ * Creates a rate-limited throttle filter with default maximum wait time.
+ *
+ * The maximum wait time defaults to {@link Integer#MAX_VALUE}, effectively
+ * allowing requests to wait indefinitely for permits.
+ *
+ * @param maxConnections the maximum number of concurrent connections allowed
+ * @param rateLimitPerSecond the maximum rate of requests per second
+ */
public RateLimitedThrottleRequestFilter(int maxConnections, double rateLimitPerSecond) {
this(maxConnections, rateLimitPerSecond, Integer.MAX_VALUE);
}
+ /**
+ * Creates a rate-limited throttle filter with a specified maximum wait time.
+ *
+ * This filter enforces both connection concurrency limits and rate limiting.
+ * The maxWaitMs applies to the total time spent waiting for both permits.
+ *
+ * @param maxConnections the maximum number of concurrent connections allowed
+ * @param rateLimitPerSecond the maximum rate of requests per second
+ * @param maxWaitMs the maximum time in milliseconds to wait for permits
+ */
public RateLimitedThrottleRequestFilter(int maxConnections, double rateLimitPerSecond, int maxWaitMs) {
this.maxWaitMs = maxWaitMs;
this.rateLimiter = RateLimiter.create(rateLimitPerSecond);
@@ -34,7 +68,17 @@ public RateLimitedThrottleRequestFilter(int maxConnections, double rateLimitPerS
}
/**
- * {@inheritDoc}
+ * Filters the request by applying both concurrency and rate limiting constraints.
+ *
+ * This method attempts to acquire both a connection permit and a rate limiter permit
+ * before allowing the request to proceed. If either acquisition fails within the
+ * configured maximum wait time, a {@link FilterException} is thrown.
+ *
+ * @param ctx the filter context containing the request and async handler
+ * @param the type of the async handler's result
+ * @return a new filter context with a handler that releases the connection permit on completion
+ * @throws FilterException if the request cannot acquire necessary permits within the maximum wait time
+ * or if the thread is interrupted while waiting
*/
@Override
public FilterContext filter(FilterContext ctx) throws FilterException {
diff --git a/extras/registry/src/main/java/org/asynchttpclient/extras/registry/AsyncHttpClientRegistry.java b/extras/registry/src/main/java/org/asynchttpclient/extras/registry/AsyncHttpClientRegistry.java
index 99279b52e4..b2bdfea3d5 100644
--- a/extras/registry/src/main/java/org/asynchttpclient/extras/registry/AsyncHttpClientRegistry.java
+++ b/extras/registry/src/main/java/org/asynchttpclient/extras/registry/AsyncHttpClientRegistry.java
@@ -16,13 +16,36 @@
import java.util.Set;
+/**
+ * A registry for managing named AsyncHttpClient instances.
+ *
+ * This interface provides a centralized repository for storing and retrieving
+ * AsyncHttpClient instances by name, enabling sharing of client instances
+ * across an application and preventing resource leaks.
+ *
+ *
Usage Examples:
+ * {@code
+ * AsyncHttpClientRegistry registry = AsyncHttpClientRegistryImpl.getInstance();
+ *
+ * // Register a client
+ * AsyncHttpClient client = asyncHttpClient();
+ * registry.addOrReplace("myClient", client);
+ *
+ * // Retrieve it later
+ * AsyncHttpClient retrieved = registry.get("myClient");
+ *
+ * // Unregister when done
+ * registry.unregister("myClient");
+ * client.close();
+ * }
+ */
public interface AsyncHttpClientRegistry {
/**
- * Returns back the AsyncHttpClient associated with this name
+ * Retrieves the AsyncHttpClient associated with the specified name.
*
* @param name the name of the client instance in the registry
- * @return the client
+ * @return the client instance, or null if no client is registered with this name
*/
AsyncHttpClient get(String name);
diff --git a/extras/rxjava/src/main/java/org/asynchttpclient/extras/rxjava/AsyncHttpObservable.java b/extras/rxjava/src/main/java/org/asynchttpclient/extras/rxjava/AsyncHttpObservable.java
index 6fb1713f7e..5f03f53423 100644
--- a/extras/rxjava/src/main/java/org/asynchttpclient/extras/rxjava/AsyncHttpObservable.java
+++ b/extras/rxjava/src/main/java/org/asynchttpclient/extras/rxjava/AsyncHttpObservable.java
@@ -21,17 +21,45 @@
import rx.subjects.ReplaySubject;
/**
- * Provide RxJava support for executing requests. Request can be subscribed to and manipulated as needed.
+ * Provides RxJava support for executing HTTP requests as Observables.
+ *
+ * This class enables integration between AsyncHttpClient and RxJava, allowing
+ * HTTP requests to be observed and manipulated using reactive programming patterns.
+ *
+ *
Usage Examples:
+ * {@code
+ * AsyncHttpClient client = asyncHttpClient();
+ *
+ * // Cold Observable - executes when subscribed
+ * Observable coldObservable = AsyncHttpObservable.toObservable(
+ * () -> client.prepareGet("http://www.example.com")
+ * );
+ * coldObservable.subscribe(response -> {
+ * System.out.println("Status: " + response.getStatusCode());
+ * });
+ *
+ * // Hot Observable - executes immediately
+ * Observable hotObservable = AsyncHttpObservable.observe(
+ * () -> client.prepareGet("http://www.example.com")
+ * );
+ * // Request starts executing immediately, even before subscription
+ * hotObservable.subscribe(response -> {
+ * System.out.println("Status: " + response.getStatusCode());
+ * });
+ * }
*
* @see https://github.com/ReactiveX/RxJava
*/
public class AsyncHttpObservable {
/**
- * Observe a request execution and emit the response to the observer.
+ * Creates a cold Observable that executes the HTTP request when subscribed.
+ *
+ * Each subscription triggers a new HTTP request execution. The request is not
+ * executed until the Observable is subscribed to.
*
- * @param supplier the supplier
- * @return The cold observable (must be subscribed to in order to execute).
+ * @param supplier a function that provides the BoundRequestBuilder for the HTTP request
+ * @return a cold Observable that emits the HTTP response when subscribed
*/
public static Observable toObservable(final Func0 supplier) {
@@ -68,10 +96,14 @@ public void onThrowable(Throwable t) {
}
/**
- * Observe a request execution and emit the response to the observer.
+ * Creates a hot Observable that executes the HTTP request immediately.
+ *
+ * The request begins execution as soon as this method is called, regardless of
+ * whether any observers have subscribed. Multiple subscribers will receive the
+ * same response via a ReplaySubject.
*
- * @param supplier teh supplier
- * @return The hot observable (eagerly executes).
+ * @param supplier a function that provides the BoundRequestBuilder for the HTTP request
+ * @return a hot Observable that emits the HTTP response (request executes eagerly)
*/
public static Observable observe(final Func0 supplier) {
//use a ReplaySubject to buffer the eagerly subscribed-to Observable
diff --git a/extras/rxjava/src/main/java/org/asynchttpclient/extras/rxjava/UnsubscribedException.java b/extras/rxjava/src/main/java/org/asynchttpclient/extras/rxjava/UnsubscribedException.java
index eeae82f281..7d97f64b03 100644
--- a/extras/rxjava/src/main/java/org/asynchttpclient/extras/rxjava/UnsubscribedException.java
+++ b/extras/rxjava/src/main/java/org/asynchttpclient/extras/rxjava/UnsubscribedException.java
@@ -15,14 +15,25 @@
import java.util.concurrent.CancellationException;
/**
- * Indicates that an {@code Observer} unsubscribed during the processing of a HTTP request.
+ * Indicates that an {@code Observer} unsubscribed during the processing of an HTTP request.
+ *
+ * This exception is used to signal early termination of HTTP request processing when
+ * the RxJava subscriber unsubscribes before the request completes.
*/
@SuppressWarnings("serial")
public class UnsubscribedException extends CancellationException {
+ /**
+ * Creates a new UnsubscribedException with no detail message.
+ */
public UnsubscribedException() {
}
+ /**
+ * Creates a new UnsubscribedException with the specified cause.
+ *
+ * @param cause the underlying cause of this exception
+ */
public UnsubscribedException(final Throwable cause) {
initCause(cause);
}
diff --git a/extras/rxjava/src/main/java/org/asynchttpclient/extras/rxjava/single/AbstractProgressSingleSubscriberBridge.java b/extras/rxjava/src/main/java/org/asynchttpclient/extras/rxjava/single/AbstractProgressSingleSubscriberBridge.java
index bea48961ef..9ee8aa26cc 100644
--- a/extras/rxjava/src/main/java/org/asynchttpclient/extras/rxjava/single/AbstractProgressSingleSubscriberBridge.java
+++ b/extras/rxjava/src/main/java/org/asynchttpclient/extras/rxjava/single/AbstractProgressSingleSubscriberBridge.java
@@ -15,27 +15,70 @@
import org.asynchttpclient.handler.ProgressAsyncHandler;
import rx.SingleSubscriber;
+/**
+ * Abstract bridge implementation that adapts ProgressAsyncHandler to RxJava SingleSubscriber.
+ *
+ * This bridge extends the base subscriber bridge to support progress tracking callbacks
+ * for content write operations during HTTP request execution.
+ */
abstract class AbstractProgressSingleSubscriberBridge extends AbstractSingleSubscriberBridge implements ProgressAsyncHandler {
+ /**
+ * Creates a new progress-aware subscriber bridge.
+ *
+ * @param subscriber the RxJava SingleSubscriber to bridge to
+ */
protected AbstractProgressSingleSubscriberBridge(SingleSubscriber subscriber) {
super(subscriber);
}
+ /**
+ * Called when request headers have been written.
+ *
+ * Aborts the request if the subscriber has unsubscribed, otherwise delegates
+ * to the underlying handler.
+ *
+ * @return the handler state indicating whether to continue or abort
+ */
@Override
public State onHeadersWritten() {
return subscriber.isUnsubscribed() ? abort() : delegate().onHeadersWritten();
}
+ /**
+ * Called when request content has been written.
+ *
+ * Aborts the request if the subscriber has unsubscribed, otherwise delegates
+ * to the underlying handler.
+ *
+ * @return the handler state indicating whether to continue or abort
+ */
@Override
public State onContentWritten() {
return subscriber.isUnsubscribed() ? abort() : delegate().onContentWritten();
}
+ /**
+ * Called to report progress of content write operations.
+ *
+ * Aborts the request if the subscriber has unsubscribed, otherwise delegates
+ * to the underlying handler.
+ *
+ * @param amount the amount of bytes written in this update
+ * @param current the total number of bytes written so far
+ * @param total the total number of bytes to write
+ * @return the handler state indicating whether to continue or abort
+ */
@Override
public State onContentWriteProgress(long amount, long current, long total) {
return subscriber.isUnsubscribed() ? abort() : delegate().onContentWriteProgress(amount, current, total);
}
+ /**
+ * Returns the underlying ProgressAsyncHandler to which operations are delegated.
+ *
+ * @return the delegate handler
+ */
@Override
protected abstract ProgressAsyncHandler extends T> delegate();
diff --git a/extras/rxjava/src/main/java/org/asynchttpclient/extras/rxjava/single/AbstractSingleSubscriberBridge.java b/extras/rxjava/src/main/java/org/asynchttpclient/extras/rxjava/single/AbstractSingleSubscriberBridge.java
index bb6749feed..cddef3c1ba 100644
--- a/extras/rxjava/src/main/java/org/asynchttpclient/extras/rxjava/single/AbstractSingleSubscriberBridge.java
+++ b/extras/rxjava/src/main/java/org/asynchttpclient/extras/rxjava/single/AbstractSingleSubscriberBridge.java
@@ -28,6 +28,12 @@
import static java.util.Objects.requireNonNull;
+/**
+ * Abstract bridge implementation that adapts AsyncHandler to RxJava SingleSubscriber.
+ *
+ * This bridge handles the integration between AsyncHttpClient's callback-based AsyncHandler
+ * and RxJava's subscriber model, managing state transitions and error handling.
+ */
abstract class AbstractSingleSubscriberBridge implements AsyncHandler {
private static final Logger LOGGER = LoggerFactory.getLogger(AbstractSingleSubscriberBridge.class);
@@ -36,30 +42,83 @@ abstract class AbstractSingleSubscriberBridge implements AsyncHandler {
private final AtomicBoolean delegateTerminated = new AtomicBoolean();
+ /**
+ * Creates a new subscriber bridge.
+ *
+ * @param subscriber the RxJava SingleSubscriber to bridge to
+ */
protected AbstractSingleSubscriberBridge(SingleSubscriber subscriber) {
this.subscriber = requireNonNull(subscriber);
}
+ /**
+ * Called when a response body part is received.
+ *
+ * Aborts the request if the subscriber has unsubscribed, otherwise delegates
+ * to the underlying handler.
+ *
+ * @param content the received body part
+ * @return the handler state indicating whether to continue or abort
+ * @throws Exception if an error occurs during processing
+ */
@Override
public State onBodyPartReceived(HttpResponseBodyPart content) throws Exception {
return subscriber.isUnsubscribed() ? abort() : delegate().onBodyPartReceived(content);
}
+ /**
+ * Called when the response status is received.
+ *
+ * Aborts the request if the subscriber has unsubscribed, otherwise delegates
+ * to the underlying handler.
+ *
+ * @param status the received response status
+ * @return the handler state indicating whether to continue or abort
+ * @throws Exception if an error occurs during processing
+ */
@Override
public State onStatusReceived(HttpResponseStatus status) throws Exception {
return subscriber.isUnsubscribed() ? abort() : delegate().onStatusReceived(status);
}
+ /**
+ * Called when response headers are received.
+ *
+ * Aborts the request if the subscriber has unsubscribed, otherwise delegates
+ * to the underlying handler.
+ *
+ * @param headers the received headers
+ * @return the handler state indicating whether to continue or abort
+ * @throws Exception if an error occurs during processing
+ */
@Override
public State onHeadersReceived(HttpHeaders headers) throws Exception {
return subscriber.isUnsubscribed() ? abort() : delegate().onHeadersReceived(headers);
}
+ /**
+ * Called when trailing headers are received.
+ *
+ * Aborts the request if the subscriber has unsubscribed, otherwise delegates
+ * to the underlying handler.
+ *
+ * @param headers the received trailing headers
+ * @return the handler state indicating whether to continue or abort
+ * @throws Exception if an error occurs during processing
+ */
@Override
public State onTrailingHeadersReceived(HttpHeaders headers) throws Exception {
return subscriber.isUnsubscribed() ? abort() : delegate().onTrailingHeadersReceived(headers);
}
+ /**
+ * Called when the request completes successfully.
+ *
+ * Delegates to the underlying handler to produce the result, then emits
+ * it to the subscriber via onSuccess unless the subscriber has unsubscribed.
+ *
+ * @return null (Void return type)
+ */
@Override
public Void onCompleted() {
if (delegateTerminated.getAndSet(true)) {
@@ -81,6 +140,15 @@ public Void onCompleted() {
return null;
}
+ /**
+ * Called when an error occurs during request processing.
+ *
+ * Delegates to the underlying handler, then emits the error to the subscriber.
+ * If the delegate throws an exception, both exceptions are combined into a
+ * CompositeException.
+ *
+ * @param t the error that occurred
+ */
@Override
public void onThrowable(Throwable t) {
if (delegateTerminated.getAndSet(true)) {
@@ -97,6 +165,14 @@ public void onThrowable(Throwable t) {
emitOnError(error);
}
+ /**
+ * Aborts the request and notifies the delegate handler.
+ *
+ * This method is called when the subscriber unsubscribes. It ensures the
+ * delegate handler receives an UnsubscribedException for cleanup purposes.
+ *
+ * @return ABORT state to signal request termination
+ */
protected AsyncHandler.State abort() {
if (!delegateTerminated.getAndSet(true)) {
// send a terminal event to the delegate
@@ -107,6 +183,11 @@ protected AsyncHandler.State abort() {
return State.ABORT;
}
+ /**
+ * Returns the underlying AsyncHandler to which operations are delegated.
+ *
+ * @return the delegate handler
+ */
protected abstract AsyncHandler extends T> delegate();
private void emitOnError(Throwable error) {
diff --git a/extras/rxjava/src/main/java/org/asynchttpclient/extras/rxjava/single/AsyncHttpSingle.java b/extras/rxjava/src/main/java/org/asynchttpclient/extras/rxjava/single/AsyncHttpSingle.java
index e52fbcf89c..4f9ff08d43 100644
--- a/extras/rxjava/src/main/java/org/asynchttpclient/extras/rxjava/single/AsyncHttpSingle.java
+++ b/extras/rxjava/src/main/java/org/asynchttpclient/extras/rxjava/single/AsyncHttpSingle.java
@@ -29,6 +29,33 @@
/**
* Wraps HTTP requests into RxJava {@code Single} instances.
+ *
+ * This utility class provides methods to create RxJava Singles from AsyncHttpClient
+ * requests, enabling reactive programming patterns for HTTP operations.
+ *
+ *
Usage Examples:
+ * {@code
+ * AsyncHttpClient client = asyncHttpClient();
+ *
+ * // Simple Single from BoundRequestBuilder
+ * Single single = AsyncHttpSingle.create(
+ * client.prepareGet("http://www.example.com")
+ * );
+ * single.subscribe(
+ * response -> System.out.println("Status: " + response.getStatusCode()),
+ * error -> System.err.println("Error: " + error)
+ * );
+ *
+ * // Single with custom AsyncHandler
+ * Single customSingle = AsyncHttpSingle.create(
+ * client.prepareGet("http://www.example.com"),
+ * () -> new AsyncCompletionHandler() {
+ * public String onCompleted(Response response) {
+ * return response.getResponseBody();
+ * }
+ * }
+ * );
+ * }
*
* @see https://github.com/
* ReactiveX/RxJava
@@ -40,12 +67,13 @@ private AsyncHttpSingle() {
}
/**
- * Emits the responses to HTTP requests obtained from {@code builder}.
+ * Creates a Single that emits the HTTP response from the given request builder.
+ *
+ * The request is executed when the Single is subscribed to, using a default
+ * AsyncCompletionHandlerBase to handle the response.
*
- * @param builder used to build the HTTP request that is to be executed
- * @return a {@code Single} that executes new requests on subscription
- * obtained from {@code builder} on subscription and that emits the
- * response
+ * @param builder the request builder used to execute the HTTP request
+ * @return a Single that executes the request on subscription and emits the response
* @throws NullPointerException if {@code builder} is {@code null}
*/
public static Single create(BoundRequestBuilder builder) {
@@ -54,17 +82,19 @@ public static Single create(BoundRequestBuilder builder) {
}
/**
- * Emits the responses to HTTP requests obtained by calling
- * {@code requestTemplate}.
+ * Creates a Single that emits HTTP responses using a request template function.
+ *
+ * This overload provides more control over request execution by accepting a function
+ * that receives an AsyncHandler and returns a Future. The Future is used for
+ * cancellation support when the Single is unsubscribed.
*
- * @param requestTemplate called to start the HTTP request with an
- * {@code AysncHandler} that builds the HTTP response and
- * propagates results to the returned {@code Single}. The
- * {@code Future} that is returned by {@code requestTemplate}
- * will be used to cancel the request when the {@code Single} is
- * unsubscribed.
- * @return a {@code Single} that executes new requests on subscription by
- * calling {@code requestTemplate} and that emits the response
+ * @param requestTemplate a function called to start the HTTP request with an
+ * AsyncHandler that builds the HTTP response and
+ * propagates results to the returned Single. The
+ * Future returned by this function is used to cancel
+ * the request when the Single is unsubscribed
+ * @return a Single that executes new requests on subscription by
+ * calling the request template and emits the response
* @throws NullPointerException if {@code requestTemplate} is {@code null}
*/
public static Single create(Func1 super AsyncHandler>, ? extends Future>> requestTemplate) {
@@ -72,17 +102,19 @@ public static Single create(Func1 super AsyncHandler>, ? extends F
}
/**
- * Emits the results of {@code AsyncHandlers} obtained from
- * {@code handlerSupplier} for HTTP requests obtained from {@code builder}.
+ * Creates a Single that emits results from a custom AsyncHandler.
+ *
+ * This method allows you to provide a custom handler supplier that produces
+ * AsyncHandler instances for processing the HTTP response. The handler's result
+ * type determines the Single's emission type.
*
- * @param builder used to build the HTTP request that is to be executed
- * @param handlerSupplier supplies the desired {@code AsyncHandler}
- * instances that are used to produce results
- * @return a {@code Single} that executes new requests on subscription
- * obtained from {@code builder} and that emits the result of the
- * {@code AsyncHandler} obtained from {@code handlerSupplier}
- * @throws NullPointerException if at least one of the parameters is
- * {@code null}
+ * @param builder the request builder used to execute the HTTP request
+ * @param handlerSupplier a function that supplies AsyncHandler instances
+ * to process the HTTP response and produce results
+ * @param the type of result produced by the AsyncHandler
+ * @return a Single that executes the request on subscription and emits
+ * the result produced by the supplied AsyncHandler
+ * @throws NullPointerException if at least one of the parameters is {@code null}
*/
public static Single create(BoundRequestBuilder builder, Func0 extends AsyncHandler extends T>> handlerSupplier) {
requireNonNull(builder);
@@ -90,24 +122,25 @@ public static Single create(BoundRequestBuilder builder, Func0 extends
}
/**
- * Emits the results of {@code AsyncHandlers} obtained from
- * {@code handlerSupplier} for HTTP requests obtained obtained by calling
- * {@code requestTemplate}.
+ * Creates a Single using both a request template and a custom handler supplier.
+ *
+ * This is the most flexible creation method, allowing full control over both
+ * request execution and response handling. The request template provides control
+ * over how the request is executed and cancelled, while the handler supplier
+ * determines how the response is processed.
*
- * @param requestTemplate called to start the HTTP request with an
- * {@code AysncHandler} that builds the HTTP response and
- * propagates results to the returned {@code Single}. The
- * {@code Future} that is returned by {@code requestTemplate}
- * will be used to cancel the request when the {@code Single} is
- * unsubscribed.
- * @param handlerSupplier supplies the desired {@code AsyncHandler}
- * instances that are used to produce results
- * @return a {@code Single} that executes new requests on subscription by
- * calling {@code requestTemplate} and that emits the results
- * produced by the {@code AsyncHandlers} supplied by
- * {@code handlerSupplier}
- * @throws NullPointerException if at least one of the parameters is
- * {@code null}
+ * @param requestTemplate a function called to start the HTTP request with an
+ * AsyncHandler that builds the HTTP response and
+ * propagates results to the returned Single. The
+ * Future returned by this function is used to cancel
+ * the request when the Single is unsubscribed
+ * @param handlerSupplier a function that supplies AsyncHandler instances
+ * to process the HTTP response and produce results
+ * @param the type of result produced by the AsyncHandler
+ * @return a Single that executes new requests on subscription by
+ * calling the request template and emits the results produced by
+ * the supplied AsyncHandler
+ * @throws NullPointerException if at least one of the parameters is {@code null}
*/
public static Single create(Func1 super AsyncHandler>, ? extends Future>> requestTemplate,
Func0 extends AsyncHandler extends T>> handlerSupplier) {
diff --git a/extras/rxjava/src/main/java/org/asynchttpclient/extras/rxjava/single/AsyncSingleSubscriberBridge.java b/extras/rxjava/src/main/java/org/asynchttpclient/extras/rxjava/single/AsyncSingleSubscriberBridge.java
index ccef13e9dc..dd6ee8ab8e 100644
--- a/extras/rxjava/src/main/java/org/asynchttpclient/extras/rxjava/single/AsyncSingleSubscriberBridge.java
+++ b/extras/rxjava/src/main/java/org/asynchttpclient/extras/rxjava/single/AsyncSingleSubscriberBridge.java
@@ -17,15 +17,32 @@
import static java.util.Objects.requireNonNull;
+/**
+ * Concrete bridge implementation for standard AsyncHandlers.
+ *
+ * This bridge adapts a regular AsyncHandler to work with RxJava Singles,
+ * without progress tracking support.
+ */
final class AsyncSingleSubscriberBridge extends AbstractSingleSubscriberBridge {
private final AsyncHandler extends T> delegate;
+ /**
+ * Creates a new async subscriber bridge.
+ *
+ * @param subscriber the RxJava SingleSubscriber to bridge to
+ * @param delegate the AsyncHandler to delegate operations to
+ */
public AsyncSingleSubscriberBridge(SingleSubscriber subscriber, AsyncHandler extends T> delegate) {
super(subscriber);
this.delegate = requireNonNull(delegate);
}
+ /**
+ * Returns the underlying AsyncHandler delegate.
+ *
+ * @return the delegate handler
+ */
@Override
protected AsyncHandler extends T> delegate() {
return delegate;
diff --git a/extras/rxjava/src/main/java/org/asynchttpclient/extras/rxjava/single/ProgressAsyncSingleSubscriberBridge.java b/extras/rxjava/src/main/java/org/asynchttpclient/extras/rxjava/single/ProgressAsyncSingleSubscriberBridge.java
index 3a1ffda2cd..4d5fb81936 100644
--- a/extras/rxjava/src/main/java/org/asynchttpclient/extras/rxjava/single/ProgressAsyncSingleSubscriberBridge.java
+++ b/extras/rxjava/src/main/java/org/asynchttpclient/extras/rxjava/single/ProgressAsyncSingleSubscriberBridge.java
@@ -17,15 +17,32 @@
import static java.util.Objects.requireNonNull;
+/**
+ * Concrete bridge implementation for ProgressAsyncHandlers.
+ *
+ * This bridge adapts a ProgressAsyncHandler to work with RxJava Singles,
+ * providing progress tracking support for content write operations.
+ */
final class ProgressAsyncSingleSubscriberBridge extends AbstractProgressSingleSubscriberBridge {
private final ProgressAsyncHandler extends T> delegate;
+ /**
+ * Creates a new progress-aware subscriber bridge.
+ *
+ * @param subscriber the RxJava SingleSubscriber to bridge to
+ * @param delegate the ProgressAsyncHandler to delegate operations to
+ */
public ProgressAsyncSingleSubscriberBridge(SingleSubscriber subscriber, ProgressAsyncHandler extends T> delegate) {
super(subscriber);
this.delegate = requireNonNull(delegate);
}
+ /**
+ * Returns the underlying ProgressAsyncHandler delegate.
+ *
+ * @return the delegate handler
+ */
@Override
protected ProgressAsyncHandler extends T> delegate() {
return delegate;
diff --git a/extras/rxjava2/src/main/java/org/asynchttpclient/extras/rxjava2/DefaultRxHttpClient.java b/extras/rxjava2/src/main/java/org/asynchttpclient/extras/rxjava2/DefaultRxHttpClient.java
index d582e3f9b4..5cad1a5f26 100644
--- a/extras/rxjava2/src/main/java/org/asynchttpclient/extras/rxjava2/DefaultRxHttpClient.java
+++ b/extras/rxjava2/src/main/java/org/asynchttpclient/extras/rxjava2/DefaultRxHttpClient.java
@@ -29,22 +29,63 @@
import static java.util.Objects.requireNonNull;
/**
- * Straight forward default implementation of the {@code RxHttpClient} interface.
+ * Default implementation of the {@code RxHttpClient} interface.
+ *
+ * This class provides RxJava 2 integration for AsyncHttpClient by wrapping
+ * HTTP requests in {@code Maybe} instances, supporting both standard and
+ * progress-aware async handlers.
+ *
+ *
Usage Examples:
+ * {@code
+ * AsyncHttpClient client = asyncHttpClient();
+ * RxHttpClient rxClient = new DefaultRxHttpClient(client);
+ *
+ * // Simple request returning Response
+ * Request request = get("http://www.example.com").build();
+ * Maybe maybe = rxClient.prepare(request);
+ * maybe.subscribe(
+ * response -> System.out.println("Status: " + response.getStatusCode()),
+ * error -> System.err.println("Error: " + error),
+ * () -> System.out.println("Completed with no result")
+ * );
+ *
+ * // Custom handler for specific result type
+ * Maybe bodyMaybe = rxClient.prepare(
+ * request,
+ * () -> new AsyncCompletionHandler() {
+ * public String onCompleted(Response response) {
+ * return response.getResponseBody();
+ * }
+ * }
+ * );
+ * }
*/
public class DefaultRxHttpClient implements RxHttpClient {
private final AsyncHttpClient asyncHttpClient;
/**
- * Returns a new {@code DefaultRxHttpClient} instance that uses the given {@code asyncHttpClient} under the hoods.
+ * Creates a new DefaultRxHttpClient that delegates to the given AsyncHttpClient.
*
- * @param asyncHttpClient the Async HTTP Client instance to be used
+ * @param asyncHttpClient the Async HTTP Client instance to be used for executing requests
* @throws NullPointerException if {@code asyncHttpClient} is {@code null}
*/
public DefaultRxHttpClient(AsyncHttpClient asyncHttpClient) {
this.asyncHttpClient = requireNonNull(asyncHttpClient);
}
+ /**
+ * Prepares a request for execution as a RxJava 2 Maybe.
+ *
+ * The request is executed when the Maybe is subscribed to, and the result
+ * is produced by the handler supplied by the handlerSupplier.
+ *
+ * @param request the HTTP request to execute
+ * @param handlerSupplier a supplier that provides the AsyncHandler for processing the response
+ * @param the type of result produced by the handler and emitted by the Maybe
+ * @return a Maybe that executes the request on subscription and emits the handler's result
+ * @throws NullPointerException if {@code request} or {@code handlerSupplier} is {@code null}
+ */
@Override
public Maybe prepare(Request request, Supplier extends AsyncHandler> handlerSupplier) {
requireNonNull(request);
diff --git a/extras/rxjava2/src/main/java/org/asynchttpclient/extras/rxjava2/DisposedException.java b/extras/rxjava2/src/main/java/org/asynchttpclient/extras/rxjava2/DisposedException.java
index dfaaf2cf81..ad42e096ff 100644
--- a/extras/rxjava2/src/main/java/org/asynchttpclient/extras/rxjava2/DisposedException.java
+++ b/extras/rxjava2/src/main/java/org/asynchttpclient/extras/rxjava2/DisposedException.java
@@ -16,11 +16,19 @@
import java.util.concurrent.CancellationException;
/**
- * Indicates that the HTTP request has been disposed asynchronously via RxJava.
+ * Indicates that the HTTP request has been disposed asynchronously via RxJava 2.
+ *
+ * This exception is used to signal early termination of HTTP request processing when
+ * the RxJava 2 observer disposes of the subscription before the request completes.
*/
public class DisposedException extends CancellationException {
private static final long serialVersionUID = -5885577182105850384L;
+ /**
+ * Creates a new DisposedException with the specified detail message.
+ *
+ * @param message the detail message explaining the disposal
+ */
public DisposedException(String message) {
super(message);
}
diff --git a/extras/rxjava2/src/main/java/org/asynchttpclient/extras/rxjava2/RxHttpClient.java b/extras/rxjava2/src/main/java/org/asynchttpclient/extras/rxjava2/RxHttpClient.java
index 9b60aed759..f505a12b16 100644
--- a/extras/rxjava2/src/main/java/org/asynchttpclient/extras/rxjava2/RxHttpClient.java
+++ b/extras/rxjava2/src/main/java/org/asynchttpclient/extras/rxjava2/RxHttpClient.java
@@ -20,16 +20,41 @@
/**
* Prepares HTTP requests by wrapping them into RxJava 2 {@code Maybe} instances.
+ *
+ * This interface provides a reactive API for executing HTTP requests using RxJava 2's
+ * Maybe type, which can emit zero or one result. Each subscription to the returned
+ * Maybe triggers a new HTTP request execution.
+ *
+ *
Usage Examples:
+ * {@code
+ * // Create an RxHttpClient
+ * AsyncHttpClient client = asyncHttpClient();
+ * RxHttpClient rxClient = RxHttpClient.create(client);
+ *
+ * // Simple GET request
+ * Request request = get("http://www.example.com").build();
+ * rxClient.prepare(request)
+ * .subscribe(
+ * response -> System.out.println("Status: " + response.getStatusCode()),
+ * error -> System.err.println("Failed: " + error)
+ * );
+ *
+ * // Chain with RxJava operators
+ * rxClient.prepare(request)
+ * .map(Response::getResponseBody)
+ * .filter(body -> body.contains("success"))
+ * .subscribe(body -> System.out.println("Body: " + body));
+ * }
*
* @see RxJava – Reactive Extensions for the JVM
*/
public interface RxHttpClient {
/**
- * Returns a new {@code RxHttpClient} instance that uses the given {@code asyncHttpClient} under the hoods.
+ * Creates a new RxHttpClient instance that delegates to the given AsyncHttpClient.
*
- * @param asyncHttpClient the Async HTTP Client instance to be used
- * @return a new {@code RxHttpClient} instance
+ * @param asyncHttpClient the Async HTTP Client instance to be used for executing requests
+ * @return a new RxHttpClient instance
* @throws NullPointerException if {@code asyncHttpClient} is {@code null}
*/
static RxHttpClient create(AsyncHttpClient asyncHttpClient) {
@@ -37,11 +62,13 @@ static RxHttpClient create(AsyncHttpClient asyncHttpClient) {
}
/**
- * Prepares the given {@code request}. For each subscription to the returned {@code Maybe}, a new HTTP request will
- * be executed and its response will be emitted.
+ * Prepares a request for execution, returning a Maybe that emits the response.
+ *
+ * Each subscription to the returned Maybe triggers a new HTTP request execution.
+ * The response is processed using a default AsyncCompletionHandlerBase.
*
- * @param request the request that is to be executed
- * @return a {@code Maybe} that executes {@code request} upon subscription and emits the response
+ * @param request the HTTP request to execute
+ * @return a Maybe that executes the request upon subscription and emits the response
* @throws NullPointerException if {@code request} is {@code null}
*/
default Maybe prepare(Request request) {
@@ -49,15 +76,16 @@ default Maybe prepare(Request request) {
}
/**
- * Prepares the given {@code request}. For each subscription to the returned {@code Maybe}, a new HTTP request will
- * be executed and the results of {@code AsyncHandlers} obtained from {@code handlerSupplier} will be emitted.
+ * Prepares a request for execution with a custom handler supplier.
+ *
+ * Each subscription to the returned Maybe triggers a new HTTP request execution.
+ * The response is processed by an AsyncHandler obtained from the handlerSupplier,
+ * and the handler's result is emitted by the Maybe.
*
- * @param the result type produced by handlers produced by {@code handlerSupplier} and emitted by the returned
- * {@code Maybe} instance
- * @param request the request that is to be executed
- * @param handlerSupplier supplies the desired {@code AsyncHandler} instances that are used to produce results
- * @return a {@code Maybe} that executes {@code request} upon subscription and that emits the results produced by
- * the supplied handlers
+ * @param the result type produced by the handler and emitted by the Maybe
+ * @param request the HTTP request to execute
+ * @param handlerSupplier a supplier that provides AsyncHandler instances for processing responses
+ * @return a Maybe that executes the request upon subscription and emits the handler's result
* @throws NullPointerException if at least one of the parameters is {@code null}
*/
Maybe prepare(Request request, Supplier extends AsyncHandler> handlerSupplier);
diff --git a/extras/rxjava2/src/main/java/org/asynchttpclient/extras/rxjava2/maybe/AbstractMaybeProgressAsyncHandlerBridge.java b/extras/rxjava2/src/main/java/org/asynchttpclient/extras/rxjava2/maybe/AbstractMaybeProgressAsyncHandlerBridge.java
index db41d1b8b5..f44d0aae47 100644
--- a/extras/rxjava2/src/main/java/org/asynchttpclient/extras/rxjava2/maybe/AbstractMaybeProgressAsyncHandlerBridge.java
+++ b/extras/rxjava2/src/main/java/org/asynchttpclient/extras/rxjava2/maybe/AbstractMaybeProgressAsyncHandlerBridge.java
@@ -18,31 +18,71 @@
/**
* An extension to {@code AbstractMaybeAsyncHandlerBridge} for {@code ProgressAsyncHandlers}.
+ *
+ * This bridge adds support for progress tracking callbacks during content write operations,
+ * while maintaining the same disposal and error handling semantics as the base class.
*
* @param the result type produced by the wrapped {@code ProgressAsyncHandler} and emitted via RxJava
*/
public abstract class AbstractMaybeProgressAsyncHandlerBridge extends AbstractMaybeAsyncHandlerBridge
implements ProgressAsyncHandler {
+ /**
+ * Creates a new progress-aware handler bridge.
+ *
+ * @param emitter the RxJava MaybeEmitter to bridge to
+ */
protected AbstractMaybeProgressAsyncHandlerBridge(MaybeEmitter emitter) {
super(emitter);
}
+ /**
+ * Called when request headers have been written.
+ *
+ * Aborts the request if the emitter has been disposed, otherwise delegates
+ * to the underlying handler.
+ *
+ * @return the handler state indicating whether to continue or abort
+ */
@Override
public final State onHeadersWritten() {
return emitter.isDisposed() ? disposed() : delegate().onHeadersWritten();
}
+ /**
+ * Called when request content has been written.
+ *
+ * Aborts the request if the emitter has been disposed, otherwise delegates
+ * to the underlying handler.
+ *
+ * @return the handler state indicating whether to continue or abort
+ */
@Override
public final State onContentWritten() {
return emitter.isDisposed() ? disposed() : delegate().onContentWritten();
}
+ /**
+ * Called to report progress of content write operations.
+ *
+ * Aborts the request if the emitter has been disposed, otherwise delegates
+ * to the underlying handler.
+ *
+ * @param amount the amount of bytes written in this update
+ * @param current the total number of bytes written so far
+ * @param total the total number of bytes to write
+ * @return the handler state indicating whether to continue or abort
+ */
@Override
public final State onContentWriteProgress(long amount, long current, long total) {
return emitter.isDisposed() ? disposed() : delegate().onContentWriteProgress(amount, current, total);
}
+ /**
+ * Returns the underlying ProgressAsyncHandler to which operations are delegated.
+ *
+ * @return the delegate handler
+ */
@Override
protected abstract ProgressAsyncHandler extends T> delegate();
diff --git a/extras/rxjava2/src/main/java/org/asynchttpclient/extras/rxjava2/maybe/MaybeAsyncHandlerBridge.java b/extras/rxjava2/src/main/java/org/asynchttpclient/extras/rxjava2/maybe/MaybeAsyncHandlerBridge.java
index d8b7e3efe6..ba66383741 100644
--- a/extras/rxjava2/src/main/java/org/asynchttpclient/extras/rxjava2/maybe/MaybeAsyncHandlerBridge.java
+++ b/extras/rxjava2/src/main/java/org/asynchttpclient/extras/rxjava2/maybe/MaybeAsyncHandlerBridge.java
@@ -18,15 +18,35 @@
import static java.util.Objects.requireNonNull;
+/**
+ * Concrete bridge implementation for standard AsyncHandlers.
+ *
+ * This bridge adapts a regular AsyncHandler to work with RxJava 2 Maybe,
+ * without progress tracking support. It handles the conversion between
+ * AsyncHttpClient's callback-based API and RxJava's reactive API.
+ *
+ * @param the result type produced by the wrapped AsyncHandler and emitted via RxJava
+ */
public final class MaybeAsyncHandlerBridge extends AbstractMaybeAsyncHandlerBridge {
private final AsyncHandler extends T> delegate;
+ /**
+ * Creates a new async handler bridge.
+ *
+ * @param emitter the RxJava MaybeEmitter to bridge to
+ * @param delegate the AsyncHandler to delegate operations to
+ */
public MaybeAsyncHandlerBridge(MaybeEmitter emitter, AsyncHandler extends T> delegate) {
super(emitter);
this.delegate = requireNonNull(delegate);
}
+ /**
+ * Returns the underlying AsyncHandler delegate.
+ *
+ * @return the delegate handler
+ */
@Override
protected AsyncHandler extends T> delegate() {
return delegate;
diff --git a/extras/rxjava2/src/main/java/org/asynchttpclient/extras/rxjava2/maybe/ProgressAsyncMaybeEmitterBridge.java b/extras/rxjava2/src/main/java/org/asynchttpclient/extras/rxjava2/maybe/ProgressAsyncMaybeEmitterBridge.java
index 0b0881d5a6..afeae08943 100644
--- a/extras/rxjava2/src/main/java/org/asynchttpclient/extras/rxjava2/maybe/ProgressAsyncMaybeEmitterBridge.java
+++ b/extras/rxjava2/src/main/java/org/asynchttpclient/extras/rxjava2/maybe/ProgressAsyncMaybeEmitterBridge.java
@@ -18,15 +18,36 @@
import static java.util.Objects.requireNonNull;
+/**
+ * Concrete bridge implementation for ProgressAsyncHandlers.
+ *
+ * This bridge adapts a ProgressAsyncHandler to work with RxJava 2 Maybe,
+ * providing progress tracking support for content write operations. It handles
+ * the conversion between AsyncHttpClient's callback-based API and RxJava's
+ * reactive API while supporting progress callbacks.
+ *
+ * @param the result type produced by the wrapped ProgressAsyncHandler and emitted via RxJava
+ */
public final class ProgressAsyncMaybeEmitterBridge extends AbstractMaybeProgressAsyncHandlerBridge {
private final ProgressAsyncHandler extends T> delegate;
+ /**
+ * Creates a new progress-aware async handler bridge.
+ *
+ * @param emitter the RxJava MaybeEmitter to bridge to
+ * @param delegate the ProgressAsyncHandler to delegate operations to
+ */
public ProgressAsyncMaybeEmitterBridge(MaybeEmitter emitter, ProgressAsyncHandler extends T> delegate) {
super(emitter);
this.delegate = requireNonNull(delegate);
}
+ /**
+ * Returns the underlying ProgressAsyncHandler delegate.
+ *
+ * @return the delegate handler
+ */
@Override
protected ProgressAsyncHandler extends T> delegate() {
return delegate;
diff --git a/extras/simple/src/main/java/org/asynchttpclient/extras/simple/AppendableBodyConsumer.java b/extras/simple/src/main/java/org/asynchttpclient/extras/simple/AppendableBodyConsumer.java
index 8ca877d337..6bf0e82fc2 100644
--- a/extras/simple/src/main/java/org/asynchttpclient/extras/simple/AppendableBodyConsumer.java
+++ b/extras/simple/src/main/java/org/asynchttpclient/extras/simple/AppendableBodyConsumer.java
@@ -20,29 +20,72 @@
import static java.nio.charset.StandardCharsets.UTF_8;
/**
- * An {@link Appendable} customer for {@link ByteBuffer}
+ * A {@link BodyConsumer} that consumes response body bytes by appending them to an {@link Appendable}.
+ *
+ * This consumer converts ByteBuffers to Strings using the specified charset and appends
+ * them to the provided Appendable (such as StringBuilder, StringBuffer, or Writer).
+ *
+ *
Usage Examples:
+ * {@code
+ * // Consume to StringBuilder
+ * StringBuilder sb = new StringBuilder();
+ * BodyConsumer consumer = new AppendableBodyConsumer(sb);
+ *
+ * // Consume to StringBuffer with custom charset
+ * StringBuffer buffer = new StringBuffer();
+ * BodyConsumer consumer = new AppendableBodyConsumer(buffer, StandardCharsets.ISO_8859_1);
+ *
+ * // Use with SimpleAsyncHttpClient
+ * SimpleAsyncHttpClient client = new SimpleAsyncHttpClient.Builder()
+ * .setUrl("http://www.example.com")
+ * .build();
+ * StringBuilder response = new StringBuilder();
+ * client.get(new AppendableBodyConsumer(response));
+ * }
*/
public class AppendableBodyConsumer implements BodyConsumer {
private final Appendable appendable;
private final Charset charset;
+ /**
+ * Creates a new AppendableBodyConsumer with a specified charset.
+ *
+ * @param appendable the Appendable to which response bytes will be appended
+ * @param charset the charset to use for decoding bytes to strings
+ */
public AppendableBodyConsumer(Appendable appendable, Charset charset) {
this.appendable = appendable;
this.charset = charset;
}
+ /**
+ * Creates a new AppendableBodyConsumer using UTF-8 charset.
+ *
+ * @param appendable the Appendable to which response bytes will be appended
+ */
public AppendableBodyConsumer(Appendable appendable) {
this.appendable = appendable;
this.charset = UTF_8;
}
+ /**
+ * Consumes the ByteBuffer by converting it to a String and appending to the Appendable.
+ *
+ * @param byteBuffer the buffer containing response body bytes
+ * @throws IOException if an I/O error occurs during appending
+ */
@Override
public void consume(ByteBuffer byteBuffer) throws IOException {
appendable
.append(new String(byteBuffer.array(), byteBuffer.arrayOffset() + byteBuffer.position(), byteBuffer.remaining(), charset));
}
+ /**
+ * Closes the underlying Appendable if it implements {@link Closeable}.
+ *
+ * @throws IOException if an I/O error occurs during closing
+ */
@Override
public void close() throws IOException {
if (appendable instanceof Closeable) {
diff --git a/extras/simple/src/main/java/org/asynchttpclient/extras/simple/BodyConsumer.java b/extras/simple/src/main/java/org/asynchttpclient/extras/simple/BodyConsumer.java
index f900b4e57d..b71a29b3ee 100644
--- a/extras/simple/src/main/java/org/asynchttpclient/extras/simple/BodyConsumer.java
+++ b/extras/simple/src/main/java/org/asynchttpclient/extras/simple/BodyConsumer.java
@@ -18,15 +18,34 @@
import java.nio.ByteBuffer;
/**
- * A simple API to be used with the {@link SimpleAsyncHttpClient} class in order to process response's bytes.
+ * A simple API to be used with the {@link SimpleAsyncHttpClient} class in order to process response bytes.
+ *
+ * Implementations of this interface consume response body chunks as they arrive, enabling
+ * streaming processing of HTTP responses without loading the entire response into memory.
+ *
+ *
Usage Examples:
+ * {@code
+ * // Consume to StringBuilder
+ * StringBuilder sb = new StringBuilder();
+ * BodyConsumer consumer = new AppendableBodyConsumer(sb);
+ *
+ * // Consume to File
+ * BodyConsumer fileConsumer = new FileBodyConsumer(new File("output.txt"));
+ *
+ * // Consume to OutputStream
+ * BodyConsumer streamConsumer = new OutputStreamBodyConsumer(outputStream);
+ * }
*/
public interface BodyConsumer extends Closeable {
/**
- * Consume the received bytes.
+ * Consumes the received bytes from an HTTP response body chunk.
+ *
+ * This method is called multiple times as response body chunks arrive,
+ * allowing for streaming processing of the response.
*
- * @param byteBuffer a {@link ByteBuffer} representation of the response's chunk.
- * @throws IOException IO exception
+ * @param byteBuffer a ByteBuffer containing a chunk of the response body
+ * @throws IOException if an I/O error occurs during consumption
*/
void consume(ByteBuffer byteBuffer) throws IOException;
}
diff --git a/extras/simple/src/main/java/org/asynchttpclient/extras/simple/ByteBufferBodyConsumer.java b/extras/simple/src/main/java/org/asynchttpclient/extras/simple/ByteBufferBodyConsumer.java
index 1dfdd38a94..3bdcc5cc10 100644
--- a/extras/simple/src/main/java/org/asynchttpclient/extras/simple/ByteBufferBodyConsumer.java
+++ b/extras/simple/src/main/java/org/asynchttpclient/extras/simple/ByteBufferBodyConsumer.java
@@ -16,18 +16,42 @@
import java.nio.ByteBuffer;
/**
- * A {@link ByteBuffer} implementation of {@link BodyConsumer}
+ * A {@link BodyConsumer} that writes response body bytes into a {@link ByteBuffer}.
+ *
+ * This consumer accumulates response bytes directly into a ByteBuffer, which is
+ * useful when you need the response data in ByteBuffer form for further processing.
+ *
+ *
Usage Examples:
+ * {@code
+ * ByteBuffer buffer = ByteBuffer.allocate(8192);
+ * ByteBufferBodyConsumer consumer = new ByteBufferBodyConsumer(buffer);
+ *
+ * SimpleAsyncHttpClient client = new SimpleAsyncHttpClient.Builder()
+ * .setUrl("http://www.example.com")
+ * .build();
+ *
+ * Future future = client.get(consumer);
+ * // After completion, buffer contains the response data
+ * }
*/
public class ByteBufferBodyConsumer implements BodyConsumer {
private final ByteBuffer byteBuffer;
+ /**
+ * Creates a new ByteBufferBodyConsumer that writes to the specified buffer.
+ *
+ * @param byteBuffer the ByteBuffer to write response body bytes into
+ */
public ByteBufferBodyConsumer(ByteBuffer byteBuffer) {
this.byteBuffer = byteBuffer;
}
/**
- * {@inheritDoc}
+ * Writes the received bytes into the underlying ByteBuffer.
+ *
+ * @param byteBuffer the buffer containing response body bytes
+ * @throws IOException if an I/O error occurs during writing
*/
@Override
public void consume(ByteBuffer byteBuffer) throws IOException {
@@ -35,7 +59,11 @@ public void consume(ByteBuffer byteBuffer) throws IOException {
}
/**
- * {@inheritDoc}
+ * Flips the underlying ByteBuffer, preparing it for reading.
+ *
+ * After this call, the buffer is ready to be read from the beginning.
+ *
+ * @throws IOException if an I/O error occurs
*/
@Override
public void close() throws IOException {
diff --git a/extras/simple/src/main/java/org/asynchttpclient/extras/simple/FileBodyConsumer.java b/extras/simple/src/main/java/org/asynchttpclient/extras/simple/FileBodyConsumer.java
index 14fe594a28..661f0df888 100644
--- a/extras/simple/src/main/java/org/asynchttpclient/extras/simple/FileBodyConsumer.java
+++ b/extras/simple/src/main/java/org/asynchttpclient/extras/simple/FileBodyConsumer.java
@@ -17,18 +17,42 @@
import java.nio.ByteBuffer;
/**
- * A {@link RandomAccessFile} that can be used as a {@link ResumableBodyConsumer}
+ * A {@link BodyConsumer} that writes response body bytes to a {@link RandomAccessFile}.
+ *
+ * This consumer supports resumable downloads by tracking the number of bytes written
+ * to the file and allowing the download to continue from the current file position.
+ *
+ *
Usage Examples:
+ * {@code
+ * RandomAccessFile raf = new RandomAccessFile("output.dat", "rw");
+ * FileBodyConsumer consumer = new FileBodyConsumer(raf);
+ *
+ * SimpleAsyncHttpClient client = new SimpleAsyncHttpClient.Builder()
+ * .setUrl("http://www.example.com/large-file.zip")
+ * .setResumableDownload(true)
+ * .build();
+ *
+ * Future future = client.get(consumer);
+ * }
*/
public class FileBodyConsumer implements ResumableBodyConsumer {
private final RandomAccessFile file;
+ /**
+ * Creates a new FileBodyConsumer that writes to the specified RandomAccessFile.
+ *
+ * @param file the RandomAccessFile to write response body bytes to
+ */
public FileBodyConsumer(RandomAccessFile file) {
this.file = file;
}
/**
- * {@inheritDoc}
+ * Writes the received bytes to the underlying file.
+ *
+ * @param byteBuffer the buffer containing response body bytes
+ * @throws IOException if an I/O error occurs during writing
*/
@Override
public void consume(ByteBuffer byteBuffer) throws IOException {
@@ -37,7 +61,9 @@ public void consume(ByteBuffer byteBuffer) throws IOException {
}
/**
- * {@inheritDoc}
+ * Closes the underlying RandomAccessFile.
+ *
+ * @throws IOException if an I/O error occurs during closing
*/
@Override
public void close() throws IOException {
@@ -45,7 +71,12 @@ public void close() throws IOException {
}
/**
- * {@inheritDoc}
+ * Returns the number of bytes already written to the file.
+ *
+ * This is used for resumable downloads to determine where to continue downloading.
+ *
+ * @return the current file length in bytes
+ * @throws IOException if an I/O error occurs reading the file length
*/
@Override
public long getTransferredBytes() throws IOException {
@@ -53,7 +84,11 @@ public long getTransferredBytes() throws IOException {
}
/**
- * {@inheritDoc}
+ * Prepares the file for resuming a download by seeking to the end.
+ *
+ * This positions the file pointer at the end so new bytes will be appended.
+ *
+ * @throws IOException if an I/O error occurs during seeking
*/
@Override
public void resume() throws IOException {
diff --git a/extras/simple/src/main/java/org/asynchttpclient/extras/simple/OutputStreamBodyConsumer.java b/extras/simple/src/main/java/org/asynchttpclient/extras/simple/OutputStreamBodyConsumer.java
index dc871d3762..7ac92d4332 100644
--- a/extras/simple/src/main/java/org/asynchttpclient/extras/simple/OutputStreamBodyConsumer.java
+++ b/extras/simple/src/main/java/org/asynchttpclient/extras/simple/OutputStreamBodyConsumer.java
@@ -17,18 +17,45 @@
import java.nio.ByteBuffer;
/**
- * A simple {@link OutputStream} implementation for {@link BodyConsumer}
+ * A {@link BodyConsumer} that writes response body bytes to an {@link OutputStream}.
+ *
+ * This consumer writes response bytes directly to any OutputStream, making it
+ * flexible for writing to files, network streams, or any other output destination.
+ *
+ *
Usage Examples:
+ * {@code
+ * // Write to file
+ * OutputStream fos = new FileOutputStream("output.dat");
+ * OutputStreamBodyConsumer consumer = new OutputStreamBodyConsumer(fos);
+ *
+ * // Write to byte array
+ * ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ * OutputStreamBodyConsumer consumer = new OutputStreamBodyConsumer(baos);
+ *
+ * SimpleAsyncHttpClient client = new SimpleAsyncHttpClient.Builder()
+ * .setUrl("http://www.example.com/data")
+ * .build();
+ * client.get(consumer);
+ * }
*/
public class OutputStreamBodyConsumer implements BodyConsumer {
private final OutputStream outputStream;
+ /**
+ * Creates a new OutputStreamBodyConsumer that writes to the specified stream.
+ *
+ * @param outputStream the OutputStream to write response body bytes to
+ */
public OutputStreamBodyConsumer(OutputStream outputStream) {
this.outputStream = outputStream;
}
/**
- * {@inheritDoc}
+ * Writes the received bytes to the underlying OutputStream.
+ *
+ * @param byteBuffer the buffer containing response body bytes
+ * @throws IOException if an I/O error occurs during writing
*/
@Override
public void consume(ByteBuffer byteBuffer) throws IOException {
@@ -36,7 +63,9 @@ public void consume(ByteBuffer byteBuffer) throws IOException {
}
/**
- * {@inheritDoc}
+ * Closes the underlying OutputStream.
+ *
+ * @throws IOException if an I/O error occurs during closing
*/
@Override
public void close() throws IOException {
diff --git a/extras/simple/src/main/java/org/asynchttpclient/extras/simple/ResumableBodyConsumer.java b/extras/simple/src/main/java/org/asynchttpclient/extras/simple/ResumableBodyConsumer.java
index 0978e4f770..1991d3a7ef 100644
--- a/extras/simple/src/main/java/org/asynchttpclient/extras/simple/ResumableBodyConsumer.java
+++ b/extras/simple/src/main/java/org/asynchttpclient/extras/simple/ResumableBodyConsumer.java
@@ -16,22 +16,49 @@
import java.io.IOException;
/**
+ * A {@link BodyConsumer} that supports resuming interrupted downloads.
+ *
+ * This interface extends BodyConsumer to provide methods for tracking the number
+ * of bytes already transferred and resuming from that position. This is useful
+ * for large file downloads that may be interrupted and need to continue from
+ * where they left off.
+ *
+ *
Usage Examples:
+ * {@code
+ * RandomAccessFile file = new RandomAccessFile("large-file.zip", "rw");
+ * ResumableBodyConsumer consumer = new FileBodyConsumer(file);
+ *
+ * SimpleAsyncHttpClient client = new SimpleAsyncHttpClient.Builder()
+ * .setUrl("http://www.example.com/large-file.zip")
+ * .setResumableDownload(true)
+ * .build();
+ *
+ * // Download will resume from current file position if interrupted
+ * Future future = client.get(consumer);
+ * }
+ *
* @author Benjamin Hanzelmann
*/
public interface ResumableBodyConsumer extends BodyConsumer {
/**
- * Prepare this consumer to resume a download, for example by seeking to the end of the underlying file.
+ * Prepares this consumer to resume a download from the current position.
+ *
+ * For example, a file-based implementation would seek to the end of the file
+ * so that new bytes will be appended.
*
- * @throws IOException IO exception
+ * @throws IOException if an I/O error occurs during preparation
*/
void resume() throws IOException;
/**
- * Get the previously transferred bytes, for example the current file size.
+ * Returns the number of bytes already transferred.
+ *
+ * For example, a file-based implementation would return the current file size.
+ * This value is used to send a Range header to continue the download.
*
* @return the number of transferred bytes
- * @throws IOException IO exception
+ * @throws IOException if an I/O error occurs reading the transferred byte count
*/
long getTransferredBytes() throws IOException;
}
diff --git a/extras/simple/src/main/java/org/asynchttpclient/extras/simple/ThrowableHandler.java b/extras/simple/src/main/java/org/asynchttpclient/extras/simple/ThrowableHandler.java
index c0956e93c2..301331b54d 100644
--- a/extras/simple/src/main/java/org/asynchttpclient/extras/simple/ThrowableHandler.java
+++ b/extras/simple/src/main/java/org/asynchttpclient/extras/simple/ThrowableHandler.java
@@ -15,9 +15,30 @@
/**
- * Simple {@link Throwable} handler to be used with {@link SimpleAsyncHttpClient}
+ * Simple {@link Throwable} handler to be used with {@link SimpleAsyncHttpClient}.
+ *
+ * This interface provides a callback mechanism for handling exceptions that occur
+ * during HTTP request execution, allowing custom error handling logic.
+ *
+ *
Usage Examples:
+ * {@code
+ * ThrowableHandler handler = throwable -> {
+ * logger.error("Request failed: " + throwable.getMessage(), throwable);
+ * // Custom error handling logic
+ * };
+ *
+ * SimpleAsyncHttpClient client = new SimpleAsyncHttpClient.Builder()
+ * .setDefaultThrowableHandler(handler)
+ * .setUrl("http://www.example.com")
+ * .build();
+ * }
*/
public interface ThrowableHandler {
+ /**
+ * Handles a throwable that occurred during HTTP request processing.
+ *
+ * @param t the throwable that was thrown during request execution
+ */
void onThrowable(Throwable t);
}
diff --git a/extras/typesafeconfig/src/main/java/org/asynchttpclient/extras/typesafeconfig/AsyncHttpClientTypesafeConfig.java b/extras/typesafeconfig/src/main/java/org/asynchttpclient/extras/typesafeconfig/AsyncHttpClientTypesafeConfig.java
index fa5d87bcf3..fe42cacd04 100644
--- a/extras/typesafeconfig/src/main/java/org/asynchttpclient/extras/typesafeconfig/AsyncHttpClientTypesafeConfig.java
+++ b/extras/typesafeconfig/src/main/java/org/asynchttpclient/extras/typesafeconfig/AsyncHttpClientTypesafeConfig.java
@@ -41,10 +41,37 @@
import static org.asynchttpclient.config.AsyncHttpClientConfigDefaults.*;
+/**
+ * An {@link AsyncHttpClientConfig} implementation that reads configuration from Typesafe Config.
+ *
+ * This class allows AsyncHttpClient to be configured using Typesafe Config (now Lightbend Config),
+ * enabling externalized configuration through application.conf, application.json, or other
+ * supported configuration sources.
+ *
+ *
Usage Examples:
+ * {@code
+ * // Load from application.conf
+ * Config config = ConfigFactory.load();
+ * AsyncHttpClientConfig ahcConfig = new AsyncHttpClientTypesafeConfig(config);
+ * AsyncHttpClient client = asyncHttpClient(ahcConfig);
+ *
+ * // Load from custom config
+ * Config custom = ConfigFactory.parseString(
+ * "org.asynchttpclient.maxConnections = 100\n" +
+ * "org.asynchttpclient.requestTimeout = 30000"
+ * );
+ * AsyncHttpClientConfig ahcConfig = new AsyncHttpClientTypesafeConfig(custom);
+ * }
+ */
public class AsyncHttpClientTypesafeConfig implements AsyncHttpClientConfig {
private final Config config;
+ /**
+ * Creates a new configuration instance from the specified Typesafe Config.
+ *
+ * @param config the Typesafe Config containing AsyncHttpClient configuration properties
+ */
public AsyncHttpClientTypesafeConfig(Config config) {
this.config = config;
}
diff --git a/netty-utils/src/main/java/org/asynchttpclient/netty/util/ByteBufUtils.java b/netty-utils/src/main/java/org/asynchttpclient/netty/util/ByteBufUtils.java
index b95829ebf4..b78822b32b 100755
--- a/netty-utils/src/main/java/org/asynchttpclient/netty/util/ByteBufUtils.java
+++ b/netty-utils/src/main/java/org/asynchttpclient/netty/util/ByteBufUtils.java
@@ -28,6 +28,26 @@
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.asynchttpclient.netty.util.Utf8ByteBufCharsetDecoder.*;
+/**
+ * Utility class for converting Netty ByteBuf instances to bytes, strings, and character arrays.
+ *
+ * This class provides optimized conversion methods that handle both heap and direct buffers
+ * efficiently, with special optimizations for UTF-8 and US-ASCII charsets.
+ *
+ *
Usage Examples:
+ * {@code
+ * ByteBuf buf = Unpooled.copiedBuffer("Hello World", UTF_8);
+ *
+ * // Convert to byte array
+ * byte[] bytes = ByteBufUtils.byteBuf2Bytes(buf);
+ *
+ * // Convert to String
+ * String str = ByteBufUtils.byteBuf2String(UTF_8, buf);
+ *
+ * // Convert to char array
+ * char[] chars = ByteBufUtils.byteBuf2Chars(UTF_8, buf);
+ * }
+ */
public final class ByteBufUtils {
private static final char[] EMPTY_CHARS = new char[0];
@@ -36,6 +56,15 @@ public final class ByteBufUtils {
private ByteBufUtils() {
}
+ /**
+ * Converts a ByteBuf to a byte array.
+ *
+ * If the ByteBuf is backed by an array and the array covers the entire readable range,
+ * the underlying array is returned directly. Otherwise, a copy is made.
+ *
+ * @param buf the ByteBuf to convert
+ * @return a byte array containing the readable bytes from the buffer
+ */
public static byte[] byteBuf2Bytes(ByteBuf buf) {
int readable = buf.readableBytes();
int readerIndex = buf.readerIndex();
@@ -50,18 +79,54 @@ public static byte[] byteBuf2Bytes(ByteBuf buf) {
return array;
}
+ /**
+ * Converts a ByteBuf to a String using the specified charset.
+ *
+ * Uses optimized UTF-8 decoding for UTF-8 and US-ASCII charsets.
+ *
+ * @param charset the charset to use for decoding
+ * @param buf the ByteBuf to convert
+ * @return a String containing the decoded bytes
+ */
public static String byteBuf2String(Charset charset, ByteBuf buf) {
return isUtf8OrUsAscii(charset) ? decodeUtf8(buf) : buf.toString(charset);
}
+ /**
+ * Converts multiple ByteBufs to a single String using the specified charset.
+ *
+ * Uses optimized UTF-8 decoding for UTF-8 and US-ASCII charsets.
+ *
+ * @param charset the charset to use for decoding
+ * @param bufs the ByteBufs to convert
+ * @return a String containing the decoded bytes from all buffers
+ */
public static String byteBuf2String(Charset charset, ByteBuf... bufs) {
return isUtf8OrUsAscii(charset) ? decodeUtf8(bufs) : byteBuf2String0(charset, bufs);
}
+ /**
+ * Converts a ByteBuf to a character array using the specified charset.
+ *
+ * Uses optimized UTF-8 decoding for UTF-8 and US-ASCII charsets.
+ *
+ * @param charset the charset to use for decoding
+ * @param buf the ByteBuf to convert
+ * @return a character array containing the decoded bytes
+ */
public static char[] byteBuf2Chars(Charset charset, ByteBuf buf) {
return isUtf8OrUsAscii(charset) ? decodeUtf8Chars(buf) : decodeChars(buf, charset);
}
+ /**
+ * Converts multiple ByteBufs to a single character array using the specified charset.
+ *
+ * Uses optimized UTF-8 decoding for UTF-8 and US-ASCII charsets.
+ *
+ * @param charset the charset to use for decoding
+ * @param bufs the ByteBufs to convert
+ * @return a character array containing the decoded bytes from all buffers
+ */
public static char[] byteBuf2Chars(Charset charset, ByteBuf... bufs) {
return isUtf8OrUsAscii(charset) ? decodeUtf8Chars(bufs) : byteBuf2Chars0(charset, bufs);
}