Skip to content

Instantly share code, notes, and snippets.

@cenkbilgen
Created January 30, 2019 23:23
Show Gist options
  • Save cenkbilgen/d49428e9a4b671396287cffd8664d2cf to your computer and use it in GitHub Desktop.
Save cenkbilgen/d49428e9a4b671396287cffd8664d2cf to your computer and use it in GitHub Desktop.
"multipart/form-data" in Swift
//
// URLRequestExtension.swift
//
// Created by Cenk Bilgen on 2019-01-30.
// Copyright © 2019 abc. All rights reserved.
//
import Foundation
extension URLRequest {
// functions to create the http body if you want to use multipart/form-data
// since the http connection is likely compressesd, the network inefficiency of transmitting text vs binary is moot
// and just base64 encoding through JSON good enough,
// but multipart/form-data is also straight forward if you know the spec
// see https://www.w3.org/Protocols/rfc1341/7_2_Multipart.html
///////////////// Example Usage
// var request = URLRequest(url: url)
// request.httpMethod = "POST"
//
// let boundary = "-----BOUNDARY"
// request.addValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
//
// // 1. single-part form (to send just a single image file for example)
// let data = try? request.formEncoded(data: data, filename: "file1.jpg", boundary: boundary)
//
// //--- OR ---
//
// // 2. multi-part form (to send an image file plus other metadata for example)
// let formParts: [URLRequest.FormPart] = [FormPart(data: data1, filename: nil, mimetype: "text/plain")
// FormPart(data: data2, filename: "file1.jpg", mimetype: "image/jpeg")]
// let data = try? request.formEncoded(parts: formParts, boundary: boundary)
// let task = URLSession.shared.uploadTask(with: request, from: data) { }
///////////////////////////////////
struct FormPart {
let data: Data
let filename: String? // not included if nil
let mimetype: String? // "application/octet-stream" if nil. see https://www.w3.org/Protocols/rfc1341/4_Content-Type.html
}
// Convert an array of form structures into the proper http request body form
func formEncoded(parts: [FormPart], boundary: String = "-----BOUNDARY") throws -> Data {
var body = Data()
for part in parts {
body += try encode(formPart: part, boundary: boundary)
}
guard body.isEmpty == false else {
throw FormEncodingError.noParts
}
body += "\r\n--\(boundary)--\r\n".data(using: .ascii)! // if there's a bad char in boundary, would've failed earlier
// make this function mutating
// self.addValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
// self.httpBody = body
return body
}
// Create data from a single form part, it's not enough to send by itself
fileprivate func encode(formPart part: FormPart, boundary: String) throws -> Data {
var boundaryHead = "--\(boundary)\r\n"
boundaryHead += "Content-Disposition: form-data; name=\"file\""
if let filename = part.filename {
boundaryHead += "; filename=\"\(filename)\"\r\n"
}
boundaryHead += "Content-Type: \(part.mimetype ?? "application/octet-stream")\r\n\r\n"
// can encode as as .utf8 if you add ,charset=UTF-8 to Content-Type
guard let head = boundaryHead.data(using: .ascii) else {
throw FormEncodingError.badboundaryValue
}
return head + part.data + "\r\n".data(using: .ascii)!
}
// This is alternative function if you just want to send a single-part form with data (like just one file)
// doesn't need resizing of mutable Data structure like the multi-part version does
func formEncoded(data: Data, filename: String, mimetype: String? = nil, boundary: String = "-----BOUNDARY") throws -> Data {
var boundaryHead = ""
boundaryHead += "--\(boundary)\r\n"
boundaryHead += "Content-Disposition: form-data; name=\"file\"; filename=\"\(filename)\"\r\n"
boundaryHead += "Content-Type: \(mimetype ?? "application/octet-stream")\r\n\r\n"
// boundaryHead += "Content-Transfer-Encoding: binary\n\n" // is this necessary sometimes?
let boundaryTail = "\r\n\r\n--\(boundary)--\r\n"
// put it together: head + data + tail
guard let head = boundaryHead.data(using: .ascii),
let tail = boundaryTail.data(using: .ascii) else {
throw FormEncodingError.badboundaryValue
}
let body = head + data + tail
// make this function mutating
// self.addValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
// self.httpBody = body
return body
}
}
// MARK: Errors
enum FormEncodingError: Error {
case badboundaryValue
case noParts
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment