结合传输与业务结果读取状态码
调用方需要的不只是三位数字,还要知道是否收到 HTTP 响应、正文是否完整到达,以及应用结果是否满足预期操作。这是三个不同的问题。二百响应可以按既定契约携带应用层失败,也可能在响应头之后发生传输中断;没有收到响应之前就连接失败,则根本没有 HTTP 状态。诊断和指标应保留这些区别,不能把它们压成一个过于简单的成功标志。 按实际取得的证据分类 HTTP 错误响应仍然是一份响应,其中可能包含有用的表示、认证挑战、请求标识或 Retry-After 字段。有些客户端库会为四开头和五开头状态抛出异常,但仍允许读取这些信息。处理时应先捕获具体 HTTP 异常,再处理更宽泛的传输异常;否则,一个带明确限流提示的四二九,就可能被归成和连接失败完全相同的通用网络错误,丢失下一步判断依据。 HTTP 交换成功也可能只代表业务信封成功传递。例如批处理或 RPC 契约可以返回二百,同时在正文里说明某个子操作失败。应读取文档定义的结构,而不是默认这种响应里的业务全部成功,也不能断言所有此类接口都违反 HTTP。监控需要验证自己承诺监控的结果;如果业务契约要求检查正文,那么只读状态码就没有完成相应检查。 坏掉的样子 在收到响应头时记录状态,可能掩盖随后出现的正文截断。Content-Length、流结束、解压与解析,都影响最后是否得到可用表示。应把已经收到的状态与未完成的传输分别报告,不能因为状态指标已经计入二百,就把只有半份内容的响应视为应用成功。收到响应头,是比完整收到并理解业务结果更早的一个验证点。 是否重试也不能只看第一位数字。四二九表示请求受到限流,可以附带 Retry-After;五零三也可能建议等待。该字段可以使用延迟秒数或 HTTP 日期,但两种形式都没有保证服务器到时一定恢复。客户端应结合服务器提示、有界重试策略、退避和操作语义,避免一遇到错误就立即重放。等待提示不是无限重试的授权,也不是恢复成功的证据。 非幂等请求的响应丢失,可能让业务效果处于未知状态:客户端失去联系之前,服务器也许已经提交。没有合适的幂等契约就重复操作,可能产生重复工作。另一方面,有些四开头错误会随着身份、状态或限流条件改变而变得可恢复。按照整个类别制定“总是重试”或“绝不重试”,会抹掉真正需要用于决策的信息,而不是简化正确的处理逻辑。 状态码本身也不能说明究竟是哪一个组件生成了响应。源站、网关和中间层都可能返回错误,归因时应结合请求标识与相应日志。curl 的 000 一类诊断值,不是注册的 HTTP 响应状态,通常表示没有取得可用的 HTTP 状态。把它当成某种服务器错误,会把连接阶段的问题错误地归到已经收到响应之后的处理阶段。 快速验证 将脚本保存为 status-client.py 并执行 python3 status-client.py。临时回环服务器提供五种响应情况;客户端使用真正的 urllib 响应对象和异常机制,并为实验关闭代理路由,不进行自动业务重试。另一个已经绑定但没有开始监听的回环端口用于产生拒绝连接,不需要访问外部主机,也不会依赖某个远端服务恰好离线。 from http.client import IncompleteRead from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from threading import Thread from urllib.error import HTTPError, URLError from urllib.request import ProxyHandler, build_opener import json import socket class Handler(BaseHTTPRequestHandler): def do_GET(self): if self.path == '/application-error': status, data, retry = 200, b'{"ok":false,"error":"demo"}', None elif self.path == '/busy': status, data, retry = 503, b'{"error":"unavailable"}', '3' elif self.path == '/limit': status, data, retry = 429, b'{"error":"limited"}', '2' else: status, data, retry = 404, b'{"error":"missing"}', None if self.path == '/truncated': self.send_response(200) self.send_header('Content-Length', '100') self.end_headers() self.wfile.write(b'partial') return self.send_response(status) self.send_header('Content-Type', 'application/json') self.send_header('Content-Length', str(len(data))) if retry: self.send_header('Retry-After', retry) self.end_headers() self.wfile.write(data) def log_message(self, *args): pass opener = build_opener(ProxyHandler({})) def inspect(url): try: try: response = opener.open(url, timeout=3) except HTTPError as error: response = error with response: result = {'http_status': response.code, 'retry_after': response.headers.get('Retry-After')} try: body = response.read() except IncompleteRead: return {**result, 'complete_body': False} return {**result, 'complete_body': True, 'payload': json.loads(body)} except URLError: return {'http_status': None, 'transport_failure': True} server = ThreadingHTTPServer(('127.0.0.1', 0), Handler) thread = Thread(target=server.serve_forever) thread.start() try: origin = f'http://127.0.0.1:{server.server_port}' results = {path: inspect(origin + path) for path in ('/application-error', '/busy', '/limit', '/missing', '/truncated')} with socket.socket() as reserved: reserved.bind(('127.0.0.1', 0)) # Bound without listen: no HTTP server on this port. results['connection-refused'] = inspect(f'http://127.0.0.1:{reserved.getsockname()[1]}/') assert results['/application-error']['http_status'] == 200 assert results['/application-error']['payload']['ok'] is False assert results['/busy']['http_status'] == 503 and results['/busy']['retry_after'] == '3' assert results['/limit']['http_status'] == 429 and results['/limit']['retry_after'] == '2' assert results['/missing']['http_status'] == 404 assert results['/truncated'] == {'http_status': 200, 'retry_after': None, 'complete_body': False} assert results['connection-refused'] == {'http_status': None, 'transport_failure': True} print(json.dumps(results)) finally: server.shutdown() thread.join() server.server_close() 应用信封返回 HTTP 二百与 ok: false。繁忙和限流端点分别返回五零三、四二九,并保留 Retry-After 的三秒和两秒文本值。缺失端点仍被记录成 HTTP 四零四,而不是传输错误。这四份正文在本实验中均完整且可解析,因而能够进一步判断其业务字段;脚本没有实际等待这些延迟,也没有宣称等待后一定成功。 ...