You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.27 07:20:18