From 4629473e6d81b894eeed56d2f35eccbf1e6d5d09 Mon Sep 17 00:00:00 2001 From: Calvin Cestari Date: Mon, 16 Dec 2024 15:36:46 -0800 Subject: [PATCH 1/5] Support new content type --- apollo-ios/Sources/Apollo/RequestChainNetworkTransport.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apollo-ios/Sources/Apollo/RequestChainNetworkTransport.swift b/apollo-ios/Sources/Apollo/RequestChainNetworkTransport.swift index 0405e1394..98b49fc3b 100644 --- a/apollo-ios/Sources/Apollo/RequestChainNetworkTransport.swift +++ b/apollo-ios/Sources/Apollo/RequestChainNetworkTransport.swift @@ -103,13 +103,13 @@ open class RequestChainNetworkTransport: NetworkTransport { if Operation.operationType == .subscription { request.addHeader( name: "Accept", - value: "multipart/mixed;\(MultipartResponseSubscriptionParser.protocolSpec),application/json" + value: "multipart/mixed;\(MultipartResponseSubscriptionParser.protocolSpec),application/graphql-response+json,application/json" ) } else { request.addHeader( name: "Accept", - value: "multipart/mixed;\(MultipartResponseDeferParser.protocolSpec),application/json" + value: "multipart/mixed;\(MultipartResponseDeferParser.protocolSpec),application/graphql-response+json,application/json" ) } From c02a3ff866df553762fc4a5b66e28c03e909658d Mon Sep 17 00:00:00 2001 From: Calvin Cestari Date: Mon, 16 Dec 2024 15:37:15 -0800 Subject: [PATCH 2/5] Refactor content type error messages --- .../Sources/Apollo/MultipartResponseDeferParser.swift | 6 ++++-- .../Apollo/MultipartResponseSubscriptionParser.swift | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/apollo-ios/Sources/Apollo/MultipartResponseDeferParser.swift b/apollo-ios/Sources/Apollo/MultipartResponseDeferParser.swift index e959be77a..96827928a 100644 --- a/apollo-ios/Sources/Apollo/MultipartResponseDeferParser.swift +++ b/apollo-ios/Sources/Apollo/MultipartResponseDeferParser.swift @@ -13,7 +13,7 @@ struct MultipartResponseDeferParser: MultipartResponseSpecificationParser { switch self { case let .unsupportedContentType(type): - return "Unsupported content type: application/json is required but got \(type)." + return "Unsupported content type: 'application/graphql-response+json' or 'application/json' are supported, received '\(type)'." case .cannotParseChunkData: return "The chunk data could not be parsed." case .cannotParsePayloadData: @@ -53,7 +53,9 @@ struct MultipartResponseDeferParser: MultipartResponseSpecificationParser { for dataLine in chunk.components(separatedBy: Self.dataLineSeparator.description) { switch DataLine(dataLine.trimmingCharacters(in: .newlines)) { case let .contentHeader(directives): - guard directives.contains("application/json") else { + guard directives.contains(where: { directive in + directive == "application/json" || directive == "application/graphql-response+json" + }) else { return .failure(ParsingError.unsupportedContentType(type: directives.joined(separator: ";"))) } diff --git a/apollo-ios/Sources/Apollo/MultipartResponseSubscriptionParser.swift b/apollo-ios/Sources/Apollo/MultipartResponseSubscriptionParser.swift index 72985d1c3..9eba3bd2f 100644 --- a/apollo-ios/Sources/Apollo/MultipartResponseSubscriptionParser.swift +++ b/apollo-ios/Sources/Apollo/MultipartResponseSubscriptionParser.swift @@ -15,7 +15,7 @@ struct MultipartResponseSubscriptionParser: MultipartResponseSpecificationParser switch self { case let .unsupportedContentType(type): - return "Unsupported content type: application/json is required but got \(type)." + return "Unsupported content type: 'application/graphql-response+json' or 'application/json' are supported, received '\(type)'." case .cannotParseChunkData: return "The chunk data could not be parsed." case let .irrecoverableError(message): @@ -71,7 +71,9 @@ struct MultipartResponseSubscriptionParser: MultipartResponseSpecificationParser break case let .contentHeader(directives): - guard directives.contains("application/json") else { + guard directives.contains(where: { directive in + directive == "application/json" || directive == "application/graphql-response+json" + }) else { return .failure(ParsingError.unsupportedContentType(type: directives.joined(separator: ";"))) } From 3cc4cb0e48aa73226078fd5e1d2cc41fabee92b9 Mon Sep 17 00:00:00 2001 From: Calvin Cestari Date: Mon, 16 Dec 2024 15:37:51 -0800 Subject: [PATCH 3/5] Update tests --- .../MultipartResponseDeferParserTests.swift | 50 +++++++++++++++++++ ...ipartResponseSubscriptionParserTests.swift | 41 +++++++++++++++ Tests/ApolloTests/RequestChainTests.swift | 12 ++--- 3 files changed, 97 insertions(+), 6 deletions(-) diff --git a/Tests/ApolloTests/Interceptors/MultipartResponseDeferParserTests.swift b/Tests/ApolloTests/Interceptors/MultipartResponseDeferParserTests.swift index f74805136..98f335b6c 100644 --- a/Tests/ApolloTests/Interceptors/MultipartResponseDeferParserTests.swift +++ b/Tests/ApolloTests/Interceptors/MultipartResponseDeferParserTests.swift @@ -218,6 +218,56 @@ final class MultipartResponseDeferParserTests: XCTestCase { wait(for: [expectation], timeout: defaultTimeout) } + func test__parsing__givenSingleChunk_withGraphQLOverHTTPContentType_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/graphql-response+json + + { + "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()) diff --git a/Tests/ApolloTests/Interceptors/MultipartResponseSubscriptionParserTests.swift b/Tests/ApolloTests/Interceptors/MultipartResponseSubscriptionParserTests.swift index 9a561f750..ebf067117 100644 --- a/Tests/ApolloTests/Interceptors/MultipartResponseSubscriptionParserTests.swift +++ b/Tests/ApolloTests/Interceptors/MultipartResponseSubscriptionParserTests.swift @@ -516,6 +516,47 @@ final class MultipartResponseSubscriptionParserTests: XCTestCase { wait(for: [expectation], timeout: defaultTimeout) } + func test__parsing__givenSingleChunk_withGraphQLOverHTTPContentType_shouldReturnSuccess() throws { + let network = buildNetworkTransport(responseData: """ + + --graphql + content-type: application/graphql-response+json + + { + "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