Skip to content

Commit 4b964d4

Browse files
authored
Fix encoding of plain text bodies (#9)
Fix encoding of plain text bodies ### Motivation When the content type `text/plain` is used, we had a bug where we were encoding the text using `Codable` into a JSON fragment, i.e. `hello` -> `"hello"` (note the added quotes), which is incorrect. We should be using `LosslessStringConvertible` for primitive types, so that `hello` is sent as `hello` (without the quotes). ### Modifications Fixed the body conversion methods to use the string conversion if possible, and only use the `Codable` implementation for complex types. ### Result When content type is `text/plain` (and other plain text variants), `hello` is sent as `hello`, not `"hello"`. ### Test Plan Updated unit tests and added new ones to cover these cases. Reviewed by: simonjbeaumont Builds: ✔︎ pull request validation (5.8) - Build finished. ✔︎ pull request validation (5.9) - Build finished. ✔︎ pull request validation (nightly) - Build finished. ✔︎ pull request validation (soundness) - Build finished. #9
1 parent 4504e17 commit 4b964d4

File tree

6 files changed

+202
-3
lines changed

6 files changed

+202
-3
lines changed

Sources/OpenAPIRuntime/Conversion/Converter+Client.swift

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,18 @@ extension Converter {
9090
from data: Data,
9191
transforming transform: (T) -> C
9292
) throws -> C {
93-
let decoded = try decoder.decode(type, from: data)
93+
let decoded: T
94+
if let myType = T.self as? _StringParameterConvertible.Type {
95+
guard
96+
let stringValue = String(data: data, encoding: .utf8),
97+
let decodedValue = myType.init(stringValue)
98+
else {
99+
throw RuntimeError.failedToDecodePrimitiveBodyFromData
100+
}
101+
decoded = decodedValue as! T
102+
} else {
103+
decoded = try decoder.decode(type, from: data)
104+
}
94105
return transform(decoded)
95106
}
96107

@@ -128,6 +139,12 @@ extension Converter {
128139
) throws -> Data {
129140
let body = transform(value)
130141
headerFields.add(name: "content-type", value: body.contentType)
142+
if let value = value as? _StringParameterConvertible {
143+
guard let data = value.description.data(using: .utf8) else {
144+
throw RuntimeError.failedToEncodePrimitiveBodyIntoData
145+
}
146+
return data
147+
}
131148
return try encoder.encode(body.value)
132149
}
133150

Sources/OpenAPIRuntime/Conversion/Converter+Server.swift

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -262,7 +262,18 @@ public extension Converter {
262262
guard let data else {
263263
return nil
264264
}
265-
let decoded = try decoder.decode(type, from: data)
265+
let decoded: T
266+
if let myType = T.self as? _StringParameterConvertible.Type {
267+
guard
268+
let stringValue = String(data: data, encoding: .utf8),
269+
let decodedValue = myType.init(stringValue)
270+
else {
271+
throw RuntimeError.failedToDecodePrimitiveBodyFromData
272+
}
273+
decoded = decodedValue as! T
274+
} else {
275+
decoded = try decoder.decode(type, from: data)
276+
}
266277
return transform(decoded)
267278
}
268279

@@ -297,7 +308,14 @@ public extension Converter {
297308
) throws -> Data {
298309
let body = transform(value)
299310
headerFields.add(name: "content-type", value: body.contentType)
300-
return try encoder.encode(body.value)
311+
let bodyValue = body.value
312+
if let value = bodyValue as? _StringParameterConvertible {
313+
guard let data = value.description.data(using: .utf8) else {
314+
throw RuntimeError.failedToEncodePrimitiveBodyIntoData
315+
}
316+
return data
317+
}
318+
return try encoder.encode(bodyValue)
301319
}
302320

303321
// MARK: Body - Data

Sources/OpenAPIRuntime/Errors/RuntimeError.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ internal enum RuntimeError: Error, CustomStringConvertible, LocalizedError, Pret
3636

3737
// Body
3838
case missingRequiredRequestBody
39+
case failedToEncodePrimitiveBodyIntoData
40+
case failedToDecodePrimitiveBodyFromData
3941

4042
// Transport/Handler
4143
case transportFailed(Error)
@@ -69,6 +71,10 @@ internal enum RuntimeError: Error, CustomStringConvertible, LocalizedError, Pret
6971
return "Failed to decode query parameter named '\(name)' to type \(type)."
7072
case .missingRequiredRequestBody:
7173
return "Missing required request body"
74+
case .failedToEncodePrimitiveBodyIntoData:
75+
return "Failed to encode a primitive body into data"
76+
case .failedToDecodePrimitiveBodyFromData:
77+
return "Failed to decode a primitive body from data"
7278
case .transportFailed(let underlyingError):
7379
return "Transport failed with error: \(underlyingError.localizedDescription)"
7480
case .handlerFailed(let underlyingError):

Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Client.swift

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,24 @@ final class Test_ClientConverterExtensions: Test_Runtime {
7373
XCTAssertEqual(body, testStruct)
7474
}
7575

76+
func testBodyGetData_success() throws {
77+
let body = try converter.bodyGet(
78+
Data.self,
79+
from: testStructData,
80+
transforming: { $0 }
81+
)
82+
XCTAssertEqual(body, testStructData)
83+
}
84+
85+
func testBodyGetString_success() throws {
86+
let body = try converter.bodyGet(
87+
String.self,
88+
from: testStringData,
89+
transforming: { $0 }
90+
)
91+
XCTAssertEqual(body, testString)
92+
}
93+
7694
func testBodyAddComplexOptional_success() throws {
7795
var headerFields: [HeaderField] = []
7896
let data = try converter.bodyAddOptional(
@@ -116,4 +134,68 @@ final class Test_ClientConverterExtensions: Test_Runtime {
116134
]
117135
)
118136
}
137+
138+
func testBodyAddDataOptional_success() throws {
139+
var headerFields: [HeaderField] = []
140+
let data = try converter.bodyAddOptional(
141+
testStructPrettyData,
142+
headerFields: &headerFields,
143+
transforming: { .init(value: $0, contentType: "application/octet-stream") }
144+
)
145+
XCTAssertEqual(data, testStructPrettyData)
146+
XCTAssertEqual(
147+
headerFields,
148+
[
149+
.init(name: "content-type", value: "application/octet-stream")
150+
]
151+
)
152+
}
153+
154+
func testBodyAddDataRequired_success() throws {
155+
var headerFields: [HeaderField] = []
156+
let data = try converter.bodyAddRequired(
157+
testStructPrettyData,
158+
headerFields: &headerFields,
159+
transforming: { .init(value: $0, contentType: "application/octet-stream") }
160+
)
161+
XCTAssertEqual(data, testStructPrettyData)
162+
XCTAssertEqual(
163+
headerFields,
164+
[
165+
.init(name: "content-type", value: "application/octet-stream")
166+
]
167+
)
168+
}
169+
170+
func testBodyAddStringOptional_success() throws {
171+
var headerFields: [HeaderField] = []
172+
let data = try converter.bodyAddOptional(
173+
testString,
174+
headerFields: &headerFields,
175+
transforming: { .init(value: $0, contentType: "text/plain") }
176+
)
177+
XCTAssertEqual(data, testStringData)
178+
XCTAssertEqual(
179+
headerFields,
180+
[
181+
.init(name: "content-type", value: "text/plain")
182+
]
183+
)
184+
}
185+
186+
func testBodyAddStringRequired_success() throws {
187+
var headerFields: [HeaderField] = []
188+
let data = try converter.bodyAddRequired(
189+
testString,
190+
headerFields: &headerFields,
191+
transforming: { .init(value: $0, contentType: "text/plain") }
192+
)
193+
XCTAssertEqual(data, testStringData)
194+
XCTAssertEqual(
195+
headerFields,
196+
[
197+
.init(name: "content-type", value: "text/plain")
198+
]
199+
)
200+
}
119201
}

Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Server.swift

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -538,6 +538,38 @@ final class Test_ServerConverterExtensions: Test_Runtime {
538538
)
539539
}
540540

541+
func testBodyAddString() throws {
542+
var headers: [HeaderField] = []
543+
let data = try converter.bodyAdd(
544+
testString,
545+
headerFields: &headers,
546+
transforming: { .init(value: $0, contentType: "text/plain") }
547+
)
548+
XCTAssertEqual(String(data: data, encoding: .utf8)!, testString)
549+
XCTAssertEqual(
550+
headers,
551+
[
552+
.init(name: "content-type", value: "text/plain")
553+
]
554+
)
555+
}
556+
557+
func testBodyAddData() throws {
558+
var headers: [HeaderField] = []
559+
let data = try converter.bodyAdd(
560+
testStructPrettyData,
561+
headerFields: &headers,
562+
transforming: { .init(value: $0, contentType: "application/octet-stream") }
563+
)
564+
XCTAssertEqual(data, testStructPrettyData)
565+
XCTAssertEqual(
566+
headers,
567+
[
568+
.init(name: "content-type", value: "application/octet-stream")
569+
]
570+
)
571+
}
572+
541573
func testBodyGetComplexOptional_success() throws {
542574
let body = try converter.bodyGetOptional(
543575
TestPet.self,
@@ -584,4 +616,40 @@ final class Test_ServerConverterExtensions: Test_Runtime {
584616
}
585617
)
586618
}
619+
620+
func testBodyGetDataOptional_success() throws {
621+
let body = try converter.bodyGetOptional(
622+
Data.self,
623+
from: testStructPrettyData,
624+
transforming: { $0 }
625+
)
626+
XCTAssertEqual(body, testStructPrettyData)
627+
}
628+
629+
func testBodyGetDataRequired_success() throws {
630+
let body = try converter.bodyGetOptional(
631+
Data.self,
632+
from: testStructPrettyData,
633+
transforming: { $0 }
634+
)
635+
XCTAssertEqual(body, testStructPrettyData)
636+
}
637+
638+
func testBodyGetStringOptional_success() throws {
639+
let body = try converter.bodyGetOptional(
640+
String.self,
641+
from: testStringData,
642+
transforming: { $0 }
643+
)
644+
XCTAssertEqual(body, testString)
645+
}
646+
647+
func testBodyGetStringRequired_success() throws {
648+
let body = try converter.bodyGetOptional(
649+
String.self,
650+
from: testStringData,
651+
transforming: { $0 }
652+
)
653+
XCTAssertEqual(body, testString)
654+
}
587655
}

Tests/OpenAPIRuntimeTests/Test_Runtime.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,14 @@ class Test_Runtime: XCTestCase {
5454
"2023-01-18T10:04:11Z"
5555
}
5656

57+
var testString: String {
58+
"hello"
59+
}
60+
61+
var testStringData: Data {
62+
"hello".data(using: .utf8)!
63+
}
64+
5765
var testStruct: TestPet {
5866
.init(name: "Fluffz")
5967
}

0 commit comments

Comments
 (0)