前言:
当前小伙伴们对“服务器上图片上传失败”大概比较关怀,我们都想要学习一些“服务器上图片上传失败”的相关内容。那么小编也在网摘上搜集了一些对于“服务器上图片上传失败””的相关资讯,希望各位老铁们能喜欢,朋友们快快来学习一下吧!我们在Okhttp上传图片失败,居然是服务端的锅?(一)提到了在开发中使用Okhttp上传图片失败的场景并给出了解决方法。
但是,我还是太天真了。
在写Okhttp上传图片失败,居然是服务端的锅?(一)这篇文章的时候,由于我们的项目工程支付模块接入了第三方的银行卡开户这个流程,需要客户端将用户所填写的身份证信息和身份证照片(需先拍照后上传至我们自己的服务端拿到对应的图片url)提交到第三方中进行开户,而我们的客户端的debug包又支持切换不同的服务端环境,自然地我们在内网和外网环境最终拿到的图片url就不同了。
开发好的迭代版本需要在不同的服务端环境中相应地测试回归,没问题后就可以发布线上版了,就是这再正常不过的流程,今天我又发现问题了。
出现问题的地方就是,我们的客户端在走第三方银行卡开户的时候,由于内网测试阶段拿到的身份证照片url对应的是内网的url,所以在进行开户的时候第三方就拿不到我们所传的身份证照片,所以每次测试的同事在内网环境需要走这个流程的时候,都要找到我们在代码层面单独对此场景的上传图片host写为外网的api域名,且我们的网络库的域名配置是整个项目工程全局的修改,所以每次改完打包给测试的同事后又要再改回来。
基于这个原因,所以我在写retrofit上传图片的时候考虑到了这点,在图片上传工具中支持在不影响全局上传图片api域名的配置上动态更换上传图片api域名,写这个工具的时候我把这个域名配置成了外网,然后一直就没改过,在一切顺利写好的时候,本着谨慎的态度,便在不同的环境下用测了这个上传工具。
结果,除了我们的外网环境可以成功上传图片外,内网环境、预发布环境、线上环境都用该工具上传图片均失败。
又到了到处排查的时间。
我们还是用charles来看看同个上传图片的请求在不同的环境的请求参数是否一致,最终发现除了内网环境外,其它环境的上传图片的请求对应的请求参数都一样(所以都失败是有一定道理的),截图如下:
我们看到在请求体里边也不存在我们在Okhttp上传图片失败,居然是服务端的锅?(一)提到的Content-length键值对,所以这里的MultipartBody写入的内容是一样的,可是为什么会上传失败呢?
我们看接口返回的response中msg文本提示“chunked request bodies not supported yet”,意思就是“不支持分块请求体”,基于我们的经验,肯定不是我们显式调用配置分块请求体的,最终还是让我们发现了请求体的端倪,问题出在“chunked”,那我们就找下有没有对应的chunked配置出现在请求体里边,果然,我们看上传图片失败的请求参数截图:
好家伙,请求header里边Transfer-Encoding:chunked,我们试着打断点去除这个Transfer-Encoding:chunked,结果,图片真的上传成功了,不可思议,我们又看回外网环境下的上传图片请求,发现请求header里边也是有Transfer-Encoding:chunked的,可是外网环境图片可以上传成功,所以我们猜测是因为不同api环境的服务端配置不同,果然,我们的服务端是永远滴神。
知道了问题后就好办了?既然不是我们显式调用的,那就先看看源码,看有没有可能找到解决问题的方法。
我们先找下往header写入Transfer-Encoding:chunked的地方在哪里,最终我们在okhttp自带的BridgeInterceptor拦截器发现了写入的代码,截图如下:
写入的条件是if条件不成立,即body.contentLength()值为-1,问题明朗了,我们在Okhttp上传图片失败,居然是服务端的锅?(一)为了解决服务器不支持我们往MultipartBody内容写入Content-length键值对,在自定义的RequestBody重写了contentLength()方法返回-1,所以BridgeInterceptor这个时候会往header写入Transfer-Encoding:chunked。
看到这里你会说我们可以在自定义拦截器中移除掉header的Transfer-Encoding键,但是这里不能这样做,原因就是okhttp自带的BridgeInterceptor拦截顺序在自定义拦截器之后,无论在自定义拦截器怎么改,最终BridgeInterceptor还是会写入Transfer-Encoding:chunked。
自带的BridgeInterceptor又不能修改,那我们只能想办法使得写入Transfer-Encoding的条件不成立,即body.contentLength()值不能为-1,所以我们的自定义RequestBody的contentLength()方法要返回file.length()。这个时候便不会有Transfer-Encoding的写入,我们用charles验证了不同api环境这个时候均可以成功上传图片。
那么在代码的层面我们还是要解决Okhttp上传图片失败,居然是服务端的锅?(一)提到的问题,同样的RequestBody和MultipartBody是源码,无法修改写入Content-length键值对的条件,源码如下:
如果可以使得以下代码不执行就好了:
sink.writeUtf8("Content-Length: ") .writeDecimalLong(contentLength) .write(CRLF);
源码无法更改,我们其实可以间接更改源码,先看下RequestBody和MultipartBody的源码,都是在okhttp3包下:
我们在项目工程的java下新建okhttp3包,然后把okhttp源码中的RequestBody和MultipartBody复制过来放到我们新建的okhttp3包下,然后修改下MultipartBody,将
sink.writeUtf8("Content-Length: ") .writeDecimalLong(contentLength) .write(CRLF);
注释掉。
这样再去上传图片的时候就可以了。
完善:
为了兼容可能存在的其它场景下上传图片需要写入Content-length键值对
1、修改RequestBody,新增个方法来动态控制是否写入Content-length键值对,方法截图如下:
2、修改MultipartBody响应RequestBody的修改,修改地方截图如下:
验证过在不同api环境下均可成功上传图片。
扩展:
对于看过Android快速集成网络库功能的,可以使用以下的上传图片工具类来上传图片,支持上传图片进度的回调、绑定id(view_id)发起的请求、绑定tag(页面)发起的请求,方便随时取消已发起的请求,比如
这种情况需要在对应的ImageView上蒙层显示进度,且可能有多个ImageView,点击对应的ImageView可重新上传,如正在上传的需要取消,退出页面时要取消所有的请求等等各种场景。
代码如下:
/** * 作者:lpx on 2021/7/2 18:19 * Email :1966353889@qq.com * Describe:配置上传接口请求服务 * update on 2021/7/6 11:01 */public interface UploadService { /** * 上传图片 */ @Headers(Constant.Request.HOST_TYPE + ":" + Constant.Request.IMG) @Multipart @POST("uploadImage") Flowable<JsonObject> upLoadImage(@QueryMap Map<String, Object> map, @Part MultipartBody.Part file);}
/** * 作者:lpx on 2021/7/5 15:43 * Email :1966353889@qq.com * Describe:上传管理工具(暂用于上传图片) * update on 2021/7/9 14:13 */public final class UploadManager { private static UploadManager mInstance; /** * 上传接口请求服务 */ private UploadService apiService; /** * 存储绑定了对应id的某个请求(辅助实现自行或外部调用取消请求) */ private ConcurrentHashMap<Integer, UploadBody> requestMap; /** * 存储绑定了同个tag的所有请求,一般是某个页面(辅助实现自行或外部调用取消请求) */ private ConcurrentHashMap<String, ConcurrentHashMap<Integer, String>> sourceMap; private UploadManager() { requestMap = new ConcurrentHashMap<>(); sourceMap = new ConcurrentHashMap<>(); } public static UploadManager getInstance() { if (mInstance == null) { synchronized (UploadManager.class) { if (mInstance == null) { mInstance = new UploadManager(); } } } return mInstance; } /** * 上传图片 */ public Disposable uploadImage(File file, OnUploadListener listener) { if (file == null) { throw new NullPointerException("file is null"); } return getDisposable(null, file, listener); } /** * 上传图片 * * @param id 标识id(一般指View的id) */ public void uploadImage(Integer id, File file, OnUploadListener listener) { if (file == null) { throw new NullPointerException("file is null"); } if (id == null) { throw new NullPointerException("id is null"); } remove(id); UploadBody uploadBody = new UploadBody.Builder() .disposable(getDisposable(id, file, listener)) .listener(listener) .build(); requestMap.put(id, uploadBody); } /** * 上传图片 * * @param id 标识id(一般指View的id) */ public void uploadImage(String tag, Integer id, File file, OnUploadListener listener) { if (file == null) { throw new NullPointerException("file == null"); } if (id == null) { throw new NullPointerException("id is null"); } remove(id); UploadBody uploadBody = new UploadBody.Builder() .disposable(getDisposable(id, file, listener)) .listener(listener) .build(); ConcurrentHashMap<Integer, String> singleSourceMap = sourceMap.get(tag); if (singleSourceMap != null) { singleSourceMap.put(id, tag); } else { singleSourceMap = new ConcurrentHashMap<>(); singleSourceMap.put(id, tag); sourceMap.put(tag, singleSourceMap); } requestMap.put(id, uploadBody); } /** * 程序退出时调用(因为可能存在多个打开的页面使用到UploadManager,所以仅退出页面时不建议调用此方法) */ public void disposeAll() { if (requestMap == null) { return; } for (Map.Entry<Integer, UploadBody> entry : requestMap.entrySet()) { UploadBody mCacheUploadBody = entry.getValue(); if (mCacheUploadBody != null) { if (!mCacheUploadBody.mDisposable.isDisposed()) { mCacheUploadBody.mDisposable.dispose(); } if (mCacheUploadBody.mListener != null) { mCacheUploadBody.mListener = null; } } } requestMap.clear(); sourceMap.clear(); } /** * 取消对应id(一般指View的id)发起的请求 */ public void disposeById(Integer id) { if (requestMap == null || id == null) { return; } remove(id); } /** * 取消对应tag(一般包含一个或多个View的id)发起的请求 */ public void disposeByTag(String tag) { if (sourceMap == null) { return; } ConcurrentHashMap<Integer, String> singleSourceMap = sourceMap.get(tag); if (singleSourceMap != null) { for (Map.Entry<Integer, String> entry : singleSourceMap.entrySet()) { remove(entry.getKey()); } sourceMap.remove(tag); } } /** * 取消请求 */ private void remove(Integer id) { if (requestMap == null || id == null) { return; } UploadBody mCacheUploadBody = requestMap.get(id); if (mCacheUploadBody != null) { if (!mCacheUploadBody.mDisposable.isDisposed()) { mCacheUploadBody.mDisposable.dispose(); } if (mCacheUploadBody.mListener != null) { mCacheUploadBody.mListener = null; } requestMap.remove(id); } } private Disposable getDisposable(final Integer id, File file, final OnUploadListener listener) { Map<String, Object> map = new HashMap<>(); map.put("watermark", listener != null && listener.watermark() ? 1 : 0); RequestBody requestBody = createRequestBody(MediaType.parse("image/jpeg"), file, listener); MultipartBody.Part body = MultipartBody.Part.createFormData("file"/*img*/, /*file.getName()*/"image.jpg", requestBody); UploadService uploadService; if (listener != null && !listener.isMatching()) { OkHttpClient okHttpClient = new OkHttpClient.Builder() .readTimeout(20, TimeUnit.SECONDS) .connectTimeout(20, TimeUnit.SECONDS) .writeTimeout(20, TimeUnit.SECONDS) .addInterceptor(new Interceptor() { @Override public okhttp3.Response intercept(Chain chain) throws IOException { Request request = chain.request(); Request.Builder builder = request.newBuilder(); List<String> headerValues = request.headers("host_type"); if (headerValues.size() > 0) { builder.removeHeader("host_type"); String headerValue = headerValues.get(0); HttpUrl newBaseUrl; if (!TextUtils.isEmpty(listener.hostUrl())) { newBaseUrl = HttpUrl.parse(listener.hostUrl()); } else { newBaseUrl = HttpUrl.parse(listener.getServer().getUrl(Integer.parseInt(headerValue))); } HttpUrl oldHttpUrl = request.url(); HttpUrl newFullUrl = oldHttpUrl.newBuilder().scheme(newBaseUrl.scheme()).host(newBaseUrl.host()).port(newBaseUrl.port()).build(); return chain.proceed(builder.url(newFullUrl).build()); } else { return chain.proceed(builder.build()); } } }) .addInterceptor(new HeaderInterceptor()) .retryOnConnectionFailure(true) .build(); Retrofit retrofit = new Retrofit.Builder() .addConverterFactory(GsonConverterFactory.create()) .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) .baseUrl("xxxx")//这里替换为请求主机头地址 .client(okHttpClient) .build(); uploadService = retrofit.create(UploadService.class); } else { if (apiService == null) { OkHttpClient okHttpClient = new OkHttpClient.Builder() .readTimeout(20, TimeUnit.SECONDS) .connectTimeout(20, TimeUnit.SECONDS) .writeTimeout(20, TimeUnit.SECONDS) .addInterceptor(new HostInterceptor()) .addInterceptor(new HeaderInterceptor()) .retryOnConnectionFailure(true) .build(); Retrofit retrofit = new Retrofit.Builder() .addConverterFactory(GsonConverterFactory.create()) .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) .baseUrl("xxxx")//这里替换为请求主机头地址 .client(okHttpClient) .build(); apiService = retrofit.create(UploadService.class); } uploadService = apiService; } return RxUtils.rx(uploadService.upLoadImage(map, body), new OnNextOnError<JsonObject>() { @Override public void onError(Response response) { if (id != null) { UploadBody mCacheUploadBody = requestMap.get(id); if (mCacheUploadBody != null) { if (mCacheUploadBody.mDisposable != null && !mCacheUploadBody.mDisposable.isDisposed()) { mCacheUploadBody.mDisposable.dispose(); } requestMap.remove(id); } } if (listener != null) { listener.onFail(id, response.status, response.getMsg()); } } @Override public void onNext(JsonObject jsonObject) { if (id != null) { UploadBody mCacheUploadBody = requestMap.get(id); if (mCacheUploadBody != null) { if (mCacheUploadBody.mDisposable != null && !mCacheUploadBody.mDisposable.isDisposed()) { mCacheUploadBody.mDisposable.dispose(); } requestMap.remove(id); } } if (listener != null) { try { JSONObject object = new JSONObject(jsonObject.toString()); if (object.optInt("state", 0) == 1) { List<ImageBean> list = GsonTools.getData(jsonObject.toString(), ImageBean.class); if (list != null && list.size() > 0) { listener.onSuccess(id, list); } else { listener.onFail(id, -1, object.optString("msg", "上传图片失败,请重新上传(未返回图片远程地址)")); } } else { listener.onFail(id, -1, object.optString("msg", "上传图片失败,请重新上传(state不等于1)")); } } catch (JSONException e) { e.printStackTrace(); listener.onFail(id, -1, "上传图片失败,请重新上传(state不等于1)"); } } } }); } /** * 创建用于上传图片的RequestBody * * @param file 当前待上传的文件 */ private RequestBody createRequestBody(final @Nullable MediaType contentType, final File file, final OnUploadListener listener) { return new RequestBody() { private long currentLength; @Override public @Nullable MediaType contentType() { return contentType; } @Override public long contentLength() { return file.length(); } @Override public void writeTo(@Nullable BufferedSink sink) throws IOException { ForwardingSink forwardingSink = new ForwardingSink(sink) { final long totalLength = file.length(); @Override public void write(Buffer source, long byteCount) throws IOException { /*这里可以获取到写入的长度*/ currentLength += byteCount; /*回调进度*/ if (listener instanceof OnProgressListener) { ((OnProgressListener) listener).onProgress(totalLength, currentLength); } super.write(source, byteCount); } }; /*转一下*/ BufferedSink bufferedSink = Okio.buffer(forwardingSink); /*写数据*/ Source source = null; try { source = Okio.source(file); bufferedSink.writeAll(source); } finally { Util.closeQuietly(source); } /*刷新一下数据*/ bufferedSink.flush(); } }; } /** * 上传监听 */ public abstract static class OnUploadListener { /** * 上传成功 * * @param id 对应view的id */ public abstract void onSuccess(Integer id, List<ImageBean> bean); /** * 上传失败 * * @param id 对应view的id */ public abstract void onFail(Integer id, int status, String message); /** * 上传的图片是否加水印(默认为true,重写可覆盖) */ public boolean watermark() { return true; } /** * 上传的图片的请求Host对应的环境(默认跟随当前设置的环境,重写可覆盖) */ public String environment() { if (NetworkConfig.getInstance().getServer() instanceof IoServer) { return Constant.IO; } else if (NetworkConfig.getInstance().getServer() instanceof OrgServer) { return Constant.ORG; } else if (NetworkConfig.getInstance().getServer() instanceof PreServer) { return Constant.PRE; } else { return Constant.ORI; } } /** * 上传的图片的请求Host地址(优先使用这里设置的地址,重写可覆盖) */ public String hostUrl() { return null; } /** * 上传的图片的请求Host对应的环境(对应设置的environment) */ final BaseServer getServer() { String envi = environment(); if (envi.equals(Constant.IO)) { return new IoServer(); } if (envi.equals(Constant.ORG)) { return new OrgServer(); } if (envi.equals(Constant.PRE)) { return new PreServer(); } return new OriServer(); } /** * 当前需要的的环境是否跟当前设置的环境匹配 */ final boolean isMatching() { String envi = environment(); if (envi.equals(Constant.IO) && NetworkConfig.getInstance().getServer() instanceof IoServer) { return true; } if (envi.equals(Constant.ORG) && NetworkConfig.getInstance().getServer() instanceof OrgServer) { return true; } if (envi.equals(Constant.PRE) && NetworkConfig.getInstance().getServer() instanceof PreServer) { return true; } if (envi.equals(Constant.ORI) && NetworkConfig.getInstance().getServer() instanceof OriServer) { return true; } return false; } } /** * 上传监听(批量上传) */ public abstract static class OnProgressListener extends OnUploadListener { /** * 上传进度回调 * * @param totalLength 文件总大小(byte) * @param currentLength 当前已上传的文件大小(byte) */ public abstract void onProgress(long totalLength, long currentLength); } public static class UploadBody { private Disposable mDisposable; private OnUploadListener mListener; private UploadBody(Builder builder) { mDisposable = builder.mDisposable; mListener = builder.mListener; } public static class Builder { private Disposable mDisposable; private OnUploadListener mListener; public Builder disposable(Disposable disposable) { mDisposable = disposable; return this; } public Builder listener(OnUploadListener listener) { mListener = listener; return this; } public UploadBody build() { return new UploadBody(this); } } }}
使用示例:
UploadManager.getInstance().uploadImage(upLoadFileDialog.getViewId(), file, new UploadManager.OnProgressListener() { @Override public String environment() { /*动态配置环境,默认是跟随项目工程api环境*/ return Constant.ORG; } @Override public void onSuccess(Integer id, List<ImageBean> bean) { } @Override public void onFail(Integer id, int status, String message) { } @Override public void onProgress(long totalLength, long currentLength) { } });
搞定,收工。
希望本文可以帮助到您,也希望各位不吝赐教,提出您在使用中的宝贵意见,谢谢。
标签: #服务器上图片上传失败