---
title: "POST 重定向后正文去哪了：实测 303、307 和 308"
date: 2026-11-13
slug: http-redirect-post-method
canonical: https://kernelranch.com/blog/2026/11/http-redirect-post-method
markdown_url: https://kernelranch.com/blog/2026/11/http-redirect-post-method.md
tags: ["http", "redirects", "nodejs"]
---网关迁移提交接口后，上游一直收到 GET，调用方却记得自己发的是 POST。先关闭自动重定向看第一跳，再按状态码重放请求。303 用于将客户端引向另一个可读取的资源，307 和 308 保留方法与请求体。

## 1. 在同一个服务里记录跳转前后

保存为 redirect_lab.mjs，运行 node redirect_lab.mjs。

```javascript
import assert from 'node:assert/strict';
import { once } from 'node:events';
import { createServer } from 'node:http';

const server = createServer(async (req, res) => {
  const chunks = [];
  for await (const chunk of req) chunks.push(chunk);
  const body = Buffer.concat(chunks).toString('utf8');
  if (req.url === '/result') {
    res.writeHead(200, { 'Content-Type': 'application/json' })
      .end(JSON.stringify({ method: req.method, body }));
    return;
  }
  const status = Number(req.url.slice(1));
  if (![302, 303, 307, 308].includes(status)) {
    res.writeHead(404).end();
    return;
  }
  res.writeHead(status, { Location: '/result' }).end();
});

server.listen(0, '127.0.0.1');
await once(server, 'listening');
const origin = 'http://127.0.0.1:' + server.address().port;
try {
  for (const status of [302, 303, 307, 308]) {
    const url = origin + '/' + status;
    const options = { method: 'POST', body: 'quantity=2' };
    const first = await fetch(url, { ...options, redirect: 'manual' });
    assert.equal(first.status, status);
    assert.equal(first.headers.get('location'), '/result');
    await first.arrayBuffer();

    const final = await fetch(url, options);
    assert.equal(final.status, 200);
    const received = await final.json();
    assert.deepEqual(received,
      status < 307 ? { method: 'GET', body: '' } : { method: 'POST', body: 'quantity=2' });
    console.log(status, '=>', received.method, JSON.stringify(received.body));
  }
} finally {
  server.close();
}
```

本机实测输出如下。

```text
302 => GET ""
303 => GET ""
307 => POST "quantity=2"
308 => POST "quantity=2"
```

## 2. 以业务动作选择重定向

| 业务场景 | 选择 |
|---|---|
| POST 已经创建成功，要跳到结果页 | 303，客户端改用 GET 读取结果 |
| 接口临时迁移，目标端接管这次 POST | 307，转发目标继续收取相同方法和正文 |
| 接口永久迁移，希望保留请求方法 | 308，并检查客户端和缓存会如何处理永久重定向 |

RFC 9110 允许客户端在 302 后把 POST 改为 GET。本机 Node fetch 选择了改写，因此接口迁移不能依赖 302 保留正文。重放上传时，字符串可以再次读取，流式请求体则可能无法重放。先测试客户端库能否自动跟随，再核对跨域鉴权头与实际路由。

## 小结

日志分别记录第一跳状态和最终收到的方法。如果还要验证正文，在测试端捕获并比较正文长度或测试字符串。

## 数据来源与检索时间

检索时间 2026-11-13。

| 来源 | URL | 对应内容 |
|---|---|---|
| RFC 9110 §15.4 | https://www.rfc-editor.org/rfc/rfc9110.html#section-15.4 | 不同重定向状态的语义 |
| WHATWG Fetch | https://fetch.spec.whatwg.org/#http-redirect-fetch | 方法改写、正文重放和跨来源规则 |

## 核验说明

- Node.js v24.19.0 对四种状态码实测。请求体为可重放字符串，使用本地同源服务。
