A user action reports success it did not get
What it costs you
This handler fires a destructive write, never looks at whether it succeeded, and then tells the user it worked — a success toast, an optimistic row removal, or a redirect. When the server rejects the request the user is left believing a key was revoked, a member removed or a session ended when none of it happened.
The defect, and the fix
Both samples are scanned as app/settings/ApiKeyRow.tsx.
Our test suite runs both through the scanner on every build: the first must be reported, the second must not.
"use client";
import { toast } from "sonner";
export function ApiKeyRow({ keyId }) {
async function handleRevoke() {
const res = await fetch(`/api/keys/${keyId}/revoke`, { method: "POST" });
if (!res.ok) return;
toast.success("API key revoked");
}
return <button onClick={handleRevoke}>Revoke</button>;
}"use client";
import { toast } from "sonner";
export function ApiKeyRow({ keyId }) {
async function handleRevoke() {
const res = await fetch(`/api/keys/${keyId}/revoke`, { method: "POST" });
if (!res.ok) {
toast.error("Could not revoke the key. Try again.");
return;
}
toast.success("API key revoked");
}
return <button onClick={handleRevoke}>Revoke</button>;
}What changed: The failure branch tells the user instead of returning silently, so a revoke that did not happen cannot show as one that did.
How to fix it
Check the response before you assert anything: `if (!res.ok) { toast.error(...); return; }`. Only then show the success state, remove the row, or navigate away.
Why this rule doesn't cry wolf
Each clause below exists because it was attacked: someone was asked to find correct code that the rule would flag, and the clause is what stopped it. This is published because a check you cannot audit is a check you have to take on faith.
Client files only, never route handlers or server actions. The fetch must be lexically inside a function reachable from an onClick/onSubmit/onChange handler (never module scope, useEffect, setInterval or a pagehide beacon), must not be a telemetry URL or carry keepalive, and must not sit in a transport wrapper or a react-query/SWR mutationFn where the caller owns the error path. The enclosing function must contain NO error path at all — no try/catch, .catch, throw, assert helper, toast.error/alert/setError, or read of body.error — except the empty-else shape (`if (!res.ok) return;` / `if (res.ok) {...}` with no alternate), which is the shipped defect. A success assertion must follow the fetch, no revalidation may follow it, and the URL, handler name or a destructive confirm() must carry real consequence (revoke/delete/api-key/session/billing); low-stakes writes are dropped rather than reported at a severity they do not deserve.
This is not a security finding
QA002 is reported in its own section, separately from security findings. It does not change your security grade, and it does not fail your build unless you pass --fail-on-quality. The security catalogue lives at /rules.
Other Correctness checks
Check your own code
npx xploitscan scan .Runs on every plan, including free. All 12 quality checks.