[PM-38743] Honor a partially supplied event date range - #8265
Conversation
GetDateRange discarded both bounds whenever either was missing, so ?start= or ?end= alone silently returned the default last 30 days across all eleven callers. Resolve each bound independently instead: an absent start anchors 30 days before the supplied end, an absent end runs to the end of the current day. The inverted-range swap and the 367-day cap now apply to every case rather than only to fully supplied ranges. Standardizes the cap message on the more descriptive of the two variants that existed, which is the one the Public API already returned. [PM-38743]
EventFilterRequestModel.ToDateRange was a copy of ApiHelpers.GetDateRange that had already diverged on its exception message. Delegate to the shared helper so the Public API picks up the partial-range fix and there is one implementation to maintain. ToDateRange still writes the resolved bounds back onto the model: EventDiagnosticLogger reads Start and End after this call to log the query's effective filters. [PM-38743]
The endpoint's Swagger remarks described only the no-filter default and implied that a partial range fell back to it. Describe what each single bound now resolves to, and state the 367-day cap that was already enforced but never documented. [PM-38743]
GetDateRange served eleven endpoints with no unit coverage, which is how the partial-range bug shipped. Cover every branch: no bounds, start only, end only, both, inverted, over the 367-day cap, and start-only past the cap. Adds EventFilterRequestModelTests to pin the write-back that EventDiagnosticLogger depends on. [PM-38743]
🤖 Bitwarden Claude Code ReviewOverall Assessment: REQUEST CHANGES Reviewed the rewrite of Code Review Details
Notes on the three open questions in the description, which need product/AppSec input rather than a code change: questions 1 and 2 (externally visible response change on |
| start = DateTime.UtcNow.Date.AddDays(-30); | ||
| } | ||
| else if (start.Value > end.Value) | ||
| start ??= end?.AddDays(-30) ?? DateTime.UtcNow.Date.AddDays(-30); |
There was a problem hiding this comment.
end bound within 30 days of DateTime.MinValue makes this throw, turning a previously-200 request into a 500.
Details and fix
end?.AddDays(-30) throws ArgumentOutOfRangeException when the result falls below DateTime.MinValue. GET /public/events?end=0001-01-10 binds successfully (no range validation on EventFilterRequestModel.End), reaches this line, and throws. ExceptionHandlerFilterAttribute has no branch for that exception type, so it lands in the final else: HTTP 500 plus an error-level "Unhandled exception" log entry.
Before this change the end-only branch discarded the supplied value and never did arithmetic on it, so the same request returned 200 with the last 30 days. This is a new failure mode rather than a pre-existing one, and it reaches all 12 entry points that route through this helper.
Clamping keeps the rest of the method unchanged — the 367-day cap then rejects the resulting range with a 400, which is the right answer for this input:
start ??= end.HasValue
? (end.Value > DateTime.MinValue.AddDays(30) ? end.Value.AddDays(-30) : DateTime.MinValue)
: DateTime.UtcNow.Date.AddDays(-30);Worth a companion test alongside GetDateRange_OnlyEndSupplied_KeepsEndAndStartsThirtyDaysBefore.
🎟️ Tracking
PM-38743
📔 Objective
Every event-log endpoint resolves its
start/endquery parameters through a guard that readsif (!end.HasValue || !start.HasValue). That treats "one bound supplied" the same as "no bounds supplied", so a caller who sends only?start=or only?end=has both values discarded and silently receives the default last 30 days, with no error and no warning.The ticket points at
EventFilterRequestModel.ToDateRange(), which serves one endpoint (GET /public/events). That is 1 of 12 affected entry points. The other 11 route throughApiHelpers.GetDateRange, which holds a byte-identical copy of the same guard, already diverged on its exception message. This PR fixes the shared helper and collapses the duplicate into it, so all 12 entry points are repaired from one place.Each bound is now resolved independently:
startonlystartthrough the end of the current dayendonlyendThe swap and the 367-day cap now apply to every case rather than only to fully supplied ranges, which also closes a latent bug: a future-dated
start-only request would previously have produced an inverted range that no branch corrected.Two details worth a reviewer's attention:
ToDateRange()still writes the resolved bounds back onto the model. This is load-bearing, not stylistic:EventDiagnosticLogger.LogAggregateDatareadsrequest.Startandrequest.Endafter the call to log the query's effective filters, so dropping the write-back would silently start loggingnullfor defaulted requests. The invariant is pinned byToDateRange_WritesResolvedBoundsBackOntoTheModelForDiagnosticLogging.GetDateRangepreviously had zero unit coverage despite serving eleven endpoints, which is how this shipped. This PR adds 10 tests covering every branch. All three tests that reproduce the bug were confirmed failing againstmainbefore the fix was applied.Open questions before this leaves draft
GET /public/eventsis an externally consumed contract, so this changes behavior for existing integrations. Three questions need a decision:starttoday receives the last 30 days and will now receive the range it actually asked for.start-only request more than 367 days back now returns 400 (Date range must be < 367 days.) where it previously returned 200. Accept that, or clamp the inferred end tostart + 367 daysand keep returning 200? Clamping avoids the new 400 but silently truncates.endonly should resolve to a 30-day window rather than "everything up toend", which would be unbounded and would immediately hit the cap.Also worth noting: this changes
GET /sm/events/service-accounts/{id}, which is owned by Secrets Manager. A reviewer from that team would be useful.📸 Screenshots
Not applicable, no UI changes.
🤖 Testing
dotnet formatclean on all five changed files.dotnet build test/Api.Test/Api.Test.csproj: 0 errors, and 0 warnings originating from the changed files.dotnet test test/Api.Test/Api.Test.csproj: 1955 passed, 0 failed. This includes the 13 pre-existingEventsControllerTestsand the 6EventDiagnosticLoggerTests, all unmodified and all still green.ApiHelpers.csreverted tomain,GetDateRange_OnlyStartSupplied_KeepsStartAndRunsToEndOfToday,GetDateRange_OnlyEndSupplied_KeepsEndAndStartsThirtyDaysBefore, andToDateRange_OnlyStartSupplied_DoesNotFallBackToThirtyDayDefaultall fail.Manual verification against a local server has not been run yet. To reproduce the original bug, compare the response counts of
GET /public/events?start=<2 hours ago>andGET /public/eventswith no query string: before this change they are identical, after it the filtered call returns fewer events.