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 RecipesFork Recipes自托管DockerPostgreSQLDjango2026/11/02

Fork Recipes 故障排查与 FAQ

目标:把 Fork Recipes(前端 recipes 服务 + 后端 forkapi 服务 + nginx + PostgreSQL 四服务形态)部署后最常撞上的故障逐一实测复现,给出「现象 → 复现 → 根因 → 解法」四段式排查结论。本文全部回显为同一台 Ubuntu 24.04 宿主机(IP 192.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(黑名单域)20426ms首行 blacklist 判断直接拒
recipetineats.com(外网,404)2048.2s后端日志两次「Response status was 404」
本地测试页(200 可达,有 og:image)50032.4sOpenAI 调用炸(见 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 打回溯。

解法(三板斧,按序):

  1. 请求行:generated 145 bytes 即默认 500 页;看耗时区分「秒回参数错」与「长等外部调用炸」(如 32416ms → 外部 AI/网络)。
  2. 分段 curl:把链路拆段验证——页面可达性(curl 目标 URL)→ 鉴权(category 200)→ 再提交 scrape,锁定断点。
  3. 代码路径:按端点翻视图函数,对可疑外部调用(openai/requests)做 1.2 那种一次性演示。临时调试可在 .env 设 DEBUG=True 重建,500 会带回溯(仅调试用,公网实例务必关回)。

3.4 图片下载 FileNotFoundError 与 http 强升 https

根因(utils.py:14 download_media_files 两缺陷,读码 + 实测确认):

  1. 下载 status != 200 时不写文件却仍返回路径,后续 open() 直接 FileNotFoundError;
  2. 地址处理是 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/

重建后实测行为链:

  1. 空命名卷会被构建产物初始化——db.sqlite3(135168B,构建期 mtime)被拷进卷,表直接可用,登录 302;但卷里是构建期快照,修复前创建的会话全部失效,需重新登录一次;
  2. 重新登录拿到新 cookie,再执行一次 up -d --force-recreate web,旧 cookie GET / → 200——持久化达成;
  3. 卷内容核验:
$ 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 看目标页,掉登录看挂载点。

Fork Recipes 故障排查与 FAQ - Quzuo