Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Availilability of zipping payload while performing mutation query #782

Closed
sauvikaktivo opened this issue Sep 22, 2019 · 5 comments
Closed
Labels
enhancement Issues outlining new things we want to do or things that will make our lives as devs easier networking-stack
Milestone

Comments

@sauvikaktivo
Copy link

sauvikaktivo commented Sep 22, 2019

The Problem I am facing

For my current project I have a mutation query

mutation updateActivities(
  $activitiesType1: [ActivitiesType1!]!, 
  $activitiesType2: [ActivitiesType2!]!,
  $activitiesType3: [ActivitiesType3!]!, 
  $activitiesType4: [ActivitiesType4!]!, 
  $activitiesType5: [activitiesType5!]!, 
  $activitiesType6: [ActivitiesType6!]!, 
  $isLifetime: Boolean!,
  $steps: [TimedValue!]!, 
  $currTime: DateTimeUtc!, 
  $timezone: String!) {
    
    updateAppleHealthkitActivities(
      activitiesType1: $activitiesType1, 
      activitiesType2: $activitiesType2, 
      activitiesType3: $activitiesType3, 
      activitiesType4: $activitiesType4,
      activitiesType5: $activitiesType5, 
      activitiesType6: $activitiesType6, 
      isLifetime: $isLifetime, 
      steps: $steps, 
      currTime: $currTime,
      timezone: $timezone) {
        ok
    }
}

Sometimes (it's unavoidable in current architecture) the total mutation payload data becomes more than 10MB in size, then, we are unable (or takes too much time for weak cellular networks) to upload the payload.

The solution I am seeking

I want to zip the payload which is getting prepared under ApolloClient.perform(mutation:queue:resultHandler:) of Apollo client using GZip. I dig into HTTPNetworkTransport.send<Operation>(operation: Operation, completionHandler: @escaping (_ response: GraphQLResponse<Operation>?, _ error: Error?) -> Void) -> Cancellable and found body data is getting prepared by serializing request body. Is there any way we can compress this data before assigning to request.httpBody?

  public func send<Operation>(operation: Operation, completionHandler: @escaping (_ response: GraphQLResponse<Operation>?, _ error: Error?) -> Void) -> Cancellable {
    var request = URLRequest(url: url)
    request.httpMethod = "POST"
    
    request.setValue("application/json", forHTTPHeaderField: "Content-Type")

    let body = requestBody(for: operation)
    request.httpBody = try! serializationFormat.serialize(value: body)

    //...
}

Thanks in advance.

@designatednerd
Copy link
Contributor

That's not currently something we handle - I'll mark this as an enhancement but I'll admit this is likely pretty low on our priority list at the moment, especially because mutations are very rarely this big.

I'd strongly recommend breaking that mutation up into smaller pieces, and I'd talk to your BE team about why they need all that data at once if they don't have the ability to upload those HealthKit data pieces separately.

@designatednerd designatednerd added the enhancement Issues outlining new things we want to do or things that will make our lives as devs easier label Sep 24, 2019
@antonbremer
Copy link

antonbremer commented Jul 7, 2020

You could create a GZippedHTTPNetowrkTransport class that implements the NetworkTransport protocol.

Create it to be very similar to Apollo's HTTPNetworkTransport class, but implement your own custom func send(...)

In there gzip the body before firing session.datatask.
You can use DataCompression or possibly there are other CocoaPods for gzipping.

As soon as you want to fire a query that needs to be Gzipped, use this class when you instance the ApolloClient

ApolloClient(networkTransport: GZippedHTTPNetworkTransport(url: url, configuration: configuration))

Hope this helps, example implementation of the GZippedHTTPNetowrkTransportclass below which is based on Apollo v0.29

Happy coding!

//
//  GZippedHTTPNetworkTransport.swift
//
//  Created by Bocaxica on 09/02/2018.
//  Copyright © 2018 Bocaxica. All rights reserved.
//
//  Permission is hereby granted, free of charge, to any person obtaining a copy
//  of this software and associated documentation files (the "Software"), to deal
//  in the Software without restriction, including without limitation the rights
//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
//  copies of the Software, and to permit persons to whom the Software is
//  furnished to do so, subject to the following conditions:
//
//  The above copyright notice and this permission notice shall be included in
//  all copies or substantial portions of the Software.
//
//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
//  THE SOFTWARE.
//

import Apollo
import DataCompression

public class GZippedHTTPNetworkTransport: NetworkTransport {
    let url: URL!
    let session: URLSession!
    let serializationFormat = JSONSerializationFormat.self
    private let sendOperationIdentifiers: Bool!
    
    /// Creates a network transport with the specified server URL and session configuration.
    ///
    /// - Parameters:
    ///   - url: The URL of a GraphQL server to connect to.
    ///   - configuration: A session configuration used to configure the session. Defaults to `URLSessionConfiguration.default`.
    ///   - sendOperationIdentifiers: Whether to send operation identifiers rather than full operation text, for use with servers that support query persistence. Defaults to false.
    public init(url: URL, configuration: URLSessionConfiguration = URLSessionConfiguration.default, sendOperationIdentifiers: Bool = false) {
        self.url = url
        self.session = URLSession(configuration: configuration)
        self.sendOperationIdentifiers = sendOperationIdentifiers
    }
    
    /// Send a GraphQL operation to a server and return a response.
    ///
    /// - Parameters:
    ///   - operation: The operation to send.
    ///   - completionHandler: A closure to call when a request completes.
    ///   - response: The response received from the server, or `nil` if an error occurred.
    ///   - error: An error that indicates why a request failed, or `nil` if the request was succesful.
    /// - Returns: An object that can be used to cancel an in progress request.
    public func send<Operation>(operation: Operation, completionHandler: @escaping (Result<GraphQLResponse<Operation.Data>, Error>) -> Void) -> Cancellable where Operation : GraphQLOperation {
        var request = URLRequest(url: url)
        request.httpMethod = "POST"
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        request.setValue("gzip", forHTTPHeaderField: "Content-Encoding")

        let rawBody = requestBody(for: operation)
        let unzippedBody = try! serializationFormat.serialize(value: rawBody)
        request.httpBody = unzippedBody.gzip()
        
        let task = session.dataTask(with: request) { (data: Data?, response: URLResponse?, error: Error?) in
            if let error = error {
                completionHandler(.failure(error))
                return
            }
            
            guard let httpResponse = response as? HTTPURLResponse else {
                fatalError("Response should be an HTTPURLResponse")
            }
            
            if (!httpResponse.isSuccessful) {
                completionHandler(.failure(GraphQLHTTPResponseError(body: data, response: httpResponse, kind: .errorResponse)))
                return
            }

            guard let data = data else {
                completionHandler(.failure(GraphQLHTTPResponseError(body: nil, response: httpResponse, kind: .invalidResponse)))
                return
            }
            do {
                guard let body =  try self.serializationFormat.deserialize(data: data) as? JSONObject else {
                    throw GraphQLHTTPResponseError(body: data, response: httpResponse, kind: .invalidResponse)
                }
                let response = GraphQLResponse(operation: operation, body: body)
                completionHandler(.success(response))
            } catch {
                completionHandler(.failure(error))
            }
        }

        task.resume()
        return task
    }
    
    private func requestBody<Operation: GraphQLOperation>(for operation: Operation) -> GraphQLMap {
        if sendOperationIdentifiers {
            guard let operationIdentifier = operation.operationIdentifier else {
                preconditionFailure("To send operation identifiers, Apollo types must be generated with operationIdentifiers")
            }
            return ["id": operationIdentifier, "variables": operation.variables]
        }
        return ["query": operation.queryDocument, "variables": operation.variables]
    }
}

extension HTTPURLResponse {
    var isSuccessful: Bool {
        return (200..<300).contains(statusCode)
    }
    
    var statusCodeDescription: String {
        return HTTPURLResponse.localizedString(forStatusCode: statusCode)
    }
    
    var textEncoding: String.Encoding? {
        guard let encodingName = textEncodingName else { return nil }
        
        return String.Encoding(rawValue: CFStringConvertEncodingToNSStringEncoding(CFStringConvertIANACharSetNameToEncoding(encodingName as CFString)))
    }
}

/// A transport-level, HTTP-specific error.
public struct GraphQLHTTPResponseError: Error, LocalizedError {
    public enum ErrorKind {
        case errorResponse
        case invalidResponse
        
        var description: String {
            switch self {
            case .errorResponse:
                return "Received error response"
            case .invalidResponse:
                return "Received invalid response"
            }
        }
    }
    
    /// The body of the response.
    public let body: Data?
    /// Information about the response as provided by the server.
    public let response: HTTPURLResponse
    public let kind: ErrorKind
    
    public var bodyDescription: String {
        if let body = body {
            if let description = String(data: body, encoding: response.textEncoding ?? .utf8) {
                return description
            } else {
                return "Unreadable response body"
            }
        } else {
            return "Empty response body"
        }
    }
    
    public var errorDescription: String? {
        return "\(kind.description) (\(response.statusCode) \(response.statusCodeDescription)): \(bodyDescription)"
    }
}

@designatednerd
Copy link
Contributor

@sauvikaktivo Please check out the RFC for networking changes at #1340 - I believe you should be able to make a subclass of HTTPRequest in the proposed structure that can gzip data for you.

@designatednerd
Copy link
Contributor

We are about to beta the new networking stack and indeed, creating a subclass of HTTPRequest and/or JSONRequest should be the way to make this work. Please check out the implementation in #1341.

@designatednerd designatednerd added this to the Networking Beta milestone Sep 9, 2020
@designatednerd
Copy link
Contributor

The networking stack is now available as a beta at 0.33.0-beta1 (available via the betas/networking-stack branch and through PR #1386). Going to close this out, please open a new issue if your problems aren't addressed by the new networking stacks.

@designatednerd designatednerd modified the milestones: Networking Beta, Next Release Sep 29, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement Issues outlining new things we want to do or things that will make our lives as devs easier networking-stack
Projects
None yet
Development

No branches or pull requests

3 participants