在雲端 Mac 上驗收 iOS 壓縮回應的完整性與解壓上限

在雲端 Mac 上驗收 iOS 壓縮回應的完整性與解壓上限

iOS App 接收壓縮 JSON 時,封包擷取工具顯示的資料可能只有幾十 KB,交給解析器的內容卻大得多。要查清 App 實際接受了什麼,不必等線上 API 發生故障:在雲端 Mac 上啟動一個只監聽本機的測試服務,再讓同一台 Mac 上的 iOS 模擬器向它發出請求。以下透過正常、校驗值損毀及解壓後內容較大的三種回應,檢查 URLSession 傳回的資料,以及 App 在解析前設定的邊界。

先區分傳輸長度與 App 收到的資料

gzip 壓縮的是傳輸中的回應本文。Content-Length 可以表示壓縮後送出的位元組數;業務程式碼拿到的 Data,則可能是已解壓縮的本文。只限制前者,無法阻止重複字元或高度相似的 JSON 片段在解壓後占用大量記憶體。本文以 256 KiB 作為範例 API 的解壓後上限。這是測試門檻,並非適用於所有 iOS API 的通用值;你應依實際回應大小另行設定。

測試也必須區分「網路呼叫成功」與「本文可用」。狀態碼正確、請求沒有逾時,都不能取代內容驗證。尤其當壓縮串流損毀時,不要預設系統一定會以某一種錯誤回報;真正需要驗收的是 App 最終不會把損毀資料當成完整的業務結果。

測試斷言應針對 App 實際使用的資料,而不只是傳輸標頭或網路面板顯示的壓縮後大小。

在本機產生三種 gzip 回應

將以下腳本儲存為 /tmp/gzip_fixture.py,並在雲端 Mac 執行 python3 /tmp/gzip_fixture.py。服務只綁定 127.0.0.1:8765,不對公網開放。/ok 傳回短本文;/broken 修改 gzip 尾端的校驗值;/large 傳送可高度壓縮、解壓後為 1 MiB 的本文。三個路徑都傳送實際壓縮資料,並以壓縮後的長度填寫回應標頭。

import gzip
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer

SMALL = b'{"status":"ready"}'
LARGE = b"A" * 1_048_576

class Handler(BaseHTTPRequestHandler):
    def do_GET(self):
        if self.path not in ("/ok", "/broken", "/large"):
            self.send_error(404)
            return

        payload = LARGE if self.path == "/large" else SMALL
        wire = bytearray(gzip.compress(payload))
        if self.path == "/broken":
            wire[-8] ^= 0xFF

        self.send_response(200)
        self.send_header("Content-Type", "application/octet-stream")
        self.send_header("Content-Encoding", "gzip")
        self.send_header("Content-Length", str(len(wire)))
        self.end_headers()
        self.wfile.write(wire)

ThreadingHTTPServer(("127.0.0.1", 8765), Handler).serve_forever()

這裡刻意修改校驗值,而不是任意中斷 TCP 連線,好讓「傳輸完整但內容損毀」與連線中斷可以分開觀察。腳本提供的是固定輸入,並不宣稱能代表真實 API 的壓縮率或吞吐量。執行測試前,使用 lsof -nP -iTCP:8765 -sTCP:LISTEN 確認監聽者確實是這次啟動的程序;若連接埠已被占用,先停止舊服務,避免測試案例誤連其他程式。

在模擬器中檢查實際本文

將以下 XCTest 範例放進專案現有的測試 target。它只適用於與服務執行在同一台 Mac 上的模擬器:這裡的 127.0.0.1 指向本機;若改用實體裝置,就必須重新設計服務位址與存取控制。測試專案需允許此開發環境中的本機 HTTP 請求;不要為了執行測試案例而放寬正式建置對遠端請求的安全設定。

import Foundation
import XCTest

final class GzipResponseTests: XCTestCase {
    private let base = URL(string: "http://127.0.0.1:8765")!

    private func fetch(_ path: String) async throws -> Data {
        let url = base.appendingPathComponent(path)
        let (data, response) = try await URLSession.shared.data(from: url)
        XCTAssertEqual((response as? HTTPURLResponse)?.statusCode, 200)
        return data
    }

    func testValidBody() async throws {
        let data = try await fetch("ok")
        XCTAssertEqual(data, Data(#"{"status":"ready"}"#.utf8))
    }

    func testLargeDecodedBodyExceedsAppLimit() async throws {
        let data = try await fetch("large")
        XCTAssertGreaterThan(data.count, 256 * 1024)
    }

    func testDamagedBodyIsNotAcceptedAsComplete() async {
        do {
            let data = try await fetch("broken")
            XCTAssertNotEqual(
                data,
                Data(#"{"status":"ready"}"#.utf8)
            )
        } catch {
            return
        }
    }
}

先執行正常案例,再執行大本文與損毀案例。如果正常案例收到的是原始 gzip 位元組,而非預期文字,應先檢查回應標頭、測試環境與請求實作,不要直接把斷言改成「只要有資料就通過」。損毀案例接受兩種可觀察結果:呼叫擲出錯誤,或傳回的資料不等於完整的預期本文。它不保證系統會在某個固定階段拒絕校驗值錯誤,也不能取代業務層對 JSON 欄位的驗證。

在解析前檢查大小限制

在 App 程式碼中,先檢查 Data.count,再交給解碼器。以下示範的是收到完整回應後的大小門檻;它能避免繼續解析過大的本文,無法避免下載及解壓縮過程中已產生的記憶體占用。若 API 可能傳回極大的內容,還需要在 App 架構中採用分塊接收、暫存至磁碟或伺服器端限制,不能只依賴這一行判斷。

enum ResponseError: Error {
    case tooLarge
}

func checkedBody(_ data: Data) throws -> Data {
    guard data.count <= 256 * 1024 else {
        throw ResponseError.tooLarge
    }
    return data
}

接著修改大本文案例,斷言 checkedBody(data) 會擲出 tooLarge,而正常本文應能通過。這樣測試目標就從「觀察到差異」推進到「確認 App 確實執行邊界檢查」。如果你的 API 傳回 JSON,應在通過大小檢查後再進行 JSON 解碼,並核對必要欄位;不要把「能解析」誤認為「內容來自預期 API」。

收尾時保留可重現的檢查項目

提交這組測試前,核對五件事:服務只監聽本機;測試明確使用同機模擬器;正常回應會比對完整本文;損毀回應不會被接受為完整內容;大小限制針對 URLSession 交給 App 的 Data。最後停止暫時服務,確認 8765 連接埠不再處於監聽狀態,並檢查測試 target 沒有把本機 HTTP 的例外設定帶進正式建置。

這組測試夾具刻意保持精簡。後續若要涵蓋空本文、錯誤狀態碼或不同編碼,應新增獨立路徑與獨立斷言,不要把多種故障塞進一個結果偶爾變動的回應。使用固定輸入、明確界定邊界,並檢查 App 實際使用的位元組,比只憑封包擷取工具中的壓縮長度判斷可靠得多。

常見問題

只檢查 Content-Length 夠嗎?

不夠。它表示傳輸時壓縮內容的長度;應用程式的大小上限應套用在 URLSession 回傳的 Data。

校驗值損毀時,是否一定要斷言 URLSession 擲出錯誤?

不用依賴特定的失敗形式。測試應確保應用程式不會將損毀回應當作完整內容接受;擲錯或資料比對失敗都符合目的。

想親自測試看看?

租用專屬 Mac mini,直接執行你的建置工作

從兩款 M4 實體主機中挑選,並依需求選擇租用期間與節點。實際供應情況以控制台即時顯示為準。

立即租用 Mac mini