{"record":{"id":"086140efc8b685a8","repo":"glanceapp/glance","slug":"authentication-response-returned-empty-session-id","errorCode":null,"errorMessage":"authentication response returned empty session ID, status code %d, message '%s'","messagePattern":"authentication response returned empty session ID, status code (.+?), message '(.+?)'","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/glance/widget-dns-stats.go","lineNumber":657,"sourceCode":"\t\tSession struct {\n\t\t\tSID     string `json:\"sid\"`\n\t\t\tMessage string `json:\"message\"`\n\t\t} `json:\"session\"`\n\t}\n\n\tif err := json.Unmarshal(body, &jsonResponse); err != nil {\n\t\treturn \"\", fmt.Errorf(\"parsing authentication response: %v\", err)\n\t}\n\n\tif response.StatusCode != http.StatusOK {\n\t\treturn \"\", fmt.Errorf(\n\t\t\t\"authentication request returned status %s with message '%s'\",\n\t\t\tresponse.Status, jsonResponse.Session.Message,\n\t\t)\n\t}\n\n\tif jsonResponse.Session.SID == \"\" {\n\t\treturn \"\", fmt.Errorf(\n\t\t\t\"authentication response returned empty session ID, status code %d, message '%s'\",\n\t\t\tresponse.StatusCode, jsonResponse.Session.Message,\n\t\t)\n\t}\n\n\treturn jsonResponse.Session.SID, nil\n}\n\nfunc checkPiholeSessionIDIsValid(instanceURL string, client *http.Client, sessionID string) (bool, error) {\n\trequest, err := http.NewRequest(\"GET\", instanceURL+\"/api/auth\", nil)\n\tif err != nil {\n\t\treturn false, fmt.Errorf(\"creating session ID check request: %v\", err)\n\t}\n\trequest.Header.Set(\"x-ftl-sid\", sessionID)\n\n\tresponse, err := client.Do(request)\n\tif err != nil {\n\t\treturn false, err","sourceCodeStart":639,"sourceCodeEnd":675,"githubUrl":"https://github.com/glanceapp/glance/blob/91324e8de762702e97b0ac5c8e36271d644d8642/internal/glance/widget-dns-stats.go#L639-L675","documentation":"Returned by fetchPiholeSessionID when the server answered HTTP 200 yet the parsed session.sid is empty. The API contract was violated: success status without a usable session token, and the message includes the status code and server session.message for diagnosis.","triggerScenarios":"A proxy or middleware returns its own 200 response (e.g. a health-check page) instead of proxying to the real /api/auth; a Pi-hole build/edge version with a changed auth payload; an empty JSON {} response.","commonSituations":"Reverse proxy misroute matching /api/auth to a static backend; unusual Pi-hole fork or beta; load balancer returning a synthetic 200 when the backend is down.","solutions":["curl -i -X POST {url}/api/auth and verify the body contains {\"session\":{\"sid\":\"...\"}}.","Bypass the proxy temporarily (hit Pi-hole directly) to isolate where the 200-without-SID originates.","Update/align the Pi-hole version to a stable v6 release if an edge build changed the payload.","Check proxy routing rules so /api/auth reaches the Pi-hole FTL webserver."],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":"// a 200 without sid means the responder is not real Pi-hole v6 auth\nif sid == \"\" {\n    return errors.New(\"got 200 but no session.sid — check proxy routing to /api/auth\")\n}","typeGuard":null,"tryCatchPattern":"if err != nil && strings.Contains(err.Error(), \"empty session ID\") {\n    // contract violation: log status+message embedded in the error and retry once\n    slog.Error(\"pihole auth contract violated\", \"error\", err)\n    sid, err = fetchPiholeSessionID(url, client, password)\n    if err != nil {\n        return err\n    }\n}","preventionTips":["Route /api/auth to the Pi-hole backend explicitly in proxy config.","Keep Pi-hole on stable v6 releases; re-test auth after upgrades.","Compare a direct (no-proxy) auth request against the proxied one when debugging."],"tags":["auth","pihole","proxy","dns-stats"],"backgroundTag":null,"analyzedSha":"91324e8de762702e97b0ac5c8e36271d644d8642","analyzedAt":"2026-08-15T14:12:54.279Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}