iOS swift post request with binary body
Asked Answered
P

1

3

I want to make a POST request from iOS (swift3) which passes a chunk of raw bytes as the body. I had done some experimenting which made me thought the following worked:

let url = URL(string: "https://bla/foo/bar")!
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.httpBody = Data(hex: "600DF00D")
let session = URLSession.shared
let task = session.dataTask(with: request) { (data, response, error) in
    "DATA \(data ?? Data()) RESPONSE \(response) ERROR \(error)".print()
}
task.resume()

Didn't know it was a problem until I tried sending something simple like a single 0xF0. At which point my tornado server started complaining that I was sending it

WARNING:tornado.general:Invalid x-www-form-urlencoded body: 'utf-8' codec can't decode byte 0xf0 in position 2: invalid continuation byte

Am I just supposed to set some header somehow? Or is there something different I need to do?

Pinkney answered 5/8, 2016 at 21:56 Comment(0)
P
2

The two common solutions are:

  1. Your error message tells us that the web service is expecting a x-www-form-urlencoded request (e.g. key=value) and in for the value, you can perform a base-64 encoding of the binary payload.

    Unfortunately, base-64 strings still need to be percent escaped (because web servers generally parse + characters as spaces), so you have to do something like:

    let base64Encoded = data
        .base64EncodedString(options: [])
        .addingPercentEncoding(withAllowedCharacters: .urlQueryValueAllowed)!
        .data(using: String.Encoding.utf8)!
    
    var body = "key=".data(using: .utf8)!
    body.append(base64Encoded)
    
    var request = URLRequest(url: url)
    request.httpBody = body
    request.httpMethod = "POST"
    request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
    
    let task = URLSession.shared.dataTask(with: request) { data, response, error in
        guard error == nil else {
            print(error!)
            return
        }
    
        ...
    }
    task.resume()
    

    Where:

    extension CharacterSet {
        static let urlQueryValueAllowed: CharacterSet = {
            let generalDelimitersToEncode = ":#[]@" // does not include "?" or "/" due to RFC 3986 - Section 3.4
            let subDelimitersToEncode = "!$&'()*+,;="
    
            var allowed = CharacterSet.urlQueryAllowed
            allowed.remove(charactersIn: generalDelimitersToEncode + subDelimitersToEncode)
            return allowed
        }()
    }
    

    For more discussion on that character set, see point 2 in this answer: https://mcmap.net/q/121143/-how-to-http-post-special-chars-in-swift.

    Anyway, when you receive this on your server, you can retrieve it as and then reverse the base-64 encoding, and you'll have your original binary payload.

  2. Alternatively, you can use multipart/formdata request (in which you can supply binary payload, but you have to wrap it in as part of the broader multipart/formdata format). See https://mcmap.net/q/119538/-upload-image-with-parameters-in-swift if you want to do this yourself.

For both of these approaches, libraries like Alamofire make it even easier, getting you out of the weeds of constructing these requests.

Phototelegraph answered 5/8, 2016 at 22:15 Comment(1)
Base64 isn't really efficient, but I finally just decided text streams in http-ville were never really that efficient any way. And it's one line of code to to turn a binary Data into it's base64 version. Since I'm using it for the body, not the url, I don't even need to escape.Pinkney

© 2022 - 2024 — McMap. All rights reserved.