I was running a small Node benchmark script against a few hosted LLM APIs from a Windows machine. One provider failed with a bare fetch failed, which at least looked like a network problem. Groq did something more convincing. It answered, with a status code and a JSON body:
HTTP 403
{"error":{"message":"Forbidden"}}
A 403 from an API that has your key reads like "your key is not allowed to do this". Revoked, suspended, wrong project, account flagged. What stopped me from treating it that way was one boring test.
Three requests, one answer
If the 403 is about the key, then changing the key should change the answer. So I sent the same request three ways:
- with my real key
- with a key I made up (
gsk_plus random characters) - with no
Authorizationheader at all
All three came back identical: 403, {"error":{"message":"Forbidden"}}.
That settles more than it looks like. A server that has actually looked at your credentials cannot give the same verdict to a valid key, a fabricated one and a missing one. Whatever said "Forbidden" made its decision before anyone checked the key. The key was never the problem, so rotating it, regenerating it or emailing support about it would all have been wasted.
What a real auth failure looks like
For contrast, here is the same endpoint today, from a network path that works, with a fake key and then with no key at all:
curl -s -w "\n%{http_code}\n" https://api.groq.com/openai/v1/models \
-H "Authorization: Bearer gsk_fake123"
curl -s -w "\n%{http_code}\n" https://api.groq.com/openai/v1/models
Both return:
{"error":{"message":"Invalid API Key","type":"invalid_request_error","code":"invalid_api_key"}}
401
Two differences from the 403, and either one is enough to tell them apart:
- The status. A missing or wrong key gets 401. A 403 for a request that carried no credentials at all doesn't mean "this key lacks permission", because there was no key to judge.
-
The body shape. The real auth error has
typeandcodefields, the same shape as the API's other errors. The 403 body had onlymessage. When an error doesn't look like the rest of the API's errors, it probably didn't come from the API's application code.
I can't reproduce the 403 any more because the network path it came from is gone. Where exactly it was produced, I can't say for certain: something between my machine and the key check, whether a network hop or an edge policy that looks at where a request comes from. What I can say is that it was upstream of authentication, and the three-key test is what proved it.
Why curl worked and Node didn't
The second half of this was a red herring of my own making. The machine had a proxy configured through HTTPS_PROXY, and curl through that proxy reached everything fine. So I assumed my script was taking the same path. It wasn't.
Node's built-in fetch (undici) does not read HTTP_PROXY / HTTPS_PROXY by default. The script inherited the variables and ignored them, so its requests went out directly, which is where the 403 and the fetch failed both came from. One provider answering normally over that same direct path made it look provider-specific instead of path-specific, which sent me further in the wrong direction.
On Node 24 there's an opt-in for this:
NODE_USE_ENV_PROXY=1 HTTPS_PROXY=http://127.0.0.1:PORT node script.mjs
With that set, the same script got normal answers from every provider, including Groq with the same key that had been "forbidden" before.
The checklist I use now
When an API rejects a key that I believe is good:
-
Send the request with no
Authorizationheader. If you get the same status and body as with your key, the key isn't being evaluated. Stop debugging the key. - Compare the error body's shape with the API's documented errors. Missing fields usually means a different layer wrote it.
-
Make sure the tool that fails takes the same path as the tool that works.
curlhonouringHTTPS_PROXYsays nothing about Node'sfetch, Python'shttpxwithtrust_env=False, or anything else with its own idea of proxies.
Step 1 takes ten seconds, and it's the one I skipped. I'm curious whether other people have a "this error isn't from where it claims to be" test they reach for first, because the general problem (an error message that blames the wrong layer) seems to come up everywhere, not just with API keys.
Top comments (1)
Small qualification to the diagnostic rule: identical 403 responses with real, fake and missing keys suggest a shared rejection path, but alone do not prove authentication was never evaluated. RFC 9110 §15.5.4 allows both credential-related and unrelated refusals. Your same-key success after changing the proxy path is stronger evidence for this incident. A provider request ID correlated with authorized server logs could narrow the rejecting layer further, without posting keys or full headers. I haven't reproduced the setup.
AI-assisted reply; I work on a VPN service.