Summary
POST /api/v1/api-keys returns HTTP 500 "failed to create API key" when the request body omits both product and identity_id. The underlying error is a Postgres type failure — an empty string is written into the uuid column api_keys.identity_id:
failed to store API key: failed to create API key:
ERROR: invalid input syntax for type uuid: "" (SQLSTATE=22P02)
This is the documented minimal request shape (only name is required:"true" in CreateAPIKeyInput), so the simplest valid-looking call fails with an opaque 500.
Reproduction
curl -s -X POST http://localhost:8899/api/v1/api-keys \
-H 'Content-Type: application/json' \
-H 'X-Account-ID: acme' -H 'X-Project-ID: prod' -H 'X-User-ID: admin' \
-d '{"name":"my-service-key"}'
Response:
{ "title": "Internal Server Error", "status": 500, "detail": "failed to create API key" }
Server log:
{"level":"error","error":"failed to store API key: failed to create API key: ERROR: invalid input syntax for type uuid: \"\" (SQLSTATE=22P02)","caller":".../internal/handler/apikey.go:147","message":"failed to create API key"}
Workaround: include a product (e.g. "product":"my-svc") so a service identity is auto-provisioned, or pass an explicit identity_id.
Root cause
internal/service/apikey.go → CreateKey:
- An identity is auto-provisioned only when
req.IdentityID == "" && req.Product != "" (line ~79). With both empty, req.IdentityID stays "".
- The key struct is built with
IdentityID: req.IdentityID (line ~184). domain.APIKey.IdentityID is tagged bun:"identity_id,type:uuid" (domain/apikey.go:36), and the column is identity_id UUID REFERENCES identities(id) ON DELETE SET NULL — nullable (migrations/001_init_schema.up.sql:96).
s.repo.Create then inserts "" into a uuid column → SQLSTATE 22P02.
- The handler maps any create error to a generic
huma.Error500InternalServerError (internal/handler/apikey.go:147).
The doc comment on CreateKey states "Every key is linked to an identity and assigned a credential policy", but that invariant is enforced only on the Product != "" path. The no-product / no-identity path violates it and fails at the DB layer instead of being handled.
Expected behavior
One of (in rough order of preference):
- Persist
NULL, not "" — the column is nullable (ON DELETE SET NULL), so an unlinked key should store SQL NULL for identity_id. Likely needs IdentityID to be *string/sql.NullString (or a nullzero bun tag) so empty maps to NULL.
- Auto-provision in the no-product path too, if the "every key has an identity" invariant is meant to be hard — derive a default/service identity even when
product is unset.
- Fail with
400 and a clear message if identity_id/product is genuinely required, rather than a 500 from a DB type error.
At minimum, this should not surface as an opaque HTTP 500.
Environment
- zeroid
main (current HEAD), standalone docker-compose, Postgres 17.
- Reproduced against
/api/v1/api-keys with default tenant headers; no auth/network specifics involved.
Summary
POST /api/v1/api-keysreturns HTTP 500 "failed to create API key" when the request body omits bothproductandidentity_id. The underlying error is a Postgres type failure — an empty string is written into theuuidcolumnapi_keys.identity_id:This is the documented minimal request shape (only
nameisrequired:"true"inCreateAPIKeyInput), so the simplest valid-looking call fails with an opaque 500.Reproduction
Response:
{ "title": "Internal Server Error", "status": 500, "detail": "failed to create API key" }Server log:
Workaround: include a
product(e.g."product":"my-svc") so a service identity is auto-provisioned, or pass an explicitidentity_id.Root cause
internal/service/apikey.go→CreateKey:req.IdentityID == "" && req.Product != ""(line ~79). With both empty,req.IdentityIDstays"".IdentityID: req.IdentityID(line ~184).domain.APIKey.IdentityIDis taggedbun:"identity_id,type:uuid"(domain/apikey.go:36), and the column isidentity_id UUID REFERENCES identities(id) ON DELETE SET NULL— nullable (migrations/001_init_schema.up.sql:96).s.repo.Createthen inserts""into auuidcolumn →SQLSTATE 22P02.huma.Error500InternalServerError(internal/handler/apikey.go:147).The doc comment on
CreateKeystates "Every key is linked to an identity and assigned a credential policy", but that invariant is enforced only on theProduct != ""path. The no-product / no-identity path violates it and fails at the DB layer instead of being handled.Expected behavior
One of (in rough order of preference):
NULL, not""— the column is nullable (ON DELETE SET NULL), so an unlinked key should store SQLNULLforidentity_id. Likely needsIdentityIDto be*string/sql.NullString(or anullzerobun tag) so empty maps toNULL.productis unset.400and a clear message ifidentity_id/productis genuinely required, rather than a 500 from a DB type error.At minimum, this should not surface as an opaque HTTP 500.
Environment
main(currentHEAD), standalone docker-compose, Postgres 17./api/v1/api-keyswith default tenant headers; no auth/network specifics involved.