langflow-ai/langflow · warning · HTTPException

`since` must be strictly less than `until`

Error message

`since` must be strictly less than `until`

What it means

Validation error from GET /api/v1/authz/audit: when both since and until query params are supplied, since must be strictly less than until. since is an inclusive lower bound and until an exclusive upper bound on the audit timestamp, so an equal or inverted range is rejected with HTTP 400 before the query runs.

Source

Thrown at src/backend/base/langflow/api/v1/authz_audit.py:86

        Query(description="Filter by action string, e.g. ``flow:read`` or ``share:create``."),
    ] = None,
    result: Annotated[
        str | None,
        Query(description="Filter by decision result (``allow`` / ``deny`` / ``owner_override``)."),
    ] = None,
    since: Annotated[datetime | None, Query(description="Inclusive lower bound on ``timestamp``.")] = None,
    until: Annotated[datetime | None, Query(description="Exclusive upper bound on ``timestamp``.")] = None,
    page: Annotated[int, Query(ge=1)] = 1,
    size: Annotated[int, Query(ge=1, le=_MAX_PAGE_SIZE)] = 50,
) -> AuthzAuditPage:
    """Return a paginated slice of the audit log filtered by the given query params.

    Superuser only. The composite indexes on ``(user_id, timestamp)`` and
    ``(resource_type, resource_id)`` keep both "show me events for user X"
    and "show me events on resource Y" fast at scale.
    """
    if since is not None and until is not None and since >= until:
        raise HTTPException(status_code=400, detail="`since` must be strictly less than `until`")

    base = select(AuthzAuditLog)
    if user_id is not None:
        base = base.where(AuthzAuditLog.user_id == user_id)
    if resource_type is not None:
        base = base.where(AuthzAuditLog.resource_type == resource_type)
    if resource_id is not None:
        base = base.where(AuthzAuditLog.resource_id == resource_id)
    if action is not None:
        base = base.where(AuthzAuditLog.action == action)
    if result is not None:
        base = base.where(AuthzAuditLog.result == result)
    if since is not None:
        base = base.where(AuthzAuditLog.timestamp >= since)
    if until is not None:
        base = base.where(AuthzAuditLog.timestamp < until)

    # Two queries: one COUNT(*) for pagination metadata, one for the page

View on GitHub (pinned to 976ec789d2)

Solutions

  1. Ensure the client sends since < until strictly; when the picker returns equal timestamps, shift until forward by at least one second
  2. Send both timestamps in UTC with explicit offsets (e.g. Z) to avoid timezone inversion
  3. Validate the range client-side before issuing the request
  4. If you want a single instant, use only since and let until be null

Example fix

// before
const q = `?since=${start.toISOString()}&until=${end.toISOString()}`;

// after
if (since >= until) throw new RangeError('since must be < until');
const q = `?since=${start.toISOString()}&until=${end.toISOString()}`;
Defensive patterns

Strategy: validation

Validate before calling

function validRange(since: Date, until: Date) {
  return since.getTime() < until.getTime();
}
if (!validRange(since, until)) throw new RangeError('since must be < until');

Type guard

const isSafeRange = (s?: string, u?: string): boolean =>
  s == null || u == null || new Date(s).getTime() < new Date(u).getTime();

Prevention

When it happens

Trigger: GET /api/v1/authz/audit?since=2026-01-01T00:00:00Z&until=2026-01-01T00:00:00Z (equal bounds), or any range where since > until, e.g. copy-pasting timestamps with the times swapped or mixing timezone-offsets so the comparison inverts.

Common situations: UI date-range pickers that default start and end to the same instant, clients building ranges from two identical 'now' timestamps, or timezone confusion where an ISO string with +02:00 looks later than a UTC string but parses earlier.

Related errors


AI-assisted analysis of langflow-ai/langflow@976ec789d2 (2026-08-14). Data as JSON: /api/errors/0b42aa05df761e63. Report an issue: GitHub.