You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
HTTP Error 403: Forbidden if user agent starts with Python-urllib #39374
The OpenCode Go server drops chat completion requests if the user agent HTTP header field starts with Python-urllib. This check is done before the API key is checked, which might narrow down your search for the source of this behavior.
Code to reproduce
importos, json, urllib.requestapi_key="sk-your-api-key-here"url="https://opencode.ai/zen/go/v1/chat/completions"data=json.dumps({
"model": "deepseek-v4-flash",
"messages": [{"role": "user", "content": "List 10 digits of pi."}],
}).encode("utf-8")
headers= {
"Content-Type": "application/json",
"Authorization": "Bearer "+api_key,
"User-Agent": "Python-urllib", # Change user agent to anything else (e.g. "asdf") and it begins to work"x-opencode-session": "tihsllubdiputs",
}
request=urllib.request.Request(url, data=data, headers=headers, method="POST")
withurllib.request.urlopen(request) asr:
print(json.load(r))
Running this code will result in urllib.error.HTTPError: HTTP Error 403: Forbidden.
If you change the User-Agent to anything else, e.g. asdf, the error will be urllib.error.HTTPError: HTTP Error 401: Unauthorized.
If you also set api_key to a valid OpenCode Go key, the server will respond correctly.
Result: 403, body error code: 1010 (Cloudflare). Switching only the User-Agent header to a normal browser string (everything else identical — same key, same body, same endpoint) makes it return 200 immediately. So it's exactly what's described here: the block triggers purely on User-Agent starting with Python-urllib, before auth is even checked.
This also affects any client built on Python's default HTTP stack (I hit it via the Hermes Agent CLI, which uses openai/httpx under the hood) — not just raw urllib. Worked around it for now with a tiny local reverse proxy that only rewrites User-Agent, but it'd be great to get a fix upstream since this silently breaks any Python-based integration against the Zen/Go API.
Adding data from a different client and endpoint coverage, since this is still costing us a working setup.
Reproduced with plain curl — only the -A value changes between cases, so the TLS stack, endpoint, and body are all identical. This rules out a JA3/JA4 fingerprint rule:
User-Agent
/zen
/inference
Python-urllib/3.10
403
403
Python-urllib/3.11
403
403
python-requests/2.32.3
200
200
Go-http-client/1.1
200
200
okhttp/4.9.0
200
200
axios/1.7.0
200
200
curl/8.14.1
200
200
Mozilla/5.0
200
200
Five points that seemed worth adding:
The rule is a narrow literal blocklist, not a bot heuristic. Every other
non-browser client I tried — including python-requests, which is Python —
is allowed. So it appears to match on the Python-urllib prefix rather than
"not a browser". That may be narrower than intended, since Python-urllib is
the stdlib default and not a meaningful bot signal on its own.
The GET /zen/v1/models endpoint is affected too, not just chat/completions. A client therefore cannot even enumerate the available
models before hitting the block:
The /inference/* family behaves the same as /zen/* — confirmed for /inference/openai/v1/chat/completions. The earlier reports cover /zen/v1/ and /zen/go/v1/, so this widens it to a third path family.
The "just change the User-Agent" workaround doesn't apply to a lot of
clients. It assumes the client sets the header. The case that hit us is KiCad-AI-Assistant (a KiCad plugin), which builds its headers in _openai_headers() / _anthropic_headers() and never sets a User-Agent
at all — so urllib supplies the blocked default and there is no config
knob to change. Fixing it required patching the library, which will be lost
on the next plugin update. Any third-party tool built the same way is
affected, and none of them can work around it themselves.
The failure is silent for those clients. The edge returns a plain-text error code: 1010 with no JSON body and no hint that a header is
responsible. It surfaces as a bare HTTP Error 403: Forbidden, which is
easy to misread as a bad API key. Returning a JSON error body on API paths
would at least make it distinguishable from an application-level 401/403.
For reference, with a browser-style User-Agent the same client works
completely — including multi-turn tool calling against a custom https://opencode.ai/zen/v1 endpoint.
Happy to provide a minimal curl script or a Python repro if that's easier to
test than the curl loop above.
Thanks for the clear report and the minimal repro, @99991!
This is about the hosted OpenCode Go service, not the OpenCode app in this repository. The Zen/Go handler code here doesn't look at the User-Agent header, and the 403 comes back before the API key is checked. So the block is most likely in the service's edge or bot-protection layer, which many setups configure to reject default script user agents like Python-urllib. That isn't something we can change through a code change in this repo.
Problems with OpenCode Go and the OpenCode Console/Zen services aren't handled in GitHub issues. Please email [email protected] with the details (the endpoint, the User-Agent that triggers it, and roughly when you tested), so the team can look at the service configuration. If you've already emailed, the team will get to it, especially now that we've seen it here. Please keep API keys and account details out of this public issue and put them in the email instead.
In the meantime, the workaround you found works: set a custom User-Agent (for example my-app/1.0) instead of the urllib default.
Closing this now, @99991. The 403 comes from the hosted OpenCode Go service, not the code in this repo, so it's handled by email rather than in GitHub issues. If it still matters to you, please email [email protected] with the details, and keep API keys and account information out of this public issue. Until then, setting a custom User-Agent should get you past it. Thanks again for the clear report and repro!
Activity
clashbrunocibar-sudo commentedon Jul 31, 2026
Can confirm this independently — reproduced it in isolation with plain
urllib.request(no SDK, no third-party libs at all):Result:
403, bodyerror code: 1010(Cloudflare). Switching only theUser-Agentheader to a normal browser string (everything else identical — same key, same body, same endpoint) makes it return200immediately. So it's exactly what's described here: the block triggers purely onUser-Agentstarting withPython-urllib, before auth is even checked.This also affects any client built on Python's default HTTP stack (I hit it via the Hermes Agent CLI, which uses
openai/httpxunder the hood) — not just rawurllib. Worked around it for now with a tiny local reverse proxy that only rewritesUser-Agent, but it'd be great to get a fix upstream since this silently breaks any Python-based integration against the Zen/Go API.simonbalog commentedon Sep 27, 2026
Adding data from a different client and endpoint coverage, since this is still costing us a working setup.
Reproduced with plain
curl— only the-Avalue changes between cases, so the TLS stack, endpoint, and body are all identical. This rules out a JA3/JA4 fingerprint rule:User-Agent/zen/inferencePython-urllib/3.10Python-urllib/3.11python-requests/2.32.3Go-http-client/1.1okhttp/4.9.0axios/1.7.0curl/8.14.1Mozilla/5.0Five points that seemed worth adding:
The rule is a narrow literal blocklist, not a bot heuristic. Every other
non-browser client I tried — including
python-requests, which is Python —is allowed. So it appears to match on the
Python-urllibprefix rather than"not a browser". That may be narrower than intended, since
Python-urllibisthe stdlib default and not a meaningful bot signal on its own.
The
GET /zen/v1/modelsendpoint is affected too, not justchat/completions. A client therefore cannot even enumerate the availablemodels before hitting the block:
The
/inference/*family behaves the same as/zen/*— confirmed for/inference/openai/v1/chat/completions. The earlier reports cover/zen/v1/and/zen/go/v1/, so this widens it to a third path family.The "just change the User-Agent" workaround doesn't apply to a lot of
clients. It assumes the client sets the header. The case that hit us is
KiCad-AI-Assistant(a KiCad plugin), which builds its headers in_openai_headers()/_anthropic_headers()and never sets aUser-Agentat all — so
urllibsupplies the blocked default and there is no configknob to change. Fixing it required patching the library, which will be lost
on the next plugin update. Any third-party tool built the same way is
affected, and none of them can work around it themselves.
The failure is silent for those clients. The edge returns a plain-text
error code: 1010with no JSON body and no hint that a header isresponsible. It surfaces as a bare
HTTP Error 403: Forbidden, which iseasy to misread as a bad API key. Returning a JSON error body on API paths
would at least make it distinguishable from an application-level 401/403.
For reference, with a browser-style
User-Agentthe same client workscompletely — including multi-turn tool calling against a custom
https://opencode.ai/zen/v1endpoint.Happy to provide a minimal
curlscript or a Python repro if that's easier totest than the
curlloop above.99991 commentedon Sep 27, 2026
Here is a workaround.
opencodejr commentedon Oct 28, 2026
Thanks for the clear report and the minimal repro, @99991!
This is about the hosted OpenCode Go service, not the OpenCode app in this repository. The Zen/Go handler code here doesn't look at the
User-Agentheader, and the 403 comes back before the API key is checked. So the block is most likely in the service's edge or bot-protection layer, which many setups configure to reject default script user agents likePython-urllib. That isn't something we can change through a code change in this repo.Problems with OpenCode Go and the OpenCode Console/Zen services aren't handled in GitHub issues. Please email [email protected] with the details (the endpoint, the
User-Agentthat triggers it, and roughly when you tested), so the team can look at the service configuration. If you've already emailed, the team will get to it, especially now that we've seen it here. Please keep API keys and account details out of this public issue and put them in the email instead.In the meantime, the workaround you found works: set a custom
User-Agent(for examplemy-app/1.0) instead of the urllib default.The bot will close this issue in 1 day.
opencodejr commentedon Oct 29, 2026
Closing this now, @99991. The 403 comes from the hosted OpenCode Go service, not the code in this repo, so it's handled by email rather than in GitHub issues. If it still matters to you, please email [email protected] with the details, and keep API keys and account information out of this public issue. Until then, setting a custom
User-Agentshould get you past it. Thanks again for the clear report and repro!