{"record":{"id":"a7d709f0f5cff083","repo":"hashicorp/nomad","slug":"cannot-update-expiration-time","errorCode":null,"errorMessage":"cannot update expiration time","messagePattern":"cannot update expiration time","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"nomad/structs/acl.go","lineNumber":819,"sourceCode":"\t\t\t\t\tfmt.Errorf(\"expiration time cannot be more than %s in the future (was %s)\",\n\t\t\t\t\t\tmaxTTL, expiresIn))\n\n\t\t\t} else if expiresIn < minTTL {\n\t\t\t\tmErr.Errors = append(mErr.Errors,\n\t\t\t\t\tfmt.Errorf(\"expiration time cannot be less than %s in the future (was %s)\",\n\t\t\t\t\t\tminTTL, expiresIn))\n\t\t\t}\n\t\t}\n\tdefault:\n\t\tif existing.Global != a.Global {\n\t\t\tmErr.Errors = append(mErr.Errors, errors.New(\"cannot toggle global mode\"))\n\t\t}\n\t\tif existing.ExpirationTTL != a.ExpirationTTL {\n\t\t\tmErr.Errors = append(mErr.Errors, errors.New(\"cannot update expiration TTL\"))\n\t\t}\n\t\tif a.ExpirationTime != nil {\n\t\t\tif !existing.ExpirationTime.Equal(*a.ExpirationTime) {\n\t\t\t\tmErr.Errors = append(mErr.Errors, errors.New(\"cannot update expiration time\"))\n\t\t\t}\n\t\t}\n\n\t}\n\n\treturn mErr.ErrorOrNil()\n}\n\n// HasExpirationTime checks whether the ACL token has an expiration time value\n// set.\nfunc (a *ACLToken) HasExpirationTime() bool {\n\tif a == nil || a.ExpirationTime == nil {\n\t\treturn false\n\t}\n\treturn !a.ExpirationTime.IsZero()\n}\n\n// IsExpired compares the ACLToken.ExpirationTime against the passed t to","sourceCodeStart":801,"sourceCodeEnd":837,"githubUrl":"https://github.com/hashicorp/nomad/blob/482b49bf1aec006f089bcfc7e632d8f6ac303e5e/nomad/structs/acl.go#L801-L837","documentation":"Nomad ACL tokens support an expiration TTL and, once created, a computed expiration time. During token update, the Validate/merge logic compares the incoming token against the existing one and rejects any mutation of expiration fields, because expiry is immutable after creation.","triggerScenarios":"Calling the ACL token update API (ACL.UpsertTokens / token Update RPC) with a token whose ExpirationTime differs from the stored token's ExpirationTime, e.g. copying an old token struct and altering ExpirationTime, or round-tripping a token where the server recomputed the time.","commonSituations":"Operators trying to extend or shorten a token's life by editing its expiration via the update endpoint; client code that copies a GET'd token, mutates expiration fields, and PUTs it back; tooling that normalizes timestamps and introduces a slight time drift so the times no longer compare equal.","solutions":["Remove the ExpirationTime (and ExpirationTTL) fields from the update request so only mutable fields are changed","Create a new token with the desired expiration instead of updating the existing one","If you must keep sending the full struct, send ExpirationTime exactly as returned by the server (same instant, equal time comparison)"],"exampleFix":"// before\ntoken.ExpirationTime = time.Now().Add(24 * time.Hour)\nclient.ACLTokens().Update(token, nil)\n// after\nnewToken := &structs.ACLToken{Name: token.Name, Policies: token.Policies, ExpirationTTL: 24 * time.Hour}\nclient.ACLTokens().Create(newToken, nil)","handlingStrategy":"validation","validationCode":"func canUpdate(existing, incoming *structs.ACLToken) error {\n  if incoming.ExpirationTime != nil && existing.ExpirationTime != nil &&\n    !existing.ExpirationTime.Equal(*incoming.ExpirationTime) {\n    return errors.New(\"expiration is immutable: create a new token instead\")\n  }\n  return nil\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Never mutate ExpirationTime/ExpirationTTL on fetched tokens before update calls","Build update requests from a minimal struct containing only mutable fields","Treat token expiry changes as create-new-token operations"],"tags":["nomad","acl","validation","immutable-field"],"backgroundTag":"immutable-field-update","analyzedSha":"482b49bf1aec006f089bcfc7e632d8f6ac303e5e","analyzedAt":"2026-09-04T07:54:14.808Z","contentChangedAt":"2026-09-04T07:54:14.808Z","schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}