Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save lexrus/e9936f76c80d7f943a1d2c56004cf552 to your computer and use it in GitHub Desktop.
Save lexrus/e9936f76c80d7f943a1d2c56004cf552 to your computer and use it in GitHub Desktop.
Swift multipart/form-data
// Public domain - https://gist.github.com/nolanw/dff7cc5d5570b030d6ba385698348b7c
import Foundation
extension URLRequest {
/**
Configures the URL request for `multipart/form-data`. The request's `httpBody` is set, and a value is set for the HTTP header field `Content-Type`.
- Parameter parameters: The form data to set.
- Parameter encoding: The encoding to use for the keys and values.
- Throws: `MultipartFormDataEncodingError` if any keys or values in `parameters` are not entirely in `encoding`.
- Note: The default `httpMethod` is `GET`, and `GET` requests do not typically have a response body. Remember to set the `httpMethod` to e.g. `POST` before sending the request.
- Seealso: https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#multipart-form-data
*/
public mutating func setMultipartFormData(_ parameters: [String: String], encoding: String.Encoding, files: [MultipartFormFile]) throws {
let makeRandom = { UInt32.random(in: (.min)...(.max)) }
let boundary = String(format: "------------------------%08X%08X", makeRandom(), makeRandom())
let contentType: String = try {
guard let charset = CFStringConvertEncodingToIANACharSetName(CFStringConvertNSStringEncodingToEncoding(encoding.rawValue)) else {
throw MultipartFormDataEncodingError.characterSetName
}
return "multipart/form-data; charset=\(charset); boundary=\(boundary)"
}()
addValue(contentType, forHTTPHeaderField: "Content-Type")
httpBody = try {
var body = Data()
for (rawName, rawValue) in parameters {
if !body.isEmpty {
body.append("\r\n".data(using: .utf8)!)
}
body.append("--\(boundary)\r\n".data(using: .utf8)!)
guard rawName.canBeConverted(to: encoding) else {
throw MultipartFormDataEncodingError.name(rawName)
}
if let data = rawValue.data(using: encoding) {
if let disposition = "Content-Disposition: form-data; name=\"\(rawName)\"\r\n".data(using: encoding) {
body.append(contentsOf: disposition)
}
body.append("\r\n".data(using: .utf8)!)
body.append(data)
} else {
throw MultipartFormDataEncodingError.value("\(rawValue)", name: rawName)
}
}
for file in files {
if !body.isEmpty {
body.append("\r\n".data(using: .utf8)!)
}
body.append("--\(boundary)\r\n".data(using: .utf8)!)
body.append("Content-Disposition: form-data; name=\"\(file.formKey)\"; filename=\"\(file.filename)\"\r\n".data(using: .utf8)!)
body.append("Content-Type: \(file.contentType)\r\n\r\n".data(using: .utf8)!)
body.append(file.data)
body.append("\r\n".data(using: .utf8)!)
}
body.append("\r\n--\(boundary)--\r\n".data(using: .utf8)!)
return body
}()
}
}
public struct MultipartFormFile {
public var formKey: String
public var filename: String
public var contentType: String
public var data: Data
}
public enum MultipartFormDataEncodingError: Error {
case characterSetName
case name(String)
case value(String, name: String)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment