traPtitech / traPtitech/Checkin

isDuplicateKeyError が 3 実装で並存し、そのうち 2 つは重複キーを検出できない

Open
#60 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

bug
Dominant language
TypeScript
Stars
0
Forks
0
Avg merge
3d 5h
Merged PRs (30d)
7

Description

症状

重複キー(ER_DUP_ENTRY / errno 1062)を判定する isDuplicateKeyError が 3 つある。

  • packages/api/src/mysql-result.ts の共有版。投げられた値から cause の連鎖を最大 5 段まで辿る。値が null でなく、型が objectfunction であれば中を読む。packages/api/src/auth/identity.tspackages/api/src/ledger/store.ts がこれを使う。
  • packages/api/src/stripe/connect.ts の export された版。投げられた値の最上位しか読まない。型の判定は共有版と同じで、function も中を読む。
  • packages/api/src/webhook/events.ts のモジュール内の版。同じく最上位しか読まないが、型の判定が typeof err !== 'object' なので、function を弾く

drizzle は mysql2 の例外を包み、codeerrno を持つ側を .cause に入れる。mysql-result.tsisDuplicateKeyError に付いているコメントが、drizzle-orm 0.45.2 と mysql2 3.23.2 を MariaDB 11 に対して測った結果として、最上位の DrizzleQueryErrorcodeerrno も持たず、1 段下の .causecode: 'ER_DUP_ENTRY'errno: 1062 を持つ、と記録している。

3 つの答えは 3 通りに分かれる。2026-09-19 に main74ee63c で、3 つに同じ値を渡して測った結果は次のとおりである。mysql-result.tsconnect.ts は export されているので import して呼び、events.ts の版はモジュール内で private なので、ソースの当該関数の記述をそのまま取り出して実行した。

渡した値 mysql-result.ts connect.ts webhook/events.ts
causecode: 'ER_DUP_ENTRY'errno: 1062 を持つ Error を包んだ Error true false false
code: 'ER_DUP_ENTRY'errno: 1062 を持たせた関数 true true false
最上位に code: 'ER_DUP_ENTRY'errno: 1062 を持つ Error true true true

drizzle が投げるのは cause に包まれた形なので、実際に効いてくるのはその違いである。関数を渡した場合は 3 つが 3 通りであることを示すもので、現在の呼び出し側にこの形が届く経路は確かめていない。

影響

どちらの呼び出し側も、この分岐に入るのは競合したときだけである。

  • packages/api/src/webhook/events.tsrecordStripeEventOnce は、呼び出し側が先に hasProcessedStripeEvent で照会してから記録するので、通常の再配信はこの分岐に届かない。同時に届いた重複配信が競ったときだけ、false を返す代わりに例外が外へ出て 500 になる。Stripe は再送し、再送時は照会で弾かれる。
  • packages/api/src/stripe/connect.tsgetOrCreateConnectedAccount は、claimConnectedAccountId が返す false で通常の敗北を扱う。IS NULL のガード自体が競合に負けて一意制約まで届いた場合だけ、won = false へ落ちる代わりに例外が外へ出て、作成済みの Connect アカウントが孤児として残る。

どちらの箇所にも、この隙間を説明するコメントが付いている。

誰が何を誤るか

新しく重複キーを扱う人が、3 つのうちどれを import すればよいか分からない。浅い 2 つを新しい箇所で使うと、drizzle 経由で投げられた重複キーを取り逃す。

3 つを「共有版と、それ以外の同じもの 2 つ」と読むことも誤りである。上の表のとおり、浅い 2 つも型の判定が揃っていない。片方に合わせて直すと、もう片方は直らない。

直さずに残した理由

浅い 2 つを共有版に差し替えると、今まで 500 になっていた経路が設計どおりの分岐に入るようになる。これは振る舞いの変更なので、変更の影響を確かめる単位を分けた。

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with the three implementations in packages/api/src/mysql-result.ts, packages/api/src/stripe/connect.ts, and packages/api/src/webhook/events.ts, then read recordStripeEventOnce and getOrCreateConnectedAccount. Trace the wrapped drizzle error through each caller and verify both affected paths handle duplicate-key races without returning 500 or leaving an orphaned Connect account.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
api, backend, database
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
52/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.