最近在負責做一個圖片加載模塊展东,測試過程中反饋一個問題:有兩個測試設備上加載不了圖片。我就納悶了傅寡,我就一個加載圖片模塊怎么還跟機型適配扯上關系了哟绊。然后查了下日志異常如下:
java.lang.IllegalArgumentException: Unexpected char 0x8d2d at 13 in content-disposition value: filename="3.6購買頁.jpg"
at com.bumptech.glide.request.RequestFutureTarget.doGet(RequestFutureTarget.java:189)
at com.bumptech.glide.request.RequestFutureTarget.get(RequestFutureTarget.java:100)
at common.disk.ImageDiskCache.lambda$putCacheImage$0$ImageDiskCache(ImageDiskCache.java:100)
at common.disk.ImageDiskCache$$Lambda$0.run(Unknown Source)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1113)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:588)
at java.lang.Thread.run(Thread.java:818)
Caused by: java.lang.IllegalArgumentException: Unexpected char 0x8d2d at 13 in content-disposition value: filename="3.6購買頁.jpg"
at okhttp3.Headers$Builder.checkNameAndValue(Headers.java:283)
at okhttp3.Headers$Builder.add(Headers.java:233)
at okhttp3.internal.http.Http2xStream.readHttp2HeadersList(Http2xStream.java:263)
at okhttp3.internal.http.Http2xStream.readResponseHeaders(Http2xStream.java:149)
at okhttp3.internal.http.HttpEngine.readNetworkResponse(HttpEngine.java:723)
at okhttp3.internal.http.HttpEngine.access$200(HttpEngine.java:81)
at okhttp3.internal.http.HttpEngine$NetworkInterceptorChain.proceed(HttpEngine.java:708)
at okhttp3.internal.http.HttpEngine.readResponse(HttpEngine.java:563)
at okhttp3.RealCall.getResponse(RealCall.java:241)
at okhttp3.RealCall$ApplicationInterceptorChain.proceed(RealCall.java:198)
at okhttp3.SNInterceptor.intercept(SNInterceptor.java:62)
at okhttp3.RealCall$ApplicationInterceptorChain.proceed(RealCall.java:187)
at okhttp3.RealCall.getResponseWithInterceptorChain(RealCall.java:160)
at okhttp3.RealCall.execute(RealCall.java:57)
at glide.MOkHttpStreamFetcher.loadData(MOkHttpStreamFetcher.java:51)
at glide.MOkHttpStreamFetcher.loadData(MOkHttpStreamFetcher.java:22)
at com.bumptech.glide.load.engine.DecodeJob.decodeSource(DecodeJob.java:170)
at com.bumptech.glide.load.engine.DecodeJob.decodeFromSource(DecodeJob.java:128)
at com.bumptech.glide.load.engine.EngineRunnable.decodeFromSource(EngineRunnable.java:127)
at com.bumptech.glide.load.engine.EngineRunnable.decode(EngineRunnable.java:106)
at com.bumptech.glide.load.engine.EngineRunnable.run(EngineRunnable.java:58)
at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:423)
at java.util.concurrent.FutureTask.run(FutureTask.java:237)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1113)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:588)
at java.lang.Thread.run(Thread.java:818)
at com.bumptech.glide.load.engine.executor.FifoPriorityThreadPoolExecutor$DefaultThreadFactory$1.run(FifoPriorityThreadPoolExecutor.java:118)
其實從日志上,問題原因已經很明顯门怪,但是查找問題的時候我犯了個錯誤庭敦。就是沒有根據堆棧信息查找問題,這是由于平時發(fā)現問題的時候習慣于定位應用的代碼入口薪缆,而不是查看源碼報錯處。
當時看到這個異常的時候伞广,第一反應是保存文件的時候使用中文出錯拣帽,但是我寫的代碼中保存圖片用的是自己定義的字符串,而這個filename在代碼里根本查不到嚼锄。所以這種情況下這個filename只能是通過圖片url獲取到的减拭,然后我打開chrome調試,可以看到圖片url的響應報頭中有這個東西:
Content-Disposition:filename="3.6購買頁.jpg
Content-disposition是 MIME 協議的擴展区丑,MIME 協議指示 MIME 用戶代理如何顯示附加的文件拧粪。當 Internet Explorer 接收到頭時修陡,它會激活文件下載對話框,它的文件名框自動填充了頭中指定的文件名可霎。
知道了filename是哪里來的之后魄鸦,再去找發(fā)生問題的原因。在我的代碼里我只調用了:
File imageFile = Glide.with(mContext).load(url).downloadOnly(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL).get();
所以當時我的理解是癣朗,glide在加載圖片時內部緩存文件時因為filename報錯拾因。看了半天源碼后旷余,發(fā)現最終調用的是項目中自定義的DataFetcher的loadData方法绢记,然后就是okhttp的正常請求調用了。其實整個調用鏈跟異常日志的堆棧信息是一樣的正卧。okhttp的詳細調用略過蠢熄,最終的問題出現在Http2xStream(這個類是負責處理Http2.0協議的,還有一個Http1xStream類處理Http1.x協議炉旷,這個會根據當前設備是否支持去初始化不同的類签孔,這也是為什么會有請求頭中文報錯只有部分機型存在)的readHttp2HeadersList方法,這里會讀取response的header砾跃,問題在這個調用
headersBuilder.add(name.utf8(), value);
okhttp3.Headers.java
/** Add a field with the specified value. */
public Builder add(String name, String value) {
checkNameAndValue(name, value);
return addLenient(name, value);
}
private void checkNameAndValue(String name, String value) {
if (name == null) throw new IllegalArgumentException("name == null");
if (name.isEmpty()) throw new IllegalArgumentException("name is empty");
for (int i = 0, length = name.length(); i < length; i++) {
char c = name.charAt(i);
if (c <= '\u001f' || c >= '\u007f') {
throw new IllegalArgumentException(String.format(
"Unexpected char %#04x at %d in header name: %s", (int) c, i, name));
}
}
if (value == null) throw new IllegalArgumentException("value == null");
for (int i = 0, length = value.length(); i < length; i++) {
char c = value.charAt(i);
if (c <= '\u001f' || c >= '\u007f') {
throw new IllegalArgumentException(String.format(
"Unexpected char %#04x at %d in %s value: %s", (int) c, i, name, value));
}
}
}
這里就是問題出現的原因骏啰,checkNameAndValue這個方法會對請求頭的name、value進行校驗抽高。
解決思路
既然發(fā)現了出現問題的原因判耕,現在就是找解決方案,其實在網上搜索okhttp請求頭中文翘骂,這個關鍵字也會搜到一些文章壁熄。但是這些給出的解決方案一般都是對請求頭進行轉碼,因為一般這種問題都出現在前端request的時候碳竟,而我碰到的服務端返回的請求頭中帶中文草丧。
出現這問題之后我首先是想讓后端協助解決掉這個問題,但是跟后端溝通過后發(fā)現他也不知道這個filename是哪里來的莹桅,他沒有對這塊進行處理昌执。出于各種原因他也不能去專門修改這個問題,同時他也指出你們使用的框架不支持請求頭中文诈泼,本身就不合理懂拾。我聽他這么一說也挺有道理,就放棄了這個想法铐达。
既然靠后端修改走不通岖赋,我又查了下資料。既然filename有一個名字瓮孙,那肯定是哪里傳過去的唐断,后臺配置圖片都配置的是英文选脊,這中文必然是上傳圖片時存儲在本地的文件名。然后跟產品和運營溝通了一下脸甘,果然這個命名是他們本地的恳啥。然后讓他們修改本地文件名重新上傳了一下,這問題算是解決了斤程。
但是角寸,問題肯定不能到這為止。如果其他人碰到這個問題忿墅,又沒辦法讓在源頭做修改扁藕,那這個問題如何解決?
下面說一下我個人的解決方案:
方案一:使用攔截器(適用于發(fā)起request時)
這個方案比較常規(guī)疚脐,你也可以在出錯的請求發(fā)起時對對應的request請求頭進行轉碼亿柑。也可以在構造OkHttpClient時addInterceptor,然后在intercept中對request統一進行轉碼棍弄。具體代碼就不贅述了望薄,到處都能找到。但是需要注意的是呼畸,在intercept中是無法規(guī)避response中請求頭有中文的痕支,因為出錯的位置在你通過chain.proceed(request)拿到response之前,這點可以在源碼中看到后續(xù)我也會寫文章講okhttp整個流程蛮原。
方案二:反射
這個方案是我自己使用的方案卧须,源于分析代碼的時候,問題出在readHttp2HeadersList的
if (!HTTP_2_SKIPPED_RESPONSE_HEADERS.contains(name)) {
headersBuilder.add(name.utf8(), value);
}
這里的add方法儒陨,而我碰到的問題是某一個請求頭會返回中文花嘶,并且客戶端不需要這個請求頭的參數。那既然這樣蹦漠,如果我去修改HTTP_2_SKIPPED_RESPONSE_HEADERS這個List椭员,不就可以實現我需要的功能并且改動最小嗎。具體代碼如下:
private boolean hookOkHttpReadHeader(){
try {
Class clz = Class.forName("okhttp3.internal.http.Http2xStream");
Field field = clz.getField("HTTP_2_SKIPPED_RESPONSE_HEADERS");
field.setAccessible(true);
field.set(new ArrayList<>(), HTTP_2_SKIPPED_RESPONSE_HEADERS);
return true;
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
return false;
}
此方法適用于客戶端不需要請求頭中的參數的情況笛园。
方案三:方法替換
雖然我自己的問題解決了隘击,但是我還是在想能不能去完美的解決這個問題而不是取巧,有沒有一個方法能夠實現替換Headers中的add方法實現研铆,讓add方法不去調用checkNameAndValue埋同,或者是修改checkNameAndValue的內部實現。
想過之后突然發(fā)現這不就是熱修復中的方法替換蚜印,andfix不就是通過替換方法指針達到修復的目的嗎,這個情況跟我想要實現的一模一樣留量。既然如此窄赋,可以使用andfix的方案解決問題哟冬。但是這樣會導致包體積增大以及兼容性問題,而且就算有源碼實現這個過程也是要耗費大量時間的忆绰,這里只是提供一種思路浩峡。
方案四:自行編譯
這個方案是你自己去pull okhttp源碼,修改對應位置错敢,然后生成依賴供自己使用翰灾。這樣可以完整的規(guī)避這種問題。
本文同步發(fā)布在:https://pengsongandroid.github.io/