Repository navigation
Conversation
sqlite: build the DSN per driver. The default pure-Go build (glebarez/go-sqlite) only understands '_pragma=' parameters, so the mattn style '_journal=WAL&_vacuum=incremental' was silently ignored and the database kept running in rollback-journal mode with the driver default 5000ms busy timeout. Verified: journal_mode=delete,busy_timeout=5000 before, journal_mode=wal,busy_timeout=10000 after. op: retry storage saves on transient SQLITE_BUSY. Drivers persist rotating credentials through MustSaveDriverStorage (115 Open, PikPak, OneDrive...); the previous token is invalidated by the server as soon as a new one is issued, so a single failed write left the storage with a dead token after a restart. 115_open: never overwrite the refresh token with an empty value and refresh up front when only a refresh token is configured, so the SDK cannot send a refresh request without the parameter (which 115 reports as '40100000 参数错误' or '40140114 refresh_token 格式错误').
The SDK assigns the refresh response to its own fields before invoking onRefreshToken, so a response without refresh_token also cleared the value used by the next refresh request. Guarding only d.Addition was not enough: the running client kept sending an empty parameter. Write the resolved pair back to the client, and add regression tests covering both the missing-field and the normal rotation cases.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
fix(storage): persist rotated tokens reliably and enable sqlite WAL
fix(storage): persist rotated tokens reliably and enable sqlite WALSummary / 摘要
115 Open 存储会间歇性报
refresh_token 无效(40140120),重新授权后恢复,但账号侧 token 实际并未失效。定位后发现是「轮换 token 落库失败」与「SQLite 未真正启用 WAL」叠加导致,与本 PR 一并修复。用户可感知的行为变化:
database is locked (SQLITE_BUSY)不再出现。重要实现变化:
internal/bootstrap:SQLite DSN 参数改为按构建分支拼装。默认构建走纯 Go 的glebarez/go-sqlite,只识别_pragma=;原?_journal=WAL&_vacuum=incremental属于 mattn 语法,被静默忽略。internal/op:saveDriverStorage对瞬时数据库错误做指数退避重试(5 次,200ms 起步)。drivers/115_open:refresh token 非空才覆盖;仅配置 refresh token 时在Init主动刷新;两者都为空时直接返回错误。配置 / 存储 / 兼容性:
-wal/-shm文件;synchronous由默认的FULL调整为 WAL 推荐值NORMAL。This PR has breaking changes.
/ 此 PR 包含破坏性变更。
This PR changes public API, config, storage format, or migration behavior.
/ 此 PR 修改了公开 API、配置、存储格式或迁移行为。
This PR requires corresponding changes in related repositories.
/ 此 PR 需要关联仓库同步修改。
Related repository PRs / 关联仓库 PR:
Related Issues / 关联 Issue
待补充。
Testing / 测试
go test ./...未执行全量测试。实际执行并通过:
Manual test / 手动测试:
DSN 语法与 PRAGMA 实际效果,在
glebarez/go-sqlite下实测对比:元信息
OpenListTeam/OpenListfix/115-open-refresh-token-persist84f421f0df4ba30e06f987e730f7c03582a6a54fmainGOTOOLCHAIN=local。问题
Checklist / 检查清单
115 Open 存储会「随机」报
refresh_token 无效(40140120),重新授权后又正常,但账号侧的 token 实际上并没有失效。同样的授权信息放到其他项目里可以正常工作。/ 我已阅读 CONTRIBUTING。
/ 我确认此贡献符合仓库许可证、贡献规范和行为准则。
gofmt,go fmt, orprettierwhere applicable./ 我已按适用情况使用
gofmt、go fmt或prettier格式化变更代码。/ 我已在适用情况下请求相关维护者或代码所有者审查。
根因
AI Disclosure / AI 使用声明
/ 此 PR 包含 AI 辅助内容。
Tools used / 使用工具:
Usage scope / 使用范围:
Code generation / 代码生成
Refactoring / 重构
Documentation / 文档
Tests / 测试
Translation / 翻译
Review assistance / 审查辅助
I have reviewed and validated all AI-assisted content included in this PR.
/ 我已审核并验证此 PR 中的所有 AI 辅助内容。
I have ensured that all AI-assisted commits include
Co-Authored-Byattribution./ 我已确保所有 AI 辅助提交都包含
Co-Authored-By归属信息。I can reproduce all AI-assisted content included in this PR without any AI tools.
/ 我可以在没有任何 AI 工具的情况下重现此 PR 中包含的所有 AI 辅助内容。
附录:根因与证据
1. 轮换式 refresh token + 尽力而为的落库
115 Open 的 refresh_token 是一次性轮换的:用旧 token 换到新 token 后,旧 token 立即作废。
op.MustSaveDriverStorage只是尽力把 Addition 写回数据库,失败时仅打一行日志、不重试也无从回滚:一旦这次写库失败,新 token 只存在于进程内存,重启后数据库里留下的是已经被作废的旧 token。
115 Open 的 refresh_token 一次性轮换,换新后旧 token 立即作废。
MustSaveDriverStorage失败只打日志,新 token 仅存在于进程内存,重启后数据库里留下的是已作废的旧 token。2. SQLite 实际没有跑在 WAL 模式
2. SQLite 实际未运行在 WAL 模式
这是
mattn/go-sqlite3的 DSN 语法。但默认构建(build.sh只为 loong64 / mips / 386 打sqlite_cgo_compat标签)走的是纯 Go 的glebarez/go-sqlite,它只认_pragma=,_journal/_vacuum被静默忽略,busy timeout 也只剩驱动内置的 5000ms。默认构建(
build.sh仅为 loong64 / mips / 386 打sqlite_cgo_compat标签)使用glebarez/go-sqlite,只认_pragma=。WAL 从未生效,busy timeout 只剩驱动内置 5000ms,rollback-journal 模式下读写互相阻塞。结果是数据库长期处于 rollback-journal(
delete)模式:读会阻塞写、写会阻塞读,DAV 高并发场景下成片出现database is locked,正好覆盖了那次 token 落库。3. 「判断有无」缺失
3. 「判断有无」缺失导致空 token 被写回
刷新响应缺少 refresh_token 时,回调会把内存中的值覆盖为空;
ReqWithForm的removeEmptyForm又会丢弃空参数,于是 115 返回40100000 参数错误或40140114 refresh_token 格式错误。115_open的刷新回调无条件执行d.Addition.RefreshToken = s2,响应里没有 refresh_token 时会把库里的值写成空串。ReqWithForm会通过removeEmptyForm把空参数丢掉,于是刷新请求根本不带该参数,115 分别返回:40100000 参数错误(未带参数)40140114 refresh_token 格式错误(格式不合法)日志证据
日志证据
「落库失败」与「重启后 token 无效」相隔数分钟,中间存在明确的启动日志(
bootstrap.LoadStorages),因果链闭合。改动
internal/bootstrap/db.go、sqlite_driver_glebarez.go、sqlite_driver_gorm.goDSN 参数改由各构建分支自行拼接,新增
appendDSNParams兼容已有参数:_pragma=journal_mode(WAL)&_pragma=busy_timeout(10000)&_pragma=synchronous(NORMAL)gorm.io/driver/sqlite):_journal=WAL&_vacuum=incremental&_busy_timeout=10000&_synchronous=NORMALinternal/op/storage.gosaveDriverStorage对瞬时数据库错误做指数退避重试(5 次,200ms 起步,合计约 3s),非瞬时错误立即返回。对所有通过该函数持久化轮换凭证的驱动(115 Open、PikPak、OneDrive、Wopan 等)一并生效。drivers/115_open/driver.goInit主动刷新一次,不再先发一个无鉴权请求换取误导性的40100000 参数错误。Init直接返回明确错误,而不是把问题留给远端。验证
DSN 语法与 pragma 效果实测对比:
影响面
synchronous=NORMAL是 SQLite 官方对 WAL 模式的推荐取值,极端断电场景可能丢掉最后若干事务,用于换取明显更好的写性能。升级须知
升级须知
数据库中已经失效的 refresh token 无法通过代码恢复,升级后需要重新授权一次 115 Open 存储。
数据库中已经失效的 refresh token 无法通过代码恢复,升级后需重新授权一次 115 Open 存储。
遗留问题(不在本 PR 范围)
遗留问题(不在本 PR 范围)
115-sdk-go@v0.2.6的RefreshToken缺少 mutex / singleflight 保护。access token 过期瞬间多个并发请求同时收到 401,会并发刷新同一个一次性 token,后到的请求必然失败(40140120 refresh_token 无效)。彻底解决需要修改 SDK 仓库并升级依赖。另外
Is401Started仅判断错误码是否以401开头,会把40100000 参数错误这类非鉴权错误也当成 token 失效而触发刷新,建议收紧到明确的鉴权错误码。115-sdk-go@v0.2.6的RefreshToken缺少 mutex / singleflight,access token 过期瞬间多个并发请求会同时刷新同一个一次性 token,后到者必然失败。另Is401Started仅判断错误码是否以401开头,会把40100000 参数错误也当作鉴权失败触发刷新。两者需在 SDK 仓库处理并升级依赖。115 Open「refresh_token 无效」根因追溯
结论摘要
115 Open 存储会间歇性报
refresh_token 无效(40140120),但账号侧 token 实际并未失效。根因不是 115 的问题,也不是 115 Open 驱动本身写错,而是:
一句话:一次「换底层驱动但没换参数语法」的重构,把另一个模块里早就埋好的 best-effort 持久化给引爆了。
故障链
关键提交
9c7e451c_journal=WAL&_locking=EXCLUSIVE&_vacuum=incremental,当时有效98872a8f3499c4db7bea29c184f421f0引入点详解:
7bea29c1(2026-04-02)提交说明:
但 diff 对 DSN 只改了一个函数名:
DSN 字符串一个字节都没动。
而新驱动
glebarez/go-sqlite的参数解析只有:_journal/_vacuum属于 mattn 语法,被完全丢弃。于是从这一天起:journal_modewaldeletebusy_timeout5000msauto_vacuum而且这是静默降级:不报错、不警告、服务正常启动,只在并发写撞锁时以
SQLITE_BUSY的形式露面。实测对比(本次修复时验证):
佐证:从该提交到本次修复前,这两个文件再无改动。
即 WAL 自 2026-04-02 起整整六个月未生效。
放大因素:
3499c4db(2025-03-17)两个问题:
MustSaveDriverStorage失败只log.Errorf,无重试、无补偿,调用方也无从感知。d.Addition.RefreshToken = s2无条件覆盖。若刷新响应缺少 refresh_token,会把库里的值写成空串;SDK 的removeEmptyForm又会丢弃空参数,115 于是返回40100000 参数错误或40140114 refresh_token 格式错误。为什么拖了六个月才爆发
这是概率事件,不是必然事件:
因此现象表现为「有时候失效、重启一段时间正常、重新授权又好了」,与真正的 token 过期几乎无法区分。
日志证据
「落库失败」与「重启后 token 无效」相隔数分钟,中间存在明确的
bootstrap.LoadStorages启动日志,因果闭合。责任划分
7bea29c1(2026-04-02)3499c4db(2025-03-17)op.saveDriverStorage修复对应
internal/bootstrap/sqlite_driver_glebarez.go、sqlite_driver_gorm.go_pragma=语法internal/op/storage.godrivers/115_open/driver.go遗留问题(未修复)
115-sdk-go@v0.2.6的RefreshToken缺少 mutex / singleflight:access token 过期瞬间多个并发请求会同时刷新同一个一次性 token,后到者必然失败。需在 SDK 仓库修复并升级依赖。Is401Started仅判断错误码是否以401开头,会把40100000 参数错误这类非鉴权错误也当作 token 失效而触发刷新,建议收紧到明确的鉴权错误码。经验教训
pragma这类「静默生效」的配置,应在启动时做一次运行期断言(例如读回journal_mode并校验)。