Repository navigation
fix(auth): 2FA 二维码改用 SVG 生成,修复 Workers 运行时无 canvas 导致 generate 500 - #153
Merged
Merged
Conversation
Cloudflare Workers / Edge 运行时没有 canvas,QRCode.toDataURL() 会抛
"no canvas element",/api/auth/2fa/generate 直接 500。改为
QRCode.toString(url, { type: "svg" }) 生成 SVG 并编码为 Data URL,前端 <img> 可直接显示。
同步更新 auth_2fa.test.ts 对 Data URL MIME 的断言。
Fixes OpenListTeam#88
Co-Authored-By: Claude <noreply@anthropic.com>
pikachuren
approved these changes
Oct 26, 2026
pikachuren
left a comment
Collaborator
There was a problem hiding this comment.
代码评审总结
这是一个针对 Cloudflare Workers 运行时环境的精准修复,解决了二维码生成功能在 Workers 环境下因缺少 canvas 导致的 500 错误。
✅ 优点
- 问题定位精准: 准确识别了
qrcode库的toDataURL()在 Workers 环境下依赖 canvas 的根本原因 - 解决方案恰当: 改用
toString()生成 SVG 是环境兼容性最佳的选择,纯 JS 实现无运行时依赖 - 影响面小: 仅修改 1 个函数,前端无需改动,Authenticator 扫码体验无差别
- 测试充分: 单测覆盖 MIME 类型变更,手动测试验证了 Workers 环境下的实际工作流
- 文档清晰: PR 描述完整说明了问题现象、根因和测试结果
📝 代码质量
totp.ts 变更:
- ✅ 注释清晰: 准确说明了 Workers/Edge 运行时无 canvas 的限制
- ✅ 实现简洁: SVG 转 Data URL 的编码逻辑正确(
btoa + unescape + encodeURIComponent) - ✅ 参数合理:
margin: 1保持了适当的二维码边距
auth_2fa.test.ts 变更:
- ✅ 断言更新正确: MIME 类型从
image/png改为image/svg+xml准确反映了变更 - ✅ 正则转义:
\/svg\+xml正确转义了特殊字符
💡 改进建议
1. SVG 编码兼容性优化
当前实现使用 btoa(unescape(encodeURIComponent(svg))) 来编码 UTF-8 字符串。这在现代浏览器中可行,但 unescape 已被废弃。建议使用更现代的方式:
// 方案 1: 使用 TextEncoder (Workers 和现代浏览器都支持)
const svg = await QRCode.toString(otpauthUrl, { type: "svg", margin: 1 })
const svgBytes = new TextEncoder().encode(svg)
const base64 = btoa(String.fromCharCode(...svgBytes))
return "data:image/svg+xml;base64," + base64
// 方案 2: 直接使用 URL 编码 (更简洁,浏览器原生支持)
const svg = await QRCode.toString(otpauthUrl, { type: "svg", margin: 1 })
return "data:image/svg+xml;charset=utf-8," + encodeURIComponent(svg)方案 2 更简洁且避免了 base64 编码的开销,现代浏览器完全支持 URL 编码的 Data URL。
2. 增加边界测试
建议增加以下测试用例:
test("2fa/generate SVG contains valid QR code structure", async () => {
// ... 获取响应
const json: any = await res.json()
const svgData = json.data.qr.replace(/^data:image\/svg\+xml;base64,/, "")
const decoded = atob(svgData)
// 验证 SVG 基本结构
assert.match(decoded, /<svg/)
assert.match(decoded, /<\/svg>/)
assert.match(decoded, /viewBox=/)
})
test("2fa/generate QR code is scannable", async () => {
// 可选: 如果有二维码解析库,验证生成的 QR 码可解析
// 这确保 SVG 格式变更不影响实际扫码功能
})3. 性能对比记录
建议在文档或注释中记录 SVG vs PNG 的体积差异:
// SVG 格式相比 PNG:/n// - 体积: 通常比 PNG 小 30-50% (典型场景 ~500 bytes vs ~800 bytes)
// - 解析: 文本格式,浏览器解析更快
// - 兼容性: 所有现代浏览器及 Authenticator 应用均支持
const svg = await QRCode.toString(otpauthUrl, { type: "svg", margin: 1 })🔍 潜在风险评估
- MIME 类型变更: Data URL 的 MIME 从
image/png变为image/svg+xml。前端<img>标签兼容,但如果有其他地方对 MIME 做了硬编码判断(如文件下载、内容类型检查),需要同步更新 - 向后兼容: 已生成的 PNG 格式二维码不受影响,但如果有客户端缓存了 MIME 类型期望,可能需要清除缓存
- 第三方依赖: 确认
qrcode库在 Workers 环境的toString()实现无额外依赖
🎯 总体评价
这是一个高质量、可立即合并的修复 PR。问题定位准确,解决方案简洁有效,测试覆盖到位。SVG 方案不仅解决了 Workers 环境问题,还在体积和性能上有潜在优势。
评分: 9/10
建议:
- ✅ 可以立即合并
- 🔄 考虑将
unescape替换为TextEncoder或直接使用 URL 编码的 Data URL - 📋 建议在文档中说明 MIME 类型变更,便于排查潜在的客户端兼容问题
- 🧪 后续可增加 SVG 结构验证测试
优秀的环境适配工作! 👍
PIKACHUIM
approved these changes
Oct 26, 2026
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.
Summary / 摘要
问题现象:部署到 Cloudflare Workers 后,在后台开启两步验证时
POST /api/auth/2fa/generate返回 500,前端二维码一直无法加载。Worker 日志里的异常为Error: You need to specify a canvas element。根因:
src/backend/pkg/totp.ts的buildQrImageUrl()调用QRCode.toDataURL(otpauthUrl)。qrcode库在非 Node 环境下的toDataURL()走浏览器分支,需要canvas;Workers / Edge 运行时没有 canvas,直接抛错。#89 修了 Promise 未 resolve 的问题,但toDataURL()这一层在 Workers 上仍然不可用。改动(只改 1 个函数 + 对应断言):
buildQrImageUrl()改为QRCode.toString(otpauthUrl, { type: "svg", margin: 1 }),再编码为data:image/svg+xml;base64,...Data URL。SVG 渲染是纯 JS 实现,不依赖 canvas / NodeBuffer。前端
<img src>对 SVG Data URL 兼容,无需改前端;Authenticator 扫码无差别。src/backend/server/auth_2fa.test.ts中对 MIME 的断言由image/png改为image/svg+xml。This PR has breaking changes.
/ 此 PR 包含破坏性变更。
This PR changes public API, config, storage format, or migration behavior.
/ 此 PR 修改了公开 API、配置、存储格式或迁移行为。
(仅
data.qr字段的 Data URL MIME 从image/png变为image/svg+xml,仍为可直接用于<img>的 Data URL)This PR requires corresponding changes in related repositories.
/ 此 PR 需要关联仓库同步修改。
Related repository PRs / 关联仓库 PR:
Related Issues / 关联 Issue
Testing / 测试
go test ./...(本仓库为 TS 项目,不适用)npx tsx --test src/backend/server/auth_2fa.test.tsnpx tsc -p tsconfig.json --noEmit:0 errorPOST /api/auth/2fa/generate由 500 变为 200,data.qr为data:image/svg+xml;base64,...,后台页面二维码正常显示,Google Authenticator 扫码后验证码可通过/api/auth/2fa/verify。Checklist / 检查清单
/ 我已阅读 CONTRIBUTING。
/ 我确认此贡献符合仓库许可证、贡献规范和行为准则。
gofmt,go fmt, orprettierwhere applicable./ 我已按适用情况使用
gofmt、go fmt或prettier格式化变更代码。(仅对改动的几行做了格式化,未重排文件中原有的其它行,保持 diff 最小)/ 我已在适用情况下请求相关维护者或代码所有者审查。
AI Disclosure / AI 使用声明
/ 此 PR 包含 AI 辅助内容。
Tools used / 使用工具:
Usage scope / 使用范围:
Code generation / 代码生成
Tests / 测试
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 辅助内容。