Flask-SQLAlchemy 踩坑记录

37 阅读4分钟

Flask-SQLAlchemy 踩坑记录:接口偶尔 404?其实是 paginate 在背后搞事

在最近的项目里,我遇到一个非常“玄学”的问题:

同一个接口,大部分时候正常返回 200,但偶尔会莫名其妙返回 404(还是 HTML 格式的那种)。

路由没变、代码没动、参数也正常……
第一次见到的时候,我真以为 Flask 路由随机罢工了 🥲

经过一番非常曲折的排查,最终找到了幕后黑手:

Flask-SQLAlchemy 的 paginate() 在页码超范围时,会自动抛 abort(404)

这个行为隐藏得非常深,而且极具迷惑性。
这篇文章记录一下整个定位过程,也希望能帮到踩类似坑的朋友。


🧩 一、现象描述

某个接口(例如 /api/list)表现如下:

  • 大部分请求正常 → 200 OK
  • 但偶尔(完全不固定)会出现 → 404 Not Found
  • 重试同样请求又正常

最关键的迷惑点是:

404 返回的是 Flask 默认 HTML 页面,而不是 JSON。

从表面看,和“路由不存在”几乎没有区别。


🔍 二、初步排查(全部被排除)

按照正常排查路径,我依次确认了:

项目是否正常
路由是否注册✔️
蓝图是否加载✔️
前端 URL 是否拼错✔️
代理 / 网关配置✔️
多实例冲突✔️
登录校验 / before_request✔️

既然所有东西都正常,那 404 到底是哪里来的?


🧪 三、关键突破:在测试环境稳定复现

线上问题偶发 → 很难抓。
于是我把场景搬到本地,通过不断切换分页、重复请求,终于复现了:

  • 一次是 404(HTML 页面)
  • 下一次就是 200
  • URL + body 几乎一致

这一步非常关键,因为有了复现就能做对比。

从浏览器 Network 对比发现:

  • 请求参数合法
  • headers 正常
  • 区别唯一可能来自分页参数 pageNum

于是把视线转回后端分页逻辑。


🎯 四、真相:paginate 默认会在“超页数”时自动 abort(404)

视图中有这样一行代码:

paginate = query.paginate(page=pageNum, per_page=pageSize)

这行代码的问题在于它的默认参数:

paginate(..., error_out=True)

这意味着:

当 pageNum 超过最大页数时,Flask-SQLAlchemy 会直接:

abort(404)

而不是抛异常,也不是返回空数组。

于是效果就是:

  • Flask 立刻返回默认的 HTML 404 页面
  • 完美伪装成“路由不存在”
  • 导致调试者(也包括我)完全朝错误方向排查

这也是为什么这个错误如此隐蔽。


🤦 吐槽一下这个设计(掘金可以吐槽,但别太狠)

老实讲,这个行为放在 Web 时代(模板渲染场景)可能还能说得过去:

“第 N 页不存在 → 返回 404 → 页不存在”

但在 前后端分离的 REST API 场景里,这种默认行为真的不太合理:

  • 分页本质是业务逻辑,不该由 ORM 决定返回状态码
  • 超页数应该返回空列表 or 自己控制的错误码
  • 插一个自动 abort(404) 进去,很容易误导排查者
  • 404 是路由语义,不是分页语义

总之:
默认 error_out=True 的选择并不适合现代 API 场景。


🧹 五、最终解决方法

error_out 设置为 False

paginate = query.paginate(
    page=pageNum,
    per_page=pageSize,
    error_out=False   # 避免自动抛 404
)

之后:

  • page 超范围 → items = []
  • 不会再出现 HTML 404
  • 响应全部由业务自行控制

问题彻底解决 🎉


📝 六、经验总结

✔ 现象:偶发 404,但路由没问题

十有八九是视图里调用了会自动 abort(404) 的函数,例如:

  • paginate(error_out=True)
  • first_or_404()
  • get_or_404()

✔ 404 返回 HTML,而不是 JSON

说明视图函数根本没执行,404 来自 Flask 内部。

✔ 稳定复现是破案关键

只有抓到对比请求,才能缩小范围。


📌 七、给未来的提醒

如果某个 API:

  • 偶尔 404
  • 大多数时候正常
  • 参数不同效果不同(尤其是 pageNum)
  • 返回是 HTML 404,而非 JSON

请立刻排查:

你的分页是不是超范围了?并且 paginate(error_out=True) 正在默默帮你 abort(404)。

注:以上内容是亲身经历,文章由AI润色