bitwarden/server · error · BadRequestException

Date range must be < 367 days.

Error message

Date range must be < 367 days.

What it means

Thrown by EventFilterRequestModel.ToDateRange() when the span between Start and End exceeds 367 days. The method defaults to a 30-day window if either bound is null, swaps reversed ranges, but hard-rejects ranges wider than one year + one day. BadRequestException returns HTTP 400.

Source

Thrown at src/Api/Dirt/Public/Models/EventFilterRequestModel.cs:55

    public string ContinuationToken { get; set; }

    public Tuple<DateTime, DateTime> ToDateRange()
    {
        if (!End.HasValue || !Start.HasValue)
        {
            End = DateTime.UtcNow.Date.AddDays(1).AddMilliseconds(-1);
            Start = DateTime.UtcNow.Date.AddDays(-30);
        }
        else if (Start.Value > End.Value)
        {
            var newEnd = Start;
            Start = End;
            End = newEnd;
        }

        if ((End.Value - Start.Value) > TimeSpan.FromDays(367))
        {
            throw new BadRequestException("Date range must be < 367 days.");
        }

        return new Tuple<DateTime, DateTime>(Start.Value, End.Value);
    }
}

View on GitHub (pinned to e93b962371)

Solutions

  1. Split the request into multiple paginated calls each within the 367-day limit.
  2. Client-side: clamp the date picker's maximum range to 367 days before submitting.
  3. If only Start is provided, the server defaults End to today — ensure the resulting range is within bounds.
  4. Use the ContinuationToken pagination mechanism for large date ranges instead of one wide window.

Example fix

// before
var events = await api.GetEventsAsync(start: new DateTime(2020,1,1), end: DateTime.UtcNow);

// after — paginate across 367-day windows
var chunkStart = new DateTime(2020, 1, 1);
var end = DateTime.UtcNow;
while (chunkStart < end)
{
    var chunkEnd = chunkStart.AddDays(367) < end ? chunkStart.AddDays(367) : end;
    var events = await api.GetEventsAsync(start: chunkStart, end: chunkEnd);
    // process...
    chunkStart = chunkEnd;
}
Defensive patterns

Strategy: validation

Validate before calling

// Validate date range client-side before calling the API
public static bool IsValidDateRange(DateTime? start, DateTime? end)
{
    if (!start.HasValue || !end.HasValue) return true; // server defaults
    var actualStart = start.Value < end.Value ? start.Value : end.Value;
    var actualEnd = start.Value < end.Value ? end.Value : start.Value;
    return (actualEnd - actualStart) <= TimeSpan.FromDays(367);
}

Type guard

public static bool IsWithinMaxRange(DateTime start, DateTime end) =>
    (end - start) <= TimeSpan.FromDays(367);

Try / catch

try
{
    var events = await _eventService.GetEventsAsync(filter);
}
catch (BadRequestException ex) when (ex.Message.Contains("367 days"))
{
    // Split into chunks and retry
    var chunked = SplitIntoMaxRanges(filter.Start.Value, filter.End.Value, TimeSpan.FromDays(367));
    var allEvents = new List<EventLog>();
    foreach (var (s, e) in chunked)
        allEvents.AddRange(await _eventService.GetEventsAsync(new EventFilter { Start = s, End = e }));
    return allEvents;
}

Prevention

When it happens

Trigger: Any event-filtering API call (e.g., GET /public/events or /organizations/{id}/events) with Start and End query parameters spanning more than 367 days. The check is (End - Start) > 367 days.

Common situations: Client UI date-picker defaulting to 'all time'; exporting a full audit history in one request; automated reporting script using a fixed start date far in the past; timezone mismatch causing the boundary to be off by a day.

Related errors


AI-assisted analysis of bitwarden/server@e93b962371 (2026-08-13). Data as JSON: /api/errors/7559d06b37e67125. Report an issue: GitHub.