---
key: api.scheduling-hardening
title: '2026-07-13 — Scheduling API hardening: null-name 500s, duplicate template items, unreachable 404 (#172)'
status: active
since: '2026-07-13'
supersedes: none
superseded-by: none
rule: Create/Replace handlers guard against null/whitespace `name` (`IsNullOrWhiteSpace`, not just `Length`) to prevent NRE-500s, template-item overlap validation compares by index (not record value-equality) to catch exact-duplicate items, and unreachable 404 `ProducesResponseType` attributes on create-only actions are trimmed.
signals: '`BlockTemplateItem` record value-equality bypass, `api-conventions.md` §3b handler-hardening checklist · paths: `ReplaceTemplateItemsHandler`, `api-conventions.md` §3b · issues: #172, #144'
mechanics: '`docs/api-conventions.md` §3b'
---

Cleared the still-live findings from issue #172 (consolidated non-blocking nits from the #144 S1/S2
reviews). Most of the 2026-07-07 list had already been ratified deliberate (§8 "(none)" synthesized
rows; §3b deep-FK non-existence-check) or fixed since (the unauthenticated `/api/logs` +
`/api/troubleshoot/info` GETs now carry `[RequiresAuthentication]` per §9; the Trakt matched-items link
points at the live `/app/search`; `GET /api/search` already fans out via `Task.WhenAll`). Three were
genuinely live:

- **Null/empty `name` → 500 (10 handlers).** Create + Replace/Update handlers for Block, Template,
  DecoTemplate, Deco (8, all genuine 500s), plus `UpdateFFmpegProfile` (genuine 500; `CreateFFmpegProfile`
  was already guarded) and `CreatePlaylist` (its DTO coalesces `null`→`""`, so an empty-name persist, not a
  500) all did `if (request.Name.Length > 50)` on a client-nullable `string Name` → unhandled
  `NullReferenceException`. Fixed to `if (string.IsNullOrWhiteSpace(request.Name) || request.Name.Length >
  50)` — kills the NRE, and also rejects empty/whitespace names (matching the group-create handlers'
  `NotEmpty` behavior, closing a latent "block/template named ''" gap). Chose the one-line guard over
  refactoring each handler onto the `NotEmpty`/`NotLongerThan` combinator to keep the blast radius tiny and
  preserve each handler's existing error message + 422 mapping. Convention captured in `api-conventions.md`
  §3b handler-hardening checklist.
- **Exact-duplicate template items bypassed overlap validation.** `ReplaceTemplateItemsHandler`'s O(n²)
  overlap loop skipped on `item == otherItem`, but `BlockTemplateItem` is a `record`, so two value-identical
  items (same BlockId + StartTime → same computed EndTime) were value-equal and skipped — both persisted
  unvalidated. Switched to index-based iteration (`i != j`) so identical items at distinct positions are
  compared and register as a (self-)intersection → rejected 422. (The SPA's index-based check already caught
  this client-side; it was an API-only gap.)
- **Unreachable 404 on create-group actions.** `POST /api/blocks/groups` and `POST /api/templates/groups`
  declared `[ProducesResponseType(ProblemDetails, 404)]` copied from precedent, but a create has no parent
  lookup that can 404 (only 201/422). Trimmed — OpenAPI spec regenerated.

Deliberately **not** fixed (documented as accepted): the §8 "(none)" synthetic rows, the §3b deep-FK
non-existence-check, and the missing `Name=` on `PlayoutController` Create/Delete/Update (moot — the
"v1"-doc `OperationIdOpenApiTransformer` (#197 Bundle C) already synthesizes stable operationIds for
`Name=`-less ops, and adding `Name=` would risk renaming generated SPA client methods). Refs #172 #197.
