Skip to content

Executing multipart request on same Part instance can cause waiting reading eternally #273

@ecoopnet

Description

@ecoopnet

This problem occurred on MultipartFormDataBodyParameters.Part(data: Data).

The declaration code was like below:

struct MyFormRequest: Request {
    let baseURL = URL(string: "https://example.com/")!

    let method: HTTPMethod = .get

    let path = "/"

    typealias Response = Void

    private let parts: [MultipartFormDataBodyParameters.Part]

    init(images: [UIImage]) throws {
        parts = try images.map {
            guard let data = $0.jpegData() else { throw AppError.invalidSession }
            return data
            }.enumerated().map { (i, data) in
                MultipartFormDataBodyParameters.Part(
                    data: data,
                    name: "images[]",
                    mimeType: "image/jpeg",
                    fileName: "imagae\(i).jpg")
        }
    }
    var bodyParameters: BodyParameters? {
        return MultipartFormDataBodyParameters(
            parts: parts,
            boundary: "0123456789abcdef")
    }

    func response(from object: Any, urlResponse: HTTPURLResponse) throws {
    }
}

Then, call it like below:

func execute(images: [UIImage]) {
   let request = MyFormRequest(images: images)
   Session.send(request: request) { result in
    if case .success = result { return }
    // retry the request. (with the same instance)
    Session.send(request: request) { result2 in
       // it will be never success or failure!!!
    }
   }
}

I noticed this problem occured because of the class MultipartFormDataBodyParameters reads InputStream without rewinding or renewing stream.

# MultipartFormDataBodyParameters.swift:

                case bodyRange:
                    if bodyPart.inputStream.streamStatus == .notOpen {
                        bodyPart.inputStream.open()
                    }

                    // this line never finished when calling `read` twice on same instance.
                    let readLength = bodyPart.inputStream.read(offsetBuffer, maxLength: availableLength) 

                    sentLength += readLength
                    totalSentLength += readLength

So I resolved it by changing Request class like below finally.

struct MyFormRequest: Request {
    let baseURL = URL(string: "https://example.com/")!

    let method: HTTPMethod = .get

    let path = "/"

    typealias Response = Void

    private let images: [Data] 

    init(images: [UIImage]) throws {
        images = try images.map {
            guard let data = $0.jpegData() else { throw AppError.invalidSession }
            return data
            }
        // Do not create Part instance while initialization.
    }
    var bodyParameters: BodyParameters? {
       // Recreate part instance for each request.
       let parts = images.enumerated().map { (i, data) in
                MultipartFormDataBodyParameters.Part(
                    data: data,
                    name: "images[]",
                    mimeType: "image/jpeg",
                    fileName: "imagae\(i).jpg")
        }
        return MultipartFormDataBodyParameters(
            parts: parts,
            boundary: "0123456789abcdef")
    }

    func response(from object: Any, urlResponse: HTTPURLResponse) throws {
    }
}

To begin with, I think the request instance should not be reuse.
But in our project, we have to reuse it. (We wrap APIKit with RxSwift. When the retryable error (network connection error or somethink) occurred, to retry the request, we have to re-subscribe the same stream(Observable) with same request instance)

I hope this post can help someone.

Environment:
APIKit version 4.0.0, iOS 12.1.4, Xcode 10.2.1(10E1001)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions