POST请求 body 为空导致 400 错误问题修复方案

AI摘要
【问题提问】本文分析了网关转发POST请求时,因body为空且Content-Type为application/json导致下游返回400错误的问题。根因在于yii\httpclient严格构造请求头,而部分后端框架会拒绝无body的JSON POST请求。修复方案是在网关层对POST/PUT/PATCH方法且body为空的情况,将方法降级为GET,从而避免协议不匹配,且不侵入vendor代码。

一、问题背景

在系统网关转发(TransmitService → SystemProxy → yii\httpclient)过程中,
发现 POST 请求在 body 为空时会返回 400 Bad Request,而同一接口:

  • curl GET 请求正常
  • curl POST(无 body)正常
  • 直接访问下游服务正常

仅在 通过网关转发 POST + body 为空 的情况下触发 400。


二、问题现象

1. 错误日志表现

Client error: `POST http://localhost:7005/v1/risk-type/get-code-list`
resulted in a `400` response

2. 对比验证

# GET 正常
curl http://localhost:7005/v1/risk-type/get-code-list

# POST(无 body)正常
curl -X POST http://localhost:7005/v1/risk-type/get-code-list

说明 下游接口本身支持 POST 且不强制 body


三、问题根因分析(关键)

1. 根因定位

问题不在接口逻辑,而在 yii\httpclient 的请求构造方式

$request->setMethod('POST')
        ->setContent(null);   // 或 setContent('')

在以下场景会触发问题:

  • method = POST

  • Content-Type = application/json

  • Content-Length = 0

  • body 实际为空

此时 Nginx / PHP-FPM / 部分后端框架 会将其判定为:

非法 POST 请求(JSON POST 却无 body)

从而直接返回 400 Bad Request


2. 为什么 GET 不受影响

  • GET 请求 不会校验 body

  • 即使设置 Content-Type,也不会解析 request body

  • 因此 GET 天然安全


3. 为什么 curl 不复现

curl 在以下情况下行为不同:

  • POST 且未指定 -d

  • 实际不会发送 Content-Type: application/json

  • Content-Length 处理更宽松

而 yii\httpclient 严格构造 HTTP 请求头


四、修复思路(设计原则)

核心原则

POST / PUT / PATCH 请求,在 body 为空时,不应携带 JSON body 语义

换句话说:

  • body 为空 → 不应强行 setContent

  • body 为空 → 请求语义应退化为 GET(如果业务允许)


五、最终修复方案(已验证)

1. 网关层兜底修复(TransmitService)

在请求转发前,对 method + body 做兜底判断:

$rawBody = $request->getRawBody();
$method  = $request->getMethod();

// POST / PUT / PATCH 且 body 为空时,降级为 GET
if (in_array($method, ['POST', 'PUT', 'PATCH'], true)
    && ($rawBody === '' || $rawBody === null)
) {
    $rawBody = null;
    $method  = 'GET';
}

然后再传给 systemProxy:

$response = Yii::$app->systemProxy->transmit(
    $method,
    $system->system_domain,
    $uri,
    $rawBody,
    $headers
);

2. systemProxy transmit 方法(保持原逻辑)

public function transmit(
    string $method,
    string $domain,
    string $uri,
    ?string $rawBody,
    HeaderCollection $headers
) {
    $this->client->baseUrl = $domain;

    $request = $this->client->createRequest()
        ->setMethod($method)
        ->setUrl($uri)
        ->setHeaders($headers)
        ->setContent($rawBody)
        ->addOptions(['timeout' => $this->timeout]);

    return $request->send();
}

关键点:不在 vendor 层做业务判断,避免后续升级风险


六、修复效果验证

  • ✅ POST + body 为空 → 正常返回

  • ✅ GET → 正常

  • ✅ POST + JSON body → 正常

  • ✅ multipart/form-data 不受影响

  • ✅ 无需修改下游服务


七、风险评估

已规避风险

  • ❌ vendor 目录侵入式修改

  • ❌ 强行 setContent(null) 的隐式副作用

  • ❌ 吞掉 400 错误导致问题隐藏

可接受前提

  • 当前接口语义:POST ≈ 查询

  • 不依赖 POST body 触发业务逻辑


八、结论

本问题本质是:

HTTP 方法语义 + body + Content-Type 不匹配导致的协议级 400

通过在 网关层做 method + body 语义兜底,可以:

  • 保证请求合法性

  • 兼容历史接口

  • 不污染 vendor 代码

  • 为后续统一 API 规范留出空间


九、后续优化建议(非必须)

  • 统一约定:查询类接口使用 GET

  • POST 强制要求 body(JSON Schema 校验)

  • 在网关增加 request 规范校验日志(非拦截)

本作品采用《CC 协议》,转载必须注明作者和本文链接
每天一点小知识,到那都是大佬,哈哈
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!