Skip to content

fix: set X-Metabase-Session header on current request in interceptor for username/password auth#32

Open
KrasnovidKE wants to merge 1 commit intoCognitionAI:mainfrom
KrasnovidKE:fix/session-header-first-request
Open

fix: set X-Metabase-Session header on current request in interceptor for username/password auth#32
KrasnovidKE wants to merge 1 commit intoCognitionAI:mainfrom
KrasnovidKE:fix/session-header-first-request

Conversation

@KrasnovidKE
Copy link
Copy Markdown

Problem

When using username/password authentication (i.e. METABASE_USERNAME + METABASE_PASSWORD without METABASE_API_KEY), every API call fails with HTTP 401 on the first invocation of any MCP tool.

Root cause

MetabaseClient uses an axios request interceptor to lazily authenticate before each request:

this.axiosInstance.interceptors.request.use(async (config) => {
    if (config.url === "/api/session") {
        return config;
    }
    await this.ensureAuthenticated();   // fetches token, sets defaults.headers.common
    return config;                       // ← config.headers is already merged, no session header
});

After ensureAuthenticated() resolves, the session token is written to this.axiosInstance.defaults.headers.common["X-Metabase-Session"].

However, axios merges defaults.headers.common into the per-request config.headers object before request interceptors run — not after. This means that by the time the interceptor sets the default header, the current request's header map is already frozen and the X-Metabase-Session header is absent. The request is dispatched unauthenticated → 401.

Subsequent calls succeed because defaults.headers.common is already populated from the previous run, so the merge at the start of the next request picks it up correctly.

The METABASE_API_KEY path is unaffected because the header is set in the constructor (before any request is created):

if (config.apiKey) {
    this.axiosInstance.defaults.headers.common["X-API-Key"] = config.apiKey;
    this.sessionToken = "api_key_used";
}

Fix

After ensureAuthenticated() resolves, explicitly assign the session token directly onto the current request's config.headers object:

await this.ensureAuthenticated();
if (this.sessionToken && this.sessionToken !== "api_key_used") {
    config.headers["X-Metabase-Session"] = this.sessionToken;
}
return config;

This guarantees the header is present on the very first request regardless of when axios performs header merging internally.


Testing

Reproduced and verified against a self-hosted Metabase instance:

Scenario Before fix After fix
First tool call with username/password 401 Unauthorized 200 OK
Subsequent calls with username/password 200 OK 200 OK
API key auth 200 OK 200 OK (unaffected)

Affected file

src/client/metabase-client.ts — request interceptor inside the MetabaseClient constructor.

…for username/password auth

When using username/password auth, the session token was written to
defaults.headers.common inside the request interceptor. However, axios
merges defaults into the per-request config.headers before interceptors
run, so the first request was always dispatched without the
X-Metabase-Session header, resulting in HTTP 401.

Fix: explicitly assign the token to config.headers after
ensureAuthenticated() resolves, ensuring it is present on the very
first request.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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.

1 participant