{"record":{"id":"2a683fc2a89dae07","repo":"wtfutil/wtf","slug":"couldn-t-get-token","errorCode":null,"errorMessage":"Couldn't get token","messagePattern":"Couldn't get token","errorType":"http","errorClass":null,"httpStatus":403,"severity":"error","filePath":"modules/spotifyweb/widget.go","lineNumber":49,"sourceCode":"\tStatus      string\n}\n\n// Widget is the struct used by all WTF widgets to transfer to the main widget controller\ntype Widget struct {\n\tview.TextWidget\n\n\tInfo\n\n\tclient      *spotify.Client\n\tclientChan  chan *spotify.Client\n\tplayerState *spotify.PlayerState\n\tsettings    *Settings\n}\n\nfunc authHandler(w http.ResponseWriter, r *http.Request) {\n\ttok, err := auth.Token(state, r)\n\tif err != nil {\n\t\thttp.Error(w, \"Couldn't get token\", http.StatusForbidden)\n\t}\n\tif st := r.FormValue(\"state\"); st != state {\n\t\thttp.NotFound(w, r)\n\t}\n\t// use the token to get an authenticated client\n\tclient := auth.NewClient(tok)\n\t_, err = fmt.Fprintf(w, \"Login Completed!\")\n\tif err != nil {\n\t\treturn\n\t}\n\ttempClientChan <- &client\n}\n\n// NewWidget creates a new widget for WTF\nfunc NewWidget(tviewApp *tview.Application, redrawChan chan bool, pages *tview.Pages, settings *Settings) *Widget {\n\tredirectURI = \"http://localhost:\" + settings.callbackPort + \"/callback\"\n\n\tauth = spotify.NewAuthenticator(redirectURI, spotify.ScopeUserReadCurrentlyPlaying, spotify.ScopeUserReadPlaybackState, spotify.ScopeUserModifyPlaybackState)","sourceCodeStart":31,"sourceCodeEnd":67,"githubUrl":"https://github.com/wtfutil/wtf/blob/bb838c1ccb0f0f3223690df44afdec663d622881/modules/spotifyweb/widget.go#L31-L67","documentation":"The spotifyweb module's OAuth2 callback handler (authHandler) calls auth.Token(state, r) to exchange the authorization response for a token during the Spotify authentication flow. If the token exchange fails, it responds with HTTP 403 \"Couldn't get token\" via http.Error. Note the handler continues execution after http.Error (no return), so auth.NewClient is still called with a possibly-nil token, which can itself cause a subsequent nil-pointer panic.","triggerScenarios":"The browser callback hits the local auth server but auth.Token fails: Spotify returns an error parameter in the redirect (user denied access), the code/state mismatch, or the request lacks a valid code. Also fires when the state FormValue doesn't match the expected state, though that path responds with 404 without returning.","commonSituations":"User cancels or rejects the Spotify authorization consent screen; the OAuth redirect arrives at a different port than the one the auth server listens on; stale browser tab replays an old callback after state was regenerated on a restart; network errors during the token exchange with Spotify's API.","solutions":["Re-run the Spotify authentication flow from scratch: restart wtfutil and open the auth URL fresh, then approve access on the consent screen","Check that the redirect URI and port in the Spotify app settings and the module config match the local auth server port","Make sure you complete authorization in one session — an old/stale callback tab will have an outdated state parameter","Fix the handler to return after http.Error and handle a nil token so a failed exchange doesn't cascade into a nil-pointer panic: add `return` after each http.Error/NotFound call"],"exampleFix":"// before\ntok, err := auth.Token(state, r)\nif err != nil {\n    http.Error(w, \"Couldn't get token\", http.StatusForbidden)\n}\nif st := r.FormValue(\"state\"); st != state {\n    http.NotFound(w, r)\n}\nclient := auth.NewClient(tok)\n// after\ntok, err := auth.Token(state, r)\nif err != nil {\n    http.Error(w, \"Couldn't get token\", http.StatusForbidden)\n    return\n}\nif st := r.FormValue(\"state\"); st != state {\n    http.NotFound(w, r)\n    return\n}\nclient := auth.NewClient(tok)","handlingStrategy":"fallback","validationCode":"// before trusting the flow, confirm the callback request is well-formed:\nfunc validCallback(r *http.Request, expectedState string) bool {\n    if r.FormValue(\"error\") != \"\" {\n        return false\n    }\n    return r.FormValue(\"state\") == expectedState && r.FormValue(\"code\") != \"\"\n}","typeGuard":null,"tryCatchPattern":"tok, err := auth.Token(state, r)\nif err != nil {\n    http.Error(w, \"Couldn't get token\", http.StatusForbidden)\n    return\n}\nclient := auth.NewClient(tok)\nctx := context.Background()\nif _, err := client.CurrentUser(ctx); err != nil {\n    log.Printf(\"spotify auth incomplete, retry flow: %v\", err)\n    return\n}","preventionTips":["Complete the Spotify consent screen in the same browser session that initiated it; don't reuse stale callback tabs","Ensure the Spotify app's redirect URI exactly matches the local callback URL (scheme, host, port)","Approve the requested scopes — denying consent produces an error callback","Restart the auth flow after any wtfutil restart, since the state parameter is regenerated per run"],"tags":["oauth","spotify","http","network"],"backgroundTag":"oauth-token-exchange-failed","analyzedSha":"bb838c1ccb0f0f3223690df44afdec663d622881","analyzedAt":"2026-09-03T17:02:45.030Z","contentChangedAt":"2026-09-03T17:02:45.030Z","schemaVersion":2},"datasetVersion":"2026-09-11T00:17:11.886Z"}