Skip to content

fix(cosmos) guard CosmosClientOptions.ApplicationName against empty/null#59

Merged
jkeeley2073 merged 1 commit into
mainfrom
Dev-FixCosmosClientApplicationNameNullCrash
May 3, 2026
Merged

fix(cosmos) guard CosmosClientOptions.ApplicationName against empty/null#59
jkeeley2073 merged 1 commit into
mainfrom
Dev-FixCosmosClientApplicationNameNullCrash

Conversation

@jkeeley2073
Copy link
Copy Markdown
Contributor

Summary

Real bug, caught the first time the CLI tried to talk to deployed Cosmos:

System.ArgumentException: Application name '' is invalid.
 ---> System.FormatException: The format of value '<null>' is invalid.
   at Microsoft.Azure.Cosmos.CosmosClientOptions.set_ApplicationName(String value)
   at PinballWizard.Infrastructure.Persistence.Cosmos.ServiceCollectionExtensions...

The Cosmos SDK appends ApplicationName to its User-Agent header. The HTTP-headers parser rejects empty/null values. AddCosmosPersistence was setting clientOptions.ApplicationName = options.ApplicationName unconditionally — but CosmosOptions.ApplicationName is nullable by design (its docstring: "Optional override... helpful in Cosmos diagnostics for distinguishing the scraper job from the API").

The existing tests (CosmosRepositoryTests, MachineRepositoryTests, etc.) all mocked at the Container level rather than constructing a real CosmosClient, so the crash never surfaced under test. The first real-config DI resolution after PR #57's deploy-validation flag was wired tripped it.

Fix: guard with string.IsNullOrWhiteSpace. Load-bearing comment on the gate documents the SDK behavior so a future reader doesn't try to "simplify" it back to the unconditional form.

Test Plan

  • dotnet test PinballWizard.slnx -> 503 / 503 passing (was 501 — +2 new tests in AddCosmosPersistenceTests)
  • New tests resolve CosmosClient through the actual DI container:
    • AddCosmosPersistence_WithoutApplicationName_ResolvesCosmosClientWithoutThrowing — pins the regression. Pre-fix, this throws ArgumentException 'Application name "" is invalid'.
    • AddCosmosPersistence_WithApplicationName_AppliesItToClient — pins the happy path. The CosmosClient setter runs without throwing for any non-empty value.
  • Live re-validation: user re-runs dotnet run --project src/PinballWizard.Cli -- --ensure-cosmos-containers against deployed Cosmos with the existing Cosmos__AccountEndpoint env var — expected output:
    Ensuring Cosmos database 'pinwiz' exists at https://pinwiz-cosmos-dev-hlpz4.documents.azure.com:443/.
    Container 'machines' ready (partition key /manufacturer, default TTL none).
    Container 'ingestion_sources' ready (partition key /partitionKey, default TTL none).
    Cosmos database + containers ensured.
    

Out of Scope

  • Defaulting ApplicationName to "PinballWizard" in CosmosOptions. Considered; rejected. The option is genuinely optional (per docstring), and a non-null default would force every future consumer (Phase 2 services) to override or accept the same name. The conditional-assign at the consumer is more flexible.

Checklist

  • CI is green
  • PR title follows the Conventional Commits format
  • No TODO / FIXME / commented-out code committed

Pre-push self-audit

Step 0 — /local-review (qualitative)

  • Skipped with justification — single-line guard fix with a load-bearing comment on the SDK behavior; regression test pins the failure mode.

Step 1 — Mechanical checklist

  • Every new *Options property has at least one real getter call in src/ — N/A (no new options; existing ApplicationName getter still in use)
  • Sibling-diffed against the closest existing implementation — N/A (single guard)
  • No bare catch { } — N/A (no catch added)
  • New ISourceScraper? — N/A
  • Tests assert behavior, not just structure — both new tests resolve CosmosClient through DI; the WithoutApplicationName test would throw pre-fix
  • Build is zero-warning
  • git log -1 --format='%an <%ae>' shows personal noreply, not work email

Real bug, caught on the first deployed-Cosmos invocation: the Cosmos
SDK rejects empty/null User-Agent additions with

  System.ArgumentException: Application name '' is invalid.
  System.FormatException: The format of value '<null>' is invalid.

Root cause: AddCosmosPersistence set
clientOptions.ApplicationName = options.ApplicationName unconditionally.
CosmosOptions.ApplicationName is nullable by design (its docstring
explicitly notes 'Optional override... helpful in Cosmos diagnostics for
distinguishing the scraper job from the API'), so the first real
config that didn't set it crashed at DI resolution.

The existing tests (CosmosRepositoryTests, MachineRepositoryTests, etc.)
mocked at the Container level rather than constructing a real
CosmosClient, so the bug never surfaced under test. Two new tests in
AddCosmosPersistenceTests resolve the CosmosClient through the DI
container — the WithoutApplicationName test pins the regression
(pre-fix throws), the WithApplicationName test pins the happy path.

Fix: only assign clientOptions.ApplicationName when the option is
populated (string.IsNullOrWhiteSpace gate). Load-bearing comment on
the gate documents the SDK behavior so the next reader doesn't try to
'simplify' it back to the unconditional form.

Tests: 501 -> 503. Build clean.

Pre-push self-audit: 7-item mechanical (all pass). /local-review
skipped — single-line guard fix with regression test.
@jkeeley2073 jkeeley2073 added the claude-code Generated with Claude Code label May 3, 2026
@jkeeley2073 jkeeley2073 merged commit 0611ef0 into main May 3, 2026
5 checks passed
@jkeeley2073 jkeeley2073 deleted the Dev-FixCosmosClientApplicationNameNullCrash branch May 3, 2026 22:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

claude-code Generated with Claude Code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant