From b561078edb0ce0a0ff5409015d0ab4ec9acb121e Mon Sep 17 00:00:00 2001 From: Bob Date: Mon, 25 May 2026 07:42:06 +0000 Subject: [PATCH 1/2] fix(api): return 400 on invalid timeperiod format instead of 500 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. --- aw_server/api.py | 15 +++++++++++++-- tests/test_server.py | 40 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 2 deletions(-) diff --git a/aw_server/api.py b/aw_server/api.py index bd7178ed..4540907c 100644 --- a/aw_server/api.py +++ b/aw_server/api.py @@ -17,6 +17,7 @@ from aw_core.log import get_log_file_path from aw_core.models import Event from aw_query import query2 +from aw_query.exceptions import QueryException from aw_transform import heartbeat_merge from .__about__ import __version__ @@ -342,8 +343,18 @@ def query2(self, name, query, timeperiods, cache): period = timeperiod.split("/")[ :2 ] # iso8601 timeperiods are separated by a slash - starttime = iso8601.parse_date(period[0]) - endtime = iso8601.parse_date(period[1]) + if len(period) != 2: + raise QueryException( + f"Invalid timeperiod '{timeperiod}': expected two ISO8601 " + "datetimes separated by a slash (start/end)" + ) + try: + starttime = iso8601.parse_date(period[0]) + endtime = iso8601.parse_date(period[1]) + except iso8601.ParseError as e: + raise QueryException( + f"Invalid timeperiod '{timeperiod}': {e}" + ) query = "".join(query) result.append(query2.query(name, query, starttime, endtime, self.db)) return result diff --git a/tests/test_server.py b/tests/test_server.py index 1c1c2ea6..0c49e440 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -89,3 +89,43 @@ def get_events(): # TODO: Add benchmark for basic AFK-filtering query + + +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"] + + +def test_query_timeperiod_missing_slash(flask_client): + """A timeperiod without a start/end slash separator must yield 400, not 500. + + Regression test: a single ISO8601 datetime (no slash) previously raised an + uncaught IndexError when indexing the split result. + """ + r = flask_client.post( + "/api/0/query/", + json={"query": ["RETURN = 1;"], "timeperiods": ["2024-01-01T00:00:00+00:00"]}, + ) + assert r.status_code == 400 + + +def test_query_valid_timeperiod(flask_client): + """A well-formed query with a valid timeperiod still succeeds.""" + r = flask_client.post( + "/api/0/query/", + json={ + "query": ["RETURN = 1;"], + "timeperiods": ["2024-01-01T00:00:00+00:00/2024-01-02T00:00:00+00:00"], + }, + ) + assert r.status_code == 200 + assert r.json == [1] From b3d8943e019bbe82d8172839cf7c57f18d586b50 Mon Sep 17 00:00:00 2001 From: Bob Date: Mon, 25 May 2026 07:48:43 +0000 Subject: [PATCH 2/2] style(api): format invalid timeperiod error handling --- aw_server/api.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/aw_server/api.py b/aw_server/api.py index 4540907c..a143c0c5 100644 --- a/aw_server/api.py +++ b/aw_server/api.py @@ -352,9 +352,7 @@ def query2(self, name, query, timeperiods, cache): starttime = iso8601.parse_date(period[0]) endtime = iso8601.parse_date(period[1]) except iso8601.ParseError as e: - raise QueryException( - f"Invalid timeperiod '{timeperiod}': {e}" - ) + raise QueryException(f"Invalid timeperiod '{timeperiod}': {e}") query = "".join(query) result.append(query2.query(name, query, starttime, endtime, self.db)) return result