Skip to content

fix: return 400 instead of 500 for malformed timeperiods in /api/0/query/#163

Open
TimeToBuildBob wants to merge 2 commits into
ActivityWatch:masterfrom
TimeToBuildBob:fix/query-timeperiod-400
Open

fix: return 400 instead of 500 for malformed timeperiods in /api/0/query/#163
TimeToBuildBob wants to merge 2 commits into
ActivityWatch:masterfrom
TimeToBuildBob:fix/query-timeperiod-400

Conversation

@TimeToBuildBob
Copy link
Copy Markdown
Contributor

Found via dogfooding — using the live aw-server API surfaced a real robustness bug.

Problem

POST /api/0/query/ with a malformed timeperiods entry returns HTTP 500 Internal Server Error instead of 400 Bad Request.

Two failure modes:

  • Non-ISO8601 string (e.g. "not-a-valid-period") → uncaught iso8601.ParseError → 500
  • Single datetime without / separator → uncaught IndexError → 500

For comparison: empty body → correct 400, bad bucket → correct 404. Only the timeperiod path 500s on client input.

Fix

Catch parse errors in ServerAPI.query2 and raise QueryException, which the endpoint already maps to 400.

Tests

Three regression tests added:

  • test_query_invalid_timeperiod — non-ISO8601 string → 400
  • test_query_timeperiod_missing_slash — single datetime → 400
  • test_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.

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-apps
Copy link
Copy Markdown

greptile-apps Bot commented May 25, 2026

Greptile Summary

This PR fixes POST /api/0/query/ returning HTTP 500 instead of 400 when timeperiods entries are malformed. The fix wraps the two failure modes (missing slash separator and unparseable ISO8601 strings) in explicit guards that raise QueryException, which rest.py already maps to a 400 response.

  • aw_server/api.py: Adds a len(period) != 2 guard after the /-split and wraps iso8601.parse_date calls in a try/except iso8601.ParseError, both raising QueryException with descriptive messages.
  • tests/test_server.py: Adds three regression tests covering the no-slash path, an invalid string path, and a happy-path sanity check.

Confidence Score: 5/5

The 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

Filename Overview
aw_server/api.py Adds a missing-slash guard and iso8601.ParseError catch in query2; both correctly raise QueryException which the REST layer maps to 400.
tests/test_server.py Three new regression tests; the iso8601.ParseError branch in api.py is not directly exercised by any test (noted in prior review thread).

Sequence Diagram

sequenceDiagram
    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
Loading

Reviews (3): Last reviewed commit: "style(api): format invalid timeperiod er..." | Re-trigger Greptile

Comment thread tests/test_server.py
Comment on lines +94 to +105
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"]
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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.

@TimeToBuildBob TimeToBuildBob force-pushed the fix/query-timeperiod-400 branch from dcad71f to b561078 Compare May 25, 2026 07:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant