{"record":{"id":"777a27175a78b577","repo":"juanfont/headscale","slug":"s-confirm-returned-status-d-s","errorCode":null,"errorMessage":"%s confirm returned status %d: %s","messagePattern":"(.+?) confirm returned status (.+?): (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"integration/scenario.go","lineNumber":1331,"sourceCode":"\tif err != nil {\n\t\treturn \"\", nil, fmt.Errorf(\"%s creating confirm request: %w\", hostname, err)\n\t}\n\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\n\tconfirmResp, err := hc.Do(req)\n\tif err != nil {\n\t\treturn \"\", nil, fmt.Errorf(\"%s sending confirm request: %w\", hostname, err)\n\t}\n\tdefer confirmResp.Body.Close()\n\n\tconfirmBytes, err := io.ReadAll(confirmResp.Body)\n\tif err != nil {\n\t\treturn \"\", nil, fmt.Errorf(\"%s reading confirm response: %w\", hostname, err)\n\t}\n\n\tif confirmResp.StatusCode != http.StatusOK {\n\t\treturn string(confirmBytes), nil, fmt.Errorf( //nolint:err113\n\t\t\t\"%s confirm returned status %d: %s\",\n\t\t\thostname, confirmResp.StatusCode, string(confirmBytes),\n\t\t)\n\t}\n\n\treturn string(confirmBytes), nil, nil\n}\n\nvar errParseAuthPage = errors.New(\"parsing auth page\")\n\nfunc (s *Scenario) runHeadscaleRegister(userStr string, body string) error {\n\t// see api.go HTML template\n\tcodeSep := strings.Split(body, \"</code>\")\n\tif len(codeSep) != 2 {\n\t\treturn errParseAuthPage\n\t}\n\n\tkeySep := strings.Split(codeSep[0], \"--auth-id \")","sourceCodeStart":1313,"sourceCodeEnd":1349,"githubUrl":"https://github.com/juanfont/headscale/blob/565fd254d06c4c7f9a8cad1714a43445c79ba420/integration/scenario.go#L1313-L1349","documentation":"The confirm POST completed but returned a non-200 status. The error embeds both the status code and the entire response body, so the server's own error text is visible — e.g. 403 from a bad/expired CSRF token, 500 from a server-side registration failure.","triggerScenarios":"POSTing the scraped CSRF token after it expired or did not match the session; registering a node whose key was already consumed; headscale returns 500 because the user does not exist or the DB rejected the node.","commonSituations":"Delay between fetching and submitting the form let the token expire; the `--user` passed to the flow does not exist; policy rejects the node registration.","solutions":["Read the response body embedded in the error message — it names the actual server-side reason.","403/CSRF: re-run the whole fetch-then-submit sequence without delay in between.","500: check headscale logs for the stack trace and fix the underlying registration failure (missing user, DB error).","Confirm the auth key/auth-id is still valid when the confirm POST lands."],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"// Branch on status instead of failing blindly.\nif confirmResp.StatusCode != http.StatusOK {\n    body, _ := io.ReadAll(confirmResp.Body)\n    if confirmResp.StatusCode >= 500 {\n        log.Printf(\"%s server error on confirm: %s\", hostname, body) // investigate\n    }\n    return string(body), nil, fmt.Errorf(\"%s confirm returned status %d\", hostname, confirmResp.StatusCode)\n}","preventionTips":["Submit the confirm POST immediately after scraping the token to avoid expiry.","Always read the embedded body — it carries the server's reason.","Create the target user before registering nodes into it."],"tags":["http","integration","registration","csrf"],"backgroundTag":null,"analyzedSha":"565fd254d06c4c7f9a8cad1714a43445c79ba420","analyzedAt":"2026-08-15T13:12:30.133Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}