diff --git a/docs/docs/httprequestbuilder.md b/docs/docs/httprequestbuilder.md index 63a5e34..b9df141 100644 --- a/docs/docs/httprequestbuilder.md +++ b/docs/docs/httprequestbuilder.md @@ -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: diff --git a/src/FluentHttpClient/HttpClientExtensions.cs b/src/FluentHttpClient/HttpClientExtensions.cs index b3a70ef..0c422fc 100644 --- a/src/FluentHttpClient/HttpClientExtensions.cs +++ b/src/FluentHttpClient/HttpClientExtensions.cs @@ -14,6 +14,13 @@ public static class HttpClientExtensions /// /// The instance to use for sending requests. /// A new instance initialized with the client's base address. + /// + /// 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 + /// as-is, so the trailing slash requirement described on UsingRoute does not apply here. It only + /// matters for derived builder types that introduce their own route, since they inherit the same + /// combination behavior. + /// public static HttpRequestBuilder UsingBase(this HttpClient client) { return new HttpRequestBuilder(client); @@ -26,6 +33,13 @@ public static HttpRequestBuilder UsingBase(this HttpClient client) /// The type of the request builder to create. /// The instance to use for sending requests. /// A new instance of initialized with the client's base address. + /// + /// 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 + /// as-is, so the trailing slash requirement described on UsingRoute does not apply here. It only + /// matters for derived builder types that introduce their own route, since they inherit the same + /// combination behavior. + /// #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 @@ -45,6 +59,17 @@ public static TBuilder UsingBase(this HttpClient client) /// The instance to use for sending requests. /// The route string for the request URI, which can be absolute or relative. /// A new instance initialized with the specified route. + /// + /// When the route is relative, must end with a trailing slash + /// for the route to be appended to it correctly. This is standard combination + /// behavior (RFC 3986 relative reference resolution), not something this library controls. For + /// example, 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. + /// public static HttpRequestBuilder UsingRoute(this HttpClient client, string route) { return new HttpRequestBuilder(client, route); @@ -58,6 +83,17 @@ public static HttpRequestBuilder UsingRoute(this HttpClient client, string route /// The instance to use for sending requests. /// The route string for the request URI, which can be absolute or relative. /// A new instance of initialized with the specified route. + /// + /// When the route is relative, must end with a trailing slash + /// for the route to be appended to it correctly. This is standard combination + /// behavior (RFC 3986 relative reference resolution), not something this library controls. For + /// example, 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. + /// #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 @@ -77,6 +113,17 @@ public static TBuilder UsingRoute(this HttpClient client, string route /// The instance to use for sending requests. /// The URI for the request. /// A new instance initialized with the specified URI. + /// + /// When the route is relative, must end with a trailing slash + /// for the route to be appended to it correctly. This is standard combination + /// behavior (RFC 3986 relative reference resolution), not something this library controls. For + /// example, 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. + /// public static HttpRequestBuilder UsingRoute(this HttpClient client, Uri uri) { return new HttpRequestBuilder(client, uri); @@ -89,6 +136,17 @@ public static HttpRequestBuilder UsingRoute(this HttpClient client, Uri uri) /// The instance to use for sending requests. /// The URI for the request. /// A new instance of initialized with the specified URI. + /// + /// When the route is relative, must end with a trailing slash + /// for the route to be appended to it correctly. This is standard combination + /// behavior (RFC 3986 relative reference resolution), not something this library controls. For + /// example, 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. + /// #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