Flow
From renewal to the guard repair
1Renewal added
Each good request signs the decoded token again.
2Good tokens rejected
Signing throws, and the guard's catch turns it into a 401.
3Middleware fix one
The refresh call returns the new token so the cookie can be set directly.
4Middleware fix two
Retry with the new token in the same request and stamp every response.
5Guard repair
Sign identity fields only, and let a failed renewal log a warning instead of blocking.
Less than an hour after I added sliding sessions to my scheduling app, I was fixing a redirect loop. Then fixing it again. Then adding debug logging. The real cause was one line I'd added that same day, inside a try block that caught too much.
My app keeps you signed in with two cookies. A short one proves who you are on each request. A long one can get you a new short one when it runs out. A guard in the API checks the short one. In front of the pages, a middleware layer asks the API who you are on every page load.
A sliding session means each good request gets a fresh short cookie with a new expiry. You stay signed in while you keep working. I put that renewal in the guard, right after the token check. It shipped in a commit about adding staff without email, which is the wrong place for a login change.
Re-signing a token that still carried its own expiry
The renewal took the checked user and signed it again. But that user object was the decoded old token. It still had the old expiry and issuer on it. The signer wants to set those itself, and my JWT library throws rather than choose.
The throw landed in the same catch as a bad token, so a good token got a 401. My reading is that this hit every cookie request, since every decoded token has an expiry. I don't have the error from that day, so that part is my best guess from the code.
A refresh that worked and a guard that didn't
As best I can tell, the middleware saw the 401, used the long cookie to get a new short one, and sent the browser back to the same page. The next page load hit the broken guard again. Round and round.
I fixed the middleware twice before I touched the guard. First, the refresh call returned the new token so the middleware could set the cookie itself. Second, the middleware retried right away with the new token instead of redirecting, and put the new cookie on every response. Both were good changes. Neither could work while the guard rejected every good token. And the second one still fell back to the old redirect when its retry failed, so I'd bet it kept looping.
Renewal gets its own try block
Renewal now signs only the identity fields, and the signer handles the timing. It also has its own try block, so a failed renewal just logs a warning and the request goes through on the token that was already good.
try { renewCookie(res, pickIdentity(who)); }
catch (err) { log.warn("renewal skipped", err.message); }
return true;
If you build one of these, test it with a signer that throws on purpose, and make sure the page still loads. None of that day's commits had a test like that. Mine also has the cookie lifetime written in several places, which I'll trip over sooner or later.