Skip to content

fix(table): return 400 from checkValidId on invalid primary keys - #721

Merged
cb1kenobi merged 4 commits into
mainfrom
fix-undefined-primary-key-patch
May 22, 2026
Merged

fix(table): return 400 from checkValidId on invalid primary keys#721
cb1kenobi merged 4 commits into
mainfrom
fix-undefined-primary-key-patch

Conversation

@cb1kenobi

@cb1kenobi cb1kenobi commented May 22, 2026

Copy link
Copy Markdown
Member

Summary

A client PATCH/PUT to /<Resource>/<literal> (where <literal> is undefined, 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:

PK type Pre-fix
Numeric (Int/Long/Float/BigInt) /undefined, /NaN, /true → 400 (via coerceType regex). /null → 500coerceType silently returned null, checkValidId then threw a plain Error.
Any All of /undefined, /null, /true → 500 (autoCast produces null/boolean; checkValidId rejects). /NaN → 204, silently persisted with NaN id — data corruption.
String Accepts all literals as legitimate string ids (correct — unchanged).

Per @kriszyp's review on the previous approach, the right boundary to fix this is checkValidId itself: convert all throws to ClientError(400) and explicitly reject NaN. 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's checkValidId — five throw new Error(...)throw new ClientError(..., 400), plus if (isNaN(id)) throw ... added to the number branch.

Behavior matrix after this PR

PK type /undefined /null /NaN /true
Numeric 400 400 (was 500) 400 400
Any 400 (was 500) 400 (was 500) 400 (was 204 — corruption!) 400 (was 500)
String 204 (round-trips) 204 204 204

Test plan

  • New unitTests/apiTests/undefinedIdInUrl-test.mjs — 18 integration tests via real HTTP server, covering all combinations above. Includes a sanity test that PUT /DogIntPK/42 still works and an explicit non-regression for String PKs.
  • New tables DogIntPK, DogStringPK, DogAnyPK in the test schema.
  • npm run test:unit:resources — 501 passing, no regressions.
  • npm run lint:required and npm run format:write — clean.
  • No associated ticket — discovered during user's hands-on testing.

Generated by Claude (Opus 4.7, 1M context). Force-push replaces the rejected previous approach (broad coerceId rejection) with the targeted fix at the right boundary.

@cb1kenobi
cb1kenobi requested a review from kriszyp May 22, 2026 05:54
@claude

claude Bot commented May 22, 2026

Copy link
Copy Markdown
Contributor

Reviewed; no blockers found.

@cb1kenobi
cb1kenobi marked this pull request as ready for review May 22, 2026 07:32

@kriszyp kriszyp left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Comment thread resources/Table.ts Outdated
// 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",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
@cb1kenobi
cb1kenobi force-pushed the fix-undefined-primary-key-patch branch from 1dc7fe2 to d10b483 Compare May 22, 2026 16:05
@cb1kenobi cb1kenobi changed the title fix(table): reject JS literal strings as primary keys in coerceId fix(table): return 400 from checkValidId on invalid primary keys May 22, 2026
@cb1kenobi

Copy link
Copy Markdown
Member Author

Reworked per your review feedback — force-pushed. The branch now contains a single commit that updates checkValidId to throw ClientError(400) (with NaN also rejected), instead of trying to gate at coerceId.

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:

  • /HasBigInt/null and /DogIntPK/null were already returning 500 — coerceType's if (value === 'null') return null swallows the bad input. (You're right that "undefined" was already throwing — only "null" is the gap for typed numeric PKs.)
  • /DogAnyPK/NaN was silently persisting a record keyed by NaN. autoCast("NaN") returns the actual NaN number, checkValidId's typeof id === 'number' branch returned true without checking isNaN. Caught by the new isNaN check.

No associated ticket — let me know if you'd like me to file one.

@cb1kenobi
cb1kenobi requested a review from kriszyp May 22, 2026 16:39
@cb1kenobi

Copy link
Copy Markdown
Member Author

@kriszyp ok, it's fixed for real this time :)

@kriszyp kriszyp left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good!

@cb1kenobi
cb1kenobi merged commit 6c19c6c into main May 22, 2026
61 of 63 checks passed
@cb1kenobi
cb1kenobi deleted the fix-undefined-primary-key-patch branch May 22, 2026 21:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants