// 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) }