Multipart request con URLSession y async/await en Swift
Marcelo Laprea
20 septiembre, 20225min de lectura
Un Multipart request se usa cuando se quiere mandar un archivo junto con un objeto JSON, el archivo puede ser un audio, una imagen, un video etc.
Hay muchos tutoriales que te muestran como hacer un multipart request usando Alamofire, que es una librería de terceros para gestionar la capa de red de una aplicación. Sin embargo, hay muy poca información usando URLSession
.
En este artículo vamos a realizar un multipart request usando URLSession
, sin enfocarnos en crear una capa de red de principio a fin.
Paso 1: Crear protocolo HTTPClient:
Primero se crea un protocolo llamado HTTPClient
:
protocol HTTPClient {func sendMultipartRequest<T: Decodable>responseModel: T.Type, data: Data?) async -> Result<T, Error>}
En donde T
es un modelo genérico Decodable
y data
es lo que se va a mandar, en este caso un audio.
Paso 2: Crear extensión del HTTPClient:
Ahora se crea una extensión privada del HTTPClient
, con una función createBody
.
private extension HTTPClient {func createBody(boundary: String, data: Data, mimeType: String, filename: String) -> Data {let body = NSMutableData()let boundaryPrefix = "--\(boundary)\r\n"body.append(boundaryPrefix)body.append("Content-Disposition: form-data; name=\"file\"; filename=\"\(filename)\"\r\n")body.append("Content-Type: \(mimeType)\r\n\r\n")body.append(data)body.append("\r\n")body.append("--".appending(boundary.appending("--")))return body as Data}}
Los parámetros que recibe esta función son:
boundary
: Parámetro que usa el servidor para saber donde empieza y donde termina el valor que se envía.data
: lo que se envía al servidor, puede ser un audio, imagen, video, etc.mimeType
: forma estandarizada de indicar el formato del archivo que se envía. Para ver los distintos tipos, ingrese en esta página. MimeTypesfilename
: Nombre del archivo que se envía.
Esta función, retorna el body
en formato Data
.
Se puede ver que lo primero y lo último que se le agrega al body
es el boundary
, así de esta forma el servidor sabe cuando empieza y cuando termina.
Luego se agrega el Content-Disposition, Content-Type y la data que se quiere enviar.
body.append("Content-Disposition: form-data; name=\"file\"; filename=\"\(filename)\"\r\n")body.append("Content-Type: \(mimeType)\r\n\r\n")body.append(data)
Paso 3: Crear nueva extensión del HTTPClient protocol con la implementación:
func sendMultipartRequest<T: Decodable>(responseModel: T.Type, data: Data?) async -> Result<T, Error> {var urlComponents = URLComponents()urlComponents.scheme = "HTTPS"urlComponents.host = "TU HOST"urlComponents.path = "TU PATH"urlComponents.port = TU PUERTOguard let url = urlComponents.url, let data = data else {return .failure(.invalidURL)}let filename = "name_\(Date().toString()).wav"let boundary = "Boundary-\(UUID().uuidString)"var request = URLRequest(url: url)request.httpMethod = "POST"request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")request.httpBody = createBody(boundary: boundary,data: data,mimeType: "audio/wav",filename: filename)do {let (data, response) = try await URLSession.shared.data(for: request, delegate: nil)guard let response = response as? HTTPURLResponse else {return .failure(.noResponse)}guard let decodedResponse = try? JSONDecoder().decode(responseModel, from: data) else {return .failure(.decode)}return .success(decodedResponse)} catch {return .failure(.unknown)}}
Ahora se explica paso a paso que hace la función:
-
Se crea el
URLComponents
:var urlComponents = URLComponents()urlComponents.scheme = "HTTPS"urlComponents.host = "YOUR HOST"urlComponents.path = "YOUR PATH"urlComponents.port = YOUR PORTguard let url = urlComponents.url, let data = data else {return .failure(.invalidURL)}Aquí se pone la información del endpoint a dónde se quiere mandar el archivo, y luego se comprueba que se tiene un
URL
correcto antes de continuar. El.failure(.invalidURL)
es un tipo de error que se creó para el ejemplo, puedes usar el tipo de error que desees. -
Luego se crea el
URLRequest
:let filename = "name_\(Date().toString()).wav"let boundary = "Boundary-\(UUID().uuidString)"var request = URLRequest(url: url)request.httpMethod = "POST"request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")request.httpBody = createBody(boundary: boundary,data: data,mimeType: "audio/wav",filename: filename)Primero se crea un
URLRequest
con el url que se creó previamente con elURLComponents
. Luego se define elhttpMethod
que será de tipo POST y luego el Content-Type de la cabecera HTTP será de tipo multipart/form-data. Por último, se asigna el httpBody haciendo uso de la funcióncreateBody
que se creó previamente. -
Obtener la respuesta del servidor:
Se usa la nueva función que nos proporciona Swift para obtener tanto la Data
como el Response
.
let (data, response) = try await URLSession.shared.data(for: request, delegate: nil)
Con la data
se puede decodificar y obtener el modelo esperado:
guard let decodedResponse = try? JSONDecoder().decode(responseModel, from: data) else {return .failure(.decode)}return .success(decodedResponse)
Y con el response
se verifica el estado del mismo. Primero se verifica que el response no es nulo:
guard let response = response as? HTTPURLResponse else {return .failure(.noResponse)}
También se puede hacer un switch
del statusCode
del response
devolviendo un tipo de error, para identificar lo que nos llega del servidor:
switch response.statusCode {case 200...299:guard let decodedResponse = try? JSONDecoder().decode(responseModel, from: data) else {return .failure(.decode)}return .success(decodedResponse)case 401...403:return .failure(.unauthorized)case 400:return .failure(.badRequest)case 500:return .failure(.internalServer)default:return .failure(.unexpectedStatusCode)}
Para finalizar, usamos la función que creamos:
-
Se crea una estructura llamada
DataProvider
que implemente nuestro protocoloHTTPClient
:struct DataProvider: HTTPClient {func postAudio(data: Data) async -> Result<Model, Error> {await sendMultipartRequest(responseModel: Model.self, data: data)}} -
Se llama a nuestro
DataProvider
:let result = await DataProvider().postAudio(data: data)
Se puede observar como usando el DataProvider
a partir del protocol HTTPClient
creado previamente, podemos enviar la data
deseada.