大多數時候,OkHttp 會自動解壓 gzip 響應。但某些老舊 server 或特殊場景,response 的 Content-Encoding 是 gzip/deflate,但 OkHttp 沒有自動幫你解。這時就要自己手動解壓。
問題場景
通常是這些情況:
- Server 回傳 Content-Encoding: gzip,但沒有設定好 headers
- 某些代理或中間層加了壓縮但沒遵循標準
- 特殊的嵌入式系統的 API
- 需要在 interceptor 層級處理壓縮和解壓
實作 UnzippingInterceptor
直接上完整代碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76
| import okhttp3.Interceptor; import okhttp3.Response; import okio.BufferedSource; import okio.GzipSource; import okio.Okio;
public class UnzippingInterceptor implements Interceptor { @Override public Response intercept(Chain chain) throws IOException { Response response = chain.proceed(chain.request()); return unzip(response); } private Response unzip(Response response) throws IOException { String contentEncoding = response.header("Content-Encoding"); if (contentEncoding == null || contentEncoding.isEmpty()) { return response; } if ("identity".equalsIgnoreCase(contentEncoding)) { return response; } if ("gzip".equalsIgnoreCase(contentEncoding)) { return unzipGzip(response); } if ("deflate".equalsIgnoreCase(contentEncoding)) { return unzipDeflate(response); } return response; } private Response unzipGzip(Response response) throws IOException { if (response.body() == null) { return response; } GzipSource gzipSource = new GzipSource(response.body().source()); BufferedSource decompressed = Okio.buffer(gzipSource); return response.newBuilder() .body(new UncompressedResponseBody(decompressed)) .removeHeader("Content-Encoding") .removeHeader("Content-Length") .build(); } private Response unzipDeflate(Response response) throws IOException { if (response.body() == null) { return response; } InflaterSource inflaterSource = new InflaterSource(response.body().source(), new Inflater(true)); BufferedSource decompressed = Okio.buffer(inflaterSource); return response.newBuilder() .body(new UncompressedResponseBody(decompressed)) .removeHeader("Content-Encoding") .removeHeader("Content-Length") .build(); } }
|
配套的 ResponseBody 類:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| import okhttp3.MediaType; import okhttp3.ResponseBody; import okio.BufferedSource;
public class UncompressedResponseBody extends ResponseBody { private final BufferedSource source; public UncompressedResponseBody(BufferedSource source) { this.source = source; } @Override public MediaType contentType() { return null; } @Override public long contentLength() { return -1; } @Override public BufferedSource source() { return source; } }
|
怎麼用
在 OkHttpClient 初始化時加上 interceptor:
1 2 3 4 5 6 7 8 9 10 11
| OkHttpClient httpClient = new OkHttpClient.Builder() .addInterceptor(new UnzippingInterceptor()) .build();
Retrofit retrofit = new Retrofit.Builder() .baseUrl("https://api.example.com") .client(httpClient) .addConverterFactory(GsonConverterFactory.create()) .build();
MyService service = retrofit.create(MyService.class);
|
之後所有的 gzip 響應都會自動解壓。
更簡潔的版本
如果只需要 gzip 支援,可以簡化:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| public class GzipInterceptor implements Interceptor { @Override public Response intercept(Chain chain) throws IOException { Response response = chain.proceed(chain.request()); if ("gzip".equalsIgnoreCase(response.header("Content-Encoding")) && response.body() != null) { GzipSource gzipSource = new GzipSource(response.body().source()); BufferedSource decompressed = Okio.buffer(gzipSource); return response.newBuilder() .body(new UncompressedResponseBody(decompressed)) .removeHeader("Content-Encoding") .removeHeader("Content-Length") .build(); } return response; } }
|
測試
怎麼驗證 interceptor 確實在解壓:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| MockWebServer mockWebServer = new MockWebServer(); mockWebServer.enqueue(new MockResponse() .setHeader("Content-Encoding", "gzip") .setBody(gzipBody) );
OkHttpClient httpClient = new OkHttpClient.Builder() .addInterceptor(new GzipInterceptor()) .build();
Request request = new Request.Builder() .url(mockWebServer.url("/")) .build();
Response response = httpClient.newCall(request).execute();
assertNull(response.header("Content-Encoding"));
String body = response.body().string(); assertEquals("original content", body);
|
進階:支援其他編碼
如果要支援更多編碼(br、compress 等),可以擴展:
1 2 3 4 5 6 7 8 9 10
| private Response handleBrotli(Response response) throws IOException { if (response.body() == null) { return response; } return response; }
|
但大多數情況,gzip 夠用了。
常見坑
1. 忘記檢查 body 是否為 null
某些 response(比如 204 No Content)沒有 body:
1 2 3 4 5 6 7 8
| GzipSource gzipSource = new GzipSource(response.body().source());
if (response.body() == null) { return response; } GzipSource gzipSource = new GzipSource(response.body().source());
|
2. 忘記移除 Content-Encoding header
解壓後應該移除:
1
| .removeHeader("Content-Encoding")
|
否則下游的 JSON 解析器可能會嘗試再解一次。
3. Content-Length 會變
解壓後長度改變,最好移除:
1
| .removeHeader("Content-Length")
|
4. 某些 library 會自動解壓
OkHttp 3.4+ 預設會移除 gzip,Retrofit 的某些版本也會。加 interceptor 前先確認是否真的需要。
實務建議
- 優先用自動解壓 - OkHttp 和 Retrofit 通常會自動處理,不需要手動
- 只在真的需要時加 interceptor - 某些舊 server 或特殊 API
- 加上日誌便於 debug - 記錄什麼時候觸發了解壓
- 測試 edge case - 包括沒有 body、已經解壓過的 response
大多數時候根本用不到這個 interceptor,但知道怎麼做能救急。踩過的坑就寫在這裡。
你的鼓勵將被轉換為我明天繼續加班的動力(真的)。 ❤️