fix(webapp): strip null bytes from idempotency and debounce keys at trigger - #4527
Conversation
…rigger A caller-supplied Unicode NUL (U+0000) in the idempotency key or debounce key reached prisma.taskRun.create() and failed the insert with a Postgres 22P05 (jsonb) error, so the trigger returned an opaque 500 and the run was never created. Strip the NUL from these keys at the single trigger-input chokepoint. The idempotency dedup identity is the hashed key and is unaffected.
|
WalkthroughAdded utilities that remove NUL characters from strings and key-bearing objects. Applied the utilities to idempotency and debounce options during engine trigger input construction. Added unit tests and container-backed integration tests for sanitization and persistence behavior. Added a webapp change note documenting the change. 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
What
A trigger request carrying a Unicode NUL (
U+0000) in the idempotency key or debounce key reachedprisma.taskRun.create()and failed the insert, so the caller got an opaque 500 and the run was never created.These two keys are stored in
jsonbcolumns (idempotencyKeyOptions,debounce), and Postgres rejects a NUL inside ajsonbvalue withSQLSTATE 22P05("unsupported Unicode escape sequence ... cannot be converted to text"). This fix strips the NUL from both keys at the single trigger-input chokepoint (#buildEngineTriggerInput), which every trigger path flows through (single, batch item, mollified, and drainer replay).Stripping matches the existing precedent for run errors and task events. It does not change dedup behaviour: the idempotency dedup identity is the hashed key (a clean 64-char digest), computed independently of the raw key we clean, so dedup keeps working exactly as before. For debounce the key is used directly, so the cleaned key also becomes the grouping key, an acceptable change for input that is already malformed.
Why not payload / metadata / tags
Those are
textcolumns fed byJSON.stringify, which escapes a NUL to a safe escape sequence, so they do not hit this failure on the normal JSON path. (A raw NUL in atextcolumn throws a different code,22021, and is not what triggers this issue.) The observed failures are thejsonb22P05variant, which is only reachable via the two key fields.Evidence
Red then green (containerTest, real Postgres): with the fix reverted, triggering through the real service with a NUL in
idempotencyKeyOptions.key/debounce.keyfails with the exact22P05signature; with the fix, the run is created and the stored key has the NUL removed.Full-stack e2e (isolated stack, real HTTP):
POST /api/v1/tasks/:taskId/triggerwith a NUL insideidempotencyKeyOptions.key("acme<NUL>inc") and, separately,debounce.key("grp<NUL>1"):HTTP 200with a created run (previously500)idempotencyKeyOptions={ "key": "acmeinc", "scope": "run" }(7 chars, NUL removed)debounce.key="grp1"(4 chars, NUL removed)Unit tests cover the helper (strip, no-op fast path, object-reference reuse, null/undefined pass-through).
Rollout / rollback
Server-only webapp change, no flag. Zero behaviour change for clean input; only affects inputs that previously 500'd. Rollback is a straight revert, no data migration.
Known limitation
A raw NUL in a plain-string idempotency key (not created via
idempotencyKeys.create()) lands in atextcolumn and throws22021instead. That variant is not addressed here because stripping it would change the dedup identity, so it warrants a separate decision. Not observed in practice.refs TRI-13030