iOS Swift使用URLSession上传音频文件异常:多部分表单数据问题
iOS Swift URLSession 多表单上传音频异常排查与修复
以下是针对你上传音频时未收到预期422响应的问题,梳理出的问题点及修复方案:
1. 音频MIME类型错误
你的代码中设置音频Content-Type为audio/mp3,但标准的MP3音频MIME类型是audio/mpeg,部分服务器仅识别标准类型,会导致文件解析失败。
修复:
let contentType = "audio/mpeg" // 替换原有的"audio/mp3"
2. 请求体参数格式化隐患
createBodyWithMultipleImages方法中直接用"\(value)"转换参数,虽Int类型可正常转换,但后续添加其他类型参数(如Bool、Double)可能出现格式错误。建议统一处理参数转字符串逻辑,同时避免强制解包data(using: .utf8)导致崩溃:
修复:
// 封装安全的Data拼接扩展 extension Data { mutating func append(_ string: String) { guard let data = string.data(using: .utf8) else { return } self.append(data) } } // 修改createBodyWithMultipleImages方法 private func createBodyWithMultipleImages(parameters: [String: Any]?, files: [URLSession.File], boundary: String) -> Data { var body = Data() if let parameters = parameters { for (key, value) in parameters { body.append("--\(boundary)\r\n") body.append("Content-Disposition: form-data; name=\"\(key)\"\r\n\r\n") body.append("\(value)\r\n") } } files.forEach{ body.append("--\(boundary)\r\n") body.append("Content-Disposition: form-data; name=\"\($0.name)\"; filename=\"\($0.fileName)\"\r\n") body.append("Content-Type: \($0.contentType)\r\n\r\n") body.append($0.data) body.append("\r\n") } body.append("--\(boundary)--\r\n") return body }
3. 响应状态码未校验
当前代码不管服务器返回什么状态码,都会调用completion(.success),即使返回422也会被误判为成功。需要先校验HTTP响应状态码:
修复:
URLSession.shared.dataTask(with: urlRequest) { data, response, error in if let error { print("Error from file upload: \(error)") completion(.failure(error)) return } guard let httpResponse = response as? HTTPURLResponse else { print("Invalid response") completion(.failure(NSError(domain: "UploadError", code: -1, userInfo: [NSLocalizedDescriptionKey: "无效响应"]))) return } print("response status code: \(httpResponse.statusCode)") if let data { do { let json = try JSONSerialization.jsonObject(with: data, options: []) print(json) // 根据状态码判断结果 if (200...299).contains(httpResponse.statusCode) { completion(.success(ResponseMessage(detail: "File Uploaded"))) } else { let errorMessage = (json as? [String: Any])?["detail"] as? String ?? "上传失败,状态码:\(httpResponse.statusCode)" completion(.failure(NSError(domain: "UploadError", code: httpResponse.statusCode, userInfo: [NSLocalizedDescriptionKey: errorMessage]))) } } catch { print("Failed to serialized json: \(error)") completion(.failure(error)) } } else { completion(.failure(NSError(domain: "UploadError", code: -2, userInfo: [NSLocalizedDescriptionKey: "无响应数据"]))) } }.resume()
4. 文件缺失时的请求拦截
如果Bundle中找不到音频文件,当前代码仍会发送不包含文件的请求,可能导致服务器返回非预期响应。建议添加文件存在校验:
修复:
// 在创建请求体之前添加判断 guard !files.isEmpty else { print("No audio file found to upload") completion(.failure(NSError(domain: "UploadError", code: -3, userInfo: [NSLocalizedDescriptionKey: "未找到待上传的音频文件"]))) return } let data = createBodyWithMultipleImages(parameters: params, files: files, boundary: boundary) urlRequest.httpBody = data
完整修复后的核心代码片段
func uploadFile(testID: Int, completion: @escaping (Result<ResponseMessage, Error>) -> Void) { var files: [URLSession.File] = [] let params: [String : Any] = ["test_id": testID] guard let url = URL(string: "\(baseURL)/speaking-file") else { completion(.failure(NSError(domain: "UploadError", code: -4, userInfo: [NSLocalizedDescriptionKey: "无效的上传地址"]))) return } var urlRequest = URLRequest(url: url) let boundary = UUID().uuidString urlRequest.httpMethod = "POST" if let token = Auth.shared.oAuth?.accessToken, let type = Auth.shared.oAuth?.tokenType { urlRequest.addValue("\(type) \(token)", forHTTPHeaderField: "Authorization") } urlRequest.addValue(Configuration.conf?.clientId ?? "", forHTTPHeaderField: "clientid") urlRequest.addValue(Configuration.conf?.clientSecret ?? "", forHTTPHeaderField: "clientsecret") urlRequest.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type") if let audioUrl = Bundle.main.url(forResource: "sample-3s", withExtension: "mp3") { do { let audioData = try Data(contentsOf: audioUrl) let fileName = audioUrl.lastPathComponent let contentType = "audio/mpeg" // 使用标准MIME类型 let file = URLSession.File(name: "file", fileName: fileName, data: audioData, contentType: contentType) files.append(file) } catch { print("Failed to convert to data: \(error)") completion(.failure(error)) return } } guard !files.isEmpty else { print("No audio file found to upload") completion(.failure(NSError(domain: "UploadError", code: -3, userInfo: [NSLocalizedDescriptionKey: "未找到待上传的音频文件"]))) return } let data = createBodyWithMultipleImages(parameters: params, files: files, boundary: boundary) urlRequest.httpBody = data Logger.log(request: urlRequest) URLSession.shared.dataTask(with: urlRequest) { data, response, error in if let error { print("Error from file upload: \(error)") completion(.failure(error)) return } guard let httpResponse = response as? HTTPURLResponse else { print("Invalid response") completion(.failure(NSError(domain: "UploadError", code: -1, userInfo: [NSLocalizedDescriptionKey: "无效响应"]))) return } print("response form file upload \(httpResponse)") if let data { do { let json = try JSONSerialization.jsonObject(with: data, options: []) print(json) if (200...299).contains(httpResponse.statusCode) { completion(.success(ResponseMessage(detail: "File Uploaded"))) } else { let errorInfo = json as? [String: Any] ?? [:] let errorMessage = errorInfo["detail"] as? String ?? "上传失败,状态码:\(httpResponse.statusCode)" completion(.failure(NSError(domain: "UploadError", code: httpResponse.statusCode, userInfo: [NSLocalizedDescriptionKey: errorMessage]))) } } catch { print("Failed to serialized json: \(error)") completion(.failure(error)) } } else { completion(.failure(NSError(domain: "UploadError", code: -2, userInfo: [NSLocalizedDescriptionKey: "无响应数据"]))) } }.resume() } private func createBodyWithMultipleImages(parameters: [String: Any]?, files: [URLSession.File], boundary: String) -> Data { var body = Data() if let parameters = parameters { for (key, value) in parameters { body.append("--\(boundary)\r\n") body.append("Content-Disposition: form-data; name=\"\(key)\"\r\n\r\n") body.append("\(value)\r\n") } } files.forEach{ body.append("--\(boundary)\r\n") body.append("Content-Disposition: form-data; name=\"\($0.name)\"; filename=\"\($0.fileName)\"\r\n") body.append("Content-Type: \($0.contentType)\r\n\r\n") body.append($0.data) body.append("\r\n") } body.append("--\(boundary)--\r\n") return body } extension Data { mutating func append(_ string: String) { guard let data = string.data(using: .utf8) else { return } self.append(data) } } extension URLSession { public struct File { public let name: String public let fileName: String public let data: Data public let contentType: String public init(name: String, fileName: String, data: Data, contentType: String) { self.name = name self.fileName = fileName self.data = data self.contentType = contentType } } }
内容的提问来源于stack exchange,提问作者sangam pokharel
相关产品推荐
相关产品推荐

