如题,最近工作遇到的问题,我们的 Android 应用网络请求埋点报表,收集到了奇怪的网络请求异常;通过日志收集与分析,确定到是服务器返回了不规范的状态码所导致。
如上是根据线上的业务场景,本地写个简单的MockServer 以及一个简单的 Java 应用(使用 okhttp),重刻出的现场后 client 端的异常堆栈。我们可以看到 okhttp 对于非法的不规范的响应码,直接就抛出ProtocolException,中断http 响应报文的解析,通过onFailure回调通知上层调用者
示例代码
server
from flask import Flask, jsonify, make_response
app = Flask(__name__)
@app.route('/')
def not_found():
response = {
'error': 'Not found'
}
return make_response(jsonify(response), 36) # HTTP 30
if __name__ == '__main__':
app.run(port=8080)
client
package com.luo;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.Call;
import okhttp3.Callback;
import java.io.IOException;
public class OkHttpGetRequestExample {
public static void main(String[] args) {
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
.url("http://127.0.0.1:8080/")
.build();
// 异步请求
client.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
e.printStackTrace();
System.out.println("请求失败:" + e.getMessage());
}
@Override
public void onResponse(Call call, Response response) throws IOException {
if (response.isSuccessful()) {
System.out.println("请求成功:" + response.body().string());
} else {
System.out.println("请求失败:" + response.code());
}
}
});
}
}
其它
如果上面那个 Server 的代码使用 python 自带的网络组件,唯一的区别是返回的响应行,只有响应码/状态码 36 ,没有状态消息
from http.server import BaseHTTPRequestHandler, HTTPServer
import json
class SimpleHTTPRequestHandler(BaseHTTPRequestHandler):
def do_GET(self):
# 创建一个字典,包含要返回的 JSON 数据
data = {
'status': 'fail',
'message': 'This is a JSON response',
'code': 36
}
# 将字典转换为 JSON 字符串
json_data = json.dumps(data)
# 设置响应状态码为 200
self.send_response(36)
# 设置响应头
self.send_header('Content-type', 'application/json')
self.end_headers()
# 设置响应内容
self.wfile.write(json_data.encode('utf-8'))
# 设置服务器的端口号
server_address = ('', 8080)
httpd = HTTPServer(server_address, SimpleHTTPRequestHandler)
print("Starting httpd on port", server_address[1])
# 开始监听
httpd.serve_forever()
如果你用 Node js 来写上面的 Mockserver,当 server 收到请求,服务就crash 了,因为代码中设置了个非法的响应码
对应 client 那边侧是EOFException的出现
上面的 Mockserver 的代码如下:
const http = require('http');
const server = http.createServer((req, res) => {
// 检查请求是否为GET方法
if (req.method === 'GET') {
// 设置响应头部为JSON
res.setHeader('Content-Type', 'application/json');
// 设置HTTP状态码为36
res.statusCode = 36;
// 发送响应数据
res.end(JSON.stringify({
status: 0,
message: 'Not Found'
}));
} else {
// 如果不是GET请求,返回404
res.statusCode = 404;
res.end(JSON.stringify({
status: -1,
message: 'Not Found'
}));
}
});
const PORT = 8080; // 你可以选择任何未被占用的端口
server.listen(PORT, () => {
console.log(`Server is running on http://localhost:${PORT}`);
});
http 协议规范
让我们一起来复习 http协议规范中的 http 响应头这块的内容
状态行
HTTP响应行,也称为状态行,是HTTP响应报文的第一行。它包含了以下三个部分:
HTTP协议版本:指定了用于通信的HTTP协议的版本,如HTTP/1.1或HTTP/2。这告诉客户端服务器支持的HTTP版本。
状态码:一个三位数字,表示请求的结果。状态码分为五类:
1xx:指示信息,表示请求已接收,继续处理。
2xx:成功,表示请求已被成功接收、理解、并接受。
3xx:重定向,表示需要后续操作以完成请求。
4xx:客户端错误,表示请求包含语法错误或无法完成请求。
5xx:服务器错误,表示服务器在处理请求的过程中发生了错误。
状态消息:状态码的简短描述,通常是状态码的文字解释,如"OK"、"Not Found"、"Internal Server Error"等。状态消息是可选的,但通常包含在响应中以提高可读性。
状态码
HTTP状态码总是三位数字,这是HTTP协议规范的一部分。如果服务器返回的状态码不是三位数字,那么这个响应是不符合HTTP协议标准的,客户端(如浏览器或其他HTTP客户端)可能会无法正确解析和理解该响应。
HTTP状态码的第一位数字定义了响应的类别:
1xx(信息性状态码):表示接收的请求正在处理。
2xx(成功状态码):表示请求正常处理完毕。
3xx(重定向状态码):需要后续操作才能完成这一请求。
4xx(客户端错误状态码):表示请求包含语法错误或无法完成。
5xx(服务器错误状态码):服务器在处理请求的过程中发生了错误。
如果遇到非标准的响应,客户端可能会采取以下措施:
显示错误:对于无法识别的响应,客户端可能会向用户显示错误信息。
忽略响应:在某些情况下,客户端可能会忽略该响应并尝试重新发起请求。
记录日志:客户端可能会记录日志信息,以供后续分析问题。
如果你是服务器端开发者,应确保返回的HTTP状态码符合标准。如果你是客户端开发者,应确保你的客户端能够妥善处理非标准响应。
值得一提的是,虽然在理论上状态码应该是三位数字,但实际上,几乎所有的HTTP客户端和服务器都对非标准的响应码有一定的容忍度。例如,如果服务器返回了200 OK后跟了额外的非标准文字,很多客户端可能会忽略这些额外的文字。但是,遵循标准总是最佳实践。
调试技巧
上面 okhttp 抛出异常的情况下,我们可以在readResponseHeaders方法设置一个断点,然后通过headersReader.readLine() 读出整个报文
直接用浏览器访问 http://localhost:8080/ 可以在网络调试窗口看到对应的报文(响应头)