PAYATHON 2026

Retrofit Response.isSuccessful() 返回 false 如何解决?

支付小周

明确结论

Response.isSuccessful() 返回 false,是因为服务器响应了 HTTP 402 Payment Required。

Retrofit 只把 200~299 范围内的状态码视为成功。这个问题发生在 JSON 反序列化之前,通常与 Topic 或 TopicResponse 的定义无关,而是接口或托管平台拒绝了请求。

对于 HTTP 错误响应:

  • response.body() 通常是 null
  • 错误内容在 response.errorBody() 中
  • Retrofit 仍会调用 onResponse()
  • 网络中断、DNS 失败、超时等情况通常才会进入 onFailure()

为什么会返回 402

402 Payment Required 是服务器返回的状态码,不是 Retrofit 生成的。原因可能是:

  • 接口服务的付费、配额或账单出现问题
  • 托管平台暂停了应用
  • 接口要求认证、订阅或其他请求参数
  • 后端代码主动返回了 402
  • 网关、代理或第三方服务拒绝了请求

仅凭现有日志无法判断具体原因,还需要查看后端日志、托管平台状态和接口的认证要求。

可以直接访问接口,或使用 curl 测试:

curl -i https://quizziyapa.herokuapp.com/getTopics

如果该命令同样返回 402,就能确认问题出在服务端或托管平台,而不是 Android 客户端。

修正 Retrofit 接口声明

当前代码使用了原始类型 Call,Retrofit 无法确定目标类型。应将接口声明为 Call<TopicResponse>:

public interface IUserApi {

    @GET("getTopics")
    Call<TopicResponse> getTop();
}

调用代码也需要使用完整泛型:

Call<TopicResponse> call = api.getTop();

call.enqueue(new Callback<TopicResponse>() {
    @Override
    public void onResponse(
            Call<TopicResponse> call,
            Response<TopicResponse> response
    ) {
        // 处理响应
    }

    @Override
    public void onFailure(Call<TopicResponse> call, Throwable t) {
        Log.e("Ayush", "Request failed", t);
    }
});

修改后,当服务器返回成功的 JSON 响应时,Gson 可以将其解析为 TopicResponse。不过,这项修改不会让 HTTP 402 变成成功响应,服务端问题仍需单独处理。

正确处理成功响应和错误响应

读取 response.body() 前,应先检查状态码:

Call<TopicResponse> call = api.getTop();

call.enqueue(new Callback<TopicResponse>() {
    @Override
    public void onResponse(
            Call<TopicResponse> call,
            Response<TopicResponse> response
    ) {
        Log.d("Ayush", "HTTP status: " + response.code());
        Log.d("Ayush", "HTTP message: " + response.message());

        if (response.isSuccessful()) {
            TopicResponse topicResponse = response.body();

            if (topicResponse == null) {
                Log.e("Ayush", "Successful response has an empty body");
                return;
            }

            Topic[] topics = topicResponse.getTopics();

            if (topics == null) {
                Log.e("Ayush", "Response does not contain topics");
                return;
            }

            for (Topic topic : topics) {
                Log.d(
                        "Ayush",
                        "id=" + topic.getId()
                                + ", topic=" + topic.getTopic()
                                + ", imageUrl=" + topic.getImageUrl()
                );
            }
        } else {
            String errorContent = "";

            try {
                ResponseBody errorBody = response.errorBody();
                if (errorBody != null) {
                    errorContent = errorBody.string();
                }
            } catch (IOException e) {
                Log.e("Ayush", "Failed to read error body", e);
            }

            Log.e(
                    "Ayush",
                    "HTTP error " + response.code()
                            + " " + response.message()
                            + ", body=" + errorContent
            );
        }
    }

    @Override
    public void onFailure(Call<TopicResponse> call, Throwable t) {
        Log.e("Ayush", "Network or conversion failure", t);
    }
});

response.errorBody().toString() 打印的是类似 okhttp3.ResponseBody$1@647c751 的对象标识,并非服务器返回的实际内容。要读取响应内容,应调用:

response.errorBody().string()

ResponseBody.string() 会消费响应流,通常只能读取一次,所以应先把结果保存到变量中。

Model 可以这样定义

JSON 顶层是一个包含 topics 数组的对象,因此可以使用 TopicResponse 包装:

public class TopicResponse {

    @SerializedName("topics")
    private Topic[] topics;

    public Topic[] getTopics() {
        return topics;
    }
}

Topic 中可以显式标注对应的 JSON 字段,避免字段名称调整后影响解析:

public class Topic {

    @SerializedName("_id")
    private int id;

    @SerializedName("topic")
    private String topic;

    @SerializedName("imageUrl")
    private String imageUrl;

    public int getId() {
        return id;
    }

    public String getTopic() {
        return topic;
    }

    public String getImageUrl() {
        return imageUrl;
    }
}

常规的 Gson 反序列化不要求这些 Model 提供包含全部参数的构造方法。

如果希望使用集合,也可以把数组改为:

public class TopicResponse {

    @SerializedName("topics")
    private List<Topic> topics;

    public List<Topic> getTopics() {
        return topics;
    }
}

需要检查的服务端配置

修正客户端泛型后,还要检查以下内容:

  1. 确认接口目前可以正常访问并返回 2xx。
  2. 查看后端日志,找出返回 402 的服务或代码。
  3. 检查托管服务的账单、配额和应用运行状态。
  4. 确认接口是否要求 Authorization、API Key、Cookie 或其他 Header。
  5. 确认请求路径和 HTTP 方法是 GET /getTopics。
  6. 确认成功响应的 Content-Type 是 application/json。
  7. 确认成功响应符合 {"topics":[...]} 结构。

如果接口要求认证,可以根据后端规范添加 Header,例如:

public interface IUserApi {

    @GET("getTopics")
    Call<TopicResponse> getTop(
            @Header("Authorization") String authorization
    );
}

调用时:

api.getTop("Bearer " + token);

是否需要这个 Header、它的名称以及值的生成方式,都应以后端接口规范为准,不能只根据 402 推断。

日志中的 retrofit2.OkHttpCall$NoContentResponseBody 也不是接口返回的 JSON。Retrofit 处理原始响应体时会使用内部占位对象。成功响应的业务数据应从 response.body() 获取,错误信息则从 response.errorBody() 获取。

解决这个问题需要分别处理服务端和客户端。服务端必须先停止返回 402,让接口正常响应 2xx;客户端则应使用 Call<TopicResponse> 和 Callback<TopicResponse>,由 Retrofit 通过 Gson 解析 JSON。

备注:内容仅供参考。