fix(table): return 400 from checkValidId on invalid primary keys - #721
Conversation
|
Reviewed; no blockers found. |
kriszyp
left a comment
There was a problem hiding this comment.
If a deeper goal is better error response if an undefined PK (value, not string) is used, we could update checkValidId to throw with a 400 status code.
BTW, is there an associated ticket with this?
| // indicate a client bug where a missing value was substituted into the URL path, and | ||
| // autoCast would silently coerce them to null/true/false/NaN. | ||
| if (typeof id === 'string' && Object.hasOwn(AUTOCAST_COMMON_STRINGS, id)) { | ||
| // For numeric primary keys, reject JS-keyword literal strings ("undefined", "null", |
There was a problem hiding this comment.
These literals are already being rejected in coerceType, aren't they? (line 4723 and rejectNaN). The only literal that seems to have the possibility of coercion is 'null', and it seems like the correct fix would be to reject 'null' conditionally in coerceType based on the nullable property of the attribute.
There was a problem hiding this comment.
Yes, coerceType() does throw for undefined, but not null. Handling null is an easy fix. I wrote the test first and I thought I was able to reproduce it. Now I'm tracing through the REST handler because I'm confident there's a bug. I'm wondering if undefined is getting stringified into null and we know 'null' returns null.
Previously checkValidId threw plain Error, surfacing as a 500. For URL
paths like /<Resource>/null, /<Resource>/undefined, /<Resource>/true on
numeric or Any-typed primary keys, the chain (coerceType → autoCast →
checkValidId) ultimately rejected — but as a 500, with a message that
didn't tie back to the original input. For Any-typed PKs, /Resource/NaN
even silently persisted a record keyed by NaN.
Convert all checkValidId throws to ClientError(400) and explicitly
reject NaN. coerceType already handles "undefined"/"NaN"/"true" for
typed numeric PKs (regex check); this commit fixes the remaining gaps
("null" silently returning null, autoCast producing null/NaN/boolean
for Any PKs).
Per @kriszyp's review feedback on the rejected previous approach.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1dc7fe2 to
d10b483
Compare
|
Reworked per your review feedback — force-pushed. The branch now contains a single commit that updates Summary table of probed behavior (live HTTP requests, all PK types × all literals) is in the PR description. The two corruption cases this catches that weren't in the original report:
No associated ticket — let me know if you'd like me to file one. |
|
@kriszyp ok, it's fixed for real this time :) |
Summary
A client
PATCH/PUTto/<Resource>/<literal>(where<literal>isundefined,null,NaN,true, etc. — common artifacts of template-stringifying a missing value into a URL) used to surface inconsistently depending on the primary key type:Int/Long/Float/BigInt)/undefined,/NaN,/true→ 400 (viacoerceTyperegex)./null→ 500 —coerceTypesilently returnednull,checkValidIdthen threw a plainError.Any/undefined,/null,/true→ 500 (autoCastproducesnull/boolean;checkValidIdrejects)./NaN→ 204, silently persisted withNaNid — data corruption.StringPer @kriszyp's review on the previous approach, the right boundary to fix this is
checkValidIditself: convert all throws toClientError(400)and explicitly rejectNaN. That turns every invalid-PK code path into a clean 400 regardless of how the bad value arrived (URL, autoCast, direct API call).Diff in one place
resources/Table.ts'scheckValidId— fivethrow new Error(...)→throw new ClientError(..., 400), plusif (isNaN(id)) throw ...added to thenumberbranch.Behavior matrix after this PR
/undefined/null/NaN/trueAnyStringTest plan
unitTests/apiTests/undefinedIdInUrl-test.mjs— 18 integration tests via real HTTP server, covering all combinations above. Includes a sanity test thatPUT /DogIntPK/42still works and an explicit non-regression forStringPKs.DogIntPK,DogStringPK,DogAnyPKin the test schema.npm run test:unit:resources— 501 passing, no regressions.npm run lint:requiredandnpm run format:write— clean.