Back to Feed
article 4m read

Two of my signup routes were redirecting into each other

New accounts could bounce between my home page and a session-refresh route while a webhook caught up. I gave the wait one owner, a half-minute budget and an error page with a reason, and the tests are thinner than I'd like.

Article DepthPractice

Flow

The pieces of a signup wait that ends

  1. 11. A home page that only routes

    Named refresh if the organization exists, waiting page if not. No redirect back to the page that sent you.

  2. 22. A small status endpoint

    Signed-in only. Says whether the user and an active membership exist, and nothing more.

  3. 33. One waiting page

    Checks every couple of seconds for about half a minute, then moves on to the error page.

  4. 44. A refresh route with its own limit

    Growing delays, one question to the login service, then the error page.

  5. 55. An error page with a reason

    Explains the failure, offers a fresh sign-in, and pre-fills a support email.

  6. 66. Tests on a fake clock

    Found right away, found late, found only upstream, never found, broken session.

The home page of the admin app sent new accounts with no organization to a refresh route, and the refresh route sent them right back if it couldn't find one either. The two pages could pass a person back and forth, and nothing ever said stop.

Why there was a gap at all

People sign up through an outside login service, which gives the browser a working session right away. My app checks its own tables before it shows anything, and those get filled in by a webhook a little later. Until then, the person is logged in but my app doesn't know them yet.

The old code dealt with that by bouncing between the two pages. The home page had a short wait of its own, but the loop between the two pages had no limit. A slow webhook meant a long blank screen. A lost one meant a loop with no end in my code.

Who waits now

The home page only decides where to send people. If my database has their organization but their session doesn't, they go to the refresh route with the organization named, and it signs them back in with it attached. If there's no organization yet, they go to a new waiting page.

The waiting page is the one place that waits. Every couple of seconds it asks a small status endpoint whether the user and their membership exist yet. If the answer is yes, it shows a quick all-set message and moves on. If half a minute passes, it sends the person to an error page that says it timed out.

The refresh route still has a mode for when no organization is named. That mode now retries with growing delays for a bit over half a minute, asks the login service once, and then goes to the error page too. Every path ends somewhere.

The outside service gets no vote

If the login service says the person has a membership that my tables don't, I only use that to refresh their session. My home page still checks my own tables, so the person ends up back on the waiting page. I'd rather they wait than let an outside answer decide who gets in.

Two traps on the waiting page

The waiting page uses a timer that fires on schedule even if the last check hasn't come back. On a slow day, checks can overlap and a late "yes" can show up after the page has already given up. A loop instead of an interval avoids that.

Each check finishes before the next one starts, and the loop stops at a deadline.

Also, the progress steps on that page move forward on their own clock. They look like status, but they aren't.

A screen with a reason on it

It explains which way the setup failed, lets the person sign in again, and opens a support email with the reason already filled in. One line on the timeout version says the team has been notified, and nothing in this change sends that notice.

Fake clocks and an empty database

The tests use fake timers so the whole wait runs in milliseconds, plus a database stub that returns nothing a few times before it finds a membership. The cases are: found right away, found late, found only through the login service, never found, and a broken session. On the webhook side, the tests cover a membership event that arrives early, an event for a membership that already exists, and giving up after the limit. My backoff test only checks that a redirect happens, though, and if I were writing it again I'd make it count the attempts as well.

Keep reading

Pick up a connected idea or branch into a nearby one.

5 paths forward
Related idea

New users logged in to nothing because sign-up waited so long to pick a workspace

Workspace creation waited for a later webhook so invited people wouldn't get a spare one, and self-serve people could log in before it fired. Now the creation handler asks the identity provider for memberships on the spot, a later event repairs misses, and the session lookup retries, but the comment that justifies it contradicts the one it replaced and nothing tests which is right.

via #webhooks · #reliability