Move Activity Tab state from cookie to localStorage#7
Conversation
The Activities widget (XNAT.app.activityTab) persisted its state (the
in-progress/completed task list plus the minimized flag) in a per-user
cookie ("activities" + encoded username). Cookies are sent on every
request, and the list only shrank on manual dismissal: start() added an
entry but stopPoll() never removed finished ones, so the cookie grew with
normal use. Stacked on the ~6 KB ALB auth cookie, it pushed request
headers past Tomcat's maxHttpHeaderSize (default 8192), and Tomcat then
rejected every request with HTTP 400 ("Request header is too large",
thrown from Http11InputBuffer while parsing headers) until cookies were
cleared.
Nothing server-side reads this cookie; it exists only so the panel can
repopulate and resume polling across page navigations, which localStorage
does without riding the request headers. Swap the four persistence calls
(get/set/remove) to localStorage, keeping the same key and JSON shape.
Completed tasks are intentionally still kept so they remain reviewable
after a reload. No legacy-cookie migration is included: that cookie has
no expiry set, so it self-clears on browser close and stops growing as
soon as this ships.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Moves the Activity Tab’s persisted state (task list + minimized flag) from per-user cookies to localStorage to prevent oversized request headers triggering Tomcat maxHttpHeaderSize 400 errors.
Changes:
- Replace
XNAT.cookie.get/set/removeusage withlocalStorage.getItem/setItem/removeItem. - Introduce a small
saveActivities()helper for persistence. - Rename the persisted key variable from
cookieTagtostorageTagwhile keeping the same key prefix (activities<user>).
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| function getActivities() { | ||
| return JSON.parse(XNAT.cookie.get(cookieTag) || '{"' + minimized + '": false, "' + processes + '": {}}'); | ||
| return JSON.parse(localStorage.getItem(storageTag) || '{"' + minimized + '": false, "' + processes + '": {}}'); | ||
| } |
There was a problem hiding this comment.
Going to leave this as-is. XNAT already assumes localStorage is available — storage.js writes to it on page load with no guard — so if storage were actually blocked, the app would break well before it got here. Adding a guard just in this spot would be inconsistent with the rest of the app.
| function saveActivities(activities) { | ||
| localStorage.setItem(storageTag, JSON.stringify(activities)); | ||
| } |
There was a problem hiding this comment.
Same thinking as the read above — we already use localStorage elsewhere without guarding, and the data here is tiny so quota isn't a realistic concern. Leaving it for consistency.
| } | ||
| } | ||
| if ($.isEmptyObject(activities[processes])) { | ||
| XNAT.cookie.remove(cookieTag); | ||
| localStorage.removeItem(storageTag); | ||
| const tab = $('#activity-tab'); |
There was a problem hiding this comment.
I think this one's a false alarm — for...in over undefined doesn't throw, it just returns true, so the close-all case works fine. It's also pre-existing behavior that this PR didn't change. Leaving as-is.
What this fixes
The Activity Tab (
XNAT.app.activityTab) stored its task list in a per-user cookie that grew with normal use and never shrank on its own. Cookies go out on every request, so once that list got big enough, request headers blew past Tomcat's 8 KBmaxHttpHeaderSizeand Tomcat started returning 400 on every request.This change moves the panel's state to localStorage, which does the same job (repopulate the panel and resume polling after a navigation) without ever riding the request headers. This fixes #6.
The change
Swap the four persistence calls to localStorage, keeping the same key and JSON shape:
Out of scope
Testing
node --checkpasses and nocookiereferences remain in the file (done).activities<user>with noactivities*cookie or request headerRisk
Low. Client-only change, no server or API impact, no data migration.