你在浏览器里敲下一个网址,回车,页面就出来了——这背后是浏览器和服务器之间的一次 HTTP 对话。可同样是请求,为什么有的用 GET、有的用 POST?为什么有时页面提示 404,有时又冒出 500?本文用 curl 和 Python 把 HTTP 方法和状态码逐个拆开,读完你就能看懂任何接口文档,也能自己动手调试接口,再看到报错心里就有底了。
1. 一次 HTTP 请求长什么样
HTTP 是一种"一问一答"的协议:客户端(浏览器、curl、手机 App)发出请求,服务器返回响应。一次完整的交互由请求行、请求头、请求体(可选)和状态行、响应头、响应体组成。用 curl 发一个最简单的 GET 请求,加 -i 参数可以看到完整的响应头:
curl -i https://www.example.com/
输出第一行是状态行,长这样:HTTP/1.1 200 OK,依次是协议版本、状态码、原因短语。后面的 Content-Type、Content-Length、Date 等是响应头,空行之后才是响应体。请求那一侧也类似:请求行写着"用什么方法、访问哪个路径、用哪个协议版本",请求头里则带着 Host、User-Agent(浏览器标识)、Accept(期望的返回格式)等信息。
记住这个骨架,后面所有内容都围绕它展开:方法决定"做什么",状态码告诉你"做得怎么样"。抓包工具和浏览器 DevTools 的 Network 面板里,你能看到的也正是这些字段。
2. 五大常用方法
HTTP 规范定义了几十种方法,日常开发 95% 的时间只用这五个:
- GET:获取资源。访问网页、拉取列表都用它,不该产生副作用;
- POST:提交数据。登录、发帖、创建订单,数据放在请求体里;
- PUT:整体更新。把某个资源整个替换成请求体里的内容;
- PATCH:部分更新。只修改资源的某几个字段;
- DELETE:删除资源。
GET 也可以带参数,但只能放在 URL 的 ? 后面,比如 /search?q=python&page=2。这带来两个限制:URL 长度有限,而且参数会出现在地址栏、日志和浏览历史里,所以敏感信息绝不能用 GET 传。需要传大量或敏感数据时,就轮到 POST 上场了。
# 提交表单数据
curl -X POST https://httpbin.org/post \
-d "name=codelab&level=beginner"
# 更常见的做法:提交 JSON
curl -X POST https://httpbin.org/post \
-H "Content-Type: application/json" \
-d '{"name": "codelab", "level": "beginner"}'
-X POST 指定方法,-H 添加请求头,-d 携带请求体。注意第二段代码里显式声明了 Content-Type: application/json,否则服务器可能把 JSON 当成普通表单解析,这是新手最容易踩的坑。表单数据里 name=codelab&level=beginner 用 & 连接多个字段。
PUT 和 DELETE 的写法类似,只是语义不同:
# PUT:整体替换资源
curl -X PUT https://httpbin.org/put \
-H "Content-Type: application/json" \
-d '{"id": 1, "name": "renamed"}'
# DELETE:删除资源,响应体通常是空的
curl -i -X DELETE https://httpbin.org/delete
另外还有个低调的 HEAD 方法:和 GET 一样,但服务器只返回响应头、不返回响应体,常用于"探测资源是否存在、看资源多大"——很多下载工具就是用 HEAD 先查文件大小的。
3. 幂等性:方法语义的底层逻辑
为什么不能"一律用 POST"?因为方法背后有两个重要属性:安全性和幂等性。安全指方法不会修改服务器上的数据,GET、HEAD、OPTIONS 都是安全的;幂等指"执行一次"和"执行一万次"效果相同。GET、PUT、DELETE 都是幂等的——GET 反复拉取没问题,PUT 反复提交同一个 JSON 结果一样,DELETE 删除一个已不存在的资源也不会更糟。POST 则既不安全也不幂等。
幂等性在工程上直接关系"重试"是否安全。比如客户端发起支付,请求超时了,程序自动重发一次——如果用的是 POST,服务器可能收到两笔订单,所以支付接口要么用幂等键去重,要么要求客户端生成唯一的订单号。而 PUT 更新、DELETE 删除这类操作,重试一万次都没问题。理解了这一点,你设计接口时就不会乱选方法了:查用 GET,增用 POST,整体改用 PUT,局部改用 PATCH,删用 DELETE。
4. 状态码:服务器用数字说话
状态码是三位数字,首位代表大类:
| 区间 | 含义 | 常见例子 |
|---|---|---|
| 2xx | 成功 | 200 OK、201 Created、204 No Content |
| 3xx | 重定向 | 301 永久迁移、302 临时跳转、304 未修改(缓存) |
| 4xx | 客户端错误 | 400 请求格式错、401 未认证、403 无权限、404 不存在 |
| 5xx | 服务器错误 | 500 内部错误、502 网关错误、503 服务不可用 |
几个高频状态码值得细说:301 表示资源永久搬家,浏览器会记住新地址并更新书签;302 是临时跳转,典型场景是"未登录跳转到登录页";401 和 403 经常被混淆——401 是"你是谁"(没带凭证),403 是"你是谁也不行"(没权限);500 是服务器自己出错了,问题多半在后端代码或数据库。
再补几个常见的:201 表示"创建成功",POST 新建资源后服务器常返回它,并在 Location 头里给出新资源的地址;204 表示"成功但没有内容",很多删除接口用它;400 是"请求格式不对",而 422 是"格式对但语义不对"(比如邮箱格式非法)。5xx 里,502 是网关或代理转发失败、503 是服务器过载或维护中、504 是网关超时——看到 5xx 先分清"问题出在谁身上",排查方向完全不同。
5. 实战:亲手触发 200、301、404
光看文档不过瘾。我们用 Python 标准库起一个本地服务器,让它对不同路径返回不同状态码,再用 http.client 发请求观察结果。这段代码可以一次跑完,不需要联网:
import threading
from http.server import BaseHTTPRequestHandler, HTTPServer
import http.client
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
if self.path == "/old":
self.send_response(301) # 永久重定向
self.send_header("Location", "/") # 告诉浏览器新地址
self.end_headers()
elif self.path == "/missing":
self.send_response(404) # 资源不存在
self.end_headers()
self.wfile.write(b"not found")
else:
self.send_response(200) # 正常返回
self.end_headers()
self.wfile.write(b"hello")
def log_message(self, *args):
pass # 关掉默认日志,保持输出干净
server = HTTPServer(("127.0.0.1", 0), Handler) # 端口 0 = 随机空闲端口
port = server.server_address[1]
threading.Thread(target=server.serve_forever, daemon=True).start()
for path in ["/", "/old", "/missing"]:
conn = http.client.HTTPConnection("127.0.0.1", port, timeout=3)
conn.request("GET", path)
resp = conn.getresponse()
print(path, "->", resp.status, resp.reason,
"| Location:", resp.getheader("Location"))
conn.close()
server.shutdown()
输出会是这样:
/ -> 200 OK | Location: None
/old -> 301 Moved Permanently | Location: /
/missing -> 404 Not Found | Location: None
几个关键点:用 http.client 而不是 urlopen,是因为 urlopen 会自动跟随重定向,你就看不到 301 了;这里我们想看"原始状态码",所以用底层一点的客户端。send_response 之后用 send_header 加头,最后 end_headers() 收尾,这三步是手写 HTTP 响应的固定套路。HTTPServer(("127.0.0.1", 0), ...) 里的端口 0 表示"让系统分配一个空闲端口",这样多次运行也不会撞车。为了让示例一次跑完,服务器被放进后台线程,测完用 server.shutdown() 关闭;真实项目里直接调用 serve_forever() 让服务器常驻即可。
6. 常见坑与调试技巧
实战中几个高频坑:第一,301 与 302 混用——POST 请求遇到 301 时,部分浏览器会把它改成 GET 重发,涉及表单提交要格外小心;第二,204 没有响应体,有些"删除成功"的接口返回 204,别期待有 JSON;第三,405 Method Not Allowed 表示方法用错了,比如对只接受 GET 的接口发 POST,这是接口文档和代码不一致时的典型报错。
调试时,浏览器里打开 DevTools 的 Network 面板,点开任意请求就能看到状态码、耗时和响应头,红色标出的就是 4xx/5xx。命令行里,curl 有几个参数很常用:-L 跟随重定向、--max-time 设置超时、-w 自定义输出。下面用第 5 节的服务器演示"只取状态码",方便写脚本判断接口健康:
# 把第 5 节的服务器端口改成固定的 8000 运行,然后:
# -o /dev/null 丢弃响应体,-w 自定义输出格式
curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:8000/
curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:8000/missing
7. 总结与练习
本文的核心就两件事:方法决定"做什么",状态码告诉你"结果如何"。GET 查、POST 增、PUT/PATCH 改、DELETE 删;2xx 成功、3xx 重定向、4xx 是客户端的问题、5xx 是服务器的问题。建议动手做这几个练习:
- 把第 5 节的服务器扩展一下:加一个 POST 接口,收到请求后返回 201 和一段 JSON,并打印出请求体内容;
- 用 Python 写一个"链接体检"脚本:准备一个 URL 列表,逐个请求,把 4xx 和 5xx 的地址打印出来;
- 用
curl -i访问你常用的 3 个网站,观察它们返回的状态码和响应头,找找有没有 301/302。
💡 判断方法用没用对,记住一句话:会修改服务器数据的就是"不安全"方法,重复执行结果不变的就是"幂等"方法。设计接口时先想清楚这两点,再选方法。