ankitects/anki · warning

Unexpected API access. Please report this message on the Ank

Error message

Unexpected API access. Please report this message on the Anki forums.

What it means

_check_dynamic_request_permissions() in qt/aqt/mediasrv.py guards the local Anki API exposed over the media server. POST requests must come from pages Anki itself trusts: they need a `Content-type: application/binary` header (to prove they aren't opaque cross-origin form posts) and must either come from a page granted full API access (_have_api_access()) or hit a small whitelist of reviewer/previewer endpoints. When either check fails, Anki shows the warning 'Unexpected API access. Please report this message on the Anki forums.' on the main thread and aborts the request with HTTP 403. This is a deliberate CSRF/origin defense, not a bug in your collection.

Source

Thrown at qt/aqt/mediasrv.py:1301

        return wrapped
    else:
        return NotFound(message=f"{path} not found")


def _check_dynamic_request_permissions():
    if request.method == "GET":
        return

    def warn() -> None:
        show_warning(
            "Unexpected API access. Please report this message on the Anki forums."
        )

    # check content type header to ensure this isn't an opaque request from another origin
    if request.headers.get("Content-type") != "application/binary":
        aqt.mw.taskman.run_on_main(warn)
        abort(403)

    # does page have access to entire API?
    if _have_api_access():
        return

    # whitelisted API endpoints for reviewer/previewer
    if request.path in (
        "/_anki/getSchedulingStatesWithContext",
        "/_anki/setSchedulingStates",
        "/_anki/i18nResources",
        "/_anki/congratsInfo",
    ):
        pass
    else:
        # other legacy pages may contain third-party JS, so we do not
        # allow them to access our API
        aqt.mw.taskman.run_on_main(warn)
        abort(403)

View on GitHub (pinned to 2fae55543c)

Solutions

  1. If you are the author of the code making the request, set the header `Content-type: application/binary` on POST requests to /_anki endpoints and post protobuf bytes, not JSON.
  2. Only call whitelisted endpoints (getSchedulingStatesWithContext, setSchedulingStates, i18nResources, congratsInfo) from reviewer/previewer contexts without full API access.
  3. Use the official anki/tslib frontend code (which sends proper binary requests from trusted pages) instead of hand-rolled fetch calls against the local server.
  4. If the warning appears during normal use of a stock deck/add-on, report it on the Anki forums with the steps to reproduce, as the message requests — it indicates untrusted JS attempted an API call.

Example fix

// before
fetch("/_anki/addNote", {
  method: "POST",
  headers: { "Content-type": "application/json" },
  body: JSON.stringify(payload),
});

// after
fetch("/_anki/addNote", {
  method: "POST",
  headers: { "Content-type": "application/binary" },
  body: protobufBytes,
});
Defensive patterns

Strategy: validation

Validate before calling

const headers = { "Content-type": "application/binary" };
const allowed = ["/_anki/getSchedulingStatesWithContext", "/_anki/setSchedulingStates", "/_anki/i18nResources", "/_anki/congratsInfo"];
if (!allowed.includes(path)) {
  throw new Error("endpoint not permitted from this page context");
}
await fetch(path, { method: "POST", headers, body: protobufBytes });

Type guard

function isBinaryPost(init: RequestInit): boolean {
  return new Headers(init.headers).get("Content-type") === "application/binary";
}

Try / catch

try {
  const resp = await fetch("/_anki/" + endpoint, { method: "POST", headers: { "Content-type": "application/binary" }, body: data });
  if (resp.status === 403) throw new Error("API access denied for this page/endpoint");
} catch (e) {
  console.error("Anki API call blocked:", e);
}

Prevention

When it happens

Trigger: An HTTP POST to any /_anki endpoint whose `Content-type` header is not exactly `application/binary` (e.g. application/json, text/plain, missing, or the browser-normalized `application/x-www-form-urlencoded` from a cross-origin form), or a POST from a page without full API access (e.g. a legacy page or page embedding third-party JS) to an endpoint outside the whitelist (getSchedulingStatesWithContext, setSchedulingStates, i18nResources, congratsInfo).

Common situations: Third-party JavaScript embedded in shared decks/add-on webviews trying to call the Anki backend API; add-on or script authors posting JSON instead of binary protobuf with the correct Content-type header; a malicious webpage on another origin submitting a form POST to localhost (the exact attack this guard blocks); custom tooling hitting the local media server without mimicking the official client headers.

Understand the failure class

Background: Permission denied / not authorized / 403 Forbidden: access-control rejections when the caller lacks the required role, grant, or ownership — this error's family across 18 libraries.


AI-assisted analysis of ankitects/anki@2fae55543c (2026-09-12). Data as JSON: /api/errors/314e5012b4e2fe3e. Report an issue: GitHub.