diff --git a/README.md b/README.md index d102794..44670c1 100644 --- a/README.md +++ b/README.md @@ -131,7 +131,7 @@ public class Post } ``` -The FluentHttpClient version expresses the same logic in fewer lines, with better readability and no loss of functionality. All configuration, sending, error handling, and deserialization happen in a single fluent chain. +Because a fluent API improves developer experience by turning tedious, repetitive setup into a readable, chainable flow that matches how you actually think about building and sending an HTTP request, The FluentHttpClient version expresses the same logic in fewer lines, with better readability and no loss of functionality. All configuration, sending, error handling, and deserialization happen in a single fluent chain. ## Usage and Support diff --git a/docs/docs/conditional-configuration.md b/docs/docs/conditional-configuration.md index 78f8f8b..ffce3a4 100644 --- a/docs/docs/conditional-configuration.md +++ b/docs/docs/conditional-configuration.md @@ -1,5 +1,5 @@ --- -sidebar_position: 8 +sidebar_position: 9 title: Conditional Configuration --- diff --git a/docs/docs/configure-timeout.md b/docs/docs/configure-timeout.md new file mode 100644 index 0000000..e620f76 --- /dev/null +++ b/docs/docs/configure-timeout.md @@ -0,0 +1,44 @@ +--- +sidebar_position: 8 +title: Configure Timeout +--- + +# Configure Timeout + +FluentHttpClient provides a set of extensions for applying **per-request** timeouts. These timeouts define how long the client will wait for the current request to complete before canceling it. Timeouts are implemented through cancellation, do not alter `HttpClient.Timeout`, and have no effect on other requests made with the same `HttpClient` instance. + +## Usage + +A timeout may be applied using either a number of seconds or a `TimeSpan`. The timeout must be a positive value. When set, the request will be canceled if the configured interval elapses before the response is received. + +```csharp +var response = await client + .UsingBase() + .WithRoute("/todos/1") + .WithTimeout(5) // 5-second timeout + .GetAsync(); +``` + +```csharp +var response = await client + .UsingBase() + .WithRoute("/todos/1") + .WithTimeout(TimeSpan.FromMilliseconds(750)) + .GetAsync(); +``` + +Timeouts link with any caller-provided `CancellationToken`; whichever triggers first will cancel the request. + +## Behavior Notes + +* The timeout applies only to the current request and does not affect any other requests sent using the same `HttpClient` instance. +* The timeout value must be a positive duration; zero or negative values are not allowed and will result in an exception. +* The timeout does not modify `HttpClient.Timeout` and is enforced solely through per-request cancellation. +* The timeout works together with any caller-provided `CancellationToken`, and the request will be canceled when either the timeout expires or the token is triggered. + +## Quick Reference + +| Method | Purpose | +| ----------------------- | ------------------------------------------------- | +| `WithTimeout(int)` | Applies a per-request timeout using seconds. | +| `WithTimeout(TimeSpan)` | Applies a per-request timeout using a `TimeSpan`. | diff --git a/docs/docs/deserializing-json.md b/docs/docs/deserializing-json.md index f12b6cc..18e1ba1 100644 --- a/docs/docs/deserializing-json.md +++ b/docs/docs/deserializing-json.md @@ -1,5 +1,5 @@ --- -sidebar_position: 12 +sidebar_position: 13 title: Deserializing JSON --- diff --git a/docs/docs/deserializing-xml.md b/docs/docs/deserializing-xml.md index 159bfde..82a6ce7 100644 --- a/docs/docs/deserializing-xml.md +++ b/docs/docs/deserializing-xml.md @@ -1,5 +1,5 @@ --- -sidebar_position: 13 +sidebar_position: 14 title: Deserializing XML --- diff --git a/docs/docs/httprequestbuilder.md b/docs/docs/httprequestbuilder.md index 282a475..4ab52a2 100644 --- a/docs/docs/httprequestbuilder.md +++ b/docs/docs/httprequestbuilder.md @@ -104,6 +104,7 @@ The table below lists the key properties on `HttpRequestBuilder` and how they ar | `bool BufferRequestContent` | Forces the request content to be fully buffered in memory before sending. Intended only for compatibility edge cases where buffering is required. See [Configure Content](./configure-content.md). | | `HttpQueryParameterCollection QueryParameters` | Represents all query string values for the request. The route and base address must not contain query components; this collection is the single source of truth. See [Configure Query Parameters](./configure-query-parameters.md). | | `string? Route` | The relative or absolute request route originally provided to the builder; validated so it contains no query or fragment. This value can be read, but cannot be changed. | +| `TimeSpan? Timeout` | A per-request timeout that limits how long the client waits for the request to complete before canceling it. See [Configure Timeout](./configure-timeout.md). | | `Version Version` | The HTTP protocol version applied to the outgoing request. Defaults to HTTP/1.1. See [Configure Version](./configure-version.md). | | `HttpVersionPolicy VersionPolicy`* | Controls how the requested HTTP version is interpreted and negotiated (e.g., upgrade, downgrade, or strict). Defaults to `RequestVersionOrLower`. See [Configure Version](./configure-version.md). | diff --git a/docs/docs/native-aot-support.md b/docs/docs/native-aot-support.md index bebcca9..870884e 100644 --- a/docs/docs/native-aot-support.md +++ b/docs/docs/native-aot-support.md @@ -1,5 +1,5 @@ --- -sidebar_position: 14 +sidebar_position: 15 title: Native AOT Support --- diff --git a/docs/docs/response-content.md b/docs/docs/response-content.md index 170d7fd..5850f18 100644 --- a/docs/docs/response-content.md +++ b/docs/docs/response-content.md @@ -1,5 +1,5 @@ --- -sidebar_position: 11 +sidebar_position: 12 title: Read Response Content --- diff --git a/docs/docs/response-handlers.md b/docs/docs/response-handlers.md index 84601b2..0e0ae23 100644 --- a/docs/docs/response-handlers.md +++ b/docs/docs/response-handlers.md @@ -1,5 +1,5 @@ --- -sidebar_position: 10 +sidebar_position: 11 title: Response Handlers --- diff --git a/docs/docs/sending-requests.md b/docs/docs/sending-requests.md index 37af636..5a21f12 100644 --- a/docs/docs/sending-requests.md +++ b/docs/docs/sending-requests.md @@ -1,5 +1,5 @@ --- -sidebar_position: 9 +sidebar_position: 10 title: Sending Requests --- diff --git a/docs/docs/testing-resources.md b/docs/docs/testing-resources.md index 3aea55b..3d3a2cb 100644 --- a/docs/docs/testing-resources.md +++ b/docs/docs/testing-resources.md @@ -1,5 +1,5 @@ --- -sidebar_position: 15 +sidebar_position: 16 title: Testing Resources --- diff --git a/src/FluentHttpClient/FluentTimeoutExtensions.cs b/src/FluentHttpClient/FluentTimeoutExtensions.cs new file mode 100644 index 0000000..272b8f8 --- /dev/null +++ b/src/FluentHttpClient/FluentTimeoutExtensions.cs @@ -0,0 +1,58 @@ +namespace FluentHttpClient; + +/// +/// Provides extension methods for configuring timeouts in Fluent HTTP Client. +/// +public static class FluentTimeoutExtensions +{ + internal static readonly string MessageInvalidTimeout = "Timeout must be a positive value."; + + /// + /// Sets a per-request timeout using the specified number of seconds. Must be positive. + /// + /// + /// The timeout applies only to the current request and determines how long + /// the client will wait for the request to complete before canceling it. + /// It does not modify or apply to + /// other requests made with the same instance. + /// + /// + /// + /// + /// + public static HttpRequestBuilder WithTimeout(this HttpRequestBuilder builder, int seconds) + { + if (seconds <= 0) + { + throw new ArgumentOutOfRangeException(nameof(seconds), MessageInvalidTimeout); + } + + return builder.WithTimeout(TimeSpan.FromSeconds(seconds)); + } + + /// + /// Sets a per-request timeout using the specified value. Must be positive. + /// + /// + /// The timeout applies only to the current request and determines how long + /// the client will wait for the request to complete before canceling it. + /// It does not modify or apply to + /// other requests made with the same instance. + /// + /// + /// + /// + /// + public static HttpRequestBuilder WithTimeout(this HttpRequestBuilder builder, TimeSpan timeout) + { + Guard.AgainstNull(timeout, nameof(timeout)); + + if (timeout <= TimeSpan.Zero) + { + throw new ArgumentOutOfRangeException(nameof(timeout), MessageInvalidTimeout); + } + + builder.Timeout = timeout; + return builder; + } +} diff --git a/src/FluentHttpClient/HttpRequestBuilder.cs b/src/FluentHttpClient/HttpRequestBuilder.cs index 48c4b21..8663f3b 100644 --- a/src/FluentHttpClient/HttpRequestBuilder.cs +++ b/src/FluentHttpClient/HttpRequestBuilder.cs @@ -197,6 +197,19 @@ internal HttpRequestBuilder(HttpClient client, Uri route) : this(client) /// public string? Route => _route?.OriginalString; + /// + /// Returns the per-request timeout applied by . + /// + /// + /// This timeout is enforced only for the current request and is implemented via + /// cancellation; it does not modify or apply to + /// other requests made with the same instance. + /// When set, the request is canceled if it does not complete within the specified + /// time interval. This timeout composes with any caller-provided + /// ; whichever triggers first will cancel the request. + /// + public TimeSpan? Timeout { get; internal set; } + /// /// Gets or sets the HTTP message version. /// @@ -368,9 +381,26 @@ public async Task SendAsync( { using var request = await BuildRequest(method, cancellationToken).ConfigureAwait(false); - return await _client - .SendAsync(request, completionOption, cancellationToken) - .ConfigureAwait(false); + var effectiveToken = cancellationToken; + var timeoutCts = (CancellationTokenSource?)null; + + if (Timeout is TimeSpan timeout) + { + timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + timeoutCts.CancelAfter(timeout); + effectiveToken = timeoutCts.Token; + } + + try + { + return await _client + .SendAsync(request, completionOption, effectiveToken) + .ConfigureAwait(false); + } + finally + { + timeoutCts?.Dispose(); + } } /// diff --git a/src/FluentHttpClient/README.md b/src/FluentHttpClient/README.md index bc2d572..8841d6a 100644 --- a/src/FluentHttpClient/README.md +++ b/src/FluentHttpClient/README.md @@ -56,4 +56,4 @@ FluentHttpClient wraps `HttpClient` (you still manage the lifetime) and provides - **Response Handlers**: Attach success and failure callbacks directly in the request chain without breaking fluency - **Reduced Boilerplate**: Express the entire request lifecycle—configuration, sending, and deserialization—in a single expression -FluentHttpClient can expresses the same logic in fewer lines, with better readability and no loss of functionality. All configuration, sending, error handling, and deserialization happen in a single fluent chain. \ No newline at end of file +Because a fluent API improves developer experience by turning tedious, repetitive setup into a readable, chainable flow that matches how you actually think about building and sending an HTTP request, FluentHttpClient can expresses the same logic in fewer lines, with better readability and no loss of functionality. All configuration, sending, error handling, and deserialization happen in a single fluent chain. \ No newline at end of file diff --git a/src/version.json b/src/version.json index ba47acf..b7d390c 100644 --- a/src/version.json +++ b/src/version.json @@ -1,6 +1,6 @@ { "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/main/src/NerdBank.GitVersioning/version.schema.json", - "version": "5.0.0-rc3", + "version": "5.0.0-rc4", "publicReleaseRefSpec": [ "^refs/heads/main$", "^refs/heads/v\\d+(?:\\.\\d+)?$"