DEV Community

chovy
chovy

Posted on Originally published at dev.profullstack.com

A 403 you could have predicted

A 403 you could have predicted

A release announcement went out from my terminal last night and one of its five destinations refused it:

FAIL  tsbb:member@bbs.hqtui.com  403 bbs.hqtui.com/api/v1/forums/news/topics — forbidden
Enter fullscreen mode Exit fullscreen mode

The forum in question is fed by a blog crawler. Members reply to those topics, they do not start new ones, which is a perfectly sensible way to run a news section. The problem was not the rule. The problem was that nothing told the client about the rule until it had already tried to post in public.

Here is what the board's forum list looked like at the time:

{ "slug": "announcements", "kind": "forum", "topics": 1, "posts": 1 }
{ "slug": "news",          "kind": "forum", "topics": 9, "posts": 9 }
Enter fullscreen mode Exit fullscreen mode

Identical. One takes topics, one does not, and a program reading that list has no way to know which is which. So my poster picked the wrong one and found out the way you least want to find out.

The board already knew

This is the part worth dwelling on. The refusal was not a surprise to the server. Its write route says:

if (!permissions.canPost || forum.isLocked) return c.json({ error: 'forbidden' }, 403);
Enter fullscreen mode Exit fullscreen mode

resolvePermissions had computed canPost for this member, correctly, before answering. The board knew the answer and then declined to publish it, so every client had to discover by experiment what one field could have stated. That is a design smell I now look for by name: a service that computes a decision, acts on it, and does not expose it.

So tsbb 0.5.1 returns it. canPost, canReply and locked on every forum in /api/v1/forums and /api/v1/board, resolved by the same function the write route uses, in the same request. Not a new rule, just the existing one said out loud.

Which caller is asking

Then the interesting bug, the one I nearly shipped.

Permissions are per viewer, so canPost is an answer to "may you post here". My poster read the forum list during login, before the sign-in flow finished, which means it read it as an anonymous guest. And a guest may post nowhere. Every field came back false.

Had I trusted that, myna would have refused every forum on every board, including the ones that work perfectly well. The check would have looked reasonable in code review and broken every login.

The same URL, twice, against the live board:

                 without a token   with the member's token
announcements    canPost=false     canPost=true
news             canPost=false     canPost=false
app-showcase     canPost=false     canPost=true
Enter fullscreen mode Exit fullscreen mode

The list is now read twice on purpose. Once before the device flow, as a guest, for the things that are true of everybody: a slug that does not exist, and a category, which holds forums rather than topics. Both cost nothing to check and refuse a mistake before anyone opens a browser to approve a code. Then once more after the token arrives, as the member, for canPost, which is the only point at which that question has a meaningful answer.

A forum the member cannot post in is dropped with a line saying why. Only a selection where nothing is left fails the login outright.

What a client should do anyway

The hint makes the common case pleasant. It does not make the runtime check optional, and I would not have shipped only the hint.

Permissions change. A forum gets locked an hour after you logged in, a member's rank changes, an admin flips the posting policy. So myna still treats a 403 at post time as the board's answer: it moves the announcement to the next forum in its rotation and stops keeping the refused one on the account. Only a 403 does that. A 500, a timeout or an expired token still fail loudly, because a board having a bad afternoon should not quietly empty your configuration one post at a time.

Read the hint, believe the refusal. The hint saves the first mistake, the refusal handles the world changing underneath you.

Get them

myna update
Enter fullscreen mode Exit fullscreen mode

tsbb boards that follow releases pick 0.5.1 up on their own within about five minutes.

Top comments (0)