DEV Community

Cover image for "Error Loading Dashboard" with Valid Session on First Visit: The Cookie Wasn't There Yet
Deepanshu Kumar
Deepanshu Kumar

Posted on

"Error Loading Dashboard" with Valid Session on First Visit: The Cookie Wasn't There Yet

Twenty-one clients. Same error. Same week.

The support tickets all said the same thing: blank dashboard, "Error loading Dashboard," reproducible in incognito. Main app working fine. Just the dashboard, just blank.

That pattern — multiple unrelated clients, same symptom, same narrow window — usually means something happened around thirty days earlier. Not a code change. Not a deployment. Something that was set once and then expired.

I ran an initial triage with Claude. What came back looked like a complete root cause analysis within a few minutes.


The theory that fit perfectly

When a user first opened the new dashboard, the system wrote two cookies. One was the main session token, refreshed on every request via a sliding window. The other was a secondary token specifically for the dashboard service, written once at first visit with a fixed thirty-day expiry.

The sliding session kept active users logged in indefinitely. The dashboard token did not slide. It was written once and never touched again.

After thirty days, the dashboard token expired. The main session was still valid. From the browser's perspective nothing had changed. But the dashboard service looked for its token, found nothing, and rejected every request.

The multi-client timing confirmed it. All twenty-one affected clients had been enabled for the new dashboard around the same time, roughly thirty days earlier. Their initial tokens all expired in the same week. That explained why support saw a flood of identical tickets with no obvious trigger.

There was one more detail that made the theory feel airtight. The controller action that renders the dashboard had a guard on the token-write:

// Simplified
if (string.IsNullOrWhiteSpace(Request.Cookies["dashboard_token"]))
{
    SetDashboardToken(user);
}
Enter fullscreen mode Exit fullscreen mode

IsNullOrWhiteSpace means: write the token only if it's completely absent. If it's present but stale, the condition is false and the token is left alone. So the cookie existed in the browser, the check saw it and skipped the refresh, and the token sat there expiring with no mechanism to renew itself.

I opened a branch. The fix: replace the presence check with a value comparison so the token gets refreshed whenever it no longer matches the live session. Affected users were told to log out and back in — that clears both tokens and resets everything on next visit.


The new user

A few weeks later another report came in. Different client. Same blank dashboard. Same error.

This one was a user who had never visited the new dashboard before.

That ruled out the thirty-day theory completely. A token that was never set cannot expire.

I pulled up the browser state from the screenshot attached to the ticket. The main session token was present. The dashboard token was completely absent — not expired, not stale, never written. The dashboard service had received a request with no usable credentials and rejected it.

So the question became: why wasn't the token written before the request went out?

I ran the analysis again with Claude, this time with the browser screenshot as evidence.


What was actually happening

The dashboard is a Vue component hosted inside the main application. When the route loads, the component mounts and onMounted fires. Inside onMounted, it immediately calls the dashboard service to fetch user preferences and widget configuration.

At the same time, the host application makes a server-side call that renders a partial view containing layout configuration. That server-side call also sets the dashboard token, writing it to the response as a Set-Cookie header.

These two things happen concurrently. The onMounted API call goes out immediately on mount. The server-side partial view returns with the Set-Cookie header. The browser stores the token after parsing the response.

The browser processes Set-Cookie after the response arrives, not before the next outbound request. If the onMounted call is already in flight when the partial view response comes back, that request carries no dashboard token. The dashboard service rejects it. The component renders blank.

Route loads
 └─ Vue component mounts
      ├─ onMounted() fires
      │    └─ GET /api/dashboard/preferences  ← no dashboard_token yet
      │
      └─ Host requests partial view (concurrent)
           └─ Response: Set-Cookie: dashboard_token=...
                └─ Browser stores token  ← arrives after the API call
Enter fullscreen mode Exit fullscreen mode

For new users, the timing was almost always wrong. The onMounted call consistently outran the cookie write.


What the first fix actually did

The IsNullOrWhiteSpace fix wasn't wrong. It solved a real problem.

A subset of users really did hit the thirty-day expiry path. Those users visited the dashboard on day one, got the token written correctly, used the app actively for a month, and then hit the dashboard again with a stale token. For them, the presence check was blocking the refresh. The fix helped them.

It did nothing for anyone hitting the timing race on first visit. Those users never had the token at all.

Two different failure modes, same symptom. The first hypothesis fit the evidence available at the time: the simultaneous multi-client reports, the thirty-day gap, the specific guard in the code. It just didn't account for the case where the token was never written in the first place.


The part I can't account for

The ticket was validated, the dashboard loads correctly now. Something fixed the timing race.

What I'm less sure about is how many of the twenty-one clients were actually hitting the race versus the expiry. Most of them were given the log-out-and-back-in workaround before the code fix deployed. Logging out clears both tokens. Their next visit recreated everything fresh. By the time the fix was live, their sessions were clean.

There's no clean way to split those two populations after the fact. The symptoms were identical and the workaround reset both failure modes simultaneously.

Some of those twenty-one clients were probably hitting the thirty-day expiry path. Some were probably hitting the race. The first fix shipped confident it had the right diagnosis. It did — for part of the problem.


Until next time,
Deepanshu

Backend engineer writing about production bugs, distributed systems, and engineering patterns learned the hard way.

Portfolio · Medium · LinkedIn · GitHub

Top comments (0)