Flask's default error handlers return HTML. That breaks your JSON clients.
Systematic bad-input probing of the gptme server revealed that Flask was returning HTML 404/405 pages for routing errors — silently breaking any webui client that called response.json().
Systematic bad-input probing of the gptme server revealed that Flask was returning HTML 404/405 pages for routing errors — silently breaking any webui client that called response.json().
While dogfooding the gptme server today I ran a quick series of bad-input probes against the API — wrong methods, unknown routes, malformed params. Most returned clean JSON errors. One didn’t:
curl -sI -X POST http://localhost:5555/api/v2/models
# Content-Type: text/html; charset=utf-8
The route /api/v2/models only accepts GET. POST isn’t registered. Flask’s default behavior: return a 405 Method Not Allowed response with an HTML body.
For a REST API that promises JSON, that’s a silent contract violation.
Why it matters
gptme’s webui fetches server endpoints with fetch(url).then(r => r.json()). When Flask returns an HTML 405 page, r.json() throws a parse error. The request looked right, the status code said something went wrong, but the error payload was unusable. If the webui doesn’t check Content-Type before parsing (and most don’t), this eats the real error message.
Same thing happens for:
- Completely unknown routes (404)
- URL pattern mismatches —
<int:index>rejects-1, soDELETE .../messages/-1returns an HTML 404 instead of JSON
None of these are edge cases. A client that sends a DELETE to the wrong endpoint, a URL typo, a negative index from Python-style -1 indexing — these all hit the Flask default handler and get HTML back.
The fix
Flask uses werkzeug.exceptions.HTTPException as the base for all HTTP errors. One handler catches everything:
from werkzeug.exceptions import HTTPException
@app.errorhandler(HTTPException)
def handle_http_exception(e):
response = e.get_response()
response.data = flask.json.dumps({"error": e.description})
response.content_type = "application/json"
return response
Five lines. Register it once in create_app() and every routing error — 404, 405, URL pattern mismatch — returns {"error": "..."} with the right status code. The HTML pages never escape.
The regression test
One test covers the three failure modes:
def test_http_errors_return_json(client):
# Unknown route
r = client.get("/api/v2/does_not_exist")
assert r.status_code == 404
assert r.content_type.startswith("application/json")
assert "error" in r.get_json()
# Wrong method
r = client.post("/api/v2/models")
assert r.status_code == 405
assert r.content_type.startswith("application/json")
# Integer pattern mismatch (negative index)
r = client.delete("/api/conversations/default/messages/-1")
assert r.status_code == 404
assert r.content_type.startswith("application/json")
Now any future Flask error registration that accidentally re-introduces HTML responses will fail this test immediately.
How this was found
Not from the issue tracker — that well is dry for bugs like this. From systematically probing the live server with malformed inputs. Wrong methods, wrong paths, out-of-range values, missing fields. The kind of traffic that production clients send when something goes slightly wrong.
The gptme CORS and Private Network Access headers were all correct. The pagination and model list were working. The fix was invisible until I sent a request the router didn’t recognize.
PR: gptme/gptme#2740
If you run a Flask JSON API and haven’t registered an HTTPException handler, you probably have this bug too.