
## 写在前面
前两天给微信机器人控制台加了个 GitCode 第三方登录。OAuth 2.0 嘛,写过不知道多少遍了,本以为半小时搞定的事。
结果从下午踩坑踩到晚上,被四个 Bug 轮番教育做人。记录一下,希望对你有用。
---
## 第一回合:把中文怼进 HTTP 头,Node.js 直接暴走
### 现象
用户点击 GitCode 登录,授权完毕跳回回调地址,Node.js 当场抛异常:
``` Error [ERR_INVALID_CHAR]: Invalid character in header content ["Location"] ```
一开始还以为是 Node 版本问题,查了半天发现是自己蠢。
### 排查
Node.js 的 HTTP 模块对响应头校验极其严格——**非 ASCII 字符不允许出现在 Header Value 中**。源码里藏了这么一句:
```c // node/src/node_http_common.h if (ptr[i] & 0x80) { // 非 ASCII return ERR_INVALID_CHAR("header content"); } ```
而我当时的代码:
```javascript res.writeHead(302, { Location: '/#/login?oauth_error=授权失败' }); ```
嗯,我把「授权失败」三个中文字直接塞进了 Location 头。写的时候想都没想,觉得浏览器会自己处理好。事实上浏览器和 Node 都不背这个锅。
### 修复
HTTP Header Value 只允许 Latin-1 可编码字符(U+0000–U+00FF),中文必须 encodeURIComponent:
```javascript res.writeHead(302, { Location: '/#/login?oauth_error=' + encodeURIComponent('授权失败') }); ```
### 一点感触
这个错误怎么说呢,属于那种**写完就觉得自己傻**的。但说实话,如果不是 Error 信息打印得足够清楚(`Invalid character in header content`),我没准还会在「是不是 Node.js 版本有 Bug」这条歪路上走很久。所以 Error Message 写得好,真能救程序员半条命。
---
## 第二回合:401 未授权,谁动了我的 Secret?
### 现象
修好第一个 Bug 后重新测试,Token 交换又炸了。这次是 GitCode 那边返回的 401:
```json { "error_code": 401, "error_code_name": "未授权", "error_message": "未授权" } ```
### 排查
401 是 GitCode 的 `/oauth/token` 端点返回的,说明是应用凭证认证失败。但 Client ID 没错,Redirect URI 也没错,奇了怪了。
翻日志才发现——这个 OAuth 应用是好早之前创建的,最近被管理员从 GitCode 后台**重置了 Client Secret**。旧密钥当然过不了认证。
### 修复
去 GitCode 应用管理页拿到新的 Secret 换上就好:
```javascript // 旧 const GITCODE_CLIENT_SECRET = '957fc6...7702'
// 新 const GITCODE_CLIENT_SECRET = 'd00a1b...459a' ```
### 一点感触
这个 Bug 暴露了一个问题:**我的错误处理太笼统了**。catch 块里直接写了个「Token失败」就 redirect 了,完全没有把 GitCode 返回的原始错误打出来。如果是第一次遇到这个问题,光看「Token失败」四个字根本不知道是 Secret 过期还是 Code 过期。
所以建议大家在 OAuth 回调的 catch 里,**一定把第三方返回的原始 error 打全**。省得后续排查全靠猜。
---
## 第三回合:表里没这个列啊
### 现象
OK,Token 拿到了,终于走到写用户数据这一步。——又跪了:
``` Error: table users has no column named avatar ```
### 排查
`users` 表建表的时候只有 `id, email, nickname, password, role` 这几个基本字段,压根没有 `avatar` 列。而 OAuth 登录流程里顺手就写了 `avatar` 字段,忘记检查表结构了。
这就是典型的**代码改了、数据库没改**。新功能加字段,忘了一起提 SQL。
### 修复
```sql ALTER TABLE users ADD COLUMN avatar TEXT DEFAULT ''; ```
### 一点感触
说实话,这个 Bug 如果在有 Migration 工具的项目里根本不会出现。但我这边是直接裸写 SQLite,连个 migration 脚本都没有,全靠手改。确实该上点工具了。
这个小问题也提醒我:**写代码前先 .schema 看一眼表结构**,就一秒的事,能省十分钟查 Bug 的时间。
---
## 第四回合:双引号?单引号?
### 现象
改完表结构,再来。——又跪了。
``` Error: no such column: "now" - should this be a string literal in single-quotes? ```
### 排查
看一眼代码:
```javascript db.prepare(`UPDATE users SET nickname = ?, avatar = ?, last_login = datetime("now") WHERE id = ?`) ```
SQLite 里,**双引号 `"now"` 被当作列名解析**,单引号 `'now'` 才是字符串字面量。
如果你在不同数据库之间切换得多,这个坑特别容易踩:
| 数据库 | 双引号 `"now"` | 单引号 `'now'` | |--------|---------------|---------------| | SQLite | **列名**
| 字符串
| | MySQL | 看 `ANSI_QUOTES` 设置,可能是字符串 | 字符串 | | PostgreSQL | 列名
| 字符串
|
我代码里 `datetime("now")` 的使用还是**混着来的**——有的地方碰巧写对了,有的地方就炸了。
### 修复
统一单引号,完事:
```javascript db.prepare(`UPDATE users SET ... last_login = datetime('now') WHERE id = ?`) ```
### 一点感触
这个 Bug 是我觉得最丢人的一个。SQL 基础不牢,地动山摇。不过说实话,在 SQLite 上写 `datetime("now")` 能报错而不是默默把 `now` 当列名处理,也算 SQLite 够耿直。换 MySQL 可能就直接当字符串处理了,你还发现不了问题。
---
## 这些 Bug 放在一起看
四个 Bug,单独看都很低级:
1. 中文怼 HTTP 头 → **HTTP 规范不熟** 2. Secret 过期没发现 → **监控缺失** 3. 表缺 avatar 列 → **没有 Migration** 4. 引号用错 → **SQL 基础不牢**
但我觉得这些坑不是「不够小心」的问题,是**没有系统化防护**的问题。如果你有一个靠谱的开发流程,这四个错误一个都活不到生产环境:
- **TypeScript + 类型检查** → `users.avatar` 不存在,编译期就报错了 - **集成测试** → OAuth 全流程跑一次,四个 Bug 全暴雷 - **结构化日志** → Secret 过期时后端打印原始响应,而不是笼统的「Token失败」 - **数据库 Migration 工具** → 自动追踪 schema 变更,代码和数据库不会脱节
Bug 不可怕,可怕的是**同一个人同一个项目反复踩同一个坑**。整理流程、上自动化、补测试,才是正道。
---