Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion docs/docs/conditional-configuration.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
sidebar_position: 8
sidebar_position: 9
title: Conditional Configuration
---

Expand Down
44 changes: 44 additions & 0 deletions docs/docs/configure-timeout.md
Original file line number Diff line number Diff line change
@@ -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`. |
2 changes: 1 addition & 1 deletion docs/docs/deserializing-json.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
sidebar_position: 12
sidebar_position: 13
title: Deserializing JSON
---

Expand Down
2 changes: 1 addition & 1 deletion docs/docs/deserializing-xml.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
sidebar_position: 13
sidebar_position: 14
title: Deserializing XML
---

Expand Down
1 change: 1 addition & 0 deletions docs/docs/httprequestbuilder.md
Original file line number Diff line number Diff line change
Expand Up @@ -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). |

Expand Down
2 changes: 1 addition & 1 deletion docs/docs/native-aot-support.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
sidebar_position: 14
sidebar_position: 15
title: Native AOT Support
---

Expand Down
2 changes: 1 addition & 1 deletion docs/docs/response-content.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
sidebar_position: 11
sidebar_position: 12
title: Read Response Content
---

Expand Down
2 changes: 1 addition & 1 deletion docs/docs/response-handlers.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
sidebar_position: 10
sidebar_position: 11
title: Response Handlers
---

Expand Down
2 changes: 1 addition & 1 deletion docs/docs/sending-requests.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
sidebar_position: 9
sidebar_position: 10
title: Sending Requests
---

Expand Down
2 changes: 1 addition & 1 deletion docs/docs/testing-resources.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
sidebar_position: 15
sidebar_position: 16
title: Testing Resources
---

Expand Down
58 changes: 58 additions & 0 deletions src/FluentHttpClient/FluentTimeoutExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
namespace FluentHttpClient;

/// <summary>
/// Provides extension methods for configuring timeouts in Fluent HTTP Client.
/// </summary>
public static class FluentTimeoutExtensions
{
internal static readonly string MessageInvalidTimeout = "Timeout must be a positive value.";

/// <summary>
/// Sets a per-request timeout using the specified number of seconds. Must be positive.
/// </summary>
/// <remarks>
/// 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 <see cref="HttpClient.Timeout"/> or apply to
/// other requests made with the same <see cref="HttpClient"/> instance.
/// </remarks>
/// <param name="builder"></param>
/// <param name="seconds"></param>
/// <returns></returns>
/// <exception cref="ArgumentOutOfRangeException"></exception>
public static HttpRequestBuilder WithTimeout(this HttpRequestBuilder builder, int seconds)
{
if (seconds <= 0)
{
throw new ArgumentOutOfRangeException(nameof(seconds), MessageInvalidTimeout);
}

return builder.WithTimeout(TimeSpan.FromSeconds(seconds));
}

/// <summary>
/// Sets a per-request timeout using the specified <see cref="TimeSpan"/> value. Must be positive.
/// </summary>
/// <remarks>
/// 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 <see cref="HttpClient.Timeout"/> or apply to
/// other requests made with the same <see cref="HttpClient"/> instance.
/// </remarks>
/// <param name="builder"></param>
/// <param name="timeout"></param>
/// <returns></returns>
/// <exception cref="ArgumentOutOfRangeException"></exception>
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;
}
}
36 changes: 33 additions & 3 deletions src/FluentHttpClient/HttpRequestBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,19 @@ internal HttpRequestBuilder(HttpClient client, Uri route) : this(client)
/// </summary>
public string? Route => _route?.OriginalString;

/// <summary>
/// Returns the per-request timeout applied by <see cref="HttpRequestBuilder"/>.
/// </summary>
/// <remarks>
/// This timeout is enforced only for the current request and is implemented via
/// cancellation; it does not modify <see cref="HttpClient.Timeout"/> or apply to
/// other requests made with the same <see cref="HttpClient"/> instance.
/// When set, the request is canceled if it does not complete within the specified
/// time interval. This timeout composes with any caller-provided
/// <see cref="CancellationToken"/>; whichever triggers first will cancel the request.
/// </remarks>
public TimeSpan? Timeout { get; internal set; }

/// <summary>
/// Gets or sets the HTTP message version.
/// </summary>
Expand Down Expand Up @@ -368,9 +381,26 @@ public async Task<HttpResponseMessage> 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();
}
}

/// <summary>
Expand Down
2 changes: 1 addition & 1 deletion src/FluentHttpClient/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
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.
2 changes: 1 addition & 1 deletion src/version.json
Original file line number Diff line number Diff line change
@@ -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+)?$"
Expand Down