Skip to content

fix: create-api-key 500s (uuid 22P02) when product and identity_id both omitted - #253

Open
safayavatsal wants to merge 1 commit into
highflame-ai:mainfrom
safayavatsal:fix/create-api-key-nil-identity-149
Open

fix: create-api-key 500s (uuid 22P02) when product and identity_id both omitted#253
safayavatsal wants to merge 1 commit into
highflame-ai:mainfrom
safayavatsal:fix/create-api-key-nil-identity-149

Conversation

@safayavatsal

Copy link
Copy Markdown
Contributor

Closes #149.

Summary

POST /api-keys with only the documented-required name field (no product, no identity_id) returned HTTP 500. CreateKey only auto-provisions an identity when Product != ""; with both empty, IdentityID stayed "", and the store inserted that empty string into the identity_id uuid column — Postgres rejects it (SQLSTATE 22P02: invalid input syntax for type uuid). The column is nullable (REFERENCES identities(id) ON DELETE CASCADE, no NOT NULL), so the schema already supports an unlinked key — the code just wasn't persisting "unlinked" correctly.

Fix

Per the issue's own preferred option: persist NULL, not "".

  • domain/apikey.goIdentityID gets the nullzero bun tag, matching the identical pattern already used on ReplacedBy in the same struct for the same class of optional-UUID-FK field. Empty now persists as SQL NULL instead of "".

Defense in depth

While in this code path, also closed the "at minimum, this should not surface as an opaque 500" ask:

  • internal/service/errors.go — new isInvalidUUIDError (SQLSTATE 22P02) and isForeignKeyViolation (23503) helpers, following the exact convention of the existing isDuplicateKeyError (23505) helper used elsewhere in this package.
  • internal/service/apikey.go — new ErrInvalidAPIKeyReference sentinel. Both CreateKey's repo.Create error path and its identity subset-check lookup (GetIdentity) now classify DB-level bad-reference errors into this sentinel instead of leaking a raw error.
  • internal/handler/apikey.go — maps ErrInvalidAPIKeyReference to 400, alongside the existing credential-policy error mappings.

Adjacent bug found in the same pass: the identity subset-check lookup had the identical failure mode from the other direction — an explicitly-supplied identity_id that's malformed or doesn't resolve in the caller's tenant also 500'd, for the same "no errors.Is handling" reason. Fixed with the same sentinel, no new abstraction.

Tests

tests/integration/apikey_test.go — three new tests:

  • TestCreateAPIKey_NoProductNoIdentity_Succeeds — the exact repro from the issue (now 201, empty identity_id in the response).
  • TestCreateAPIKey_NonexistentIdentityIDRejected — a well-formed but nonexistent identity_id (400).
  • TestCreateAPIKey_MalformedIdentityIDRejected — a malformed (non-UUID) identity_id (400).

Verification

Confirmed the nullzero fix is load-bearing and not redundant with the error-classification hardening: temporarily reverted just the tag and reran the primary test — it failed. With the error classification alone in place, the failure had already improved from 500 → 400, but the request that should succeed per the documented contract (only name is required) still didn't succeed until the tag was restored. Both pieces are independently necessary.

  • GOEXPERIMENT=jsonv2 go build ./... clean
  • GOEXPERIMENT=jsonv2 go vet ./... clean
  • gofmt -l . clean
  • Full test suite green: unit tests with -race, full tests/integration (testcontainers) suite including the 3 new tests and the existing api-key regression tests

POST /api-keys with only the documented-required `name` field (no
product, no identity_id) 500'd: CreateKey left IdentityID as "" when
neither was supplied, and the store inserted that empty string into
the identity_id uuid column, which Postgres rejects (SQLSTATE 22P02,
"invalid input syntax for type uuid"). The column is nullable
(REFERENCES identities(id) ON DELETE CASCADE, no NOT NULL) — the
schema already supports an unlinked key, the code just wasn't
persisting "unlinked" correctly.

- domain/apikey.go: IdentityID gets the `nullzero` bun tag, matching
  the existing pattern already used on ReplacedBy for the same class
  of optional-UUID-FK field. Empty now persists as SQL NULL instead
  of "".
- internal/service/errors.go: new isInvalidUUIDError (SQLSTATE
  22P02) and isForeignKeyViolation (23503) helpers, alongside the
  existing isDuplicateKeyError (23505).
- internal/service/apikey.go: new ErrInvalidAPIKeyReference
  sentinel. Both CreateKey's repo.Create error path and its identity
  subset-check lookup (GetIdentity) now classify DB-level bad-
  reference errors into this sentinel instead of leaking a raw
  error. The subset-check path was a second, adjacent instance of
  the same bug class found in the same pass: an explicitly-supplied
  identity_id that's malformed or doesn't resolve in the tenant also
  500'd, for the identical "no errors.Is handling" reason.
- internal/handler/apikey.go: maps ErrInvalidAPIKeyReference to 400,
  alongside the existing credential-policy error mappings.
- tests/integration/apikey_test.go: three new tests — the exact
  no-product/no-identity repro from the issue (now 201, empty
  identity_id in the response), a well-formed but nonexistent
  identity_id (400), and a malformed (non-UUID) identity_id (400).

Verified the fix is load-bearing, not redundant with the error
classification: temporarily reverted just the nullzero tag and
reran the primary test — it failed. With the error classification
alone in place the failure had already improved from 500 to 400,
but the request that should succeed per the documented contract
(only `name` is required) still didn't succeed until the nullzero
tag was restored.

Full build/vet/gofmt clean. Full test suite (unit -race + testcontainers
integration) green.

Closes highflame-ai#149
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix: create-api-key 500s (uuid 22P02) when product and identity_id both omitted

1 participant