Skip to content

Commit

Permalink
fix: Multipart chunk content type (#572)
Browse files Browse the repository at this point in the history
  • Loading branch information
calvincestari authored Jan 8, 2025
1 parent 5272703 commit 103805e
Show file tree
Hide file tree
Showing 5 changed files with 115 additions and 22 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,56 @@ final class MultipartResponseDeferParserTests: XCTestCase {
wait(for: [expectation], timeout: defaultTimeout)
}

func test__parsing__givenSingleChunk_withMultipleContentTypeDirectives_shouldReturnSuccess() throws {
let subject = InterceptorTester(interceptor: MultipartResponseParsingInterceptor())

let expectation = expectation(description: "Received callback")

let expected: JSONObject = [
"data": [
"key": "value"
],
"hasNext": true
]

subject.intercept(
request: .mock(operation: MockQuery.mock()),
response: .mock(
headerFields: ["Content-Type": "multipart/mixed;boundary=graphql;deferSpec=20220824"],
data: """
--graphql
Content-Type: application/json; charset=utf-8
{
"data" : {
"key" : "value"
},
"hasNext": true
}
--graphql--
""".crlfFormattedData()
)
) { result in
defer {
expectation.fulfill()
}

expect(result).to(beSuccess())

guard
let response = try! result.get(),
let deserialized = try! JSONSerialization.jsonObject(with: response.rawData) as? JSONObject
else {
return fail("data could not be deserialized!")
}

expect(deserialized).to(equal(expected))
}

wait(for: [expectation], timeout: defaultTimeout)
}

func test__parsing__givenMultipleChunks_shouldReturnMultipleSuccess() throws {
let subject = InterceptorTester(interceptor: MultipartResponseParsingInterceptor())

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -475,6 +475,47 @@ final class MultipartResponseSubscriptionParserTests: XCTestCase {
wait(for: [expectation], timeout: defaultTimeout)
}

func test__parsing__givenSingleChunk_withMultipleContentTypeDirectives_shouldReturnSuccess() throws {
let network = buildNetworkTransport(responseData: """
--graphql
Content-Type: application/json; charset=utf-8
{
"payload": {
"data": {
"__typename": "Time",
"ticker": 1
}
}
}
--graphql--
""".crlfFormattedData()
)

let expectedData = try Time(data: [
"__typename": "Time",
"ticker": 1
], variables: nil)

let expectation = expectation(description: "Multipart data received")

_ = network.send(operation: MockSubscription<Time>()) { result in
defer {
expectation.fulfill()
}

switch (result) {
case let .success(data):
expect(data.data).to(equal(expectedData))
case let .failure(error):
fail("Unexpected failure result - \(error)")
}
}

wait(for: [expectation], timeout: defaultTimeout)
}

func test__parsing__givenSingleChunk_withDashBoundaryInMessageBody_shouldReturnSuccess() throws {
let multipartBoundary = "-"
let network = buildNetworkTransport(
Expand Down
18 changes: 6 additions & 12 deletions apollo-ios/Sources/Apollo/MultipartResponseDeferParser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ struct MultipartResponseDeferParser: MultipartResponseSpecificationParser {
}

private enum DataLine {
case contentHeader(type: String)
case contentHeader(directives: [String])
case json(object: JSONObject)
case unknown

Expand All @@ -32,14 +32,8 @@ struct MultipartResponseDeferParser: MultipartResponseSpecificationParser {
}

private static func parse(_ dataLine: String) -> DataLine {
var contentTypeHeader: StaticString { "content-type:" }

if dataLine.starts(with: contentTypeHeader.description) {
let contentType = (dataLine
.components(separatedBy: ":").last ?? dataLine
).trimmingCharacters(in: .whitespaces)

return .contentHeader(type: contentType)
if let directives = dataLine.parseContentTypeDirectives() {
return .contentHeader(directives: directives)
}

if
Expand All @@ -58,9 +52,9 @@ struct MultipartResponseDeferParser: MultipartResponseSpecificationParser {
static func parse(_ chunk: String) -> Result<Data?, any Error> {
for dataLine in chunk.components(separatedBy: Self.dataLineSeparator.description) {
switch DataLine(dataLine.trimmingCharacters(in: .newlines)) {
case let .contentHeader(type):
guard type == "application/json" else {
return .failure(ParsingError.unsupportedContentType(type: type))
case let .contentHeader(directives):
guard directives.contains("application/json") else {
return .failure(ParsingError.unsupportedContentType(type: directives.joined(separator: ";")))
}

case let .json(object):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -182,4 +182,16 @@ extension String {

return nil
}

func parseContentTypeDirectives() -> [String]? {
var lowercasedContentTypeHeader: StaticString { "content-type:" }

guard lowercased().starts(with: lowercasedContentTypeHeader.description) else {
return nil
}

return dropFirst(lowercasedContentTypeHeader.description.count)
.components(separatedBy: ";")
.map({ $0.trimmingCharacters(in: .whitespaces) })
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ struct MultipartResponseSubscriptionParser: MultipartResponseSpecificationParser

private enum DataLine {
case heartbeat
case contentHeader(type: String)
case contentHeader(directives: [String])
case json(object: JSONObject)
case unknown

Expand All @@ -46,12 +46,8 @@ struct MultipartResponseSubscriptionParser: MultipartResponseSpecificationParser
return .heartbeat
}

if dataLine.lowercased().starts(with: contentTypeHeader.description) {
let contentType = (dataLine
.components(separatedBy: ":").last ?? dataLine
).trimmingCharacters(in: .whitespaces)

return .contentHeader(type: contentType)
if let directives = dataLine.parseContentTypeDirectives() {
return .contentHeader(directives: directives)
}

if
Expand All @@ -74,9 +70,9 @@ struct MultipartResponseSubscriptionParser: MultipartResponseSpecificationParser
// Periodically sent by the router - noop
break

case let .contentHeader(type):
guard type == "application/json" else {
return .failure(ParsingError.unsupportedContentType(type: type))
case let .contentHeader(directives):
guard directives.contains("application/json") else {
return .failure(ParsingError.unsupportedContentType(type: directives.joined(separator: ";")))
}

case let .json(object):
Expand Down

0 comments on commit 103805e

Please sign in to comment.