From 045280c07745a6ca14d44ab796f96852600f1bb1 Mon Sep 17 00:00:00 2001 From: tan Date: Sat, 29 Aug 2026 17:04:05 +0530 Subject: [PATCH 1/2] fix(client): keep form-style CSV separators literal in query strings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The OAS form style (RFC6570 form-style expansion) serializes a non-exploded array as ?color=blue,black,brown — the separator comma stays literal while the items are percent-encoded (Style Examples table, OAS 3.0.4/3.1.1/3.2.0). The client escaped the fully joined query value instead, turning separators into %2C, which spec-compliant servers (including OpenAPI.jl 1.0 generated servers) read as a single item containing commas and reject. Build the query string by percent-encoding each comma-separated segment individually: form-style CSV separators stay literal, spaceDelimited and pipeDelimited separators become %20/%7C as the specification requires, and a comma in a scalar value stays literal, which is valid per RFC3986 and percent-decodes identically. Existing 0.2 servers are unaffected: they percent-decode before splitting, so literal separators decode the same way. Verified against the full test suite with live servers (3080 tests) and live against an OpenAPI.jl 1.0 generated server, where multi-value CSV query parameters from this client previously failed with 400. --- src/client.jl | 19 ++++++++++++++- test/param_deserialize.jl | 49 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 1 deletion(-) diff --git a/src/client.jl b/src/client.jl index 906192b..20b88c2 100644 --- a/src/client.jl +++ b/src/client.jl @@ -136,6 +136,23 @@ function set_param(params::Dict{String,String}, name::String, value; collection_ end end +# Escape one query parameter value for the URL. The OAS `form` style keeps the +# list separator comma literal (`?color=blue,black,brown` in the specification's +# Style Examples, per RFC6570 form-style expansion) while percent-encoding the +# items themselves. `set_param` joins collections before the item boundaries +# reach this point, so escaping each comma-separated segment individually +# preserves the separators. The spaceDelimited and pipeDelimited separators are +# percent-encoded to `%20`/`%7C`, which is what the specification requires for +# those styles. A comma inside a scalar value stays literal, which is valid in +# query strings (RFC3986 sub-delim) and percent-decodes identically. +escape_query_param_value(value::AbstractString) = + join((escapeuri(part) for part in split(value, ','; keepempty=true)), ",") + +query_string(query::Dict{String,String}) = join( + (string(escapeuri(name), "=", escape_query_param_value(value)) for (name, value) in query), + "&", +) + prep_args(ctx::Ctx) = prep_args(Val(ctx.client.httplib), ctx) response(::Type{Nothing}, resp::HTTPLibResponse, body) = nothing::Nothing @@ -178,7 +195,7 @@ function do_request(ctx::Ctx, stream::Bool=false; stream_to::Union{Channel,Nothi end # append query params if needed if !isempty(ctx.query) - resource_path = string(URIs.URI(URIs.URI(resource_path); query=escapeuri(ctx.query))) + resource_path = string(URIs.URI(URIs.URI(resource_path); query=query_string(ctx.query))) end body, kwargs = prep_args(ctx) diff --git a/test/param_deserialize.jl b/test/param_deserialize.jl index 2a9b7a6..d30ba3e 100644 --- a/test/param_deserialize.jl +++ b/test/param_deserialize.jl @@ -128,3 +128,52 @@ end end end + +@testset "Query parameter serialization" begin + # The OAS `form` style (RFC6570) keeps the collection separator comma + # literal — `?color=blue,black,brown` in the specification's Style + # Examples — while percent-encoding the items. Previously the whole query + # value was escaped, turning separators into `%2C`, which spec-compliant + # servers read as a single item containing commas. + import OpenAPI.Clients: set_param, query_string + + @testset "form-style csv keeps literal separators" begin + params = Dict{String,String}() + set_param(params, "status", ["pending", "sold"]; style="form", is_explode=false) + @test params["status"] == "pending,sold" + @test query_string(params) == "status=pending,sold" + end + + @testset "items are percent-encoded individually" begin + params = Dict{String,String}() + set_param(params, "tags", ["a b", "c/d", "x&y"]; style="form", is_explode=false) + @test query_string(params) == "tags=a%20b,c%2Fd,x%26y" + end + + @testset "space and pipe separators are percent-encoded" begin + # per the specification's Style Examples, spaceDelimited and + # pipeDelimited separators must be percent-encoded (%20 / %7C) + params = Dict{String,String}() + set_param(params, "s", ["blue", "black"]; collection_format="ssv") + set_param(params, "p", ["blue", "black"]; collection_format="pipes") + query = query_string(params) + @test occursin("s=blue%20black", query) + @test occursin("p=blue%7Cblack", query) + end + + @testset "scalar values" begin + params = Dict{String,String}() + set_param(params, "q", "a b&c") + @test query_string(params) == "q=a%20b%26c" + # a comma in a scalar stays literal: valid per RFC3986 and + # percent-decodes identically on the server + params = Dict{String,String}() + set_param(params, "note", "a,b") + @test query_string(params) == "note=a,b" + end + + @testset "keys are escaped" begin + params = Dict{String,String}("a key" => "v") + @test query_string(params) == "a%20key=v" + end +end From 2258247036a8143d8d444839299d4faeb72006e5 Mon Sep 17 00:00:00 2001 From: tan Date: Sat, 29 Aug 2026 18:18:04 +0530 Subject: [PATCH 2/2] bump patch version for tagging v0.2.9 --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 6b41141..c753c79 100644 --- a/Project.toml +++ b/Project.toml @@ -4,7 +4,7 @@ keywords = ["Swagger", "OpenAPI", "REST"] license = "MIT" desc = "OpenAPI server and client helper for Julia" authors = ["JuliaHub Inc."] -version = "0.2.8" +version = "0.2.9" [deps] Base64 = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f"