fix: return 400 instead of 500 for malformed timeperiods in /api/0/query/#163
fix: return 400 instead of 500 for malformed timeperiods in /api/0/query/#163TimeToBuildBob wants to merge 2 commits into
Conversation
Malformed timeperiods (missing slash separator, or non-ISO8601 datetimes) previously caused uncaught iso8601.ParseError/IndexError, surfacing as HTTP 500 Internal Server Error. Now raises QueryException in both cases, which the REST layer maps to 400. Adds three regression tests. Fixes: query2() with timeperiods like 'not-a-valid-period' or 'noSlash' returning 500 instead of a proper client error.
Greptile SummaryThis PR fixes
Confidence Score: 5/5The change is a well-scoped error-handling addition; the happy path is unchanged and the new guards only activate on malformed input that previously crashed the server. Both new guard paths are narrow and self-contained: they raise an exception type the REST layer already catches and maps to 400. The existing happy-path test confirms no regression for valid timeperiods. The only gap is that the iso8601.ParseError branch lacks a direct test, but this was noted in the existing review thread and does not affect runtime correctness of the fix. No files require special attention; the changes are limited to the timeperiod-parsing block in query2 and its tests. Important Files Changed
Sequence DiagramsequenceDiagram
participant Client
participant REST as rest.py /api/0/query/
participant API as ServerAPI.query2
participant iso8601
Client->>REST: "POST /api/0/query/ {timeperiods: [...]}"
REST->>API: query2(name, query, timeperiods, cache)
loop for each timeperiod
API->>API: "period = timeperiod.split("/")[:2]"
alt "len(period) != 2 (no slash)"
API-->>REST: raise QueryException("missing slash")
REST-->>Client: 400 Bad Request
else "len(period) == 2"
API->>iso8601: parse_date(period[0]), parse_date(period[1])
alt ParseError
iso8601-->>API: iso8601.ParseError
API-->>REST: raise QueryException("invalid ISO8601")
REST-->>Client: 400 Bad Request
else valid dates
iso8601-->>API: starttime, endtime
API->>API: query2.query(...)
API-->>REST: result
REST-->>Client: 200 OK + result
end
end
end
Reviews (3): Last reviewed commit: "style(api): format invalid timeperiod er..." | Re-trigger Greptile |
| def test_query_invalid_timeperiod(flask_client): | ||
| """Malformed timeperiods must yield 400 (client error), not 500. | ||
|
|
||
| Regression test: a non-ISO8601 timeperiod previously raised an uncaught | ||
| iso8601.ParseError, surfacing as an Internal Server Error. | ||
| """ | ||
| r = flask_client.post( | ||
| "/api/0/query/", | ||
| json={"query": ["RETURN = 1;"], "timeperiods": ["not-a-valid-period"]}, | ||
| ) | ||
| assert r.status_code == 400 | ||
| assert "not-a-valid-period" in r.json["message"] |
There was a problem hiding this comment.
iso8601.ParseError handler is untested
"not-a-valid-period" contains no slash, so it is caught by the new len(period) != 2 guard before the try/except iso8601.ParseError block is ever reached. The actual except iso8601.ParseError branch — triggered by inputs like "not-valid/also-not-valid" (two slash-separated parts, neither parseable as ISO 8601) — has no test covering it. If that handler were accidentally removed or malformed, the existing suite would still pass.
dcad71f to
b561078
Compare
Found via dogfooding — using the live aw-server API surfaced a real robustness bug.
Problem
POST /api/0/query/with a malformedtimeperiodsentry returns HTTP 500 Internal Server Error instead of 400 Bad Request.Two failure modes:
"not-a-valid-period") → uncaughtiso8601.ParseError→ 500/separator → uncaughtIndexError→ 500For comparison: empty body → correct 400, bad bucket → correct 404. Only the timeperiod path 500s on client input.
Fix
Catch parse errors in
ServerAPI.query2and raiseQueryException, which the endpoint already maps to 400.Tests
Three regression tests added:
test_query_invalid_timeperiod— non-ISO8601 string → 400test_query_timeperiod_missing_slash— single datetime → 400test_query_valid_timeperiod— well-formed query still succeeds (200)Context
Discovered during dogfooding as a work-supply pipeline candidate (ErikBjare/bob#745). The ActivityWatch surface was explicitly called out as a dogfooding lane worth exercising.