Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Runtime] Improve parameter handling of MIME types in content types #113

Merged
merged 9 commits into from
Nov 19, 2024
41 changes: 36 additions & 5 deletions Sources/OpenAPIRuntime/Conversion/Converter+Server.swift
Original file line number Diff line number Diff line change
Expand Up @@ -56,14 +56,21 @@ extension Converter {
// Drop everything after the optional semicolon (q, extensions, ...)
value.split(separator: ";")[0].trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
}

if acceptValues.isEmpty { return }
if acceptValues.contains("*/*") { return }
if acceptValues.contains("\(substring.split(separator: "/")[0].lowercased())/*") { return }
if acceptValues.contains(where: { $0.localizedCaseInsensitiveContains(substring) }) { return }
guard let parsedSubstring = OpenAPIMIMEType(substring) else {
throw RuntimeError.invalidAcceptSubstring(substring)
}
// Look for the first match.
for acceptValue in acceptValues {
// Fast path.
if acceptValue == substring { return }
guard let parsedAcceptValue = OpenAPIMIMEType(acceptValue) else {
throw RuntimeError.invalidExpectedContentType(acceptValue)
}
simonjbeaumont marked this conversation as resolved.
Show resolved Hide resolved
if parsedSubstring.satisfies(acceptValue: parsedAcceptValue) { return }
}
throw RuntimeError.unexpectedAcceptHeader(acceptHeader)
}

/// Retrieves and decodes a path parameter as a URI-encoded value of the specified type.
///
/// - Parameters:
Expand Down Expand Up @@ -469,3 +476,27 @@ extension Converter {
)
}
}

fileprivate extension OpenAPIMIMEType {
/// Checks if the type satisfies the provided Accept header value.
/// - Parameter acceptValue: A parsed Accept header MIME type.
/// - Returns: `true` if it satisfies the Accept header, `false` otherwise.
func satisfies(acceptValue: OpenAPIMIMEType) -> Bool {
switch (acceptValue.kind, self.kind) {
case (.concrete, .any), (.concrete, .anySubtype), (.anySubtype, .any):
// The response content-type must be at least as specific as the accept header.
return false
case (.any, _):
// Accept: */* -- Any content-type satisfies the accept header.
return true
case (.anySubtype(let acceptType), .anySubtype(let substringType)),
(.anySubtype(let acceptType), .concrete(let substringType, _)):
// Accept: type/* -- The content-type should match the partially-specified accept header.
return acceptType.lowercased() == substringType.lowercased()
case (.concrete(let acceptType, let acceptSubtype), .concrete(let substringType, let substringSubtype)):
// Accept: type/subtype -- The content-type should match the concrete type.
return acceptType.lowercased() == substringType.lowercased()
&& acceptSubtype.lowercased() == substringSubtype.lowercased()
}
}
}
2 changes: 2 additions & 0 deletions Sources/OpenAPIRuntime/Errors/RuntimeError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ internal enum RuntimeError: Error, CustomStringConvertible, LocalizedError, Pret
case invalidServerURL(String)
case invalidServerVariableValue(name: String, value: String, allowedValues: [String])
case invalidExpectedContentType(String)
case invalidAcceptSubstring(String)
case invalidHeaderFieldName(String)
case invalidBase64String(String)

Expand Down Expand Up @@ -85,6 +86,7 @@ internal enum RuntimeError: Error, CustomStringConvertible, LocalizedError, Pret
return
"Invalid server variable named: '\(name)', which has the value: '\(value)', but the only allowed values are: \(allowedValues.map { "'\($0)'" }.joined(separator: ", "))"
case .invalidExpectedContentType(let string): return "Invalid expected content type: '\(string)'"
case .invalidAcceptSubstring(let string): return "Invalid Accept header content type: '\(string)'"
case .invalidHeaderFieldName(let name): return "Invalid header field name: '\(name)'"
case .invalidBase64String(let string):
return "Invalid base64-encoded string (first 128 bytes): '\(string.prefix(128))'"
Expand Down
12 changes: 9 additions & 3 deletions Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Server.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,25 +39,31 @@ final class Test_ServerConverterExtensions: Test_Runtime {
.accept: "text/html, application/xhtml+xml, application/xml;q=0.9, image/webp, */*;q=0.8"
]
let multiple: HTTPFields = [.accept: "text/plain, application/json"]
let params: HTTPFields = [.accept: "application/json; foo=bar"]
let cases: [(HTTPFields, String, Bool)] = [
// No Accept header, any string validates successfully
(emptyHeaders, "foobar", true),

// Accept: */*, any string validates successfully
(wildcard, "foobar", true),
// Accept: */*, any MIME type validates successfully
(wildcard, "foobaz/bar", true),

// Accept: text/*, so text/plain succeeds, application/json fails
(partialWildcard, "text/plain", true), (partialWildcard, "application/json", false),

// Accept: text/plain, text/plain succeeds, application/json fails
(short, "text/plain", true), (short, "application/json", false),
(short, "text/plain", true), (short, "application/json", false), (short, "application/*", false),
(short, "*/*", false),

// A bunch of acceptable content types
(long, "text/html", true), (long, "application/xhtml+xml", true), (long, "application/xml", true),
(long, "image/webp", true), (long, "application/json", true),

// Multiple values
(multiple, "text/plain", true), (multiple, "application/json", true), (multiple, "application/xml", false),

// Params
(params, "application/json; foo=bar", true), (params, "application/json; charset=utf-8; foo=bar", true),
(params, "application/json", true), (params, "text/plain", false),
]
for (headers, contentType, success) in cases {
if success {
Expand Down