PostgreSQL: /Shows/NextUp 500s on unnormalised DateTime.Kind=Unspecified cutoff (Npgsql rejects it for timestamptz) #26
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Problem
Both
fafflixJellyfin pods (jellyfin-ha:v0.3.2, Jellyfin 12.0, PostgreSQL backend) return a 500 onGET /Shows/NextUp— the "Next Up" row on the home screen — whenever the client omitsnextUpDateCutoff:Verified against current
main(ec581b5e5f):Jellyfin.Server.Implementations/Item/NextUpService.cs:69isreturn query.ToArray();— that's where LINQ materialises the query and Npgsql throws, but the value it can't write isdateCutoff, compared at line 60:.Where(g => g.Key != null && g.LastPlayedDate != null && g.LastPlayedDate >= dateCutoff).dateCutofftraces back toJellyfin.Api/Controllers/TvShowsController.cs:111:NextUpDateCutoff = nextUpDateCutoff ?? DateTime.MinValue,wherenextUpDateCutoffis bound from[FromQuery] DateTime? nextUpDateCutoff. Both the default (DateTime.MinValue) and, on ASP.NET Core, an explicitly-supplied query-string value areDateTimeKind.Unspecified. SQLite doesn't care aboutKind; Npgsql refuses to bind anUnspecifiedDateTimeto atimestamptzcolumn/parameter.Jellyfin.Api/Controllers/ItemsController.cs:448-451does this correctly for the equivalent/Itemsdate filters —MinPremiereDate = minPremiereDate?.ToUniversalTime(), etc.TvShowsController.GetNextUpis simply missing the same normalisation.Lineage: this is the same family as three PostgreSQL-portability bugs already fixed on this fork (SQLite tolerates something Npgsql doesn't):
fix(db): restore the PostgreSQL upgrade pathfix(db): materialise the rating list before updatingfix(db): collapse presentation-key groups without min(uuid)This is not the statelessness/multi-replica class tracked by #12 — it reproduces identically on a single pod, single request. #12 is not relevant here.
Scope — this is a pattern, not an isolated call site. Grepping
[FromQuery] DateTime?controller parameters against how they're threaded into EF LINQ comparisons inJellyfin.Server.Implementations/Item/BaseItemRepository.TranslateQuery.csandJellyfin.Server.Implementations/Activity/ActivityManager.csturns up two more confirmed-vulnerable siblings (unnormalisedKind=UnspecifiedDateTime compared against atimestamptzcolumn) alongside/Shows/NextUp:GET /LiveTv/Programs(and its POST body form) —Jellyfin.Api/Controllers/LiveTvController.cs:585-588and:639-642assignminStartDate/maxStartDate/minEndDate/maxEndDatestraight from[FromQuery]/body binding ontoInternalItemsQuerywith no.ToUniversalTime(), which then hitBaseItemRepository.TranslateQuery.cs:397-414(e.StartDate >= minStartDate,e.EndDate <= maxEndDate, etc.).GET /System/ActivityLog/Entries—Jellyfin.Api/Controllers/ActivityLogController.cs:77-78assignsminDate/maxDateunnormalised onto the query object, compared directly inJellyfin.Server.Implementations/Activity/ActivityManager.cs:70-77(e.ActivityLog.DateCreated >= query.MinDate.Value,<= query.MaxDate.Value).Confirmed not vulnerable: the main
/Itemsand/Items(GetItemsByUserId) filters and/Trailers(which delegates into the sameGetItemspath) — all go through the.ToUniversalTime()normalisation atItemsController.cs:448-451before reachingTranslateQuery.cs.Proposal
Normalise every externally-supplied
DateTimeto UTC at the API boundary, the same wayItemsController.GetItemsalready does — not deep in the query builder. Concretely:TvShowsController.cs:111—NextUpDateCutoff = (nextUpDateCutoff ?? DateTime.MinValue).ToUniversalTime()(or equivalently, default viaDateTime.SpecifyKind(DateTime.MinValue, DateTimeKind.Utc)and.ToUniversalTime()the supplied value).LiveTvController.cs:585-588,639-642— same.ToUniversalTime()treatment forMinStartDate/MaxStartDate/MinEndDate/MaxEndDateon both the query-string and request-body paths.ActivityLogController.cs:77-78— same forMinDate/MaxDate.I'd lean toward the per-call-site fix over a global one (e.g. an EF
ConfigureConventions/HasConversion<DateTime>()onPostgreSqlDatabaseProvider.OnModelCreating/ConfigureConventions, both currently empty). Reasoning:dateCutoff,minStartDate,query.MinDate) are plain closure-captured C# variables compared inline in aWhere(...), not mapped entity properties — they're translated as query parameters, not through a property'sHasConversion. A model-level value converter is not guaranteed to intercept them (EF Core applies a property's converter when reading/writing that property; a parameter's relational type mapping for a closure value isn't the same code path), so a global converter risks looking like a fix while leaving exactly this class of bug in place for the next[FromQuery] DateTime?someone adds.ItemsController.csalready establishes the correct convention (?.ToUniversalTime()at the controller boundary). The other three controllers just didn't follow it. Fixing at the boundary is a small, mechanical, low-blast-radius change consistent with existing code, versus a DbContext-wide change that touches every provider (SQLite too) and every DateTime column.DbCommand/SaveChangesinterceptor that hard-fails or auto-corrects anyUnspecified-KindDateTimeparameter before it reaches Npgsql) is worth a follow-up as defense-in-depth so the next new query filter can't reintroduce this silently — but that's an addition on top of the boundary fixes, not a replacement for them, and shouldn't gate this fix.Acceptance
GET /Shows/NextUpsucceeds against PostgreSQL both withnextUpDateCutoffomitted and with an explicit value supplied.GET /LiveTv/Programs(both query-string and POST-body forms) succeeds against PostgreSQL withminStartDate/maxStartDate/minEndDate/maxEndDatesupplied.GET /System/ActivityLog/Entriessucceeds against PostgreSQL withminDate/maxDatesupplied.PostgreSqlTestServer(tests/Jellyfin.Server.Tests) or extend thepostgres-migration-chainWoodpecker step (PostgreSQL 16 in-step viaJELLYFIN_TEST_POSTGRES). This class of bug (works on SQLite/in-memory, throws on Npgsql) has now escaped to production four times (#4, #6, #10, this one) specifically because SQLite and EF's in-memory provider don't enforce the sameKindsemantics Npgsql does — a green in-memory test suite is not evidence of anything here..ToUniversalTime()on an already-UTCDateTimeis a no-op; on an already-Unspecifiedvalue it's exactly the normalisation being added).