Skip to content

Comments

STCOR-1012 💉 botox for the botched RTR implementation#1696

Open
zburke wants to merge 20 commits intomainfrom
STCOR-1012
Open

STCOR-1012 💉 botox for the botched RTR implementation#1696
zburke wants to merge 20 commits intomainfrom
STCOR-1012

Conversation

@zburke
Copy link
Member

@zburke zburke commented Feb 13, 2026

RTR, boy oh boy, where to start?

The implementations thus far all worked by setting a timer that pinged shortly before the AT expired, executing a callback that paused API requests while executing the rotation request. Of course, it was never that simple; starting rotation after authenticating was subtly different than starting rotation for an existing session because the login-response includes token-expiration data but the /bl-users/_self doesn't. All those extra tweaks made the code even messier, and while they each made the UX a little better, none of them made it actually make it good. And then STCOR-1012 showed up, telling us things were not only not good; they were, in fact, pretty bad. It was a mess. It was just a mess. 💩

And that brings us here, to the fourth full-blown rewrite of RTR. Hopefully, the logic here is simpler, better isolated, and easier to reason about. At the highest level, things are still the same: we're replacing native-fetch with FFetch, which transparently handles rotation. The FFetch algorithm now works like this:

  • get a shared lock, preventing API requests from firing while rotation is in progress
    • make the API request
    • release the lock
  • if the response isn't ok, call rotateAndReplay() to rotate the tokens and replay the original request
  • return the response

rotateAndReplay works like this:

  • inspect the original response. if it doesn't look like an authentication failure, reject with the original response, allowing it to bubble back to the code that made the original request
  • get an exclusive lock, preventing any API requests from firing while rotation is in progress
    • request token-expiration data from storage; if it appears valid, replay the original request and return its response
    • make the rotation request
    • callback to store the updated token-expiration data
    • replay the original request
    • release the lock

Checking, while inside the lock, whether the tokens have already been rotated is a crucial step that handles the situation of many requests firing simultaneously after the tokens have expired. The Web Locks API elects one request, allowing rotation, storage, and replay to proceed for that request only while all others wait for the lock's promise to resolve. After that first request releases its lock, the next request in the queue proceeds through rotateAndReplay(), but this time it will return early since the token-expiration data in storage will be valid. Note: this means requests that are queued together proceed single-threaded through the lock. This sounds worse than it is since rotateAndReplay() short-circuits after the first request, jumping immediately to "replay" since valid tokens are now available.

But wait! There's more! The original implementation stored the end-of-session timer-IDs in redux. It's important to keep track of those IDs so they can be cleared if the user logs out early or session expiration is configured as a window that slides on rotation, as non-keycloak sessions are. Anybody remember UICHKOUT-869? Using redux was problematic for two reasons:

  1. Mind-bogglingly, clearing the old timer was buried inside the redux action, which, just, why??? 🤯 Dude, have you never heard of single-responsibility? Redux actions should be responsible for redux and only redux.
  2. Bigly-mind-bogglingly, keeping timer data on the okapi slice of redux meant that Root re-rendered every time tokens rotated, which was a real head-scratcher (STCOR-1008 et al), until @vashjs made things plain to us in UITEN-333 🙇. Like I said, it was a mess. 💩 💩

The self-contained, self-clearing timer ResetTimer (maybe I should've called it SelfClearingTimer? 🤔) resolves both: redux actions are once again pure, and Root no longer re-renders with each rotation.

Refs STCOR-1012, UITEN-333, STCOR-1025

@github-actions
Copy link

github-actions bot commented Feb 13, 2026

Jest Unit Test Results

  1 files  ± 0   85 suites  ±0   1m 5s ⏱️ -38s
503 tests  - 22  503 ✅  - 22  0 💤 ±0  0 ❌ ±0 
509 runs   - 22  509 ✅  - 22  0 💤 ±0  0 ❌ ±0 

Results for commit a4f1623. ± Comparison against base commit 5ab64db.

This pull request removes 55 and adds 33 tests. Note that renamed tests count towards both.
FFetch class Calling a FOLIO API fetch calls native fetch once ‑ FFetch class Calling a FOLIO API fetch calls native fetch once
FFetch class Calling a non-FOLIO API calls native fetch once ‑ FFetch class Calling a non-FOLIO API calls native fetch once
FFetch class Calling an okapi fetch when all tokens are expired triggers an RTR error ‑ FFetch class Calling an okapi fetch when all tokens are expired triggers an RTR error
FFetch class Calling an okapi fetch with a malformed resource triggers an Unexpected Resource Error ‑ FFetch class Calling an okapi fetch with a malformed resource triggers an Unexpected Resource Error
FFetch class Calling an okapi fetch with missing token and failing rotation... triggers rtr...calls fetch 2 times, failed call, failed token call, throws error ‑ FFetch class Calling an okapi fetch with missing token and failing rotation... triggers rtr...calls fetch 2 times, failed call, failed token call, throws error
FFetch class Calling an okapi fetch with missing token and reported error from auth service... throws an RTR error ‑ FFetch class Calling an okapi fetch with missing token and reported error from auth service... throws an RTR error
FFetch class Calling an okapi fetch with missing token... returns the error ‑ FFetch class Calling an okapi fetch with missing token... returns the error
FFetch class Calling an okapi fetch with valid token, but failing request... returns response from failed fetch, only calls fetch once. ‑ FFetch class Calling an okapi fetch with valid token, but failing request... returns response from failed fetch, only calls fetch once.
FFetch class calling authentication resources avoids rotation when AT and RT expire together ‑ FFetch class calling authentication resources avoids rotation when AT and RT expire together
FFetch class calling authentication resources handles RTR data in the response ‑ FFetch class calling authentication resources handles RTR data in the response
…
FFetch behavior and rotation helpers passes through non-Okapi requests ‑ FFetch behavior and rotation helpers passes through non-Okapi requests
FFetch behavior and rotation helpers rotationConfig helpers isValidToken returns true when token expiry is in future and false otherwise ‑ FFetch behavior and rotation helpers rotationConfig helpers isValidToken returns true when token expiry is in future and false otherwise
FFetch behavior and rotation helpers rotationConfig helpers replaceXMLHttpRequest sets global.XMLHttpRequest and preserves the original ‑ FFetch behavior and rotation helpers rotationConfig helpers replaceXMLHttpRequest sets global.XMLHttpRequest and preserves the original
FFetch behavior and rotation helpers rotationConfig helpers rotate() performs a refresh and returns parsed expirations on success ‑ FFetch behavior and rotation helpers rotationConfig helpers rotate() performs a refresh and returns parsed expirations on success
FFetch behavior and rotation helpers rotationConfig helpers rotate() throws when refresh response is missing fields ‑ FFetch behavior and rotation helpers rotationConfig helpers rotate() throws when refresh response is missing fields
FFetch behavior and rotation helpers rotationConfig helpers rotate() throws when refresh response is not ok ‑ FFetch behavior and rotation helpers rotationConfig helpers rotate() throws when refresh response is not ok
FFetch behavior and rotation helpers rotationConfig helpers rotationConfig.onFailure dispatches an RTR error event ‑ FFetch behavior and rotation helpers rotationConfig helpers rotationConfig.onFailure dispatches an RTR error event
FFetch behavior and rotation helpers rotationConfig helpers rotationConfig.onSuccess calls the provided onRotate callback with new tokens ‑ FFetch behavior and rotation helpers rotationConfig helpers rotationConfig.onSuccess calls the provided onRotate callback with new tokens
FFetch behavior and rotation helpers rotationConfig helpers rotationConfig.options merges OKAPI_FETCH_OPTIONS into options ‑ FFetch behavior and rotation helpers rotationConfig helpers rotationConfig.options merges OKAPI_FETCH_OPTIONS into options
FFetch behavior and rotation helpers rotationConfig helpers shouldRotate given a 400 with generic text, returns false ‑ FFetch behavior and rotation helpers rotationConfig helpers shouldRotate given a 400 with generic text, returns false
…

♻️ This comment has been updated with latest results.

@github-actions
Copy link

github-actions bot commented Feb 13, 2026

Bigtest Unit Test Results

  1 files  ±0    1 suites  ±0   0s ⏱️ ±0s
  2 tests ±0    2 ✅ ±0  0 💤 ±0  0 ❌ ±0 
152 runs  ±0  152 ✅ ±0  0 💤 ±0  0 ❌ ±0 

Results for commit a4f1623. ± Comparison against base commit 5ab64db.

♻️ This comment has been updated with latest results.

@zburke zburke changed the title STCOR-1012 botox for the botched RTR implementation STCOR-1012 💉 botox for the botched RTR implementation Feb 13, 2026
@zburke zburke marked this pull request as ready for review February 13, 2026 20:50
@zburke zburke requested a review from a team as a code owner February 13, 2026 20:50
@zburke zburke requested a review from a team February 13, 2026 20:51
Copy link
Contributor

@JohnC-80 JohnC-80 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tiny cleanup is all I've got so far... this really cleaned a lot of house. 👏 👏 👏

Copy link
Member

@maccabeelevine maccabeelevine left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

// Ruhroh, Raggy, rotation railed!

😆

Previously, XHR.send() requests would blow up because they called
`rotateAndReplay()` with only two args but three were required.
shouldRotate
alternative to status code inspection when a response is present:
deal with Okapi's non-standard 400 response for missing/invalid tokens
by cloning the response and inspecting the body text.
optional; defaults to a function that resolves to false, i.e. do not force rotation
Start timers when reloading a window. DUH! Hard to believe I completely
missed this the first time around. h/t @Dmytro-Melnyshyn

Prop-drilling > stripes. Only a few components need these RTR functions,
so only a few components should receive them.

When XHR notices the AT has expired, force rotation.
…hat return promises ... should return promises. Your hed asplode now, huh, Sonar?
@sonarqubecloud
Copy link

@zburke zburke requested a review from JohnC-80 February 20, 2026 22:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants