Fork Recipes 故障排查与 FAQ
Fork Recipes 故障排查 FAQ:三层定位法、后端失联误报密码错、SERVICE_BASE_URL 不可达 500、CSRF 403、API 403/500/400 三种、scrape 204/500 三连与 500 静默定位、uwsgi socket 502 窗口现场、SQLite 挂载错位修复与会话持久化、SMTP 缺失、404-with-200 监控坑,全部实测复现。
Fork Recipes 故障排查与 FAQ
目标:把 Fork Recipes(前端
recipes服务 + 后端forkapi服务 + nginx + PostgreSQL 四服务形态)部署后最常撞上的故障逐一实测复现,给出「现象 → 复现 → 根因 → 解法」四段式排查结论。本文全部回显为同一台 Ubuntu 24.04 宿主机(IP192.168.1.172,端口 8146 前端 / 8148 API / 5434 PostgreSQL)2026-11-01~02 实测,无编造输出。部署过程见 《Fork Recipes Docker Compose 部署指南》,备份/升级见 《Fork Recipes 日常运维与升级手册》,项目背景见 《Fork Recipes 介绍与选型指南》。
〇、排查总纲:三层定位法
Fork Recipes 的请求链是「浏览器 → nginx → uwsgi socket → Django 应用」,故障排查按三层往下钻:
| 层 | 看什么 | 命令 |
|---|---|---|
| 入口层 | HTTP 状态码 | docker logs nginx(access + error 直出) |
| 进程层 | uwsgi 请求行(状态码、字节数、耗时) | docker logs forkapi / docker logs recipes |
| 应用层 | 代码路径 + 配置项 | 源码 + .env |
三个探活点(全绿即链路健康,判定口径同运维手册 2.3):
$ curl -s -o /dev/null -w "FE login HTTP:%{http_code}\n" http://192.168.1.172:8146/login/
FE login HTTP:200
$ curl -s -o /dev/null -w "API category HTTP:%{http_code}\n" http://192.168.1.172:8148/api/recipe/category -H "X-Auth-Header: ced9106d-4f94-4754-b20d-0bdbf49b4fb2"
API category HTTP:200
$ curl -s -o /dev/null -w "admin HTTP:%{http_code}\n" http://192.168.1.172:8148/admin/
admin HTTP:302
状态码速查:502 → 一、四章;403 → 二章;前端 500 → 一章 1.2;API 500 → 三章;204 → 三章。
一、登录类故障
1.1 凭据明明正确,却提示「Wrong email or password!」
现象:前端登录页输入正确账号密码,页面红字提示凭据错误,反复尝试无果。
复现(停掉后端服务,用正确凭据登录):
$ docker stop forkapi
forkapi
带 CSRF 提交 POST /login/(email=admin@example.com、password=ChangeMe,均为正确值),实测返回:
HTTP:200
页面含: <p class="text-[#DC3545]">Wrong email or password!</p>
docker logs recipes 对应请求行:
[pid: 7|app: 0|req: 26/26] 192.168.1.172 () {40 vars in 583 bytes} [Sun Nov 1 17:47:53 2026] POST /login/ => generated 18666 bytes in 1 msecs (HTTP/1.1 200) 8 headers in 365 bytes (1 switches on core 0)
根因:前端登录不是本地验密,而是由 ws/api_request.py 的 get_user_token() 调后端 api/auth/token。forkapi 停掉后 nginx 仍监听 8148,对 /api/auth/token 返回 502;而该函数只判断 response.status_code == 200,其余一律 return False(api_request.py:47),视图层(recipes/views.py login_view)把 falsy 统一渲染成「Wrong email or password!」。后端不可达被伪装成了密码错误。
解法:先分清「密码错」还是「后端挂」。
$ docker exec nginx ls -la /tmp/uwsgi/uwsgi.sock
$ curl -s -o /dev/null -w "%{http_code}\n" http://192.168.1.172:8148/api/recipe/category -H "X-Auth-Header: <.env 里的 X_AUTH_HEADER>"
socket 不在或 API 非 200 → 查 forkapi:docker ps -a | grep forkapi、docker logs forkapi --tail 50。恢复实测:
$ docker start forkapi
forkapi
# 约 8 秒后
srw-rw---- 1 www-data www-data 0 Nov 1 17:49 uwsgi.sock
API probe HTTP:200
1.2 登录页直接 500「Internal server error.」
现象:提交登录后整页变成「500 / Internal server error. / We are already working to solve the problem.」,连表单都回不来。
复现与证据:部署初期(后端地址不可达阶段)实测捕获的 500 页面(recipes/500.html 模板):
<h1 ...>500</h1>
<p ...>Internal server error.</p>
<a href="/login/" ...>Back to Homepage</a>
本次在同一环境做代码路径验证——在 recipes 进程内把 SERVICE_BASE_URL 指向一个无人监听的端口后直接调用登录链路函数(一次性演示,不改配置、不重启服务):
$ docker exec recipes sh -c "cd /fork_recipes && python -c \"
import sys; sys.path.insert(0,'/'); sys.path.insert(0,'/fork_recipes')
import os, django
os.environ.setdefault('DJANGO_SETTINGS_MODULE','backend.settings')
django.setup()
from fork_recipes.backend import settings as frs
print('ORIGINAL:', frs.SERVICE_BASE_URL)
frs.SERVICE_BASE_URL='http://127.0.0.1:18148'
from fork_recipes.ws import api_request
try:
r = api_request.get_user_token('admin@example.com','ChangeMe')
print('RETURNED:', r)
except Exception as e:
print('RAISED:', type(e).__name__)
\""
ORIGINAL: http://192.168.1.172:8148
RAISED: ConnectionError
根因:api_request_read_only()(api_request.py:24)用 requests.request() 直连 SERVICE_BASE_URL,没有任何 try/except。与 1.1 的区别在于:nginx 活着返回 502 → 走「误报密码错」;地址完全不可达(连接被拒/超时)→ ConnectionError 一路上冒 → Django 500。
解法:核对 .env 里 SERVICE_BASE_URL=http://192.168.1.172:8148 与 nginx 实际监听端口是否一致(本部署改造后为 8148,官方默认 80);docker logs nginx 确认 8148 入口在转发。若长期无人改代码,可在 api_request_read_only 外包一层 try/except 返回 None,把「后端失联」降级为 1.1 的页面提示(上游 issue 方向)。
1.3 脚本化登录 POST 返回 403 Forbidden
现象:用 curl/脚本模拟登录,返回 1004 字节的 Django 默认「403 Forbidden」页,而不是登录结果。
复现:登录成功(302)之后,用同一张旧表单 token 再次 POST:
[pid: 7|app: 0|req: 22/22] 192.168.1.172 () {40 vars in 627 bytes} [Sun Nov 1 17:47:17 2026] POST /login/ => generated 1004 bytes in 2 msecs (HTTP/1.1 403) 7 headers in 235 bytes (1 switches on core 0)
根因:Django CSRF 双提交校验——POST 必须同时带 csrfmiddlewaretoken 表单字段、csrftoken cookie、Referer 头;且登录成功 302 后 token 轮换,旧 token 立刻作废。另外已登录会话 GET /login/ 会被 302 到首页,拿不到表单 token。
解法:脚本化登录固定三步(本文实测用的小封装):
#!/bin/bash
# usage: do_login.sh <cookiefile> <outfile>
CK=$1; OUT=$2
BASE=http://192.168.1.172:8146
curl -s -b "$CK" -c "$CK" $BASE/login/ -o /tmp/_lp.html
TOK=$(grep -o 'csrfmiddlewaretoken" value="[^"]*"' /tmp/_lp.html | head -1 | sed 's/.*value="//;s/"//')
curl -s -b "$CK" -c "$CK" -X POST $BASE/login/ -H "Referer: $BASE/login/" \
-d "csrfmiddlewaretoken=${TOK}&email=admin@example.com&password=ChangeMe" \
-o "$OUT" -w "HTTP:%{http_code} redirect:%{redirect_url} size:%{size_download}\n"
要点:cookie jar 全程 -b/-c 同文件;每次 POST 前重新 GET 取新 token;已登录时换新 jar。
二、API 鉴权与参数类
2.1 只读接口 403:两种 detail 文案
现象:调 /api/recipe/category 等只读接口返回 403。
实测两种文案(区别一眼可辨):
不带 X-Auth-Header 头:
{"detail":"You must use authentication header"}
带但值不对:
{"detail":"Authentication header invalid"}
带正确头:
HTTP:200(分类列表 JSON)
根因与解法:Fork Recipes 的只读 API 不走 Token,走环境变量 X_AUTH_HEADER(.env 里定义,本部署为 ced9106d-4f94-4754-b20d-0bdbf49b4fb2)。写进请求头 -H "X-Auth-Header: <值>" 即可;POST /api/auth/token 换 token 时同样必须带此头,否则 403(实测)。
2.2 GET /api/recipe/ 必现 500
现象:想列表查询食谱,GET /api/recipe/(哪怕带对 Token 和头)返回 145 字节 Django 默认页「Server Error (500)」。
根因:后端 generics.py:33 的 RetrieveCreateDestroyViewSet 只混入 Retrieve/Create/Destroy 三个 mixin——没有 ListMixin,GET /api/recipe/(无 pk)被路由到 retrieve(),取不到主键直接 KeyError → 500。这是代码形态,不是环境故障。
解法:列表走专用端点,实测 200:
$ curl -s "http://192.168.1.172:8148/api/recipe/home/preview/?page=1" -H "X-Auth-Header: ced9106d-4f94-4754-b20d-0bdbf49b4fb2"
{"count":1,...西红柿炒蛋...}
单条取回用 GET /api/recipe/<pk>/。
2.3 建谱 400:字段校验与「必须传图」
现象:POST /api/recipe/ 只传 name 返回 400。
实测回显:
{"servings":["This field is required."],"description":["This field is required."]}
根因与解法:servings、description 为必填;且部署篇实测新建食谱必须带 image,缺图返回 400 "Image is required for new recipes."。最小可用请求 = name + servings + description + image 文件(multipart)。
三、抓取(scrape)类故障
scrape 是故障密度最高的链路,实测三连(同一接口 POST /api/recipe/scrape,带 Token + X-Auth-Header):
| 目标 URL | 结果 | 耗时 | 判定 |
|---|---|---|---|
| foodnetwork.com(黑名单域) | 204 | 26ms | 首行 blacklist 判断直接拒 |
| recipetineats.com(外网,404) | 204 | 8.2s | 后端日志两次「Response status was 404」 |
| 本地测试页(200 可达,有 og:image) | 500 | 32.4s | OpenAI 调用炸(见 3.2) |
3.1 返回 204 是「抓不到」不是成功
现象:scrape 返回 204 No Content,前端表现为没反应。
根因:204 有两类来源——① 域名在内置黑名单(scrape_recipe 首行判断,秒回);② browser.py 抓页失败(目标 404/反爬/超时),重试耗尽后放弃。后端日志会留痕:
$ docker logs forkapi --tail 20
... Response status was 404 ... # browser 抓取重试的两次
解法:先 curl -o /dev/null -w "%{http_code}" <目标URL> 确认页面可达;黑名单域无解(换源站);反爬站考虑手动建谱 + API 补图。
3.2 页面明明可达,scrape 却 500 且等了 30 多秒
现象:本地测试页(HTTP 200、og:image 齐全)提交 scrape,32.4 秒后 500。
根因:抓取链后半段要把页面正文交给 OpenAI 做结构化抽取。本部署 .env 里 OPENAI_API_KEY=''(空),browser.py 抓页成功后,messages.py 的 __openai_chat_completion 调 openai SDK 抛错 → 500。耗时 32s = 抓页 + 模型调用重试。
解法:.env 配置有效的 OPENAI_API_KEY(及对应 base_url 变量)后 docker compose up -d be 重建生效;不用 AI 抓取就把 scrape 视为不可用,走手动建谱 API。
3.3 500 静默无回溯:怎么定位
现象:应用 500,但 docker logs 里找不到任何 Traceback。
实测(scrape 500 当时 forkapi 日志只有这一行):
[pid: ...] POST /api/recipe/scrape => generated 145 bytes in 32416 msecs (HTTP/1.1 500) 8 headers in 243 bytes
根因:DEBUG=False 且项目没有 LOGGING 配置,Django 异常只回默认 500 页,不向 stdout 打回溯。
解法(三板斧,按序):
- 请求行:
generated 145 bytes即默认 500 页;看耗时区分「秒回参数错」与「长等外部调用炸」(如 32416ms → 外部 AI/网络)。 - 分段 curl:把链路拆段验证——页面可达性(curl 目标 URL)→ 鉴权(category 200)→ 再提交 scrape,锁定断点。
- 代码路径:按端点翻视图函数,对可疑外部调用(openai/requests)做 1.2 那种一次性演示。临时调试可在
.env设DEBUG=True重建,500 会带回溯(仅调试用,公网实例务必关回)。
3.4 图片下载 FileNotFoundError 与 http 强升 https
根因(utils.py:14 download_media_files 两缺陷,读码 + 实测确认):
- 下载
status != 200时不写文件却仍返回路径,后续open()直接FileNotFoundError; - 地址处理是
address if 'https://' in address else f'https://{address}'——http 链接被强升 https,源站无 https 时必挂。
解法:目标图 URL 显式给 https 可达的源;og:image 指向 404 图(本部署测试页 missing.png 即此场景)时 scrape 即使 AI 正常也会在这一步失败。属上游代码缺陷,长期修在 download_media_files 里判 status 并保留原始 scheme。
四、502 与 uwsgi socket
4.1 重启窗口 502(现场复现)
现象:前端/API 偶发 502 Bad Gateway,稍后自愈。
复现:docker restart recipes 同时以 0.4s 间隔轮询 GET /login/,40 次采样实测:
200 200 ... 200(27次) 502 200 200 ... 200(12次)
nginx 同期错误行与访问行(UTC):
2026/11/01 17:54:10 [error] 10#10: *219 connect() to unix:/tmp/uwsgi/uwsgi_recipes.sock failed (111: Connection refused) while connecting to upstream, client: 192.168.1.172, server: localhost, request: "GET /login/ HTTP/1.1", upstream: "uwsgi://unix:/tmp/uwsgi/uwsgi_recipes.sock:", host: "192.168.1.172:8146"
192.168.1.172 - - [01/Nov/2026:17:54:10 +0000] "GET /login/ HTTP/1.1" 502 157 "-" "curl/8.5.0"
根因:uwsgi 启动参数带 die-on-term + vacuum——进程退出时删除 socket 文件,新进程起来才重建。重启的 1 秒左右窗口里 nginx 连不上 socket 即 502。这是设计行为,非故障。
解法:单发 502 + 秒级自愈 → 不用管;客户端重试即可。持续 502 才要查:docker exec nginx ls -la /tmp/uwsgi/ 看两个 socket 在不在(uwsgi.sock=后端、uwsgi_recipes.sock=前端),不在就 docker logs <对应服务> 看进程为何没起来(迁移失败、环境变量缺失最常见)。
4.2 socket Permission denied(13)
现象:502,但 nginx 错误是 (13: Permission denied) 而非 (111: Connection refused)。
历史实测(本部署 15:36:53 曾出现):socket 文件在,但 nginx 的 worker 用户连不上——socket 属主/权限不对。
解法:compose 启动命令里的 --chown-socket=www-data:www-data 就是为此(uwsgi.ini 默认 chmod-socket=660,属主不对时 660 也进不去)。确认 be/web 两个服务的 command 都带该参数;核对稳态:
$ docker exec nginx ls -la /tmp/uwsgi/
srw-rw---- 1 www-data www-data 0 ... uwsgi.sock
srw-rw---- 1 www-data www-data 0 ... uwsgi_recipes.sock
五、会话与 SQLite 持久化
5.1 前端重启后全员被踢回登录页
现象:只重启/升级了前端(docker compose up -d --force-recreate web),后端和数据库都没动,所有人却都掉登录。
复现(实测):旧 cookie GET / 先 200,重建后立刻变:
old cookie after recreate GET / HTTP:302 redirect:http://192.168.1.172:8146/login/?next=/
根因:前端会话存 SQLite(django.contrib.sessions),而官方 compose 把卷 data 挂在 /fork_recipes/sql/——挂错了位置。实测目录结构:
$ docker exec recipes ls -la /fork_recipes/sql/ /fork_recipes/backend/sql/
/fork_recipes/sql/: # 卷挂在这,空目录
total 12
/fork_recipes/backend/sql/: # 真正的库在这(settings.py BASE_DIR 指向 backend/)
-rw-rw---- 1 root root 135168 Nov 1 17:52 db.sqlite3
库文件活在进程可写层,重建即丢 → 会话清空(食谱主数据在 PostgreSQL,不受影响)。
修复(改 compose 一处挂载并重建,保留在本部署):
web:
volumes:
- data:/fork_recipes/backend/sql/ # 原 /fork_recipes/sql/
重建后实测行为链:
- 空命名卷会被构建产物初始化——
db.sqlite3(135168B,构建期 mtime)被拷进卷,表直接可用,登录 302;但卷里是构建期快照,修复前创建的会话全部失效,需重新登录一次; - 重新登录拿到新 cookie,再执行一次
up -d --force-recreate web,旧 cookieGET /→ 200——持久化达成; - 卷内容核验:
$ docker volume inspect forkrecipes_data --format '{{.Mountpoint}}' | xargs ls -la
-rw-rw---- 1 root root 135168 Nov 2 01:53 db.sqlite3
-rw-r--r-- 1 root root 79 Nov 1 22:18 README.md
5.2 改挂载后首次登录 500(空目录场景)
若你用的是 bind 挂载空目录(./sql:/fork_recipes/backend/sql/),没有命名卷的初始化行为,db.sqlite3 不存在 → 会话表缺失 → 登录链路 500。解法一条:
$ docker exec recipes python manage.py migrate
建表后登录恢复 302(migrate 幂等性见运维手册 5.4)。
六、找回密码与邮件
6.1 提交找回密码提示 Missing SMTP configuration settings
现象:忘记密码页提交邮箱,页面顶部红块:
Missing SMTP configuration settings: EMAIL_HOST, EMAIL_PORT, EMAIL_USE_TLS, EMAIL_HOST_USER, EMAIL_HOST_PASSWORD in environment variables!
(实测 POST /forgot-password/ 返回 200 页面含此 error 块。)
根因:email_util.check_smtp_configuration() 逐项检查 .env 的 5 个 EMAIL_* 变量,缺任一即拦下不发信。
解法:.env 补齐 EMAIL_HOST/EMAIL_PORT/EMAIL_USE_TLS/EMAIL_HOST_USER/EMAIL_HOST_PASSWORD(如 QQ/163 邮箱用 SMTP 授权码而非登录密码),docker compose up -d web 重建生效。
附带坑:路由是连字符 /forgot-password/(recipes/urls.py:12),写成下划线 /forgot_password/ 会落到 404 模板——而且见 7.1,它还是 200。
七、监控类坑
7.1 404 页返回 HTTP 200
现象:任何不存在的路径都渲染「404 / Something's missing.」页,但状态码是 200:
$ curl -s -o /dev/null -w "HTTP:%{http_code} size:%{size_download}\n" http://192.168.1.172:8146/totally-bogus/
HTTP:200 size:1193
根因:recipes/views.py:576 的 handler404 用 render(request, 'recipes/404.html'),没传 status=404,Django 默认 200。
解法:探活脚本别用「随机垃圾路径判 404」这种写法(永远绿);按状态码 200 判活时必须用已知真实路径(如 /login/、/api/recipe/category,本文第〇章口径)。彻底修法是把 handler404 改为 render(request, 'recipes/404.html', status=404)(上游补丁方向)。
八、FAQ 速答
Q:三个端口分别是什么?
A:本部署宿主端口:8146 前端 UI、8148 后端 API(含 /admin 与 /media)、127.0.0.1:5434 PostgreSQL。官方默认是 80/80,改造原因见部署篇。
Q:登录口令、API Token、X-Auth-Header、q1w2e3r4 都是什么?
A:前端登录 admin@example.com / ChangeMe(部署篇初始化);API 写操作 Token 由 POST /api/auth/token 换取(59cfa1… 那串,存 .token 文件);X-Auth-Header 是只读 API 的环境变量鉴权头(.env 的 X_AUTH_HEADER);q1w2e3r4 只是 PostgreSQL 口令,不是登录口令——四串东西别混。
Q:日志去哪看?
A:docker logs nginx(入口状态码+错误)、docker logs forkapi(后端 uwsgi 请求行、抓取与迁移输出)、docker logs recipes(前端)。没有独立日志文件,全部 stdout(原因见 3.3)。
Q:recipes 的 Up 时间总比别的短?
A:正常。前端重建频率高(会话 SQLite、compose 调整都会 --force-recreate web),只要不是反复 Up Less than a minute 就没事(口径同运维手册 2.1)。
Q:scrape 返回 204 算失败吗? A:算「没抓到」。204 是本应用对「黑名单域 / 页面不可达 / 抽取失败」的统一静默返回,按 3.1 分诊。
Q:为什么登录报「Wrong email or password!」但密码肯定对? A:大概率后端失联,见 1.1;若登录页整个变 500 见 1.2。
Q:备份、升级、回滚怎么做?
A:见 《Fork Recipes 日常运维与升级手册》:backupper 快照 + pg_dump 双通道、pull → up -d → 验回显 升级、pinned tag 回滚。
Q:这套故障清单适用于 SQLite 单库形态吗?
A:一、三、四、六、七章通用;二章 API 行为通用;五章的挂载错位是官方 compose 通病,SQLite 形态下 db.sqlite3(前端会话+后端主数据合一)丢失影响更大,务必按 5.1 修正挂载点。
小结:Fork Recipes 的故障面高度集中在「前后端互信链路」(socket、X-Auth-Header、SERVICE_BASE_URL)与「静默失败」(204、无回溯 500、404-with-200)两类。排障口诀:502 看 socket,403 看头,500 看请求行耗时,204 看目标页,掉登录看挂载点。