{"record":{"id":"27715e9e24fff378","repo":"Significant-Gravitas/AutoGPT","slug":"since-must-be-earlier-than-or-equal-to-until","errorCode":null,"errorMessage":"`since` must be earlier than or equal to `until`.","messagePattern":"`since` must be earlier than or equal to `until`\\.","errorType":"http","errorClass":"HTTPException","httpStatus":422,"severity":"warning","filePath":"autogpt_platform/backend/backend/api/features/v1.py","lineNumber":2124,"sourceCode":"    user_id: Annotated[str, Security(get_user_id)],\n    since: datetime | None = Query(\n        None,\n        description=\"Window start (UTC). Defaults to start of current calendar month.\",\n    ),\n    until: datetime | None = Query(\n        None,\n        description=\"Window end (UTC). Defaults to now.\",\n    ),\n    top_runs_limit: int = Query(\n        10,\n        ge=1,\n        le=50,\n        description=\"Maximum number of top-cost runs to return.\",\n    ),\n) -> UserExecutionCostSummary:\n    \"\"\"Aggregated cost breakdown for the calling user's graph executions.\"\"\"\n    if since is not None and until is not None and since > until:\n        raise HTTPException(\n            status_code=422,\n            detail=\"`since` must be earlier than or equal to `until`.\",\n        )\n    return await get_user_cost_summary(\n        user_id=user_id,\n        since=since,\n        until=until,\n        top_runs_limit=top_runs_limit,\n    )\n\n\n@v1_router.get(\n    path=\"/graphs/{graph_id}/executions\",\n    summary=\"List graph executions\",\n    tags=[\"graphs\"],\n    dependencies=[Security(requires_user)],\n)\nasync def list_graph_executions(","sourceCodeStart":2106,"sourceCodeEnd":2142,"githubUrl":"https://github.com/Significant-Gravitas/AutoGPT/blob/9c8bb5550f446ba5d3046b78896578742495b3cf/autogpt_platform/backend/backend/api/features/v1.py#L2106-L2142","documentation":"Raised (422) by GET /usage/execution_costs (user cost summary) when both since and until query params are provided and since > until. The endpoint aggregates a time window; an inverted window is nonsensical and rejected before querying. top_runs_limit is separately constrained to [1,50] by Query(ge/le) metadata.","triggerScenarios":"Passing ?since=2026-08-10T00:00Z&until=2026-08-01T00:00Z — typically a date-picker allowing the user to select an end date before the start date, or timezone math that flips the boundaries.","commonSituations":"Analytics dashboards with free-form date range pickers; UTC-vs-local conversion bugs producing since one day after until; copy-pasted query params with swapped order.","solutions":["Validate/normalize the range client-side: if since > until, swap them or block submission.","Check timezone handling — build both timestamps in the same (UTC) zone.","Leave since/until omitted to default to a window ending now."],"exampleFix":"// before\napi.getCostSummary({ since: end, until: start }); // swapped\n\n// after\nconst [lo, hi] = start <= end ? [start, end] : [end, start];\napi.getCostSummary({ since: lo, until: hi });","handlingStrategy":"validation","validationCode":"let [sinceN, untilN] = [since?.getTime() ?? -Infinity, until?.getTime() ?? Infinity];\nif (sinceN > untilN) [sinceN, untilN] = [untilN, sinceN]; // or reject in the picker\nawait api.getCostSummary({ since: new Date(sinceN), until: new Date(untilN) });","typeGuard":"const isValidRange = (s?: Date, u?: Date) => !s || !u || s.getTime() <= u.getTime();","tryCatchPattern":"catch (e) { if (e.response?.status === 422 && /since/.test(e.response.data.detail)) { swapRangeAndRetry(); } else throw e; }","preventionTips":["Constrain the date picker so end < start cannot be submitted.","Generate both bounds in UTC to avoid local-offset flips.","Also keep top_runs_limit within [1,50] — Query metadata enforces it with its own 422."],"tags":["api","usage","analytics","http-422","validation","time"],"backgroundTag":null,"analyzedSha":"9c8bb5550f446ba5d3046b78896578742495b3cf","analyzedAt":"2026-08-14T17:17:21.957Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}