Skip to content
Open
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
4 changes: 2 additions & 2 deletions Sources/OpenAPIRuntime/Conversion/Converter+Server.swift
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ extension Converter {
configuration: .init(
style: .init(style),
explode: explode,
spaceEscapingCharacter: .percentEncoded,
spaceEscapingCharacter: .plus,
dateTranscoder: configuration.dateTranscoder
)
)
Expand Down Expand Up @@ -177,7 +177,7 @@ extension Converter {
configuration: .init(
style: .init(style),
explode: explode,
spaceEscapingCharacter: .percentEncoded,
spaceEscapingCharacter: .plus,
dateTranscoder: configuration.dateTranscoder
)
)
Expand Down
9 changes: 8 additions & 1 deletion Sources/OpenAPIRuntime/Conversion/CurrencyExtensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -296,7 +296,14 @@ extension Converter {
/// - Returns: The decoded value, or `nil` if the query item isn't present.
/// - Throws: An error if decoding the query item or the JSON value fails.
func convertJSONQueryItemToCodable<T: Decodable>(_ query: Substring, name: String) throws -> T? {
let uriDecoder = URIDecoder(configuration: uriCoderConfiguration(style: .form, explode: true, inBody: false))
let uriDecoder = URIDecoder(
configuration: .init(
style: .form,
explode: true,
spaceEscapingCharacter: .plus,
dateTranscoder: configuration.dateTranscoder
)
)
guard let json = try uriDecoder.decodeIfPresent(String.self, forKey: name, from: query) else { return nil }
return try decoder.decode(T.self, from: Data(json.utf8))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,16 @@ public import HTTPTypes
/// query string to `%20`, so that requests from clients that use
/// `application/x-www-form-urlencoded` conventions decode correctly.
///
/// - Important: Deprecated. The server now decodes `+` as a space in query
/// items, so this middleware is no longer needed and can be removed.
///
/// The OpenAPI specification follows RFC 3986, which requires spaces in query
/// strings to be percent-encoded as `%20`. However, many widely-used HTTP
/// client libraries — including Python's `requests`, Java's `URLEncoder`,
/// Scala's `sttp`, and JavaScript's `URLSearchParams` — encode spaces as `+`,
/// following the `application/x-www-form-urlencoded` convention. Without this
/// middleware, query parameters sent from such clients arrive at handlers with
/// literal `+` characters instead of spaces.
/// following the `application/x-www-form-urlencoded` convention. Previously,
/// query parameters sent from such clients arrived at handlers with literal
/// `+` characters instead of spaces.
///
/// This middleware rewrites `+` to `%20` in the query portion of the request
/// path before the request is parsed, so both encoding conventions produce the
Expand All @@ -41,6 +44,7 @@ public import HTTPTypes
///
/// - Note: Only the query of the request is modified. The path, fragment, and
/// body are not touched.
@available(*, deprecated, message: "`+` in query items is now decoded as a space by default; remove this middleware.")
public struct QuerySpaceNormalizingMiddleware: ServerMiddleware {
/// Creates a new middleware.
public init() {}
Expand Down
45 changes: 45 additions & 0 deletions Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Server.swift
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,28 @@ final class Test_ServerConverterExtensions: Test_Runtime {
XCTAssertEqual(value, "foo")
}

func test_getOptionalQueryItemAsURI_string_plusAsSpace() throws {
let value: String? = try converter.getOptionalQueryItemAsURI(
in: "search=foo+bar%20baz%2Bqux",
style: nil,
explode: nil,
name: "search",
as: String.self
)
XCTAssertEqual(value, "foo bar baz+qux")
}

func test_getRequiredQueryItemAsURI_string_plusAsSpace() throws {
let value: String = try converter.getRequiredQueryItemAsURI(
in: "search=foo+bar%20baz%2Bqux",
style: nil,
explode: nil,
name: "search",
as: String.self
)
XCTAssertEqual(value, "foo bar baz+qux")
}

func test_getOptionalQueryItemAsURI_arrayOfStrings() throws {
let query: Substring = "search=foo&search=bar"
let value: [String]? = try converter.getOptionalQueryItemAsURI(
Expand Down Expand Up @@ -234,6 +256,18 @@ final class Test_ServerConverterExtensions: Test_Runtime {
XCTAssertEqual(value, ["foo", "bar"])
}

func test_getRequiredQueryItemAsURI_arrayOfStrings_plusAsSpace() throws {
let query: Substring = "search=foo+bar&search=baz%2Bqux"
let value: [String] = try converter.getRequiredQueryItemAsURI(
in: query,
style: nil,
explode: nil,
name: "search",
as: [String].self
)
XCTAssertEqual(value, ["foo bar", "baz+qux"])
}

func test_getRequiredQueryItemAsURI_arrayOfStrings_unexploded() throws {
let query: Substring = "search=foo,bar"
let value: [String] = try converter.getRequiredQueryItemAsURI(
Expand Down Expand Up @@ -298,6 +332,17 @@ final class Test_ServerConverterExtensions: Test_Runtime {
XCTAssertEqual(value, [Filter(id: 1, name: "a/b")])
}

func test_getRequiredQueryItemAsJSON_plusAsSpace() throws {
let value = try converter.getRequiredQueryItemAsJSON(
in: "filter=%22foo+bar%2Bbaz%22",
style: nil,
explode: nil,
name: "filter",
as: String.self
)
XCTAssertEqual(value, "foo bar+baz")
}

func test_getOptionalQueryItemAsJSON_absent() throws {
let value = try converter.getOptionalQueryItemAsJSON(
in: "existing=1",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import HTTPTypes
import XCTest
@_spi(Generated) @testable import OpenAPIRuntime

final class Test_QuerySpaceNormalizingMiddleware: XCTestCase {
@available(*, deprecated) final class Test_QuerySpaceNormalizingMiddleware: XCTestCase {
static let middleware = QuerySpaceNormalizingMiddleware()

func testPlusInQueryIsReplacedWithPercent20() async throws {
Expand Down