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

Alamofire请求Docker部署FastAPI流接口时JSON分片解码失败

问题分析与解决方案

核心问题定位

你的问题本质是Docker部署环境下,FastAPI的流式输出被代理/服务器缓冲,导致iOS客户端收到的TCP分片并非按单条JSON行分割,而本地运行时无缓冲,客户端能正确拿到单条JSON的分片。Postman、Swagger、安卓正常是因为这些客户端会自动处理NDJSON(换行分隔JSON)的分片合并与分割,而你的Alamofire代码直接解码每个原始TCP分片,自然会出现部分JSON的解码错误。


解决方案分两步:服务器端+客户端调整

一、服务器端:关闭输出缓冲

Docker部署时,Uvicorn或反向代理会默认开启输出缓冲,把多个yield的JSON行合并成一个大分片发送,必须禁用缓冲:

1. Uvicorn启动参数添加无缓冲

修改Docker启动命令,在uvicorn后加--no-buffer:

CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--no-buffer"]

2. 若使用反向代理(如Nginx),禁用代理缓冲

如果你的Docker服务前有Nginx等反向代理,添加以下配置:

location /json/stream {
    proxy_pass http://fastapi:8000;
    proxy_buffering off;  # 核心:关闭代理缓冲
    chunked_transfer_encoding on;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
}

二、客户端:手动按行分割NDJSON

Alamofire的streamRequest返回的是TCP层面的原始分片,不一定对应服务器yield的单条JSON。必须维护一个缓冲区,按换行符分割出完整的JSON行再解码:

1. 修改ViewModel代码,添加缓冲区与行分割逻辑

import SwiftUI
import Combine
import Alamofire

class ViewModel: ObservableObject {
    
    @Published var jsonObjects: [AllJsonsStream] = []
    private var buffer = Data()  // 新增:数据缓冲区
    private let newlineData = "\n".data(using: .utf8)!

    fileprivate lazy var alamoSession: Session = {
        let configuration = URLSessionConfiguration.default
        configuration.timeoutIntervalForRequest = 20
        configuration.urlCache?.removeAllCachedResponses()
        return Alamofire.Session(configuration: configuration)
    }()

    
    fileprivate func alamofireChunked () {
        guard let url = URL(string: "https://mydockerfastapiurl/json/stream") else {
            return
        }
        let parameters: [String: Any] = [
            "arg1": val1
        ]
        
        var request = URLRequest(url: url)
        request.httpMethod = "POST"
        request.addValue("application/json", forHTTPHeaderField: "Content-Type")
        // 修正:Accept改为服务器返回的NDJSON类型
        request.addValue("application/x-ndjson", forHTTPHeaderField: "Accept")
        // 移除:不要手动设置transfer-encoding,URLSession会自动处理
        print(request.headers)
        
        do {
            request.httpBody = try JSONSerialization.data(withJSONObject: parameters, options: .prettyPrinted)
        } catch {
            print("ERROR: \(error)")
        }

        alamoSession.streamRequest(request).validate().responseStream { [weak self] response in
            guard let self = self else { return }
            switch response.event {
            case .stream(let data):
                self.buffer.append(data)
                self.processBuffer()  // 处理缓冲区中的完整JSON行
            case .complete(let result):
                self.processBuffer(finish: true)  // 处理剩余的最后一行
                print("stream is finished")
                print("response:", result)
            case .failure(let error):
                print("stream error:", error)
            }
        }
    }
    
    // 新增:按换行分割缓冲区,解码完整JSON行
    private func processBuffer(finish: Bool = false) {
        while let newlineIndex = buffer.firstIndex(of: newlineData.first!) {
            let lineData = buffer[..<newlineIndex]
            buffer = buffer[newlineIndex+1...]
            
            if !lineData.isEmpty {
                do {
                    let decoded = try JSONDecoder().decode(AllJsonsStream.self, from: lineData)
                    DispatchQueue.main.async {
                        self.jsonObjects.append(decoded)
                    }
                    print("Decoded:", decoded)
                } catch {
                    print("Decode error:", error)
                }
            }
        }
        
        // 流结束时处理缓冲区剩余的最后一行(可能无换行)
        if finish && !buffer.isEmpty {
            do {
                let decoded = try JSONDecoder().decode(AllJsonsStream.self, from: buffer)
                DispatchQueue.main.async {
                    self.jsonObjects.append(decoded)
                }
                print("Decoded final line:", decoded)
            } catch {
                print("Final line decode error:", error)
            }
            buffer.removeAll()
        }
    }
}


struct ContentView: View {
    private var urlString = ""
    @StateObject var viewModel = ViewModel()  // 修正:用@StateObject管理ObservableObject
    
    var body: some View {
        VStack {
            Button {
                viewModel.alamofireChunked()
            } label: {
                Text("Fetch stream")
            }
            // 示例:展示收到的JSON对象
            List(viewModel.jsonObjects) { item in
                Text("\(item)")
            }
        }
    }
}

关键修正点说明

  1. 服务器端禁用缓冲:确保每个yield的JSON行被立即发送,不被合并
  2. 客户端按行分割:NDJSON的标准是每行一个JSON,必须手动处理分片合并与分割,不能依赖TCP分片刚好对应单条JSON
  3. 修正请求头:Accept改为application/x-ndjson,与服务器返回的media_type一致,避免代理或服务器的内容协商异常
  4. 移除手动设置的transfer-encoding:URLSession会自动处理分块传输,手动设置可能导致冲突

内容的提问来源于stack exchange,提问作者rockettradr

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.26 08:10:02