每日一问-Android-20181031

为什么 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 处理.