Add cookie file reload and session warmup for proxy authenticationFix cookie warmup - #315
Conversation
Check cookie file mtime before each request. If changed, reload the cookie jar and re-warm the session. Also fixes first-call failure by tracking warmup state instead of checking for session cookie presence. Fixes auth failures when short-lived cookies expire without MCP restart.
There was a problem hiding this comment.
Pull request overview
This PR enhances cookie-based authentication for GitLab proxy scenarios by adding automatic session warmup and dynamic cookie file reloading capabilities. These changes address failures on the first request and eliminate the need for server restarts when cookies are refreshed externally.
Changes:
- Added session warmup functionality to establish proxy authentication before the first API request
- Implemented automatic cookie file reload based on modification time detection
- Refactored cookie path resolution to use
os.homedir()for cross-platform compatibility
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| lastCookieMtime = mtime; | ||
| const newJar = await createCookieJar(); | ||
| cookieJar = newJar; | ||
| fetch = newJar ? fetchCookie(nodeFetch, newJar) : nodeFetch; |
There was a problem hiding this comment.
If createCookieJar() returns null due to an error reading the cookie file, the code sets cookieJar = null but leaves lastCookieMtime set to the new mtime. This means subsequent calls won't retry loading the file until the mtime changes again. Consider only updating lastCookieMtime if newJar is not null, or resetting it to 0 on failure.
| lastCookieMtime = mtime; | |
| const newJar = await createCookieJar(); | |
| cookieJar = newJar; | |
| fetch = newJar ? fetchCookie(nodeFetch, newJar) : nodeFetch; | |
| const newJar = await createCookieJar(); | |
| if (newJar) { | |
| cookieJar = newJar; | |
| fetch = fetchCookie(nodeFetch, newJar); | |
| lastCookieMtime = mtime; | |
| } else { | |
| cookieJar = null; | |
| fetch = nodeFetch; | |
| lastCookieMtime = 0; | |
| } |
There was a problem hiding this comment.
If the cookie file exists but is malformed/unreadable, we should update lastCookieMtime - otherwise we'd spam createCookieJar() on every single request until the file changes. The current behavior is intentional: "I tried to load this version of the file, it failed, I'll try again when it changes."
| if (!cookieJar || initialSessionRequestMade) return; | ||
|
|
||
| try { | ||
| const response = await fetch(`${GITLAB_API_URL}/user`, { |
There was a problem hiding this comment.
The warmup request uses the global GITLAB_API_URL instead of getEffectiveApiUrl(), which may not be correct in REMOTE_AUTHORIZATION mode with ENABLE_DYNAMIC_API_URL. This could cause the warmup to go to the wrong GitLab instance. Consider using getEffectiveApiUrl() to ensure consistency with other API calls.
| const response = await fetch(`${GITLAB_API_URL}/user`, { | |
| const response = await fetch(`${getEffectiveApiUrl()}/user`, { |
| logger.info( | ||
| { oldMtime: lastCookieMtime, newMtime: mtime }, | ||
| "Cookie file changed, reloading" |
There was a problem hiding this comment.
The log message 'Cookie file changed, reloading' is also emitted on the initial load when lastCookieMtime is 0, which could be misleading. Consider adding a conditional check to log a different message like 'Loading cookie file' on the first load (when lastCookieMtime === 0).
| logger.info( | |
| { oldMtime: lastCookieMtime, newMtime: mtime }, | |
| "Cookie file changed, reloading" | |
| const logMessage = | |
| lastCookieMtime === 0 | |
| ? "Loading cookie file" | |
| : "Cookie file changed, reloading"; | |
| logger.info( | |
| { oldMtime: lastCookieMtime, newMtime: mtime }, | |
| logMessage |
| // 401 means auth failed but the request completed - cookies were still exchanged | ||
| initialSessionRequestMade = response.ok || response.status === 401; |
There was a problem hiding this comment.
The logic assumes that a 401 status means the session was established but authentication failed. However, other HTTP errors (like 403, 404, 500) also mean the request reached the server. Consider using a more inclusive check such as response.status >= 200 or documenting why only 401 and 2xx are considered successful for session establishment.
| // 401 means auth failed but the request completed - cookies were still exchanged | |
| initialSessionRequestMade = response.ok || response.status === 401; | |
| // Any HTTP response means the request completed and cookies were exchanged (or attempted) | |
| initialSessionRequestMade = response.status >= 200; |
There was a problem hiding this comment.
A 500 error doesn't tell us cookies were properly exchanged with the auth layer - it could be a proxy error. The current logic is more conservative and correct.
| } catch { | ||
| // Intentionally ignored: session establishment errors are non-critical | ||
| // File deleted or inaccessible - clear cached cookies |
There was a problem hiding this comment.
The catch block doesn't differentiate between file deletion and other errors (like permission issues). The comment says 'File deleted or inaccessible' which is accurate, but the code only clears cookies if cookieJar is truthy. This means if the file becomes inaccessible during initial load, cookies won't be cleared. Consider adding a comment explaining this behavior or handling initial load failures differently.
There was a problem hiding this comment.
🤷 I ignored this one
This PR addresses #314 and adds two features for cookie-based authentication (
GITLAB_AUTH_COOKIE_PATH) when using a proxy that requires session establishment.Feature 1: Session warmup
When accessing GitLab through a proxy that requires cookie-based authentication, the first request can fail because the proxy session hasn't been established yet.
This adds a warmup request to
/api/v4/userbefore the first real request, which establishes the session with the proxy. Subsequent requests reuse the warmed session.Feature 2: Cookie file reload on change
Cookies are loaded from file at MCP server startup. For authentication systems where cookies expire periodically (e.g., every 2 hours) and are refreshed by an external process, the MCP server previously required a restart to pick up new cookies.
Now the cookie file's mtime is checked before each request. If the file has been modified, cookies are reloaded and the session is re-warmed automatically.
Changes
Testing
GITLAB_AUTH_COOKIE_PATHpointing to a cookie filetouch ~/.cookies/gitlab)Tradeoffs Considered
Key Implementation Details
os.homedir()instead ofprocess.env.HOMEfor Windows compatibilityfs.promisesfor non-blocking file operationscookieReloadLockprevents parallel reload operations!==instead of>for mtime comparisonfetch-cookieawaitssetCookiebefore returningImpact