Search code examples
iosswiftnsurlrequesturlrequest

iOS swift post request with binary body


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?


Solution

  • 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://stackoverflow.com/a/35912606/1271826.

      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://stackoverflow.com/a/26163136/1271826 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.