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

File Upload Issue #1015

Closed
m-skolnick opened this issue Feb 13, 2020 · 31 comments
Closed

File Upload Issue #1015

m-skolnick opened this issue Feb 13, 2020 · 31 comments

Comments

@m-skolnick
Copy link

Hello. I'm also having a file upload issue. It may be related to #1013 I tried the docs, and this reply: kimdv commented on Aug 7, 2019 all to no avail. At this point I'm not really sure what else to try. Any help would be appreciated. Thanks in advance.

Mutation (The generated class is at the bottom because of length)

mutation UploadFile($file: Upload!) {
  fileUpload(input: { files: [$file] }) {
    uploads {
      file {
        id
        url
      }
      path
    }
  }
}

UploadFile

private func sendAttachment(attachment: Attachment) {
        guard let name = attachment.filename else {
            print("Name was null" )
            return
        }
        guard let data = attachment.data else {
            print("Data was null")
            return
        }
        guard let contentType = attachment.contentType else {
            print("ContentType was null ")
            return
        }

        let file = GraphQLFile(fieldName: name, originalName: name, mimeType: contentType, data: data)
        GQLClient.shared.apollo.upload(operation: UploadFileMutation(file: name), files: [file]) { result in
            switch result {
            case .success:
                print("Image upload success")
                print("\(result)")
            case .failure(let error):
                print("Image upload failed")
                print(error.localizedDescription)
                print(error)
            }
        }
    }

Error Response

error.localizedDescription

Received error response (400 bad request): {"error":"Bad request","value":"Unexpected token - in JSON at position 0"}

error

GraphQLHTTPResponseError(body: Optional(155 bytes), response: <NSHTTPURLResponse: 0x28299cce0> { URL: https://MYURL/gql } { Status Code: 500, Headers {
    "Content-Length" =     (
        155
    );
    "Content-Type" =     (
        "application/json; charset=utf-8"
    );
    Date =     (
        "Thu, 13 Feb 2020 19:53:41 GMT"
    );
    Etag =     (
        "W/\"9b-IZ31rUW4AdpvxLvcMC41QuvxcC4\""
    );
    "Strict-Transport-Security" =     (
        "max-age=15724800; includeSubDomains"
    );
    "x-powered-by" =     (
        Express
    );
} }, kind: Apollo.GraphQLHTTPResponseError.ErrorKind.errorResponse, serializationFormat: Apollo.JSONSerializationFormat)

Generated Mutation

public final class UploadFileMutation: GraphQLMutation {
  /// The raw GraphQL definition of this operation.
  public let operationDefinition =
    """
    mutation UploadFile($file: Upload!) {
      fileUpload(input: {files: [$file]}) {
        __typename
        uploads {
          __typename
          file {
            __typename
            id
            url
          }
          path
        }
      }
    }
    """

  public let operationName = "UploadFile"

  public var file: Upload

  public init(file: Upload) {
    self.file = file
  }

  public var variables: GraphQLMap? {
    return ["file": file]
  }

  public struct Data: GraphQLSelectionSet {
    public static let possibleTypes = ["Mutation"]

    public static let selections: [GraphQLSelection] = [
      GraphQLField("fileUpload", arguments: ["input": ["files": [GraphQLVariable("file")]]], type: .nonNull(.object(FileUpload.selections))),
    ]

    public private(set) var resultMap: ResultMap

    public init(unsafeResultMap: ResultMap) {
      self.resultMap = unsafeResultMap
    }

    public init(fileUpload: FileUpload) {
      self.init(unsafeResultMap: ["__typename": "Mutation", "fileUpload": fileUpload.resultMap])
    }

    public var fileUpload: FileUpload {
      get {
        return FileUpload(unsafeResultMap: resultMap["fileUpload"]! as! ResultMap)
      }
      set {
        resultMap.updateValue(newValue.resultMap, forKey: "fileUpload")
      }
    }

    public struct FileUpload: GraphQLSelectionSet {
      public static let possibleTypes = ["UploadMutationPayload"]

      public static let selections: [GraphQLSelection] = [
        GraphQLField("__typename", type: .nonNull(.scalar(String.self))),
        GraphQLField("uploads", type: .nonNull(.list(.nonNull(.object(Upload.selections))))),
      ]

      public private(set) var resultMap: ResultMap

      public init(unsafeResultMap: ResultMap) {
        self.resultMap = unsafeResultMap
      }

      public init(uploads: [Upload]) {
        self.init(unsafeResultMap: ["__typename": "UploadMutationPayload", "uploads": uploads.map { (value: Upload) -> ResultMap in value.resultMap }])
      }

      public var __typename: String {
        get {
          return resultMap["__typename"]! as! String
        }
        set {
          resultMap.updateValue(newValue, forKey: "__typename")
        }
      }

      public var uploads: [Upload] {
        get {
          return (resultMap["uploads"] as! [ResultMap]).map { (value: ResultMap) -> Upload in Upload(unsafeResultMap: value) }
        }
        set {
          resultMap.updateValue(newValue.map { (value: Upload) -> ResultMap in value.resultMap }, forKey: "uploads")
        }
      }

      public struct Upload: GraphQLSelectionSet {
        public static let possibleTypes = ["UploadResult"]

        public static let selections: [GraphQLSelection] = [
          GraphQLField("__typename", type: .nonNull(.scalar(String.self))),
          GraphQLField("file", type: .object(File.selections)),
          GraphQLField("path", type: .nonNull(.scalar(String.self))),
        ]

        public private(set) var resultMap: ResultMap

        public init(unsafeResultMap: ResultMap) {
          self.resultMap = unsafeResultMap
        }

        public init(file: File? = nil, path: String) {
          self.init(unsafeResultMap: ["__typename": "UploadResult", "file": file.flatMap { (value: File) -> ResultMap in value.resultMap }, "path": path])
        }

        public var __typename: String {
          get {
            return resultMap["__typename"]! as! String
          }
          set {
            resultMap.updateValue(newValue, forKey: "__typename")
          }
        }

        public var file: File? {
          get {
            return (resultMap["file"] as? ResultMap).flatMap { File(unsafeResultMap: $0) }
          }
          set {
            resultMap.updateValue(newValue?.resultMap, forKey: "file")
          }
        }

        public var path: String {
          get {
            return resultMap["path"]! as! String
          }
          set {
            resultMap.updateValue(newValue, forKey: "path")
          }
        }

        public struct File: GraphQLSelectionSet {
          public static let possibleTypes = ["File"]

          public static let selections: [GraphQLSelection] = [
            GraphQLField("__typename", type: .nonNull(.scalar(String.self))),
            GraphQLField("id", type: .nonNull(.scalar(GraphQLID.self))),
            GraphQLField("url", type: .nonNull(.scalar(String.self))),
          ]

          public private(set) var resultMap: ResultMap

          public init(unsafeResultMap: ResultMap) {
            self.resultMap = unsafeResultMap
          }

          public init(id: GraphQLID, url: String) {
            self.init(unsafeResultMap: ["__typename": "File", "id": id, "url": url])
          }

          public var __typename: String {
            get {
              return resultMap["__typename"]! as! String
            }
            set {
              resultMap.updateValue(newValue, forKey: "__typename")
            }
          }

          /// GUID for a resource
          public var id: GraphQLID {
            get {
              return resultMap["id"]! as! GraphQLID
            }
            set {
              resultMap.updateValue(newValue, forKey: "id")
            }
          }

          public var url: String {
            get {
              return resultMap["url"]! as! String
            }
            set {
              resultMap.updateValue(newValue, forKey: "url")
            }
          }
        }
      }
    }
  }
}
@designatednerd
Copy link
Contributor

The fieldName of the GraphQLFile here needs to be either file or files - I think files but I'm not positive. It should not be the name of the file itself.

@m-skolnick
Copy link
Author

m-skolnick commented Feb 13, 2020

Thank you for the super fast reply!

Ah you're right. My bad.
After switching to "file", I started getting the error.localizedDescription of:

Received error response (500 internal server error): {"error":"Internal server error","value":{"message":"request entity too large","expected":807681,"length":807681,"limit":102400,"type":"entity.too.large"}}

I then adjusted the image size by using:

 guard let data = image.jpegData(compressionQuality: 0.1) else { //Should bring that image down to a size of 807681*.01 = 80,768.1
            delegate.onAttachmentFailure(error: "Unable to attach image")
            print("Unable to convert image to JPEG")
            return
        }

And now I'm right back to throwing the same error:

Received error response (400 bad request): {"error":"Bad request","value":"Unexpected token - in JSON at position 0"}

@designatednerd
Copy link
Contributor

Hm, I bet it was just blindly looking at the size of the object and going NOPE rather than trying to evaluate anything, so it's probably still the same problem. What happens if you try fields?

If that doesn't work, do you have the ability through Charles or another proxy to send the multipart-form data (not the bit of it that's the file itself, just the headers of that part)?

@m-skolnick
Copy link
Author

I tried setting the fieldName to: "file", "files", "field", and "fields". It throws the same error with them all.
I talked with my server guys about going through a proxy. They advised me to just go with our REST api for uploading files if this wasn't working. I'm going to go that route for now, but if you have any ideas for things I could try I would be happy to try them for the sake of this project.

Thanks again for all your hard work on this.

@designatednerd
Copy link
Contributor

I think you might be able to log out the contents of the request before it hits the network if you implement the HTTPNetworkTransportPreflightDelegate and get a string of the request.body.

@m-skolnick
Copy link
Author

shouldSend in the HTTPNetworkTransportPreflightDelegate gives me this:
It also hits in the willSend. Does that mean it got through error checks in the shouldSend?
Also if you know of a way to get the actual request.httpBody to print, I'm all ears. I couldn't get anything useful.

(lldb) po request
▿ https://myurl/gql
  ▿ url : Optional<URL>
    ▿ some : https://myurl/gql
      - _url : https://myurl/gql
  - cachePolicy : 0
  - timeoutInterval : 60.0
  - mainDocumentURL : nil
  - networkServiceType : __C.NSURLRequestNetworkServiceType
  - allowsCellularAccess : true
  ▿ httpMethod : Optional<String>
    - some : "POST"
  ▿ allHTTPHeaderFields : Optional<Dictionary<String, String>>
    ▿ some : 4 elements
      ▿ 0 : 2 elements
        - key : "Content-Type"
        - value : "multipart/form-data; boundary=apollo-ios.boundary.C0EC2146-D845-4127-AC11-FBE3A8612F27"
      ▿ 1 : 2 elements
        - key : "apollographql-client-name"
        - value : "com.my.package.app-apollo-ios"
      ▿ 2 : 2 elements
        - key : "apollographql-client-version"
        - value : "0.5.0-1"
      ▿ 3 : 2 elements
        - key : "X-APOLLO-OPERATION-NAME"
        - value : "UploadFile"
  ▿ httpBody : Optional<Data>
    ▿ some : 83256 bytes
      - count : 83256
      ▿ pointer : 0x000000011ebc0000
        - pointerValue : 4810604544
  - httpBodyStream : nil
  - httpShouldHandleCookies : true
  - httpShouldUsePipelining : false

@designatednerd
Copy link
Contributor

Frak. I meant willSend. 🤦‍♀️Sorry!

And there aren't any error checks in the shouldSend unless you implement them.

And if you can convert the body data to a string, that would be what i'm looking for - that'll have all the multipart stuff in it (the last part will be basically illegible because it's JPEG data, but I care more about the other parts).

@m-skolnick
Copy link
Author

I'm not having any luck converting the body data to a string.

Breakpoint and po result.httpBody yields:

(lldb) po request.httpBody
▿ Optional<Data>
  ▿ some : 76650 bytes
    - count : 76650
    ▿ pointer : 0x0000000121970000
      - pointerValue : 4858511360

print("(request.httpBody)") yields:

Optional(76650 bytes)

print(NSString(data: request.httpBody!, encoding: String.Encoding.utf8.rawValue)) yields:

nil

@designatednerd
Copy link
Contributor

Try String(data: request.httpBody, encoding: .utf8), and ONLY that - IIRC the URLRequest keeps the httpBody as a stream under the hood so once you've read it once, it's gone on subsequent calls

@m-skolnick
Copy link
Author

No luck. Just prints nil.

@designatednerd
Copy link
Contributor

Is that in shouldSend or willSend? I don't think the attachment is set up by the time it hits shouldSend

@m-skolnick
Copy link
Author

m-skolnick commented Feb 17, 2020

That's in willSend
Edit: It seems to me like something is there. Just can't figure out how to print it.

@designatednerd
Copy link
Contributor

🤔yeah let me fight with that a bit with the tests.

@designatednerd
Copy link
Contributor

po String(data: request.httpBody!, encoding: .utf8)! is printing out the request on my end, at least for non upload things, if I throw a breakpoint into the end of HTTPNetworkTransport.createRequest.

@m-skolnick
Copy link
Author

po String(data: request.httpBody!, encoding: .utf8)! gives me:

Fatal error: Unexpectedly found nil while unwrapping an Optional value: file <EXPR>, line 7
2020-02-18 11:24:48.523355-0500 Core[14465:4296040] Fatal error: Unexpectedly found nil while unwrapping an Optional value: file <EXPR>, line 7
error: Execution was interrupted, reason: EXC_BREAKPOINT (code=1, subcode=0x1b3143aa4).
The process has been returned to the state before expression evaluation.

Looks like there is a file in the fileUpload, but somewhere there's a null value causing it to fail.

Screen Shot 2020-02-18 at 11 27 45 AM

@designatednerd
Copy link
Contributor

Yeah the print appears to be failing because you're force-unwrapping a null body.

Can you print out operation.variables? I think that could give some indication of what should be getting replaced here, and I think it might need to have a field name of files because of the way the input is structured there.

@m-skolnick
Copy link
Author

Screen Shot 2020-02-21 at 12 46 18 PM

@designatednerd
Copy link
Contributor

I still think the fieldName probably has to be files, because what's ultimately getting replaced is the {files: [$file]} bit, but it sounds like you've tried that and it didn't work.

Can you please send me a sample project so I can dig into this deeper? ellen at apollographql dot com.

@m-skolnick
Copy link
Author

Hi, sorry. I've been out for a little while. I have been trying each time once with "file" and once with "files". What would you need for the sample project?

@designatednerd
Copy link
Contributor

  • The schema
  • The mutation you're using to upload the file
  • If possible, the URL for your service

@designatednerd
Copy link
Contributor

One thing I will note for this and all other uploading issues: One strategy we've seen have folks have better long-term success with over GQL-based uploads is uploading the file to another service (such as S3 or even your own backend) and then only sending the URL of the file via GraphQL. This is an article about the web, but it's got useful examples.

@m-skolnick
Copy link
Author

Hi @designatednerd. I also ended up using a multipartFormData file upload using AlamoFire straight to our server, then just patching the GQL object using the fileID. As far as the sample project, I can't share most of that info. I would be happy to try any possible solutions you come across in the future though.

@designatednerd
Copy link
Contributor

Yeah that's a totally reasonable (and tbh, recommended) workaround. Do you want to keep this issue open or close it out?

@jessjpj
Copy link

jessjpj commented Mar 30, 2020

@m-skolnick How did you get Upload object in public var file: Upload ? For me codegen is generating with type "String" instead of "Upload".

@m-skolnick
Copy link
Author

@designatednerd I would say keep it open as it's still an issue. Completely up to you though.

@jessjpj I ended up uploading the file outside of GraphQL, then patching the fileID using GraphQL. I did not put the upload object in public var file: Upload.

@designatednerd
Copy link
Contributor

@jessjpj You'll probably need to make sure you're using the passthroughCustomScalars flag on the code generation since it's a custom scalar. If you still have problems after that, please open a new issue.

I'm gonna close this issue out - we're now actively recommending people use a more purpose-built uploader in the docs, and I've already got on my roadmap to try to deal with uploads a bit better in new codegen, so I think the existence of problems here is pretty well-covered.

Thanks a lot for getting back to me @m-skolnick, and sorry I couldn't be of more help!

@Dreamystify
Copy link

where is passthroughCustomScalars set exactly? in the codegen folder? where abouts?

@designatednerd
Copy link
Contributor

If you're using the Swift Scripting method, it's on ApolloCodegenOptions. If you're using bash, it's a flag you can pass into the script.

@Dreamystify
Copy link

Dreamystify commented Oct 3, 2021

i get the error of extra argument in call

let codegenOptions = ApolloCodegenOptions(targetRootURL: targetRootURL, customScalarFormat: .passthrough)

Is there an example of the otherway? other than:

let codegenOptions = ApolloCodegenOptions(targetRootURL: targetRootURL)

@designatednerd
Copy link
Contributor

Use the initializer with a whole mess of parameters on it - the one that just starts with target root URL is a convenience initializer and is not meant for use with this.

@Dreamystify
Copy link

Dreamystify commented Oct 4, 2021

I was able to get a BigInt custom scalar working using

 let outputFormatURL = fileStructure.sourceRootURL.apollo.childFolderURL(folderName: "<ApplicationFolder>/API.swift")
 let urlToSchemaFileURL = fileStructure.sourceRootURL.apollo.childFolderURL(folderName: "<ApplicationFolder>/schema.graphqls")
            let codegenOptions = ApolloCodegenOptions(outputFormat: .singleFile(atFileURL: outputFormatURL), customScalarFormat: .passthrough, urlToSchemaFile: urlToSchemaFileURL)

But the custom scalar 'Upload' now errors with "//API.swift:3891:14: Property cannot be declared public because its type uses an internal type" and "Cannot find type 'Upload' in scope" and I have set "typealias Upload = String", so Im not sure what Im missing here

I use the following for the BigInt custom scalar:

typealias BigInt = Int64

extension Int64: JSONDecodable, JSONEncodable {
  public init(jsonValue value: JSONValue) throws {
    guard let string = value as? String else {
      throw JSONDecodingError.couldNotConvert(value: value, to: String.self)
    }
    guard let number = Int64(string) else {
      throw JSONDecodingError.couldNotConvert(value: value, to: Int64.self)
    }

    self = number
  }

  public var jsonValue: JSONValue {
    return String(self)
  }
}

Do I need to create something for the Upload scalar now?

EDIT: Turns out I just needed to add public in front of the type alias

public typealias Upload = String
public typealias BigInt = Int64

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants