Skip to content
39 changes: 35 additions & 4 deletions Sources/OpenAPIRuntime/Conversion/Converter+Server.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

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