API sources: header auth sends the stored credential object as the header value
Craft Agents Version
0.13.3
Operating System
macOS (Apple Silicon)
OS Version
macOS 27.0
AI Provider
Anthropic API (direct)
Model
Claude Opus 5 (the bug is independent of provider and model)
Description
An API source configured with authType: "header" puts the stored credential on the wire exactly as it was stored, without unpacking it. When the credential was stored in the multi-header format, which is a JSON object, the whole object becomes the header value. The receiving API sees a JSON blob where a key should be, and rejects the request.
The headerNames feature documented in docs/sources.md is implemented in the credential prompt and in source_test, but it is absent from the code that builds requests.
In resources/bridge-mcp-server/index.js, buildHeaders:
} else if (config.authType === "header") {
headers[config.headerName || "X-API-Key"] = credential;
}credential is assigned straight through, whatever shape it has. The string headerNames does not appear anywhere in that file, against twenty occurrences in dist/main.cjs.
The basic branch five lines below already does the thing that is missing:
} else if (config.authType === "basic") {
let authString = credential;
try {
const parsed = JSON.parse(credential);
if (parsed?.username && parsed?.password) authString = `${parsed.username}:${parsed.password}`;
} catch {}So the pattern exists in the same function. It was written for basic and never for header.
There is a second problem that cost me far more time than the first. source_test reports 200 OK, authenticated on a configuration where every real call fails, because the validator and the request path do not share credential assembly. A source shows a green tick in the sources list and is completely unusable. I would rank fixing that divergence above fixing the header path, because the divergence is what hides every future bug of this class.
Steps to Reproduce
- Create an API source with
authType: "header"and aheaderName, then authenticate it. - Run
source_test. It passes. - Call any endpoint through the generated
api_<source>tool. It fails with an authentication error. - Capture the outgoing request to see the header value.
I hit this on a Google Gemini source. A local proxy recorded what the app sent:
x-goog-api-key: {"x-goog-api-key":"<53 character key>"}That value is 74 characters: nineteen for {"x-goog-api-key":", fifty three for the key, two for the closing "}. Google returns 400 API_KEY_INVALID. The same key sent by hand as a bare header value returns 200.
To rule out a stale or wrong key, I compared SHA-256 of the captured header against SHA-256 of the JSON-wrapped keychain key. They matched exactly, so the key was correct throughout and only its packaging was wrong.
Expected Behavior
The header named in the config carries the credential value. With headerNames, each named header carries its own value, as docs/sources.md describes.
source_test passing should mean the source can actually make calls.
Actual Behavior
One header carries the serialised credential object, so the API rejects the request.
source_test returns 200 and reports the source as authenticated on exactly that broken configuration.
Debug Logs
400 {
"error": {
"code": 400,
"message": "API key not valid. Please pass a valid API key.",
"status": "INVALID_ARGUMENT",
"details": [{
"@type": "type.googleapis.com/google.rpc.ErrorInfo",
"reason": "API_KEY_INVALID",
"domain": "googleapis.com",
"metadata": { "service": "generativelanguage.googleapis.com" }
}]
}
}Additional Context
Suggested fix: in buildHeaders, mirror the basic branch. Parse the credential, and if it is an object, set each key as its own header. Fall back to current behaviour when it is a plain string, so existing single-header sources keep working. Then make source_test call through the same path it validates.
Workaround for anyone else hitting this, where the API supports it. Switch the source to query-parameter auth, since buildUrl handles the credential correctly:
"authType": "query",
"queryParam": "key"Re-enter the credential after changing this so it is stored as a bare value. My Gemini source works normally on that configuration.
Note on error codes, which may help triage similar reports. With no credential attached Google returns 403, and with a malformed one it returns 400. The 400 is indistinguishable from a genuinely invalid key, so this bug presents to the user as "my API key does not work" and sends them off to rotate keys that were never broken.
Source: craft-ai-agents/craft-agents-oss