POST 创建资源如何返回 201 与 Location?
解读
国内一线、二线互联网公司面试时,这道题表面考“状态码”,实则验证候选人是否真正理解 RESTful 设计、HTTP 语义以及 PHP 对原生 Header 的操控能力。
很多候选人只答“header() 函数”,却忽略以下扣分点:
- 201 必须在响应体为空或仅含 JSON 描述时返回,不能混用 200;
- Location 必须是绝对 URI(RFC 2616 推荐),且需先校验资源是否落库成功;
- 框架场景下,须说明如何关闭默认 200 并防止重复渲染;
- 高并发场景下,需保证事务提交后再发送 Header,否则会出现“已发送 output”警告。
面试官常追问:
“如果提前 echo 了日志,header() 失效怎么办?”
“Laravel 里如何一句话返回 201 并带 Location?”
“ swoole 或 RoadRunner 常驻进程下为何要用响应对象而非 header()?”
答到这些细节,才能体现“工程级”经验。
知识点
- HTTP/1.1 201 Created 语义:请求已被满足,新资源已生成,Location 指向其 URI。
- PHP 输出控制:header() 前不能有任何字符输出;output_buffering / ob_start 可兜底。
- RFC 7231 对 Location 格式要求:绝对 URI,建议同时返回 Content-Location 区分版本。
- RESTful 资源标识策略:自增主键用 /orders/123,UUID 用 /orders/a1b2c3…,面试需给出理由。
- 框架封装:
- Laravel: response()->json(url);
- Symfony: new JsonResponse(url]);
- ThinkPHP8: return json(url]);
- 事务安全:PDO 事务提交成功后再发送 Header,防止“回滚但已 201”的脏数据。
- Swoole/RoadRunner 常驻进程:禁止 header(),须用 url)。
- 单元测试:PHPUnit 断言 response->getStatusCode() === 201 && response->hasHeader('Location)。
答案
原生 PHP 写法(最简可运行,含防御性 ob):
<?php
declare(strict_types=1);
try {
$pdo = new PDO($dsn, $user, $pass, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
$pdo->beginTransaction();
$stmt = $pdo->prepare("INSERT INTO posts (title, body) VALUES (?, ?)");
$stmt->execute([$_POST['title'], $_POST['body']]);
$id = (int) $pdo->lastInsertId();
$pdo->commit(); // 必须提交后再输出头部
$location = 'https://api.example.com/posts/' . $id; // 绝对 URI
ob_start(); // 防止之前意外输出导致 header 失效
header('HTTP/1.1 201 Created'); // 有些 CGI 环境需用 "HTTP/1.1"
header('Location: ' . $location);
ob_end_clean();
// 可选:返回极简 JSON 描述
echo json_encode(['id' => $id, 'uri' => $location], JSON_UNESCAPED_SLASHES);
exit;
} catch (Throwable $e) {
$pdo->rollBack();
http_response_code(500);
echo json_encode(['error' => 'Internal Server Error']);
}
Laravel 一句话写法(符合国内主流框架习惯):
public function store(PostRequest $request)
{
$post = Post::create($request->validated());
return response()
->json(['data' => new PostResource($post)], 201)
->header('Location', route('posts.show', ['post' => $post->id]));
}
关键点:
- 201 状态码由 response()->json 第二个参数控制;
- route() 生成绝对 URI,满足 RFC;
- 无手动 echo,框架自动处理输出缓冲。
拓展思考
- 如果资源创建是异步任务(队列、工单系统),应返回 202 Accepted,并在响应体里给出“任务状态查询地址”,此时不能再给 201。
- 批量导入场景:一次 POST 创建多条记录,可返回 207 Multi-Status,每个子项携带自己的 Location;国内电商 ERP 接口常用此技巧。
- API 网关层(Nginx+Lua、Kong)可能强制重写状态码,需在 Location 中加入自定义头(X-Resource-Id)供前端兜底。
- 高并发下主从延迟:刚插入主库,立即 302 到从库读详情可能 404,可在 Location 后加“grace=1s”参数,或读主库一次再返回。
- GraphQL mutation 没有“状态码”概念,但可在响应扩展字段里返回“location”,面试可借此展示对 REST vs GraphQL 差异的理解。