remote-api-footguns

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Remote API footguns

远程 API 陷阱

17 traps mined from a production Kotlin and Compose Multiplatform app, one short file each under
references/
. Every file gives the working pattern, the Traps (the specific ways it fails in practice, and why), and a Verifying it section with commands to run against the user's own tree.
How to use this index. Match the code about to be written, or the symptom being chased, against the entries below: each names its topic and the symptom that should send you to it. Read every file that plausibly applies with the Read tool before proposing code or a fix; each is under 150 lines. Where a Verifying command names the source project's paths, substitute the equivalent paths in the user's tree.
Cross-references. A backticked trap name inside a file that is not listed here lives in a sibling area skill: glob
../*/references/<name>.md
to open it.
从生产级 Kotlin 与 Compose Multiplatform 应用中提炼出的 17 个陷阱,每个陷阱对应
references/
下的一个简短文件。每个文件都会给出可行模式、陷阱(实际失败的具体方式及原因),以及验证方法一节,并附带可针对用户自身项目目录运行的命令。
如何使用本索引。 将即将编写的代码或正在排查的症状与下面的条目进行匹配:每个条目都列出了主题,以及应据此查阅该条目的症状。在提出代码或修复方案之前,使用 Read 工具阅读所有可能适用的文件;每个文件不超过 150 行。如果“验证方法”命令中提到源项目的路径,请替换为用户项目目录中的等效路径。
交叉引用。 文件中出现的反引号包裹的陷阱名称如果未在此列出,则位于同级的 area skill 中:使用 glob
../*/references/<name>.md
打开它。

Clients, parsing and response handling

客户端、解析与响应处理

  • ktor-kmp-client-architecture
    — A multiplatform HTTP stack where one expect/actual hands back the engine and each integration builds its own client from it — an instrumented client for API calls next to a deliberately bare one for bulk downloads, content negotiation registered per format, and a settings change that rebuilds the client rather than mutating it. Use when standing up networking in a Kotlin Multiplatform module, when a proxy setting appears to be ignored by some requests but not others, or when downloading a large file crawls and floods the log.
  • curl-logger-ktor-plugin
    — A client plugin that logs every outgoing request as one paste-ready curl command — POSIX single-quoting so a body full of quotes, dollars or newlines survives the shell, the whole command in a single log call, a redaction list, and a body read that does not consume a one-shot channel. Use when you want to replay a failing request outside the app, when a logged command will not run when pasted, or when reproducing a bug means rebuilding a request by hand from a log.
  • parallel-chunked-download
    — Splitting one file into N byte-range requests issued in parallel over a bare HTTP client, each chunk to its own temp file, merged in order, with progress reported through a channel-backed flow — plus when ranges are actually safe, how big a chunk should be, and why a failure retries one chunk rather than the file. Use when a large download is slower than the link allows, when a download restarts from zero after a hiccup, or when a progress bar sticks just short of full.
  • structural-defensive-parsing
    — Reading a response whose shape drifts — classify each field by the marker the payload itself declares rather than by its position, treat a filtering map as data loss and count what it drops, refuse to substitute a placeholder for a failed parse, and parse composite strings from their stable end. Use when a parser works in one locale and not another, when a list arrives shorter than the source shows, or when a made-up value turns up somewhere it was never entered.
  • response-to-domain-flow
    — The five stages a remote response passes through — transport model in a per-integration service module, a pure parser layer, a domain model, a result envelope, then collection — with the rule that each integration is its own module so one source's breakage cannot spread, and the placement rules that keep transport types out of screens. Use when adding a second remote source, when a UI file has started importing response classes, or when a screen shows a spinner forever after a response shape changed.
  • api-ok-but-ignored
    — A remote write can answer "ok" and still have discarded what you sent, saying so only in a secondary field riding along with the success. Model accepted-but-discarded as its own outcome, read that field on every write, and log a discard loudly. Reach for it when a submission reports success on every call and the data never appears on the other side.
  • unknown-not-a-valid-score
    — A parse-failure fallback must be a sentinel outside the legal domain, or expressed in the type — never a value the success path can also produce. Expose "not known" as its own question. Use when a field means two different things depending on where it came from, when a placeholder reaches the screen, or when a consumer cannot tell absent from measured.
  • enum-normalize-over-legacy-data
    — Reading a type marker the remote source declares for itself — normalizing before every comparison because locally stored rows from older app versions hold labels the app invented, treating null as "the source did not say" rather than as a default, exposing an is-known predicate so callers branch on knowledge, and correcting old rows by write-through instead of a migration. Use when a stored type column holds several spellings, when an item is treated as the wrong kind, or before adding a database migration to fix historical values.
  • ktor-kmp-client-architecture
    — 一种多平台 HTTP 架构:由一个 expect/actual 返回引擎,每个集成再基于该引擎构建自己的客户端——API 调用使用带观测能力的客户端,而批量下载则搭配一个刻意保持精简的客户端;按格式注册内容协商机制;设置变更时重建客户端,而不是修改现有客户端。适用于在 Kotlin Multiplatform 模块中搭建网络层、代理设置似乎只对部分请求生效,或大文件下载速度缓慢且日志被刷满时。
  • curl-logger-ktor-plugin
    — 一个客户端插件,可将每个出站请求记录为一条可直接粘贴执行的 curl 命令——采用 POSIX 单引号规则,确保包含大量引号、美元符号或换行符的 body 在 shell 中仍能安全执行;整条命令通过单次日志调用输出;提供脱敏列表;读取 body 时不会消耗一次性 channel。适用于需要在应用外重放失败请求、粘贴日志命令后无法运行,或复现问题时必须根据日志手动重建请求的场景。
  • parallel-chunked-download
    — 将一个文件拆分为 N 个字节范围请求,通过精简的 HTTP 客户端并行发出,每个分块写入独立的临时文件,再按顺序合并,并通过由 channel 驱动的 flow 报告进度——同时说明何时可安全使用范围请求、分块应有多大,以及为何失败时只重试某个分块而不是整个文件。适用于大文件下载速度低于链路允许的速率、下载遇到波折后从头重试,或进度条在接近完成时卡住的场景。
  • structural-defensive-parsing
    — 读取结构会漂移的响应——根据 payload 自身声明的标记而非位置来分类每个字段;将带过滤条件的 map 视为数据丢失,并统计被丢弃的内容;解析失败时拒绝使用占位值替代;从复合字符串中结构稳定的一端进行解析。适用于解析器在一种 locale 下有效、换一种 locale 后失效,列表到达时长度短于源数据,或某个凭空生成的值出现在从未输入过的位置时。
  • response-to-domain-flow
    — 远程响应经历的五个阶段——集成专属 service 模块中的传输模型、纯解析层、领域模型、结果封装,最后是 collection——并遵循每项集成都作为独立模块的规则,以防某个数据源的故障扩散;同时规定模型位置,确保传输类型不会进入 UI 屏幕。适用于新增第二个远程数据源、UI 文件开始导入响应类,或响应结构改变后屏幕一直显示加载动画时。
  • api-ok-but-ignored
    — 远程写入可能返回“ok”,却仍然丢弃了所发送的数据,而这一情况只通过随成功响应一同返回的次要字段来表明。将“已接受但被丢弃”建模为独立结果,在每次写入时都读取该字段,并醒目记录丢弃事件。当每次提交都报告成功,但数据始终未出现在另一端时,应查阅此条目。
  • unknown-not-a-valid-score
    — 解析失败时的回退值必须是合法值域之外的哨兵值,或者直接体现在类型中——绝不能使用成功路径也可能产生的值。将“未知”作为独立问题暴露。适用于同一字段因来源不同而含义不同、占位值显示到屏幕上,或消费方无法区分“缺失”与“已测量”时。
  • enum-normalize-over-legacy-data
    — 读取远程数据源自行声明的类型标记——由于旧版本应用在本地存储的行中保留了应用自行创造的标签,因此在每次比较前先归一化;将 null 视为“数据源未说明”,而不是默认值;公开 is-known 谓词,让调用方根据已知状态分支;通过 write-through 修正旧数据,而不是依赖 migration。适用于存储的类型列中出现多种拼写、某项被当作错误的类型处理,或准备通过数据库 migration 修复历史值之前。

Auth, retries and account state

认证、重试与账户状态

  • oauth-callback-not-through-nav
    — Deliver a returning auth callback's token straight to session state and let the login screen close itself by observing the stored session — routing the token through navigation pushes a second login screen and the post-login close peels the wrong one. Use when a browser-based login succeeds but the user is left staring at the login screen.
  • retry-needs-backoff-and-cap
    — Give every reconnect loop exponential backoff, a ceiling, a class of failures it refuses to retry, and a lifecycle gate — then give the feature a health signal, because one that fails silently stays broken for months. Use when a background connection drains the battery, when a bad credential produces an endless reconnect, or when an integration quietly stopped working.
  • login-state-fans-out-to-settings
    — Logging out must reset every setting that depended on being logged in, at the logout choke point itself — otherwise a gated switch stays on for a service you are no longer authenticated to and silently no-ops forever, or errors on every tick. Use when a feature toggle is stuck on, cannot be switched off, or keeps running against a credential that is gone.
  • nested-flag-settings-auto-disable
    — A child toggle gated by a parent condition must key its auto-disable effect on the parent's current value, grey out rather than hide when the gate is closed, and be gated again at the consumer — otherwise the child sticks ON with no way for the user to clear it. Use when a settings switch is stuck on, is greyed out while reading enabled, or keeps acting after its precondition is gone.
  • oauth-callback-not-through-nav
    — 将认证回调返回的 token 直接交给 session state,并让登录界面通过观察已存储的 session 自行关闭——若让 token 经过 navigation,会额外压入一个登录界面,而登录后关闭逻辑又会移除错误的界面。适用于浏览器登录成功,但用户仍停留在登录界面的情况。
  • retry-needs-backoff-and-cap
    — 为每个重连循环加入指数退避、退避上限、一类明确不重试的失败,以及生命周期门控——随后为该功能提供健康信号,因为静默失败的功能可能数月都无法恢复。适用于后台连接耗尽电量、无效凭据导致无限重连,或集成悄然停止工作时。
  • login-state-fans-out-to-settings
    — 退出登录时,必须在退出登录的关键收口点重置所有依赖登录状态的设置——否则某个受条件控制的开关会继续为已不再认证的服务保持开启,并永久静默无效,或者每次触发时都报错。适用于功能开关卡在开启状态、无法关闭,或仍在使用已经失效的凭据运行时。
  • nested-flag-settings-auto-disable
    — 由父级条件控制的子开关,其自动禁用效果必须以父级当前值为依据;父级条件关闭时应置灰而非隐藏;消费端还必须再次执行门控——否则子开关会一直处于开启状态,用户无法清除。适用于设置开关卡在开启状态、显示为置灰但读取时却表现为已启用,或前置条件消失后仍持续生效时。

Realtime sessions and wire protocols

实时会话与 wire 协议

  • monotonic-clock-offset-sync
    — Estimate a peer's clock offset from ping/pong round trips — take the peer's own processing time out before halving, weight each sample against the best round trip seen, insist the local time source is monotonic, and fall back to the uncorrected value while the estimate is not yet usable. Use when several devices must agree what time it is before they can agree where a stream is, when a group drifts apart on a congested network, or when a position correction jumps after the device adjusts its clock.
  • protobuf-without-codegen-kmp
    — Speak protobuf from shared Kotlin by annotating ordinary data classes with field numbers instead of generating a code layer — with the encoder setting that makes the bytes match a generated encoder, the equality override a byte-array field needs, and the conformance test that pins the equivalence. Use when a schema-driven protocol has to work on every target rather than only the JVM, when encoding a message with an absent nested field throws, or when round-trip tests pass while real peers reject the frames.
  • borrowed-wire-protocol-discipline
    — Rules for implementing a protocol someone else defined — no renaming, no reordering, constants the schema omits read off the counterpart implementation rather than guessed, unknown message types decoded to null instead of thrown, and negotiated capabilities narrowed but never widened. Use when your client must interoperate with an implementation you do not control, when a connection opens and then never gets anywhere, or when a peer on a newer version breaks your session.
  • websocket-session-handshake-lifecycle
    — The order a WebSocket session has to be brought up and torn down — reader started before the first message because the answer comes back through it, the handshake settled on a deferred with a timeout, the close frame sent under a non-cancellable context, and an event buffer that suspends rather than drops. Use when a socket connects but the session never becomes usable, when a deliberate disconnect leaves the peer thinking you are still there, or when clients drift out of sync after a burst of traffic.
  • publish-a-snapshot-on-taking-the-role
    — Every publisher in a shared session is edge-triggered off a change, so a participant who was already running when they took the publishing role emits nothing and the group sits in silence — publish a full snapshot on becoming the source, and again when a new member arrives. Use when a session starts empty until someone touches the transport, when a late joiner sees nothing, or when your state watchers all look correct and the group still knows nothing.
  • monotonic-clock-offset-sync
    — 通过 ping/pong 往返时间估算对端的时钟偏移——先将两端处理时间从往返时间中扣除再除以二;依据目前观测到的最佳往返时间对每个样本加权;要求本地时间源具备单调性;估算尚不可用时,回退到未经校正的值。适用于多台设备必须先对时间达成一致才能确定流中位置、拥塞网络中一组设备逐渐产生漂移,或设备调整时钟后位置校正发生跳变时。
  • protobuf-without-codegen-kmp
    — 通过为普通 data class 标注字段编号,让 shared Kotlin 使用 protobuf,而不生成代码层——包括使字节输出与生成的 encoder 一致的设置、byte array 字段所需的 equality override,以及锁定等价性的 conformance test。适用于由 schema 驱动的协议必须支持所有目标平台而非仅 JVM、编码包含缺失嵌套字段的消息时抛出异常,或往返测试通过但真实对端拒绝帧时。
  • borrowed-wire-protocol-discipline
    — 实现他人定义的协议时需遵循以下规则——不得重命名、不得调整顺序;schema 未给出的常量应从对应实现中读取,而不是猜测;遇到未知 message type 时解码为 null,而不是抛出异常;协商得到的 capabilities 只能收窄,绝不能扩大。适用于客户端必须与不受你控制的实现互操作、连接建立后却无法推进,或较新版本的对端破坏当前 session 时。
  • websocket-session-handshake-lifecycle
    — WebSocket session 必须按特定顺序启动和拆除——由于应答会经 reader 返回,因此在首条消息前启动 reader;使用带 timeout 的 deferred 确认握手完成;在 non-cancellable context 下发送 close frame;事件缓冲区应挂起等待,而不是丢弃事件。适用于 socket 已连接但 session 始终无法使用、主动断开后对端仍认为本端在线,或流量突增后客户端状态逐渐不同步时。
  • publish-a-snapshot-on-taking-the-role
    — 共享 session 中的每个 publisher 都由状态变化边沿触发,因此某个 participant 在接任 publisher 时若早已运行,就不会发出任何内容,整个 group 便会陷入沉默——在成为数据源时发布完整 snapshot,并在新成员加入时再次发布。适用于 session 启动后始终为空,直到有人操作 transport;迟到加入者看不到任何内容;或所有 state watcher 看起来都正常,但 group 仍一无所知时。