状态码需要放在请求方法的上下文里理解,它告诉客户端怎样解释本次 HTTP 响应。首位数字只能缩小类别,不能说明完整业务结果。例如,任务已经接受,仍可能没有完成;三开头的响应,也未必表示页面搬家。服务器应根据实际完成的事情选择具体状态,并提供相应响应头和内容,避免让一个看起来合理的数字承担它从未承诺的含义。
成功不只有一种含义
二百表示请求按照相应方法的语义成功。成功读取任务状态资源,可以如实返回“任务仍在等待”的表示;这不代表任务本身已经结束。二零一表示创建了一个或多个资源,提供 Location 时,它用于识别主要的新建资源。接口需要说明客户端接下来应操作哪个资源,而不是把所有二开头的响应都解释成“用户最初想要的整个业务已经完成”。
二零二表示请求已经接受处理,但尚未完成,也不会对最终一定执行成功作出保证。应提供观察进度的途径,并定义最终成功与失败,否则客户端很难区分任务缓慢与任务丢失。二零四表示请求成功完成且没有响应内容;不能因为框架平常总是序列化 JSON,就给这种响应也附加正文。具体状态对应的消息约束需要贯穿最终输出。
三开头也可能是在验证已有表示
三零一与三零八表达永久重定向,三零二与三零七表达临时重定向,并具有不同的方法保留规则。三零三可以让调用方在处理步骤之后转而读取另一个资源。请求携带正文或具有副作用时,更要明确选择:可能改变方法的跳转,与必须保留方法的跳转,不能随意互换。客户端是否跟随,也应依据它实际要完成的动作判断。
三零四是另一种情况:它回应条件 GET 或 HEAD,表示无需重新传输选中的表示。接收方按缓存规则使用合适的已存内容,并更新相关元数据。三零四没有响应内容,也不是要求客户端沿 Location 导航。此时显示为空,是协议预期,并不能证明服务器把文件弄丢了。如果调用方没有相应缓存表示,就不能把这个空正文直接当作完整资源交给上层。
坏掉的样子
把所有四开头都归为“不能重试”,会误处理限流等情况;把所有五开头都归为“立即重试”,又可能重复非幂等操作或加剧过载。五零三覆盖暂时的服务不可用,包括过载或维护,并不只用于计划停机。重试决策还要结合方法、服务器提示和客户端预算。状态类别提供线索,不是能够无条件套用的完整重试算法。
四零四没有保证缺失只是暂时的:它可以表示找不到当前表示,也可以表示服务器不愿披露该表示。四一零才传达可能永久不可用的含义。同样,四零三并不普遍证明调用者已通过身份验证。业务代码不能在这些数字上附加比协议更强的假设;响应正文也应符合接口已声明的契约,否则下游即使正确识别了状态,仍可能误解实际处理结果。
快速验证
保存为 status-contract.py,运行 python3 status-contract.py。实验通过真实回环 HTTP 请求操作一个很小的内存任务状态和固定资源。这里使用的 Python HTTP 客户端不会自动跟随 Location,也没有实现缓存,所以脚本中每一步后续动作都明确写出。它不需要真实队列或数据库,避免把未验证的后台处理能力混进状态语义实验。
from http.client import HTTPConnection
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from threading import Thread
import json
job = {'state': 'absent'}
class Handler(BaseHTTPRequestHandler):
def reply(self, status, data=b'', headers=None):
self.send_response(status)
if status not in (204, 304):
self.send_header('Content-Length', str(len(data)))
for key, value in (headers or {}).items():
self.send_header(key, value)
self.end_headers()
if status not in (204, 304):
self.wfile.write(data)
def do_POST(self):
if self.path != '/jobs':
self.reply(404)
return
job['state'] = 'pending'
self.reply(202, b'{"state":"pending"}',
{'Content-Type': 'application/json', 'Location': '/jobs/42'})
def do_GET(self):
if self.path == '/jobs/42':
if job['state'] == 'complete':
self.reply(303, headers={'Location': '/results/42'})
elif job['state'] == 'pending':
self.reply(200, b'{"state":"pending"}', {'Content-Type': 'application/json'})
else:
self.reply(404)
elif self.path == '/results/42' and job['state'] == 'complete':
self.reply(200, b'{"value":7}', {'Content-Type': 'application/json'})
elif self.path == '/asset':
if self.headers.get('If-None-Match') == '"v1"':
self.reply(304, headers={'ETag': '"v1"'})
else:
self.reply(200, b'artifact', {'ETag': '"v1"'})
else:
self.reply(404)
def log_message(self, *args):
pass
server = ThreadingHTTPServer(('127.0.0.1', 0), Handler)
thread = Thread(target=server.serve_forever)
thread.start()
try:
responses = []
def request(method, path, headers=None):
connection = HTTPConnection('127.0.0.1', server.server_port, timeout=5)
try:
connection.request(method, path, headers=headers or {})
response = connection.getresponse()
value = {'status': response.status, 'body': response.read().decode(),
'location': response.getheader('Location'), 'etag': response.getheader('ETag')}
responses.append(value)
return value
finally:
connection.close()
accepted = request('POST', '/jobs')
assert accepted['status'] == 202 and job['state'] == 'pending'
assert request('GET', '/results/42')['status'] == 404
assert json.loads(request('GET', accepted['location'])['body']) == {'state': 'pending'}
job['state'] = 'complete' # Explicit fixture transition, not a background worker.
redirect = request('GET', '/jobs/42')
assert redirect['status'] == 303 and redirect['location'] == '/results/42'
assert json.loads(request('GET', redirect['location'])['body']) == {'value': 7}
cached = request('GET', '/asset')
validation = request('GET', '/asset', {'If-None-Match': cached['etag']})
assert validation['status'] == 304 and validation['body'] == '' and validation['location'] is None
assert cached['body'] == 'artifact'
print(json.dumps({'responses': responses, 'reuse_cached_body': cached['body']}))
finally:
server.shutdown()
thread.join()
server.server_close()
POST 首先返回二零二,任务此时仍为等待状态;结果地址仍返回四零四,读取状态资源则返回二百和 pending 表示。之后脚本显式把内存状态改成完成,这不是已经测试过的后台工作进程。状态端点随即返回三零三,客户端再读取 Location 指向的位置,取得数值为七的结果。每个数字描述的都是当前响应,并没有提前承诺后面的步骤一定成功。
固定资源首次 GET 返回 artifact 与 ETag "v1"。客户端把验证器带回去后,得到没有内容、也没有 Location 的三零四,同时保留之前收到的字节用于复用。这展示响应契约,不意味着已经运行过浏览器缓存测试。实验只识别一个完全匹配的 If-None-Match 字符串,没有实现列表、通配符,也没有实现一般条件 GET 所要求的弱比较规则。初次表示与条件响应需要共同解释,不能只拿最后一条响应的正文是否为空来判断资源是否可用。
七个实际响应区分了接受处理、成功读取状态、跳转到结果,以及条件复用,没有证明任务持久入队或最终一定完成。审阅接口时,应把状态选择与响应头、内容和状态迁移放在一起检查。仅有一个看似合理的数字,既不能让未完成的业务自动完成,也不能让不符合契约的表示变成可用结果。
延伸阅读
来源
- RFC 9110: HTTP Semantics,成功、重定向和错误响应的精确定义,已核验 2026-09-13。
- MDN: HTTP response status codes,具体状态行为及相应响应头,已核验 2026-09-13。