Přeskočit na obsah

PATCH endpoint v UI mlčky zahazuje změny — snake_case vs camelCase mismatch

import { Aside } from ‘@astrojs/starlight/components’;

  • Uživatel mění hodnotu v UI tabulce (např. “next number” číselné řady)
  • Klikne Uložit → 200 OK, žádná chyba, UI dokonce uloží edit state
  • Při dalším refresh hodnota je původní
  • Žádná entry v auditu, žádný error v logu

API endpoint mapuje whitelist polí pouze v camelCase (např. nextNumber: "next_number"). UI posílá přímo DB-style snake_case (next_number). For-loop neprojde keys, sets je prázdný, endpoint vrátí {updated: 0} jako úspěch:

// API endpoint
const map: Record<string, string> = {
nextNumber: "next_number", // ← očekává camelCase
currentYear: "current_year",
};
for (const [k, col] of Object.entries(map)) {
if (body[k] !== undefined) { // ← snake_case key NEZASÁHNE
sets.push(`${col} = $${pi}`);
values.push(body[k]);
pi++;
}
}
if (sets.length === 0) {
return NextResponse.json({ data: { updated: 0 } }); // ← tichý úspěch
}
// UI komponenta — Series typing odpovídá DB rows (snake_case)
const [edits, setEdits] = useState<Record<number, Partial<Series>>>({});
// ...
<input onChange={e => setEdit(id, { next_number: parseInt(e.target.value) })} />
// ...
await fetch(`/api/invoice-series/${id}`, {
method: "PATCH",
body: JSON.stringify(patch), // ← posílá { next_number: 5000 }, NE { nextNumber: 5000 }
});

Vrstva převodu v UI před fetch — explicitní mapping podle endpoint kontraktu:

const apiPatch: Record<string, unknown> = {};
if (patch.name !== undefined) apiPatch.name = patch.name;
if (patch.next_number !== undefined) apiPatch.nextNumber = patch.next_number;
if (patch.current_year !== undefined) apiPatch.currentYear = patch.current_year;
if (patch.is_active !== undefined) apiPatch.isActive = patch.is_active;
await fetch(url, { method: "PATCH", body: JSON.stringify(apiPatch) });

Lepší trvalá oprava: endpoint by měl vracet 400 při neznámém klíči, ne mlčet:

const knownKeys = new Set(["nextNumber", "currentYear", "isActive", "name", "prefix"]);
for (const k of Object.keys(body)) {
if (!knownKeys.has(k)) {
return NextResponse.json({ error: `Unknown field: ${k}` }, { status: 400 });
}
}

E2E test, který:

  1. Načte aktuální hodnotu → before
  2. PATCH novou hodnotu (např. before + 1000)
  3. Otestuje viditelný side-effect (např. další záznam má číslo z nového setpointu), nikoli jen 200 OK

Pokud test ověřuje jen response code, tenhle bug uteče. Mocney await readNextNumberForSeries(code) po PATCH a porovnej.

  • Žádná chyba v logu nebo Sentry
  • {updated: 0} ve výstupu PATCH endpointu, kdyby někdo monitoroval
  • Audit log mutací prázdný kdyby existoval
Přidal aiarchitekt.cz · 7. 6. 2026 16:50
Provozuje aiarchitekt.cz