QA002highCorrectness

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.

Reported
"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>;
}
Not reported
"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.