{"record":{"id":"359351f49f89cf09","repo":"oauth2-proxy/oauth2-proxy","slug":"unable-to-extract-username-from-userinfo-endpoint","errorCode":null,"errorMessage":"unable to extract username from userinfo endpoint: %v","messagePattern":"unable to extract username from userinfo endpoint: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"providers/srht.go","lineNumber":98,"sourceCode":"\t\tSetHeader(\"Content-Type\", \"application/json\").\n\t\tSetHeader(\"Authorization\", \"Bearer \"+s.AccessToken).\n\t\tWithBody(bytes.NewBufferString(`{\"query\": \"{ me { username, email } }\"}`)).\n\t\tDo().\n\t\tUnmarshalSimpleJSON()\n\tif err != nil {\n\t\tlogger.Errorf(\"failed making request %v\", err)\n\t\treturn err\n\t}\n\n\temail, err := json.GetPath(\"data\", \"me\", \"email\").String()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to extract email from userinfo endpoint: %v\", err)\n\t}\n\ts.Email = email\n\n\tusername, err := json.GetPath(\"data\", \"me\", \"username\").String()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to extract username from userinfo endpoint: %v\", err)\n\t}\n\ts.PreferredUsername = username\n\ts.User = username\n\n\treturn nil\n}\n\n// ValidateSession validates the AccessToken\nfunc (p *SourceHutProvider) ValidateSession(ctx context.Context, s *sessions.SessionState) bool {\n\treturn validateToken(ctx, p, s.AccessToken, makeOIDCHeader(s.AccessToken))\n}\n","sourceCodeStart":80,"sourceCodeEnd":110,"githubUrl":"https://github.com/oauth2-proxy/oauth2-proxy/blob/33c2eb92dea78204f7a18bc2dfdbccc220f39257/providers/srht.go#L80-L110","documentation":"Same EnrichSession flow in providers/srht.go: after successfully extracting email, the provider reads data.me.username; if that path is missing or not a string, it returns \"unable to extract username from userinfo endpoint: %v\". It populates session.PreferredUsername and session.User, so failure here leaves the session without a username.","triggerScenarios":"EnrichSession on the Sourcehut provider gets a 200 JSON response where data.me.username is absent, null, or non-string — e.g. truncated response, error object under 'errors' instead of 'data', or a proxy stripping fields.","commonSituations":"Token scope too narrow for account metadata; SourceHut GraphQL returning {\"errors\":[...]} due to an expired/revoked token while HTTP status stays 200; network middleboxes (corporate proxy) mangling the body; wrong instance URL (sr.ht vs custom SourceHut deployment) with different schema.","solutions":["Inspect the wrapped %v message and the raw response body; a GraphQL 'errors' array with 200 status is the usual culprit.","Re-authenticate: an expired/revoked access token can yield error-shaped responses; ensure token refresh happens before EnrichSession.","Verify requested scopes include the fields needed for username and that the configured srht API base URL matches your instance.","Add a check for a GraphQL errors key in the response before path extraction and surface the upstream error message."],"exampleFix":"// before\nusername, err := json.GetPath(\"data\", \"me\", \"username\").String()\n// after\nif errs := json.Get(\"errors\"); errs != nil {\n    return fmt.Errorf(\"sourcehut API returned errors: %v\", errs)\n}\nusername, err := json.GetPath(\"data\", \"me\", \"username\").String()","handlingStrategy":"try-catch","validationCode":"var probe map[string]any\njson.NewDecoder(resp.Body).Decode(&probe)\nif e, ok := probe[\"errors\"]; ok { /* GraphQL error payload despite HTTP 200 */ _ = e }\nif m, ok := probe[\"data\"].(map[string]any); ok {\n    if me, ok := m[\"me\"].(map[string]any); ok {\n        if _, ok := me[\"username\"].(string); !ok { /* username missing */ }\n    }\n}","typeGuard":"func hasUsername(body map[string]any) bool {\n    d, ok := body[\"data\"].(map[string]any)\n    if !ok { return false }\n    me, ok := d[\"me\"].(map[string]any)\n    if !ok { return false }\n    u, ok := me[\"username\"].(string)\n    return ok && u != \"\"\n}","tryCatchPattern":"if err := p.EnrichSession(ctx, s, tok); err != nil {\n    if strings.Contains(err.Error(), \"unable to extract username\") {\n        // token likely expired/revoked or scope missing; trigger re-auth and log body\n        return fmt.Errorf(\"sourcehut username unavailable (re-auth suggested): %w\", err)\n    }\n    return err\n}","preventionTips":["Detect GraphQL 'errors' arrays in 200 responses before path extraction.","Ensure token refresh runs so EnrichSession never uses an expired access token.","Confirm the requested scopes cover account username fields on your SourceHut instance.","Log the full userinfo body on enrichment failure to distinguish schema vs auth problems."],"tags":["json","userinfo","oauth2"],"backgroundTag":"unexpected-response-shape","analyzedSha":"33c2eb92dea78204f7a18bc2dfdbccc220f39257","analyzedAt":"2026-09-06T08:51:53.077Z","contentChangedAt":"2026-09-06T08:51:53.077Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}