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
14 changes: 14 additions & 0 deletions docs/docs/httprequestbuilder.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,20 @@ At a high level, the workflow looks like this:

This keeps each request self-contained and avoids mutating shared `HttpClient` state like `DefaultRequestHeaders`.

:::danger[BaseAddress Trailing Slash]

When you build a request with a relative route, `HttpClient.BaseAddress` must end with a trailing slash for the route to be appended to it correctly. This is standard `Uri` combination behavior (RFC 3986 relative reference resolution), not something FluentHttpClient controls.

A base address of `https://example.com/api/posts` combined with the route `1` resolves to `https://example.com/api/1`, dropping the `posts` segment, because the base address has no trailing slash. Use `https://example.com/api/posts/` instead to get `https://example.com/api/posts/1`.

Adding a leading slash to the route does not fix this either. A route of `/1` is treated as an absolute path and replaces the entire base address path, giving you `https://example.com/1` instead, dropping `api` and `posts` both.

This only applies when building a request from a relative route. `UsingBase` does not accept or set a route, so it is not affected.

**See: https://www.rfc-editor.org/info/rfc3986/#section-5**

:::

## Fluent Workflow

FluentHttpClient is built around a simple pattern:
Expand Down
58 changes: 58 additions & 0 deletions src/FluentHttpClient/HttpClientExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@ public static class HttpClientExtensions
/// </summary>
/// <param name="client">The <see cref="HttpClient"/> instance to use for sending requests.</param>
/// <returns>A new <see cref="HttpRequestBuilder"/> instance initialized with the client's base address.</returns>
/// <remarks>
/// This method does not accept or set a route, and this library gives you no public way to add one
/// afterward. Because of that, the resulting request always targets <see cref="HttpClient.BaseAddress"/>
/// as-is, so the trailing slash requirement described on <c>UsingRoute</c> does not apply here. It only
/// matters for derived builder types that introduce their own route, since they inherit the same
/// <see cref="Uri"/> combination behavior.
/// </remarks>
public static HttpRequestBuilder UsingBase(this HttpClient client)
{
return new HttpRequestBuilder(client);
Expand All @@ -26,6 +33,13 @@ public static HttpRequestBuilder UsingBase(this HttpClient client)
/// <typeparam name="TBuilder">The type of the request builder to create.</typeparam>
/// <param name="client">The <see cref="HttpClient"/> instance to use for sending requests.</param>
/// <returns>A new instance of <typeparamref name="TBuilder"/> initialized with the client's base address.</returns>
/// <remarks>
/// This method does not accept or set a route, and this library gives you no public way to add one
/// afterward. Because of that, the resulting request always targets <see cref="HttpClient.BaseAddress"/>
/// as-is, so the trailing slash requirement described on <c>UsingRoute</c> does not apply here. It only
/// matters for derived builder types that introduce their own route, since they inherit the same
/// <see cref="Uri"/> combination behavior.
/// </remarks>
#if NET7_0_OR_GREATER
[RequiresDynamicCode("Constructs TBuilder with Activator.CreateInstance, which can require runtime code generation. For Native AOT, use the UsingRoute overload that takes a factory delegate.")]
#endif
Expand All @@ -45,6 +59,17 @@ public static TBuilder UsingBase<TBuilder>(this HttpClient client)
/// <param name="client">The <see cref="HttpClient"/> instance to use for sending requests.</param>
/// <param name="route">The route string for the request URI, which can be absolute or relative.</param>
/// <returns>A new <see cref="HttpRequestBuilder"/> instance initialized with the specified route.</returns>
/// <remarks>
/// When the route is relative, <see cref="HttpClient.BaseAddress"/> must end with a trailing slash
/// for the route to be appended to it correctly. This is standard <see cref="Uri"/> combination
/// behavior (RFC 3986 relative reference resolution), not something this library controls. For
/// example, a base address of <c>https://example.com/api/posts</c> combined with the route <c>1</c>
/// resolves to <c>https://example.com/api/1</c>, dropping the <c>posts</c> segment, because the base
/// address has no trailing slash. Use <c>https://example.com/api/posts/</c> instead to get
/// <c>https://example.com/api/posts/1</c>. Adding a leading slash to the route does not fix this
/// either. A route of <c>/1</c> is treated as an absolute path and replaces the entire base address
/// path, giving you <c>https://example.com/1</c> instead, dropping <c>api</c> and <c>posts</c> both.
/// </remarks>
public static HttpRequestBuilder UsingRoute(this HttpClient client, string route)
{
return new HttpRequestBuilder(client, route);
Expand All @@ -58,6 +83,17 @@ public static HttpRequestBuilder UsingRoute(this HttpClient client, string route
/// <param name="client">The <see cref="HttpClient"/> instance to use for sending requests.</param>
/// <param name="route">The route string for the request URI, which can be absolute or relative.</param>
/// <returns>A new instance of <typeparamref name="TBuilder"/> initialized with the specified route.</returns>
/// <remarks>
/// When the route is relative, <see cref="HttpClient.BaseAddress"/> must end with a trailing slash
/// for the route to be appended to it correctly. This is standard <see cref="Uri"/> combination
/// behavior (RFC 3986 relative reference resolution), not something this library controls. For
/// example, a base address of <c>https://example.com/api/posts</c> combined with the route <c>1</c>
/// resolves to <c>https://example.com/api/1</c>, dropping the <c>posts</c> segment, because the base
/// address has no trailing slash. Use <c>https://example.com/api/posts/</c> instead to get
/// <c>https://example.com/api/posts/1</c>. Adding a leading slash to the route does not fix this
/// either. A route of <c>/1</c> is treated as an absolute path and replaces the entire base address
/// path, giving you <c>https://example.com/1</c> instead, dropping <c>api</c> and <c>posts</c> both.
/// </remarks>
#if NET7_0_OR_GREATER
[RequiresDynamicCode("Constructs TBuilder with Activator.CreateInstance, which can require runtime code generation. For Native AOT, use the UsingRoute overload that takes a factory delegate.")]
#endif
Expand All @@ -77,6 +113,17 @@ public static TBuilder UsingRoute<TBuilder>(this HttpClient client, string route
/// <param name="client">The <see cref="HttpClient"/> instance to use for sending requests.</param>
/// <param name="uri">The URI for the request.</param>
/// <returns>A new <see cref="HttpRequestBuilder"/> instance initialized with the specified URI.</returns>
/// <remarks>
/// When the route is relative, <see cref="HttpClient.BaseAddress"/> must end with a trailing slash
/// for the route to be appended to it correctly. This is standard <see cref="Uri"/> combination
/// behavior (RFC 3986 relative reference resolution), not something this library controls. For
/// example, a base address of <c>https://example.com/api/posts</c> combined with the route <c>1</c>
/// resolves to <c>https://example.com/api/1</c>, dropping the <c>posts</c> segment, because the base
/// address has no trailing slash. Use <c>https://example.com/api/posts/</c> instead to get
/// <c>https://example.com/api/posts/1</c>. Adding a leading slash to the route does not fix this
/// either. A route of <c>/1</c> is treated as an absolute path and replaces the entire base address
/// path, giving you <c>https://example.com/1</c> instead, dropping <c>api</c> and <c>posts</c> both.
/// </remarks>
public static HttpRequestBuilder UsingRoute(this HttpClient client, Uri uri)
{
return new HttpRequestBuilder(client, uri);
Expand All @@ -89,6 +136,17 @@ public static HttpRequestBuilder UsingRoute(this HttpClient client, Uri uri)
/// <param name="client">The <see cref="HttpClient"/> instance to use for sending requests.</param>
/// <param name="uri">The URI for the request.</param>
/// <returns>A new instance of <typeparamref name="TBuilder"/> initialized with the specified URI.</returns>
/// <remarks>
/// When the route is relative, <see cref="HttpClient.BaseAddress"/> must end with a trailing slash
/// for the route to be appended to it correctly. This is standard <see cref="Uri"/> combination
/// behavior (RFC 3986 relative reference resolution), not something this library controls. For
/// example, a base address of <c>https://example.com/api/posts</c> combined with the route <c>1</c>
/// resolves to <c>https://example.com/api/1</c>, dropping the <c>posts</c> segment, because the base
/// address has no trailing slash. Use <c>https://example.com/api/posts/</c> instead to get
/// <c>https://example.com/api/posts/1</c>. Adding a leading slash to the route does not fix this
/// either. A route of <c>/1</c> is treated as an absolute path and replaces the entire base address
/// path, giving you <c>https://example.com/1</c> instead, dropping <c>api</c> and <c>posts</c> both.
/// </remarks>
#if NET7_0_OR_GREATER
[RequiresDynamicCode("Constructs TBuilder with Activator.CreateInstance, which can require runtime code generation. For Native AOT, use the UsingRoute overload that takes a factory delegate.")]
#endif
Expand Down