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
22 changes: 9 additions & 13 deletions src/SwaggerProvider.Runtime/ProvidedApiClientBase.fs
Original file line number Diff line number Diff line change
Expand Up @@ -74,19 +74,15 @@ type ProvidedApiClientBase(httpClient: HttpClient, options: JsonSerializerOption

// Use Array.IndexOf to avoid allocating a partial-application closure on every error response.
let errorIdx = System.Array.IndexOf(errorCodes, codeStr)
let! body = readBody()

if errorIdx >= 0 then
let desc = errorDescriptions[errorIdx]
let! body = readBody()
return raise(OpenApiException(code, desc, response.Headers, response.Content, body))
else
let! body = readBody()
let desc =
if errorIdx >= 0 then
errorDescriptions[errorIdx]
elif String.IsNullOrEmpty(response.ReasonPhrase) then
$"HTTP {code}"
else
response.ReasonPhrase

let desc =
if String.IsNullOrEmpty(response.ReasonPhrase) then
$"HTTP {code}"
else
response.ReasonPhrase

return raise(OpenApiException(code, desc, response.Headers, response.Content, body))
return raise(OpenApiException(code, desc, response.Headers, response.Content, body))
}
47 changes: 45 additions & 2 deletions tests/SwaggerProvider.Tests/RuntimeHelpersTests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -851,6 +851,18 @@ type private StubHttpMessageHandler(statusCode: HttpStatusCode, responseBody: st
response.Content <- new StringContent(responseBody)
Task.FromResult(response)

/// A stub handler that returns a fixed status code with an explicitly empty ReasonPhrase,
/// exercising the "HTTP {code}" fallback branch in CallAsync.
type private EmptyReasonPhraseHandler(statusCode: HttpStatusCode) =
inherit HttpMessageHandler()

override _.SendAsync(_request: HttpRequestMessage, cancellationToken: CancellationToken) =
cancellationToken.ThrowIfCancellationRequested()
let response = new HttpResponseMessage(statusCode)
response.ReasonPhrase <- ""
response.Content <- new StringContent("")
Task.FromResult(response)


module OpenApiExceptionTests =

Expand Down Expand Up @@ -1002,7 +1014,25 @@ module OpenApiExceptionTests =
()
}

/// Test types for formatObject tests β€” must be plain .NET classes with declared public properties.
[<Fact>]
let ``CallAsync uses HTTP status code in description when reason phrase is empty``() =
task {
// EmptyReasonPhraseHandler sets ReasonPhrase = "" so CallAsync falls back to "HTTP {code}".
use handler = new EmptyReasonPhraseHandler(HttpStatusCode.ServiceUnavailable)
let client = makeClient handler
use request = new HttpRequestMessage(HttpMethod.Get, "http://stub/test")

let! ex =
Assert.ThrowsAsync<Swagger.OpenApiException>(fun () ->
task {
let! _ = client.CallAsync(request, [||], [||], CancellationToken.None)
()
})

ex.StatusCode |> shouldEqual 503
ex.Message |> shouldEqual "HTTP 503"
}

type FmtSingle(name: string) =
member _.Name = name

Expand Down Expand Up @@ -1264,7 +1294,20 @@ module ToMultipartFormDataContentTests =
}

[<Fact>]
let ``toMultipartFormDataContent skips values when toParam returns null``() =
let ``toMultipartFormDataContent adds each stream in a Stream array as a separate part``() =
use stream1 = new MemoryStream([| 1uy; 2uy |])
use stream2 = new MemoryStream([| 3uy; 4uy |])
let streams: IO.Stream[] = [| stream1; stream2 |]
use content = toMultipartFormDataContent(seq { ("files", box streams) })
let parts = content |> Seq.toArray
// Each stream in the array becomes a separate multipart part
parts.Length |> shouldEqual 2

parts
|> Array.forall(fun p -> p.Headers.ContentDisposition.Name.Trim('"') = "files")
|> shouldEqual true


task {
let nestedNone = box(Some(None: string option))

Expand Down
Loading