{"record":{"id":"8708a7e06bc3c134","repo":"juanfont/headscale","slug":"tag-should-be-lowercase","errorCode":null,"errorMessage":"tag should be lowercase","messagePattern":"tag should be lowercase","errorType":"validation","errorClass":null,"httpStatus":400,"severity":"error","filePath":"hscontrol/api/v1/tags.go","lineNumber":12,"sourceCode":"package apiv1\n\nimport (\n\t\"errors\"\n\t\"strings\"\n)\n\n// ACL tag validation, shared by the node and pre-auth-key resources. These\n// reproduce the gRPC validateTag checks and messages.\nvar (\n\terrTagMissingPrefix = errors.New(\"tag must start with the string 'tag:'\")\n\terrTagNotLowercase  = errors.New(\"tag should be lowercase\")\n\terrTagHasSpaces     = errors.New(\"tags must not contain spaces\")\n)\n\n// validateTag reports whether an ACL tag is well formed: it must start with\n// \"tag:\", be lowercase, and contain no spaces.\nfunc validateTag(tag string) error {\n\tswitch {\n\tcase !strings.HasPrefix(tag, \"tag:\"):\n\t\treturn errTagMissingPrefix\n\tcase strings.ToLower(tag) != tag:\n\t\treturn errTagNotLowercase\n\tcase len(strings.Fields(tag)) > 1:\n\t\treturn errTagHasSpaces\n\tdefault:\n\t\treturn nil\n\t}\n}\n","sourceCodeStart":1,"sourceCodeEnd":30,"githubUrl":"https://github.com/juanfont/headscale/blob/565fd254d06c4c7f9a8cad1714a43445c79ba420/hscontrol/api/v1/tags.go#L1-L30","documentation":"Error from submitConfirmForm when the OIDC confirmation interstitial HTML does not contain an action=\" attribute. The function scrapes the rendered form to POST the confirmation, so a missing action attribute means the page is not the expected form (wrong page, error page, or template change).","triggerScenarios":"submitConfirmForm receiving HTML that is not the confirmation interstitial — e.g. an error page or login page was rendered instead, or a headscale template change removed/renamed the action attribute.","commonSituations":"OIDC callback rendering an error instead of the confirmation form, headscale version drift changing the interstitial markup, or a session/cookie mismatch causing re-render of a different page.","solutions":["Log the passed htmlBody to identify which page was actually returned.","Check headscale OIDC logs for why the interstitial was not rendered (failed state/nonce validation).","If you modified templates, ensure the form keeps action=\"...\" with a double-quoted attribute.","Ensure the same HTTP client (with CSRF cookie) is used for the callback and the confirm POST."],"exampleFix":null,"handlingStrategy":"validation","validationCode":"if !strings.Contains(htmlBody, `action=\"`) {\n    log.Printf(\"unexpected interstitial page: %.500s\", htmlBody)\n    return fmt.Errorf(\"%s page is not the confirmation form\", hostname)\n}","typeGuard":"func isConfirmForm(htmlBody string) bool {\n    return strings.Contains(htmlBody, `action=\"`) && strings.Contains(htmlBody, \"csrf\")\n}","tryCatchPattern":null,"preventionTips":["Log the HTML body whenever interstitial scraping fails.","Keep headscale templates and this scraper in sync — the double-quoted action attribute is load-bearing.","Reuse the same cookied HTTP client for callback and confirm POST so the interstitial is actually served."],"tags":["integration","oidc","html-parsing","login","template"],"backgroundTag":null,"analyzedSha":"565fd254d06c4c7f9a8cad1714a43445c79ba420","analyzedAt":"2026-08-15T13:12:30.133Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}