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
31 changes: 27 additions & 4 deletions Sources/OpenAPIRuntime/Conversion/Converter+Server.swift
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,34 @@ 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)
}
guard case .concrete(let substringType, let substringSubtype) = parsedSubstring.kind else {
// If the substring content type has a wildcard, just let it through.
// It's not well defined how such a case should behave, so be permissive.
return
}
simonjbeaumont marked this conversation as resolved.
Show resolved Hide resolved

// 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
switch parsedAcceptValue.kind {
case .any: return
case .anySubtype(type: let type): if substringType.lowercased() == type.lowercased() { return }
case .concrete(type: let type, subtype: let subtype):
if type.lowercased() == substringType.lowercased()
&& subtype.lowercased() == substringSubtype.lowercased()
{
return
}
}
}
throw RuntimeError.unexpectedAcceptHeader(acceptHeader)
}

Expand Down
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
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,13 @@ 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),
Expand All @@ -58,6 +59,10 @@ final class Test_ServerConverterExtensions: Test_Runtime {

// 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