diff --git a/Sources/OpenAPIRuntime/Conversion/Converter+Server.swift b/Sources/OpenAPIRuntime/Conversion/Converter+Server.swift index 75b0f52..185711b 100644 --- a/Sources/OpenAPIRuntime/Conversion/Converter+Server.swift +++ b/Sources/OpenAPIRuntime/Conversion/Converter+Server.swift @@ -56,11 +56,42 @@ 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) + } + switch (parsedAcceptValue.kind, parsedSubstring.kind) { + case (.any, _): + // Accept: */* always matches + return + case (.anySubtype(type: let acceptType), let substring): + switch substring { + case .any: + // */* as a concrete content type is NOT a match for an Accept header of foo/* + break + case .anySubtype(type: let substringType): + // Only match if the types match. + if substringType.lowercased() == acceptType.lowercased() { return } + case .concrete(type: let substringType, _): + if substringType.lowercased() == acceptType.lowercased() { return } + } + case (.concrete(type: let acceptType, subtype: let acceptSubtype), let substring): + if case let .concrete(substringType, substringSubtype) = substring { + if acceptType.lowercased() == substringType.lowercased() + && acceptSubtype.lowercased() == substringSubtype.lowercased() + { + return + } + } + } + } throw RuntimeError.unexpectedAcceptHeader(acceptHeader) } diff --git a/Sources/OpenAPIRuntime/Errors/RuntimeError.swift b/Sources/OpenAPIRuntime/Errors/RuntimeError.swift index b0c776e..549e3a1 100644 --- a/Sources/OpenAPIRuntime/Errors/RuntimeError.swift +++ b/Sources/OpenAPIRuntime/Errors/RuntimeError.swift @@ -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) @@ -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))'" diff --git a/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Server.swift b/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Server.swift index b2305a0..9e16fbf 100644 --- a/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Server.swift +++ b/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Server.swift @@ -39,18 +39,20 @@ 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), @@ -58,6 +60,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 {