为什么 HTTP Code 204 会导致 Retrofit 出现 NullPointerException?
答:
今天在做网络接口的时候, 一个返回结果应该是 Map
的接口出现了异常:
java.lang.NullPointerException: The mapper function returned a null value.
at io.reactivex.internal.functions.ObjectHelper.requireNonNull(ObjectHelper.java:39)
at io.reactivex.internal.operators.observable.ObservableMap$MapObserver.onNext(ObservableMap.java:59)
at retrofit2.adapter.rxjava2.CallExecuteObservable.subscribeActual(CallExecuteObservable.java:44)
at io.reactivex.Observable.subscribe(Observable.java:12030)
at io.reactivex.internal.operators.observable.ObservableMap.subscribeActual(ObservableMap.java:33)
at io.reactivex.Observable.subscribe(Observable.java:12030)
at io.reactivex.internal.operators.observable.ObservableSubscribeOn$SubscribeTask.run(ObservableSubscribeOn.java:96)
at io.reactivex.Scheduler$DisposeTask.run(Scheduler.java:579)
at io.reactivex.internal.schedulers.ScheduledRunnable.run(ScheduledRunnable.java:66)
at io.reactivex.internal.schedulers.ScheduledRunnable.call(ScheduledRunnable.java:57)
at java.util.concurrent.FutureTask.run(FutureTask.java:266)
根据错误信息, 进入代码可得:
static final class MapObserver<T, U> extends BasicFuseableObserver<T, U> {
final Function<? super T, ? extends U> mapper;
//省略代码
@Override
public void onNext(T t) {
if (done) {
return;
}
if (sourceMode != NONE) {
actual.onNext(null);
return;
}
U v;
try {
v = ObjectHelper.requireNonNull(mapper.apply(t), "The mapper function returned a null value.");
} catch (Throwable ex) {
fail(ex);
return;
}
actual.onNext(v);
}
//省略代码
}
此处代码, 说明服务器返回的成功, 已经进入 onNext 回调了, 但是其 value 是 null, 我们在往上看调用信息, 在实际的 subscribeActual
方法中将 Response 进行传递.
//retrofit2.adapter.rxjava2.CallExecuteObservable#subscribeActual
@Override protected void subscribeActual(Observer<? super Response<T>> observer) {
// Since Call is a one-shot type, clone it for each new observer.
Call<T> call = originalCall.clone();
CallDisposable disposable = new CallDisposable(call);
observer.onSubscribe(disposable);
boolean terminated = false;
try {
Response<T> response = call.execute();
if (!disposable.isDisposed()) {
observer.onNext(response);
}
if (!disposable.isDisposed()) {
terminated = true;
observer.onComplete();
}
} catch (Throwable t) {
//省略代码
}
}
这里的 Response 是 retrofit 内置的 Resonse 不是 Okhttp 里面的 Response, 部分代码如下:
//
/** An HTTP response. */
public final class Response<T> {
//省略代码
/**
* Create a successful response from {@code rawResponse} with {@code body} as the deserialized
* body.
*/
public static <T> Response<T> success(@Nullable T body, okhttp3.Response rawResponse) {
checkNotNull(rawResponse, "rawResponse == null");
if (!rawResponse.isSuccessful()) {
throw new IllegalArgumentException("rawResponse must be successful response");
}
return new Response<>(rawResponse, body, null);
}
//省略代码
}
这个 Response 怎么来的呢?看 subscribeActual
方法这个代码块:
Response<T> response = call.execute();
if (!disposable.isDisposed()) {
observer.onNext(response);
}
是 Okhttp 的 Call 的执行结果, 这里的实现类是 OkHttpCall<T>
:
@Override public Response<T> execute() throws IOException {
okhttp3.Call call;
//省略代码
return parseResponse(call.execute());
}
Call 执行以后对结果进行解析处理:
Response<T> parseResponse(okhttp3.Response rawResponse) throws IOException {
ResponseBody rawBody = rawResponse.body();
// Remove the body's source (the only stateful object) so we can pass the response along.
rawResponse = rawResponse.newBuilder()
.body(new NoContentResponseBody(rawBody.contentType(), rawBody.contentLength()))
.build();
int code = rawResponse.code();
if (code < 200 || code >= 300) {
try {
// Buffer the entire body to avoid future I/O.
ResponseBody bufferedBody = Utils.buffer(rawBody);
return Response.error(bufferedBody, rawResponse);
} finally {
rawBody.close();
}
}
if (code == 204 || code == 205) {
rawBody.close();
return Response.success(null, rawResponse);
}
ExceptionCatchingRequestBody catchingBody = new ExceptionCatchingRequestBody(rawBody);
try {
T body = serviceMethod.toResponse(catchingBody);
return Response.success(body, rawResponse);
} catch (RuntimeException e) {
// If the underlying source threw an exception, propagate that rather than indicating it was
// a runtime exception.
catchingBody.throwIfCaught();
throw e;
}
}
重点来了:在这个解析的代码中, 如果服务器返回的 HTTP Code 是 204 或者 205, 那么其 body 会置为 null, 所以会出现 NullPointException
那么, HTTP Code 204 是什么意思呢? 这里引用 Mozilla 的说明:
HTTP协议中 204 No Content 成功状态响应码表示目前请求成功,但客户端不需要更新其现有页面。204 响应默认是可以被缓存的。在响应中需要包含头信息 ETag。
其实就是, 请求服务器成功了, 但是没有数据返回给你.
实际的网络请求是什么样呢(接口已做马赛克处理)?
D/OkHttp: <-- 204 No Content https://xxxxxxxx (2241ms)
D/OkHttp: Server: nginx
D/OkHttp: Date: Wed, 31 Oct 2018 13:50:15 GMT
D/OkHttp: Connection: keep-alive
D/OkHttp: X-Application-Context: dating:publicCloud
D/OkHttp: Access-Control-Allow-Headers: DNT,X-FROM-APP,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Content-Range,Range
D/OkHttp: Access-Control-Expose-Headers: DNT,X-FROM-APP,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Content-Range,Range
D/OkHttp: Access-Control-Allow-Origin: *
D/OkHttp: Access-Control-Allow-Methods: GET,POST,HEAD,OPTIONS,PUT,DELETE
D/OkHttp: Access-Control-Max-Age: 1728000
D/OkHttp: <-- END HTTP
从日志上上看,服务器确实返回了 204, 那么我们怎么处理这个异常呢?
从实际的现象来看, 这个异常只是打印了异常信息, 而不会引发程序的崩溃. 个人认为可以这样处理:
- 不做任何处理
- 将此 204 使用 Observable 的 map 转换分发到 onException 分支
如果服务器返回的数据格式类似下面这样:
{
"code" : 10000,
"desc" : "success",
"data" : "",
}
那就在 结果返回时添加 Function 转换, 返回一个data 为 null 的 JavaBean.
操作过程:
1.将 rest service 的接口返回值改为:Observable<Response<BaseResult<Map<String, String>>>>
2.创建新的 Function类:
public class ResponseFun<T> implements Function<Response<BaseResult<T>>, BaseResult<T>> {
@Override
public BaseResult<T> apply(Response<BaseResult<T>> response) {
if (response.isSuccessful()) {
if (response.code() == HttpURLConnection.HTTP_NO_CONTENT
|| response.code() == HttpURLConnection.HTTP_RESET
|| response.body() == null) {
BaseResult<T> result = new BaseResult<>();
result.setCode(10000);
result.setDesc("success");
return result;
} else {
return response.body();
}
}
throw new HttpException(response);
}
}
3.调用处在处理数据的时候要做判 null 处理.