龙空技术网

Okhttp上传图片失败,居然是服务端的锅?(二)

我本人的 170

前言:

当前小伙伴们对“服务器上图片上传失败”大概比较关怀,我们都想要学习一些“服务器上图片上传失败”的相关内容。那么小编也在网摘上搜集了一些对于“服务器上图片上传失败””的相关资讯,希望各位老铁们能喜欢,朋友们快快来学习一下吧!

我们在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) {                                                                    }                            });

搞定,收工。

希望本文可以帮助到您,也希望各位不吝赐教,提出您在使用中的宝贵意见,谢谢。

标签: #服务器上图片上传失败