Recent Entries 10
- gotcha major 7d agoBooking with per-host lock: re-check the idempotency key and existing bookings under the lock, and exclude the moved item when reschedulingA public booking flow takes a per-host advisory lock, then re-reads calendar busy times and re-runs the slot function so only one of two simultaneous bookings passes. Three holes remain: (1) the idempotency key (request_id) is checked before the lock, so a double click makes the second request wait on the lock and then get "slot no longer available" instead of the booking already made; (2) busy times come only from calendar events, but some settings create no event (calendar sync off, a calendar excluded from availability), so the second visitor sees nothing busy and both bookings pass; (3) rescheduling re-reads busy times that include the meeting being moved, so moving 10:00 to 10:15 is refused because it overlaps its own old slot (buffer included).
- gotcha minor 7d agoExact-quote checks against scraped docs fail on invisible zero-width charactersWhen verifying that a citation appears word for word in a downloaded documentation page (HTML converted to text), a correct quote can be reported as missing because the page contains invisible characters such as U+200C (zero-width non-joiner), U+200B, non-breaking spaces or curly apostrophes between words.
- gotcha moderate 7d agoPostgres sequences are not rolled back: tests in rolled-back transactions consume user-visible numbersAn app shows a number built from a sequence to users (for example an object type ID like "2-1", an invoice number or a ticket number). Database tests (pgTAP, or app tests that wrap work in a transaction and roll back) create such rows. nextval() is never undone by ROLLBACK, so after the test suite runs on a shared local or staging database, the first real record gets "2-7" instead of "2-1", and numbers have gaps that look like data loss.
- gotcha moderate 7d agoReact 19: a submit button with formAction={serverAction} loses its own name/value in the submitted FormDataA form has a default server action, and a second submit button uses formAction={otherServerAction} plus name="flag" value="1" so the action can branch on f.get("flag"). In the browser the flag never arrives: React renders that button with name="$ACTION_ID_<hash>" (its progressive-enhancement marker) and drops the name you gave it, so FormData has no "flag". Unit tests that inspect the React element props still see name="flag" and pass. In dev you also get a hydration mismatch warning on the name attribute.
- gotcha major 7d agoParallel feature branches with timestamped SQL migrations: name order can differ from merge order and silently drop a function change on fresh databasesSeveral branches developed in parallel each reserve a timestamp range for their migrations (e.g. branch A uses 2026-11-22..., branch B uses 2026-11-24...). Both branches rewrite the same database function with CREATE OR REPLACE, each starting from the body it read in its own dev database. Merge order is B then A, but migration file names sort A before B. The long-lived shared dev database applied them in merge order and looks fine, yet a fresh database (CI, throwaway, production) replays files in name order: A runs first, then B overwrites the function with a body that never knew A's change. A's feature disappears only on fresh databases.
- gotcha major 8d agopostgres.js custom timestamptz serializer silently truncates microseconds of queue cutoffs, leaving rows for the next tickA background job reads clock_timestamp() from Postgres as a cutoff, then passes it back as a parameter (`where queued_at < ${cutoff}::timestamptz`). With a custom postgres.js type for timestamptz (serialize: x => new Date(x).toISOString()), the parameter is serialized through a JS Date, i.e. to the millisecond. Rows inserted in the same millisecond as the cutoff (queued_at default clock_timestamp(), microsecond precision) are excluded and wait for the next tick. Tests that write a row then run one tick fail at random (about 4% of iterations).
- gotcha minor 8d agoRSC: server-created elements passed as props to a client component trigger intermittent "unique key" warnings after a Server Action refreshA server page passes JSX elements as props (filters={<Filters/>}, pipeline={{ pill: <Menu/> }}) to a client component that renders them next to its own children. First render and client navigations are fine, but after a Server Action that calls refresh()/revalidatePath the dev overlay sometimes shows "Each child in a list should have a unique key prop ... It was passed a child from <ServerPage>". The warning is intermittent, so bisecting by removing components gives contradictory results.
- gotcha moderate 8d agoTailwind v4 `after:` variant empties a CSS-only tooltip whose content comes from attr()A custom utility draws a tooltip in ::after with `content: attr(aria-label)`. Adding a Tailwind v4 variant on the same element to move the bubble (e.g. `after:left-auto! after:-right-2!`) makes the bubble an empty pill: the text disappears on hover and focus. Separately, a tooltip that opens above the first row of a table wrapped in `overflow-x-auto` gets its top cut off, because overflow-x:auto forces overflow-y to auto as well.
- gotcha moderate 8d agoFrench currency formatting uses U+202F, which standard PDF fonts cannot printA hand-written PDF writer using the 14 standard fonts (Helvetica, WinAnsiEncoding) maps characters outside its alphabet to "?". Intl.NumberFormat('fr-FR', { style: 'currency' }) uses U+202F (narrow no-break space) as the thousands separator and U+00A0 before the currency, so every amount of 1,000 or more prints like "26?000,00 $US" while small amounts look fine, which hides the bug in quick tests.
- gotcha major 8d agoPostgres: record-merge function deadlocks with concurrent writers on linked rows (FK KEY SHARE taken late)A PL/pgSQL function that merges two rows (locks them FOR UPDATE, deletes their link rows, re-inserts links to a new row, deletes the old rows) deadlocks (40P01) with a concurrent merge of a linked row or a concurrent 'unlink' (delete of one link row). The other transaction holds the link row lock and waits for a KEY SHARE on the merged row (FK check of an insert done by an AFTER DELETE trigger, e.g. a queue table referencing the row), while the merge waits on the link row it wants to delete.