diff --git a/src/SwaggerProvider.Runtime/RuntimeHelpers.fs b/src/SwaggerProvider.Runtime/RuntimeHelpers.fs index c7f07e3a..950dcbeb 100644 --- a/src/SwaggerProvider.Runtime/RuntimeHelpers.fs +++ b/src/SwaggerProvider.Runtime/RuntimeHelpers.fs @@ -222,6 +222,12 @@ module RuntimeHelpers = | :? float32 as f -> f.ToString() | :? double as d -> d.ToString() | :? Guid as g -> g.ToString() + // Byte arrays are encoded as base64 — the OpenAPI "byte" format (RFC 4648). + // Without this arm, byte[] falls through to obj.ToString() → "System.Byte[]", + // which is incorrect for path, header, cookie, and form-urlencoded parameters. + // Note: toQueryParams has its own byte[]/Option arms that use + // client.Serialize; this arm aligns with that behaviour for all other contexts. + | :? array as xs -> Convert.ToBase64String xs | _ -> // Hoist GetType() once; previously tryFormatDateOnly and tryFormatTimeOnly // each called GetType() internally, resulting in up to 3 GetType() calls for diff --git a/tests/SwaggerProvider.Tests/RuntimeHelpersTests.fs b/tests/SwaggerProvider.Tests/RuntimeHelpersTests.fs index 14ed60c0..0b533f7e 100644 --- a/tests/SwaggerProvider.Tests/RuntimeHelpersTests.fs +++ b/tests/SwaggerProvider.Tests/RuntimeHelpersTests.fs @@ -142,6 +142,32 @@ module ToParamTests = let result = toParam(box(None: TimeOnly option)) result |> shouldEqual null + [] + let ``toParam encodes byte array as base64 (OpenAPI 'byte' format)``() = + // Without the byte[] arm, obj.ToString() would yield "System.Byte[]". + // The correct encoding for the OpenAPI "byte" format is standard base64. + let bytes = [| 1uy; 2uy; 3uy |] + let result = toParam(box bytes) + result |> shouldEqual(Convert.ToBase64String bytes) + + [] + let ``toParam encodes empty byte array as empty base64 string``() = + let result = toParam(box([||]: byte[])) + result |> shouldEqual "" + + [] + let ``toParam unwraps Some(byte array) and encodes as base64``() = + // Option Some is unwrapped by the generic option arm, then toParam + // is called recursively on the inner byte[] value. + let bytes = [| 72uy; 101uy; 108uy; 108uy; 111uy |] // "Hello" in ASCII + let result = toParam(box(Some bytes)) + result |> shouldEqual(Convert.ToBase64String bytes) + + [] + let ``toParam returns null for None(byte array)``() = + let result = toParam(box(None: byte[] option)) + result |> shouldEqual null + // ── CLI enum serialization via toParam ────────────────────────────────────────────────────── // toParam must serialize string enums to their original OpenAPI wire values and integer @@ -1292,6 +1318,23 @@ module ToFormUrlEncodedContentTests = body |> shouldNotContainText "nestedNone" } + [] + let ``toFormUrlEncodedContent encodes byte array as base64``() = + // Without the byte[] arm in toParam, this produced "data=System.Byte%5B%5D". + // With the fix, byte arrays are encoded as standard base64 (OpenAPI 'byte' format). + task { + let bytes = [| 1uy; 2uy; 3uy |] + let expected = Convert.ToBase64String bytes + + use content = toFormUrlEncodedContent(seq { ("data", box bytes) }) + + let! body = content.ReadAsStringAsync() + let encodedValue = body.Substring("data=".Length) + let decodedValue = WebUtility.UrlDecode(encodedValue) + + decodedValue |> shouldEqual expected + } + module ToMultipartFormDataContentTests =