Android 集成 Paytm All-in-One SDK 时响应体为 null
结论
response.isSuccessful() 为 true,只说明 HTTP 状态码在 200~299 范围内,不代表响应中一定有 JSON 数据。
日志已经把问题说明得很清楚:
HTTP/1.1 200 OK
Content-Length: 0
Content-Type: text/html; charset=UTF-8
0-byte body
服务器返回了一个响应体为空的 200 OK。Retrofit 没有内容可以交给 Gson 反序列化,而项目中的 NullOnEmptyConverterFactory 会把空响应转换成 null,所以 response.body() 必然是 null。
问题首先出在服务端响应。同时,客户端把 @Multipart 和 Content-Type: application/json 用在同一个请求中,也属于错误配置,可能导致服务器无法解析请求参数。
1. 修正 Multipart 请求头
接口使用了 @Multipart,请求的实际格式就是 multipart/form-data。不要再手动设置:
@Headers({"Content-Type: application/json;charset=UTF-8"})
Multipart 请求头必须带有自动生成的 boundary。Retrofit 和 OkHttp 会生成类似下面的请求头:
Content-Type: multipart/form-data; boundary=...
如果手动把它覆盖成 application/json,就会出现请求体是 Multipart 数据、请求头却声明为 JSON 的情况。服务器可能因此无法解析 MID、`ORDER_ID“ 等参数。
接口应改为:
@Multipart
@POST("paytm")
Call<Token_Res> generateTokenCall(
@HeaderMap Map<String, String> headers,
@Part("code") RequestBody code,
@Part("MID") RequestBody mid,
@Part("ORDER_ID") RequestBody orderId,
@Part("AMOUNT") RequestBody amount
);
不要自行添加 Multipart 的 Content-Type,由 Retrofit 设置请求类型和对应的 boundary。
2. 确认服务端确实返回 JSON
客户端修正后,还需要检查 https://jagdishshop.com/api/paytm 对应的服务端控制器或 PHP 脚本。
服务端至少应该返回类似下面的 JSON:
{
"body": {
"txnToken": "generated-token"
},
"status": "success"
}
响应头应为:
Content-Type: application/json
例如,PHP 端可以明确输出 JSON:
<?php
header('Content-Type: application/json; charset=utf-8');
$result = [
'body' => [
'txnToken' => $txnToken
],
'status' => 'success'
];
echo json_encode($result);
exit;
如果 /api/paytm 是框架路由,控制器必须显式返回响应。例如在 Laravel 中:
return response()->json([
'body' => [
'txnToken' => $txnToken,
],
'status' => 'success',
]);
需要重点检查这些情况:
- 控制器调用了
init_transaction.php,却没有return或echo最终结果。 - Paytm 请求失败后,异常虽然被捕获,接口却仍返回空的
200 OK。 - 请求字段没有被正确解析,代码提前结束。
- 返回值只保存在变量中,没有写入 HTTP 响应。
- 路由、中间件或身份验证逻辑返回了空响应。
- PHP 的
curl_exec()结果没有输出,也没有转换成 JSON。 - 服务端只把错误写入日志,没有向客户端返回错误状态和错误信息。
请求失败时也不应返回空的 200。服务端可以返回明确的状态码和 JSON:
http_response_code(502);
header('Content-Type: application/json; charset=utf-8');
echo json_encode([
'status' => 'error',
'message' => 'Unable to generate Paytm transaction token'
]);
exit;
3. 根据服务端协议选择一种请求格式
客户端采用哪种请求格式,必须与服务端读取参数的方式一致。
如果服务端接收 Multipart
保留 @Multipart 和 @Part,删除手动设置的 Content-Type:
@Multipart
@POST("paytm")
Call<Token_Res> generateTokenCall(
@HeaderMap Map<String, String> headers,
@Part("code") RequestBody code,
@Part("MID") RequestBody mid,
@Part("ORDER_ID") RequestBody orderId,
@Part("AMOUNT") RequestBody amount
);
如果服务端接收 JSON
不要使用 @Multipart 和 @Part,改用 @Body:
public class TokenRequest {
private final String code;
private final String MID;
private final String ORDER_ID;
private final String AMOUNT;
public TokenRequest(
String code,
String MID,
String ORDER_ID,
String AMOUNT
) {
this.code = code;
this.MID = MID;
this.ORDER_ID = ORDER_ID;
this.AMOUNT = AMOUNT;
}
}
@POST("paytm")
Call<Token_Res> generateTokenCall(
@HeaderMap Map<String, String> headers,
@Body TokenRequest request
);
调用方式如下:
TokenRequest request = new TokenRequest(
"12345",
midString,
orderIdString,
txnAmountString
);
Call<Token_Res> call =
mServiceInterface.generateTokenCall(headers, request);
GsonConverterFactory 会把对象序列化成 JSON,并设置合适的请求头。
如果服务端读取普通表单字段
如果 PHP 通过类似 $_POST['MID'] 的方式读取参数,也可以使用 URL 编码表单:
@FormUrlEncoded
@POST("paytm")
Call<Token_Res> generateTokenCall(
@HeaderMap Map<String, String> headers,
@Field("code") String code,
@Field("MID") String mid,
@Field("ORDER_ID") String orderId,
@Field("AMOUNT") String amount
);
同一个请求不能同时声明为 JSON、Multipart 和 URL 编码表单。
4. 理解 Converter 的作用
当前 Converter 的注册顺序是:
.addConverterFactory(new NullOnEmptyConverterFactory())
.addConverterFactory(GsonConverterFactory.create(gson))
如果 NullOnEmptyConverterFactory 的实现是在响应长度为零时返回 null,那么它现在的行为符合预期。它只是避免 Gson 解析空内容,并不能生成响应数据。
暂时删除它可能会暴露类似 “End of input” 的转换异常,但无法解决服务端没有返回内容的问题:
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://jagdishshop.com/api/")
.addConverterFactory(GsonConverterFactory.create(gson))
.client(mOkHttpClient)
.build();
GsonBuilder().setLenient() 也无法处理 0-byte body。宽松解析只会改变非标准 JSON 的解析规则,不会把空响应转换成 Token_Res。
5. 增加有效的响应诊断
处理响应时,应同时记录状态码、响应头和错误体:
@Override
public void onResponse(
Call<Token_Res> call,
Response<Token_Res> response
) {
progressBar.setVisibility(View.GONE);
Log.e(TAG, "HTTP code: " + response.code());
Log.e(TAG, "HTTP message: " + response.message());
Log.e(TAG, "Headers: " + response.headers());
if (!response.isSuccessful()) {
try {
String error = response.errorBody() != null
? response.errorBody().string()
: "<empty error body>";
Log.e(TAG, "Error body: " + error);
} catch (IOException e) {
Log.e(TAG, "Failed to read error body", e);
}
return;
}
Token_Res tokenResponse = response.body();
if (tokenResponse == null) {
Log.e(TAG, "Successful HTTP response contained an empty body");
return;
}
if (tokenResponse.getBody() == null) {
Log.e(TAG, "Response JSON does not contain body");
return;
}
String txnToken = tokenResponse.getBody().getTxnToken();
if (txnToken == null || txnToken.trim().isEmpty()) {
Log.e(TAG, "Response does not contain txnToken");
return;
}
startPaytmPayment(txnToken);
}
errorBody() 通常用于非成功响应。当前服务端返回的是 200,所以空内容属于成功响应体,不在 errorBody() 中。
如果需要临时查看服务端的原始返回值,可以把调试接口改成 ResponseBody:
@Multipart
@POST("paytm")
Call<ResponseBody> generateTokenDebug(
@HeaderMap Map<String, String> headers,
@Part("code") RequestBody code,
@Part("MID") RequestBody mid,
@Part("ORDER_ID") RequestBody orderId,
@Part("AMOUNT") RequestBody amount
);
然后读取响应:
String rawBody = response.body() != null
? response.body().string()
: "<null>";
Log.e(TAG, "Raw response: " + rawBody);
不过,当前日志已经显示响应长度为零。即使改用 ResponseBody,读取到的仍然是空内容。这个调试方法更适合服务端已经返回内容,但 Gson 映射仍然失败的情况。
6. 检查 Token_Res 与实际 JSON 是否匹配
服务端开始返回 JSON 后,还要确认模型结构与 JSON 完全对应。例如:
{
"body": {
"txnToken": "abc123"
}
}
对应的模型可以写成:
public class Token_Res {
private Body body;
public Body getBody() {
return body;
}
public static class Body {
private String txnToken;
public String getTxnToken() {
return txnToken;
}
}
}
如果服务端使用了不同的字段名,可以添加 @SerializedName:
@SerializedName("txnToken")
private String txnToken;
模型不匹配通常会导致对象内部的字段为 null,但无法解释日志中的 Content-Length: 0。排查时应先确认服务端返回了非空 JSON,再检查模型映射。
7. 修正字符串判断
下面的代码比较的是对象引用:
if (response.body().getBody().getTxnToken() != "")
Java 不能用 != 判断字符串内容,应改为:
String txnToken = response.body().getBody().getTxnToken();
if (txnToken != null && !txnToken.trim().isEmpty()) {
startPaytmPayment(txnToken);
}
这个错误不会导致整个 response.body() 为 null,但会让 token 的判断结果不可靠。
最终排查顺序
- 删除
@Headers({"Content-Type: application/json;charset=UTF-8"}),确保 Multipart 请求头和请求体一致。 - 确认服务端是否成功读取
code、MID、ORDER_ID和AMOUNT。 - 查看服务端日志,以及调用 Paytm 初始化交易接口得到的原始结果。
- 确保
/api/paytm显式返回非空 JSON,并设置Content-Type: application/json。 - 失败时返回合适的
4xx/5xx状态码和错误 JSON,不要返回空的200 OK。 - 得到非空响应后,再核对
Token_Res的字段结构。 - 使用
isEmpty()判断 token,不要使用!= ""。
所以,响应体并不是被 Paytm SDK 清空的,Gson 也没有凭空返回 null。现有日志已经证明服务端实际发送了零字节响应。请求格式冲突可能是服务端返回空响应的原因之一,但服务端仍需修正,确保接口无论成功还是失败,都返回明确且可以解析的响应。
备注:内容仅供参考。
