diff --git a/src/SwaggerProvider.Runtime/RuntimeHelpers.fs b/src/SwaggerProvider.Runtime/RuntimeHelpers.fs index 4807e259..504c38f3 100644 --- a/src/SwaggerProvider.Runtime/RuntimeHelpers.fs +++ b/src/SwaggerProvider.Runtime/RuntimeHelpers.fs @@ -343,7 +343,7 @@ module RuntimeHelpers = let vTy = v.GetType() if vTy = typeof then - String.Format("\"{0}\"", v) + "\"" + v.ToString() + "\"" elif vTy.IsArray then let elements = (v :?> Array) @@ -351,13 +351,13 @@ module RuntimeHelpers = |> Seq.map(fun x -> if isNull x then "null" else x.ToString()) |> Array.ofSeq - String.Format("[{0}]", String.Join("; ", elements)) + "[" + String.Join("; ", elements) + "]" else v.ToString() - String.Format("{0}={1}", p.Name, s)) + p.Name + "=" + s) - String.Format("{{{0}}}", String.Join("; ", strs)) + "{" + String.Join("; ", strs) + "}" // Cached constructor for JsonPropertyNameAttribute to avoid repeated reflection lookups // when compiling large schemas with many properties. @@ -501,7 +501,7 @@ module RuntimeHelpers = new HttpClient(handler, true, BaseAddress = Uri(host)) let combineUrl (urlA: string) (urlB: string) = - sprintf "%s/%s" (urlA.TrimEnd('/')) (urlB.TrimStart('/')) + urlA.TrimEnd('/') + "/" + urlB.TrimStart('/') // Pre-built map of standard HTTP method names to their corresponding static HttpMethod // instances. Uses an ordinal case-insensitive comparer so callers passing different @@ -529,18 +529,26 @@ module RuntimeHelpers = | true, m -> m | false, _ -> HttpMethod(method.ToUpperInvariant()) - let createHttpRequest (httpMethod: string) address queryParams = + let createHttpRequest (httpMethod: string) (address: string) (queryParams: seq) = let requestUrl = - let fakeHost = "http://fake-host/" - let builder = UriBuilder(combineUrl fakeHost address) - let query = System.Web.HttpUtility.ParseQueryString(builder.Query) + // Fast path: avoid UriBuilder + ParseQueryString allocation when there are no query params. + // TrimStart('/') mirrors the UriBuilder path's PathAndQuery.TrimStart('/') normalisation, + // which strips the leading slash from schema paths such as "/pets" → "pets". A leading- + // slash relative URI resolves from the host root and silently drops any base path, so + // normalisation must be applied on both branches. + if Seq.isEmpty queryParams then + address.TrimStart('/') + else + let fakeHost = "http://fake-host/" + let builder = UriBuilder(combineUrl fakeHost address) + let query = System.Web.HttpUtility.ParseQueryString(builder.Query) - for name, value in queryParams do - if not <| isNull value then - query.Add(name, value) + for name, value in queryParams do + if not <| isNull value then + query.Add(name, value) - builder.Query <- query.ToString() - builder.Uri.PathAndQuery.TrimStart('/') + builder.Query <- query.ToString() + builder.Uri.PathAndQuery.TrimStart('/') let method = resolveHttpMethod httpMethod new HttpRequestMessage(method, Uri(requestUrl, UriKind.Relative)) diff --git a/tests/SwaggerProvider.Tests/RuntimeHelpersTests.fs b/tests/SwaggerProvider.Tests/RuntimeHelpersTests.fs index 5a1bbb85..e36ee868 100644 --- a/tests/SwaggerProvider.Tests/RuntimeHelpersTests.fs +++ b/tests/SwaggerProvider.Tests/RuntimeHelpersTests.fs @@ -553,6 +553,27 @@ module CreateHttpRequestTests = use req = createHttpRequest "GET" "v1/pets/42" [] req.RequestUri.ToString() |> shouldContainText "v1/pets/42" + [] + let ``createHttpRequest strips leading slash from path (no query params)``() = + // OpenAPI schema path keys start with '/'; the leading slash must be removed so that + // the relative URI is resolved relative to the HttpClient.BaseAddress path rather + // than from the host root. + use req = createHttpRequest "GET" "/pets" [] + req.RequestUri.ToString() |> shouldEqual "pets" + + [] + let ``createHttpRequest strips leading slash from path (with query params)``() = + use req = createHttpRequest "GET" "/pets" [ ("status", "available") ] + let uri = req.RequestUri.ToString() + uri |> shouldNotContainText "/pets" + uri |> shouldContainText "pets" + uri |> shouldContainText "status=available" + + [] + let ``createHttpRequest strips leading slash from nested path (no query params)``() = + use req = createHttpRequest "GET" "/pets/42" [] + req.RequestUri.ToString() |> shouldEqual "pets/42" + module FillHeadersTests =