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?
The two common solutions are:
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.
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.