`lark-cli base +record-upsert` 创建记录时偶发落库两条(疑似 HTTP 传输层在响应丢失后重试非幂等 POST)
Nobody has claimed this yet.
- Dominant language
- Go
- Stars
- 17.3k
- Forks
- 1.4k
- Avg merge
- 2d 4h
- Merged PRs (30d)
- 105
Description
一、现象概述
使用 lark-cli base +record-upsert(不带 --record-id,即创建模式)向飞书多维表格写入一条记录时,单次命令执行偶发在目标表生成两条字段完全一致的记录。
调用方只发起了一次创建、只收到一条记录的响应,但服务端实际插入了两条。怀疑根因:lark-cli 的 HTTP 传输层在创建请求的响应丢失/超时后自动重试了 POST,而创建接口不具备幂等性,重试导致服务端再次插入一条。
二、环境信息
| 项 | 值 |
|---|---|
| lark-cli 版本 | 1.0.96(已复现);1.0.93 同样复现 |
| 安装方式 | 用户级 npm install -g lark-cli(C:\Users\<USER>\AppData\Roaming\npm\lark-cli.cmd) |
| 操作系统 | Windows 11 |
| 命令形态 | lark-cli base +record-upsert --as user --base-token <BASE_TOKEN> --table-id <TABLE_ID> --json @payload.json |
是否含 --dry-run |
否(--dry-run 已单独验证不写库,见第四节) |
是否带 --record-id |
否(即纯创建) |
三、复现现象(确定性结果)
一次命令、单次进程、调用方无任何重试逻辑的前提下,创建后查询目标表,发现存在两条相邻 record_id、字段完全一致的记录:
<REC_1><REC_2>
而该命令的写入响应 stdout 仅包含一个 record id:
{
"ok": true,
"identity": "user",
"data": {
"created": true,
"record": {
"record_id_list": ["<REC_2>"]
}
}
}
即:客户端只“看到”一条响应,服务端却落了两条。两条记录内容逐字段核对一致(业务主键字段、各文本字段均相同),可判定为同一次创建的重复产物。
补充:同表中另一次创建(内容不同)也出现相同现象,产生 <REC_3> / <REC_4> 两条,后人工删除其一。两次实测分别在 1.0.93 与 1.0.96 上发生,说明该问题跨越版本、与具体 payload 内容无关。
四、已排除项
- 不是
--dry-run写库:用 marker payload 单独执行--dry-run后查询目标表,命中 0 条,确认--dry-run仅打印请求体、不写库。重复记录全部来自真实写入步骤。 - 不是调用方重试:调用脚本对
lark-cli是单次subprocess.run,无任何 retry / attempt 机制;本次仅执行了一次创建命令。 - 不是 Agent / 人为二次执行:创建流程在会话中只触发一次。
五、根因推测(供官方排查)
-
lark-cli 为 Go 编译二进制;从其二进制字符串可见存在 HTTP 重试传输层,典型字符串如
cmdutil.RetryTransport、RoundTrip retrying after failure: %v、shouldRetryRequest、ratelimit等,说明在网络错误 / 响应超时时会对请求做自动重试。 -
base +record-upsert创建模式(无--record-id)为非幂等写操作。--help明确说明:Without --record-id this creates a record; It does not auto-upsert by business key.
-
关键时序:创建请求已到达服务端并成功插入,但客户端在读取响应前遭遇连接重置 / 超时(如代理或网络抖动、服务端处理偏慢,或受限网络环境)。此时传输层重试该 POST;由于请求不带幂等键,服务端将其视为“第二次创建”再次插入 → 表中出现两条。客户端拿到的响应只对应其中一次重试,故
record_id_list仅含一个 id。
该推测与“响应只返回 1 个 id、表中有 2 条相邻 id”的现象高度自洽。
六、期望行为
- 写入类(非幂等)请求在网络异常时不应盲目重试;或重试必须携带幂等键 / client-token,使服务端对重复请求去重。
- 至少在帮助文档 / 配置中显式说明该重试行为,并提供关闭重试的开关(环境变量或 flag)。
- 创建响应在重试场景下应聚合 / 返回所有已创建 record id,或在发生不确定写入时以非零退出码提示“写入结果不确定”,避免调用方误以为成功且仅一条。
七、建议修复方向
| 优先级 | 建议 |
|---|---|
| 高 | 非幂等方法(POST 创建)默认不重试;仅对可安全重试的场景(纯查询,或带幂等键的写)重试 |
| 高 | 写请求支持 --idempotency-key / client_token,服务端据此去重 |
| 中 | 提供关闭传输层重试的开关,如 LARK_CLI_NO_RETRY=1 或 --no-retry |
| 中 | 创建响应在重试场景下返回全部已创建 id,或在不确定时以非零退出码告警 |
八、当前影响与临时规避
- 影响:自动化 / 脚本批量建记录时偶发重复数据,需人工核对清理;在 CI / Agent 编排中会被静默当作成功,埋下数据一致性隐患。
- 规避(已在调用方实现):创建后按业务键(多个业务关键字段组合)查询目标表,命中多条即告警并保留一条、删除其余、禁止重试创建。该方案可拦截重复,但无法从根因消除,且依赖调用方自行实现。
九、给官方的复现建议
由于该问题依赖“响应丢失 / 超时”的偶发网络条件,可尝试以下方式稳定复现:
- 在飞书 API 网关前注入响应延迟或连接重置(如代理层在写请求返回后、客户端读取前断开连接);
- 或在网络受限 / 高延迟环境下连续执行若干次创建,统计目标表记录数是否大于执行次数;
- 开启 lark-cli 的 verbose / debug 日志(如有),观察是否存在
retrying after failure类日志与两次请求发出。
如需要,我可以提供两次实测的完整命令序列、payload 结构与查询佐证(已脱敏)。
附:关键证据片段
创建响应(仅 1 个 id,但表中有 2 条):
{ "ok": true, "identity": "user",
"data": { "created": true, "record": { "record_id_list": ["<REC_2>"] } } }
查表结果(同内容 2 条):
record count: 2
ids: ['<REC_1>', '<REC_2>']
dry-run 控制测试(确认不写库):
marker payload 执行 --dry-run 后查表 -> record count: 0
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start by locating the Go HTTP retry transport and the shouldRetryRequest logic referenced in the issue, then trace the base +record-upsert create path when --record-id is absent. Reproduce with a delayed or reset response if possible and inspect verbose retry logs. Done means non-idempotent POST creation is not blindly retried, or the behavior has an explicit safe idempotency or failure signal.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- go
- Domain
- cli, networking
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 52/100