01基本模型:两张条子,然后各走各的
整个 HTTP 就是一次交换:客户端发一个 request,也就是一张有格式的条子;服务端回一个 response,另一张条子。然后是大多数人忽略的那一步,服务端就把你忘了。HTTP 是无状态的。你体验过的每一种「已登录」,都是靠每张条子上重新贴一次身份证做出来的,这张身份证就是 cookie 或者 token。
## the request — literally this readable:
GET /users/42 HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbG...
## the response:
HTTP/1.1 200 OK
Content-Type: application/json
{"id": 42, "name": "Ada"}
正因为无状态,你的会话才会在操作到一半时突然过期;调 API 才必须每一次都带上 key,而不是只在第一次带;负载均衡才成立,任何一台服务器都能回答任何一张条子,因为没有一张条子依赖上一张。所以当鉴权「莫名其妙失效」时,是身份证没贴上去,不是服务器把你忘了。
02拆开一个 URL
URL 是一个地址,有四个真正干活的部分:协议、楼(host)、房间(path),还有门上贴的便签(query 参数)。学会读它,API 文档就不再吓人了。
https://api.example.com/users/42/orders?status=paid&limit=10
└─┬──┘ └──────┬──────┘└──────┬──────┘ └────────┬─────────┘
scheme host path query params
(how) (which server) (which thing) (options: key=value, &-separated)
query 参数会被到处记下来:服务端日志、代理、浏览器历史、统计工具。所以密钥永远不放 URL 里,
?api_key=sk_live_... 这种 key,你就当它已经泄露了。密钥要走请求头(第 07 节),请求头默认不会被写进日志。
03方法:信封上的那个动词
method 告诉服务端这是哪一类条子。五个就够用了:GET 读,POST 建,PUT 整体替换,PATCH 改一部分,DELETE 删。它们背后真正值得弄懂的概念是幂等性:同一个请求发两遍安全吗?
GET /users/42 # read — never changes anything
POST /users # create a new one ⚠ twice = two users
PUT /users/42 # replace entirely — twice = same result
PATCH /users/42 # change some fields
DELETE /users/42 # delete — twice = still deleted
「支付按钮别点两次」就是一个幂等性 bug。GET、PUT、DELETE 重试都安全,POST 不安全。这就是支付类 API 要求你带上
Idempotency-Key 请求头的原因:重试过去的请求只会产生一笔扣款,不是两笔。POST 发出去一半网络超时,「到底成没成」这个问题,就是这个词存在的全部理由。
04状态码:到底是谁错了
第一位数字就是全部剧情:2xx 成了,3xx 搬走了,4xx 是你错了,5xx 是他们错了。就这一句话,能把你以后遇到的每个 API 报错分成两堆:「改我的请求」和「等一等或者去报障」。还有那对千古纠缠的兄弟,这里一次说清:
200 OK # here you go
201 Created # made it (answer to a good POST)
301 Moved # new address, go there
400 Bad Request # your note is malformed (bad JSON, missing field)
401 Unauthorized # WHO ARE YOU? — no/invalid credentials
403 Forbidden # I know who you are. No. — valid login, no rights
404 Not Found # no such thing at this path
429 Too Many Requests # slow down (rate limit — wait and retry)
500 Internal Error # server crashed. not your fault
503 Unavailable # server overloaded/down. also not your fault
401 = 门口的保安看不懂你的证件:没带、过期,或者格式不对,去查你的 Authorization 请求头。403 = 你的证件没问题,但名单上没你:去查权限和 scope,别再折腾 token 了。这两个名字本身是反的,
Unauthorized 表达的其实是未认证,一个五十年前的命名事故,所有人都将就着用到今天。
05请求头与 body:信封和里面的信
header 是写在信封上的元信息:什么格式、什么语言、谁在问、拿的是什么凭证。body 是信封里那张信,对 API 来说基本都是 JSON。你会反复设置两个请求头,再反复读第三个:
Content-Type: application/json # "the body I'm SENDING is JSON"
Authorization: Bearer <token> # "here's my ID card"
Accept: application/json # "please REPLY in JSON"
# the body (POST /users):
{ "name": "Ada", "country": "UK" }
你 POST 了一份完全正确的 JSON,拿回来一个
400 Bad Request,或者一个字段全是 null 的怪结果。因为你忘了写 Content-Type: application/json,服务端把你的 JSON 当表单提交解析了。body 没错,是信封上的标签错了。一个请求头,解释了 Stack Overflow 上十年的提问。
06curl:在终端里跟任何 API 说话
curl 从命令行发 HTTP 请求。它的用处是把你自己的应用挪开去测 API,这也是最好用的一招排查:curl 通、代码不通,bug 在你的代码里;curl 也不通,问题在 API 那边。分而治之。
# read
curl https://api.example.com/users/42
# create (-X method, -H header, -d body)
curl -X POST https://api.example.com/users \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d '{"name": "Ada"}'
# see the STATUS CODE and headers too (the debugging view)
curl -i https://api.example.com/users/42
07鉴权:API key 与 bearer token
两种模式覆盖了大多数 API。API key 是给程序用的长期密码。bearer token 通常有效期很短,靠登录换来,用 Authorization: Bearer … 的形式出示。bearer 的字面意思是谁拿着它,谁就是它。这一个词既说明了安全模型,也就是那句安全警告。
# both ride in the Authorization header:
Authorization: Bearer eyJhbGciOi... # token (often a JWT)
x-api-key: sk_live_4242... # some APIs use a custom header
# keys live in environment variables, never in code:
curl -H "Authorization: Bearer $API_TOKEN" ...
不进 URL(到处都会被记下来,见第 02 节)。不进 git(爬虫几分钟就能扒到,事故处理流程看 Git 那一页)。不进前端代码(查看源码就等于泄露)。key 放环境变量和密钥管理服务里;一旦怀疑某个 key 可能泄露了,先轮换,再调查。
08REST,不谈教条
REST 是一套命名约定,不是一种技术:URL 里放东西(名词),方法说要对它做什么(动词)。百分之九十就这些。一个 API 算不算 RESTful,标准是你能不看文档猜出下一个 endpoint,可预测性就是它的全部意义。
# nouns in the URL, verbs in the method:
GET /articles # list articles
POST /articles # create one
GET /articles/7 # read one
PATCH /articles/7 # edit it
DELETE /articles/7 # remove it
GET /articles/7/comments # nested: its comments
✗ POST /getArticleById ✗ GET /deleteUser?id=7 # verbs in URLs = smell
你会遇到有人争论「什么才是真正的 REST」,扯 HATEOAS、扯 Roy Fielding 的博士论文。笑一笑,走开。实际工作里,「REST API」就是这一节展示的东西:可预测的名词 URL、标准动词、JSON。这套词汇足够你在任何真实代码库里干活,也足够应付面试题。
09CORS:那个报错不是它看起来的意思
有一天你的前端去调一个 API,控制台开始尖叫。这里有个几乎人人搞错的地方:这个报错不是服务端拒绝了你,是你自己的浏览器在执行一条安全规则:来自站点 A 的 JavaScript 不能读站点 B 的响应,除非 B 明确允许,也就是回一个 Access-Control-Allow-Origin 响应头。证据很简单,同一个请求用 curl 发就完全正常。
Access to fetch at 'https://api.other.com/data' from origin
'http://localhost:3000' has been blocked by CORS policy: No
'Access-Control-Allow-Origin' header is present...
# decode: api.other.com didn't say localhost:3000 may read it.
# the browser fetched the data — then withheld it from your JS.
只有服务端能修 CORS,方法是回那个允许头。前端再怎么写,都不可能正当地让它消失。你的选择是:这个 API 是你的,就为你前端的 origin 加上响应头;是别人的,就从你自己的后端去调,让前端调你的后端(后端不是浏览器,CORS 管不到它)。浏览器上那些「关闭 CORS」的插件只修好了你这台机器,然后对每一个真实用户都是坏的。
10速查表
这几张表能回答百分之九十的 API 问题。
# methods
GET read · POST create (⚠ not retry-safe) · PUT replace
PATCH edit · DELETE remove
# status: first digit = who messed up
2xx ✓ · 3xx moved · 4xx you · 5xx them
401 who are you? → fix the token
403 you specifically? no. → fix the permissions
429 slow down → wait and retry
# the POST that "mysteriously" fails
→ did you send Content-Type: application/json ?
# debugging order
curl -i first. code second.
curl works + code fails → your bug
curl fails too → their bug (or your token)
# CORS error in console
→ browser rule, server must allow it. curl proves the API works.
# secrets
headers ✓ · env vars ✓ · URLs ✗ · git ✗ · frontend ✗
这就是能干活的核心。再花哨的东西,webhook、GraphQL、gRPC,都是同样两张条子的变体。等你想用代码去调 API,Python 加上这一页就是一套完整的工具。