Reddit will not answer an extension

DHSeaDev — Chrome Extensions, Windows Tools, & Idle Games

AniDeck was going to read the episode discussion thread and show it in the panel. It does not. Reddit answers an extension with HTTP 403 regardless of what you ask for, and finding that out on a live run rather than in the docs is the reason the extension now makes exactly one network request instead of two.

AniDeck is a Chrome side panel for watching anime: it blurs spoilers before you read them, shows your AniList watching statistics, and keeps episode context next to the player. Three features, picked from a shortlist of ten, on the constraint that none of them may need a server.

The plan that did not survive a live run

The episode companion was supposed to fetch reddit.com/r/anime/search.json, find the discussion thread for the episode you were on, and render the top comments in the panel. Straightforward, documented, and wrong.

Reddit returns HTTP 403 with an HTML body to extension-origin requests. Not to unauthenticated requests, not to datacenter IPs specifically, not to requests missing a host permission — I granted the host permission and it made no difference. The origin is the problem, and there is no header that fixes it.

Two things fell out of that. The first is a parsing rule I now apply everywhere: check the content type before you call JSON.parse. A 403 that hands you an HTML error page will throw somewhere unhelpful, three frames from the request that caused it, and the stack trace will point at your parser rather than at the server that lied to you.

The second is that I deleted the feature rather than working around it. The panel now shows a plain deep link into the r/anime search, which you click. It is one interaction worse. It also removed the reddit.com host permission and the entire second network call site, which means the privacy section of the listing went from a paragraph of qualifications to one sentence: it talks to AniList, and nothing else. That is a better product than the one I set out to build, and I would not have chosen it on purpose.

A false positive costs a click. A false negative is permanent.

The Spoiler Shield is the part people will actually judge, and its whole design comes from one asymmetry. If it blurs something harmless, you click once and read it. If it misses something, you have read that a character dies forty episodes before you were meant to, and no setting will give that back to you.

So the default mode over-blurs. The settings let you dial it down rather than up, which is the opposite of how filters usually ship. And when several signals disagree about one post, they resolve in a fixed published order instead of by whichever check happened to run last:

Spoiler Shield precedence order Four decision rules stacked in priority order. A per-show override decides outright. If none is set, an episode number ahead of the viewer hides the post. If there is no episode number, a weak-alias guard refuses to blur on an ambiguous series nickname alone. Anything still undecided falls through to the mode setting, which defaults to Conservative. 1. Per-show override An explicit Always or Never you set for that series. Decides outright. WINS if unset 2. Episode number A post naming an episode ahead of you is hidden, regardless of wording. if absent 3. Weak-alias guard A short or ambiguous nickname alone is not enough to blur an unrelated post. everything left over 4. Mode Conservative, Balanced or Permissive. Conservative is the default. FALLBACK
Spoiler Shield precedence: the first rule that applies decides, and only genuinely undecided posts reach the mode setting.

One bug in that area is worth naming because it looked like it worked. Revealing a blurred post was remembered by series ID, so clicking through one comment about a show quietly unblurred every comment about that show for the rest of the session. The filter was on, the setting said Conservative, and it was doing nothing. It is now keyed by a hash of the post text, bounded at 400 entries and scoped to the page.

What the canvas would not let me draw

The stats feature produces a share card: your watching history as an image you can post. The obvious version puts your AniList avatar in the corner.

AniList’s image CDN sends no access-control-allow-origin header. Drawing one of its images onto a canvas taints the canvas, and a tainted canvas throws on toBlob — at the export step, which is the last thing the user does and the only thing they actually wanted. The failure mode is a button that works nine times during development and fails the first time someone uses it for real.

The card is now drawn with zero external images and a deterministic identicon generated from the username. It is less pretty than the avatar version and it cannot fail.

Two other things the live API told me that the documentation did not. AniList returns two different 404 strings that need two different messages — Private User for a list you are not allowed to read, Not Found. for a username that does not exist, and collapsing them means telling people their account is missing when it is merely private. And a Media(search:) query with an explicitly supplied id: null returns Not Found., because the id is applied as a filter even when it is null. That one would have broken every title lookup in the extension, and it is invisible until you send the request.

Where it is

Version 1.3.0 is built and packaged, with light, dark and system themes, six accents, three text scales, settings export and import, and a keyboard shortcut to toggle the shield. The Chrome Web Store listing is in preparation. The full feature list, the permission table and the privacy details are on the AniDeck project page, and the privacy section is at /projects/anideck/#privacy.

The pattern I keep relearning: the interesting constraints are not in the documentation. They are in the response you get the first time you actually send the request.