这确实是一次非常经典的全栈排雷实录。从前端请求到 Nginx 转发,再到 Node.js 后端逻辑,最后到云服务器的系统配置,每一个环节我们都踩到了真实的坑。
这份总结不仅是技术笔记,更是一份排障思维导图。我为你整理成了四个核心阶段:
第一阶段:发现问题/复盘------"为什么会报错?为什么 GET 可以,POST 不行?"
1. 现象描述
访问 http://IP:3000/api/test 能看到 JSON,但在网页提交表单(POST)时报 405 Not Allowed。
- GET 请求成功 :直接访问
http://IP:3000/api/test能看到数据。 - POST 请求失败 :前端表单提交时,浏览器返回
405 Not Allowed。 - 修改失效 :修改了 Nginx 的
default配置文件并重启,但报错依然存在。
2. 深度原因分析
- GET 是"读",POST 是"写":Nginx 默认允许任何人"读"静态资源,但出于安全,它严禁向静态目录"写"(POST)数据。
- Nginx 的默认行为:Nginx 默认不允许对静态目录发送 POST 请求。如果请求没有被转发(Proxy)到后端,它会因为在磁盘上找不到对应的路径且方法不匹配而拦截。
- Nginx 的默认误判 :如果你没告诉 Nginx 如何处理
/api,它会去服务器硬盘里找一个叫/api/submit-form的文件。找不到文件且又是 POST 请求,Nginx 就会直接甩出一个 405 错误。 - "调包计":配置文件位移 :在 Ubuntu 多站点环境中,真正的配置文件是
/etc/nginx/sites-available/文件名,而你之前一直在改default。改错文件是调试中最大的"隐形成本"。 - 混合内容(Mixed Content)拦截 :当主站是
https域名时,浏览器会拦截所有发送到http://IP的请求。
第二阶段:初步调查------"后端到底活没活着?"
【关键动作:本地验证】
我们通过 curl -X POST http://127.0.0.1:3000/api/xxx 验证了后端。
【踩坑点:启动方式与持久化】
- ts-node 兼容性 :Node 20+ 版本对 TypeScript 加载比较挑剔,必须指定
commonjs编译选项。 - PM2 托管 :手动运行
npx只要关闭窗口服务就断。使用pm2才能保证"接线员"永远在线。
第三阶段:核心突破------"抓内鬼"与"定位真凶"
【现象】
改了 /etc/nginx/sites-available/default,重启了 Nginx,但 POST 依然 405。
【排查真相】
通过 sudo nginx -T(打印当前运行的所有配置)发现:Nginx 根本没读那个 default 文件!
- 原因 :Ubuntu 的 Nginx 采用多站点管理。真正管事的是
sites-available/文件名,它通过软链接存在于sites-enabled中。 - 教训 :在多站点服务器上,第一步永远是确认"谁在管这个域名" 。
- 指令:
grep -rnw '/etc/nginx/' -e '你的域名'
- 指令:
1. 关键转折点:grep 命令
当你发现修改无效时,必须确认"谁在管我的域名"。
bash
# 在 Nginx 目录下全局搜索包含你域名的文件
grep -rnw '/etc/nginx/' -e '域名'
- 发现结果 :输出指向了
sites-available/文件名。这解释了为什么修改default无效------因为 Nginx 压根没读它。
2. 本地自测:排除网络干扰
在服务器内部执行:
bash
curl -X POST http://127.0.0.1:3000/api/接口 \
-H "Content-Type: application/json" \
-d '{"name":"测试"}'
- 意义:如果本地通了,说明后端没问题,问题出在 Nginx 或前端。
第四阶段:终极修复方案(三位一体)
1. Nginx 层:强制转发与安全策略
编辑正确的文件:sudo nano /etc/nginx/sites-available/文件
- 解决 405 :通过
error_page 405 =200 $uri;。这相当于给了 Nginx 一个通行证:如果遇到 405,别拦截,直接转交给后端。 - 解决跨域 :加入了
OPTIONS预检请求的处理,让浏览器觉得安全。
nginx
location /api/ {
# 【杀手锏】解决 405:如果不匹配文件,强制转发给后端处理
error_page 405 =200 $uri;
# 【跨域处理】支持浏览器 OPTIONS 预检请求
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization';
return 204;
}
# 【反向代理】转发到后端 Node.js
proxy_pass http://127.0.0.1:3000; # 注意:末尾不要带斜杠
proxy_http_version 1.1;
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;
}
2. 前端层:同源化请求
彻底抛弃 IP 地址:
typescript
// ❌ 错误:axios.post('http://43.106.x.x:3000/api/...')
// ✅ 正确:使用相对路径
const response = await axios.post('/api/submit-form', formData);
-
好处:
- 跳过跨域检查:浏览器认为前端和后端是"一家人"。
- HTTPS 兼容 :自动跟随域名的 SSL 证书,解决了
Mixed Content(混合内容安全)错误。
-
原理 :浏览器会将请求发送到当前域名下的
/api,由 Nginx 在服务器内部"握手"3000 端口。自动绕过了 CORS 和 Mixed Content 限制。
3. 后端层:规范化解析
- JSON 解析 :必须在所有路由前加上
app.use(express.json());。 - CORS 去重 :只保留一个精细配置的
cors(),避免响应头冲突。
第五阶段:安全闭环------"关窗户,走大门"
动作:在阿里云防火墙中关闭 3000 端口访问。
- 安全逻辑 :由于 Nginx 已经在同一台服务器内通过
127.0.0.1:3000转发请求,公网已经不需要暴露 3000 端口。外网流量全部走 443(HTTPS),更加专业且安全。 - 注意 "在关闭 3000 端口前,请确保 Nginx 里的 proxy_pass 使用的是 127.0.0.1 而不是公网 IP,否则内网转发会失效。"
📋 总结:下次遇到问题的"三板斧"
| 步骤 | 检查内容 | 核心指令 |
|---|---|---|
| 1. 确定配置 | 确认 Nginx 到底在读哪个文件 | grep -r "域名" /etc/nginx/ |
| 2. 本地自测 | 排除网络干扰,在服务器内部测后端 | curl -X POST http://127.0.0.1:3000/... |
| 3. 查看日志 | 看看报错是 Nginx 给的还是 Node 给的 | tail -f /var/log/nginx/error.log 或 pm2 logs |
📋 避坑口诀(建议置顶记录)
改前先搜域名位 (
grep),改后必查语法对 (nginx -t)。
前端不写 IP 号,后端必加 JSON 包。
公网端口锁死它,飞书消息妥妥到!
🛠️ 常用故障诊断工具箱
| 需求 | 命令 |
|---|---|
| 找配置 | grep -rnw '/etc/nginx/' -e '域名' |
| 查语法 | sudo nginx -t |
| 看日志 | pm2 logs 文件名-api 或 tail -f /var/log/nginx/error.log |
| 测本地 | curl -X POST http://127.0.0.1:3000/api/... |
| 查端口 | sudo fuser -v 3000/tcp |
| 查看 Nginx 安装时加载了哪些模块,或者配置文件存放的默认根目录 | sudo nginx -V(大写的 V) |
💡 专家建议
关于安全 :你现在可以放心地在阿里云后台关闭 3000 端口了。所有的流量都走 443(HTTPS),由 Nginx 在内部安全地传给 Node.js。这不仅更专业,也让黑客无处下手。
关于未来的扩展 :既然你的"普通人清醒录"项目已经跑通了飞书推送,后期如果你想增加邮件通知 或者保存数据到数据库 ,逻辑是一模一样的,只需要在 index.ts 里增加新的函数即可。