本地部署正常;服务器部署 POST 方法参数丢失解决方案
本地部署正常;服务器部署 POST 方法参数丢失解决方案
背景
同一套 JAR 代码,本地127.0.0.1:6060直连测试正常,部署到服务器后通过域名http://web.mycdn66.com调用时,POST 请求 body 中的参数丢失,接口返回{"msg":"无效参数","ok":false}。
本文档记录问题原因及解决方案。
问题
问题现象
| 环境 | 访问方式 | 参数位置 | 结果 |
|---|---|---|---|
| 本地 | http://127.0.0.1:6060/... | POST body | 正常 |
| 服务器 | http://web.mycdn66.com/... | POST body | 参数丢失,返回「无效参数」 |
| 服务器 | http://web.mycdn66.com/...?mobile=... | URL 查询字符串 | 正常 |
| 服务器 | https://web.mycdn66.com/... | POST body | 正常 |
问题描述
通过http://访问域名并发送POST请求(参数在 body 中,如multipart/form-data)时,接口返回{"msg":"无效参数","ok":false}。
改为https://或把参数放在 URL 查询字符串上则正常。
该问题与后端框架无关(TIO / Spring Boot / JFinal 均可能遇到),根因在Nginx 的 HTTP→HTTPS 跳转与HTTP 客户端对 301 的处理方式。
复现步骤(失败)
Unirest.post("http://web.mycdn66.com/mytio/sms/beforeCheck.tio_x").multiPartContent().field("p_is_android","1").field("_lau","cn").field("biztype","2").field("mobile","15837827991").asString();响应:
{"msg":"无效参数","ok":false}与本项目相关的代码
接口路径:/mytio/sms/beforeCheck.tio_x
控制器:SmsController.beforeCheck
@RequestPath(value="/beforeCheck")publicRespbeforeCheck(Bytebiztype,Stringmobile,HttpRequestrequest)throwsException{returnbizPhoneCheck(biztype,mobile,request);}publicstaticRespbizPhoneCheck(Bytebiztype,Stringmobile,HttpRequestrequest){if(biztype==null||mobile==null){returnResp.fail().msg(RetUtils.INVALID_PARAMETER);// "无效参数"}// ...}当biztype或mobile为null时返回"无效参数",说明参数没有到达 Java 应用,而非业务逻辑错误。
根因分析
请求链路对比
| 访问方式 | 实际路径 | 是否有 301 跳转 | POST Body |
|---|---|---|---|
http://web.mycdn66.com/... | 客户端 → Nginx → 301 → 客户端 → Nginx → Java | 有 | 可能在跳转时丢失 |
https://web.mycdn66.com/... | 客户端 → Nginx → Java | 无 | 正常到达 |
http://127.0.0.1:6060/... | 客户端 → Java | 无 | 正常到达 |
301 是什么
301 Moved Permanently表示资源已永久迁移到新地址。Nginx 常见配置:
server { listen 80; server_name web.mycdn66.com; return 301 https://$host$request_uri; }访问http://时,Nginx不会把请求转发给 Java,而是直接返回 301,告诉客户端去https://再请求一次。
为什么会丢 Body
第 1 次:POST http://域名/xxx body 含 mobile、biztype 第 2 次:Nginx 返回 301 → Location: https://域名/xxx 第 3 次:客户端再 POST https://域名/xxx 很多 HTTP 客户端(Unirest、Apifox、部分 HttpClient)在 301/302 跳转时 不会把原来的 POST body 再发一遍 第 4 次:Java 收到空 body → biztype、mobile 为 null → "无效参数"注意:
- Nginx不会主动把参数改成
null - Java不会因为框架不同而免疫(Spring Boot 同样会遇到)
- 问题发生在请求到达 Java 之前
为什么 URL 传参不受影响
301 跳转时,新 URL 会保留 query string:
http://域名/xxx?mobile=15837827991&biztype=2 ↓ 301 https://域名/xxx?mobile=15837827991&biztype=2URL 上的参数会保留,所以即使用http://也能成功。
为什么本地 127.0.0.1 正常
本地直连127.0.0.1:6060,不经过 Nginx,没有 301 跳转,POST body 一次到达 Java。
为什么「同一个 JAR」表现不同
JAR 代码相同,但:
- 本地测试:
127.0.0.1:6060直连 - 远程测试:
web.mycdn66.com经 Nginx,且使用http://
环境链路不同,不是代码不一致。
HTTP 状态码对比
| 状态码 | 含义 | POST Body 是否保留(取决于客户端) |
|---|---|---|
| 301 | 永久跳转 | 很多客户端不保留 |
| 302 | 临时跳转 | 很多客户端不保留 |
| 307 | 临时跳转,方法不变 | 应保留 |
| 308 | 永久跳转,方法不变 | 应保留 |
常见误解
| 误解 | 实际情况 |
|---|---|
| JAR 代码和服务器不一致 | 同一 JAR,链路不同(直连 vs 经 Nginx 301) |
JFinal 需要先getFile()才能getPara() | 本项目 HTTP 层是 TIO,不是 JFinal MVC |
| Nginx 把参数改成了 null | Nginx 未转发或客户端跳转时未带 body,Java 读不到参数 |
| Spring Boot 不会有这个问题 | 任何框架都会在 body 未到达时得到 null |
| 只有 multipart 会出问题 | 任何 POST body(JSON、表单、文件)都可能受影响 |
问题结论
- 本地正常、服务器异常:不是 JAR 代码不一致,而是请求链路不同(本地直连 vs 服务器经 Nginx)
- 根因:Nginx 301 跳转 HTTPS,客户端跳转时未保留 POST body
- 验证:改为
https://后问题消失 - 性质:非 Java 代码问题,属 HTTP 跳转与客户端行为问题
解决方案
方案 1:客户端统一使用 HTTPS(推荐,已验证有效)
将所有http://web.mycdn66.com改为https://web.mycdn66.com。
Unirest.post("https://web.mycdn66.com/mytio/sms/beforeCheck.tio_x").header("Cookie","tio_session=...").multiPartContent().field("p_is_android","1").field("_lau","cn").field("biztype","2").field("mobile","15837827991").asString();注意:
- 不要手动设置
Content-Type: multipart/form-data; boundary=...,交给.multiPartContent()自动生成 - Apifox、移动端、第三方调用方均需统一改为
https://
方案 2:参数放 URL 查询字符串
适用于无文件上传的普通参数接口:
Unirest.post("https://web.mycdn66.com/mytio/sms/beforeCheck.tio_x?p_is_android=1&_lau=cn&biztype=2&mobile=15837827991").header("Cookie","tio_session=...").asString();无需.multiPartContent()。
方案 3:普通表单代替 multipart(无文件时)
Unirest.post("https://web.mycdn66.com/mytio/sms/beforeCheck.tio_x").field("p_is_android","1").field("_lau","cn").field("biztype","2").field("mobile","15837827991").asString();不调用.multiPartContent(),使用application/x-www-form-urlencoded。
方案 4:Nginx 改用 307 跳转(服务端兜底)
将 301 改为 307,要求客户端跳转时保留 POST 方法和 body:
server { listen 80; server_name web.mycdn66.com; return 307 https://$host$request_uri; }改完后执行:
nginx-tsystemctl reload nginx方案 5:80 端口 API 直接代理(不跳转)
对 API 路径在 80 端口直接proxy_pass,仅页面对用户做 http→https 跳转:
upstream tio_site_api { server 127.0.0.1:6060; keepalive 32; } server { listen 80; server_name web.mycdn66.com; client_max_body_size 200m; location /mytio/ { proxy_pass http://tio_site_api; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_http_version 1.1; proxy_set_header Connection ""; } location / { return 301 https://$host$request_uri; } }注意:HTTP 明文传输安全性较低,生产环境仍建议客户端优先使用 HTTPS。
验证步骤
在服务器上执行:
# 1. 直连 Java(应成功)curl-v-XPOST"http://127.0.0.1:6060/mytio/sms/beforeCheck.tio_x"\-H"Cookie: tio_session=你的session"\-F"p_is_android=1"-F"_lau=cn"-F"biztype=2"-F"mobile=15837827991"# 2. 走域名 HTTP(可能因 301 丢 body 而失败)curl-v-XPOST"http://web.mycdn66.com/mytio/sms/beforeCheck.tio_x"\-H"Cookie: tio_session=你的session"\-F"p_is_android=1"-F"_lau=cn"-F"biztype=2"-F"mobile=15837827991"# 3. 走域名 HTTPS(应成功)curl-v-XPOST"https://web.mycdn66.com/mytio/sms/beforeCheck.tio_x"\-H"Cookie: tio_session=你的session"\-F"p_is_android=1"-F"_lau=cn"-F"biztype=2"-F"mobile=15837827991"观察第 2 步是否出现301 Moved Permanently,以及第 3 步是否返回{"ok":true}。
推荐实践
- 所有客户端、Apifox、App 统一使用
https:// - Nginx 将
return 301改为return 307作为兜底 - 无文件上传的接口优先用 URL 参数或
x-www-form-urlencoded - 不要手动写 multipart 的
Content-Typeboundary
相关文件
- 部署说明:
DEPLOY.md(Nginx 配置示例) - 接口代码:
http-server-api/src/main/java/org/tio/sitexxx/web/server/controller/base/sms/SmsController.java - 运行时配置:
all/src/main/resources/app-env.properties(http.isproxied=true)
