{"record":{"id":"465fa11e4c4f04db","repo":"tailscale/tailscale","slug":"errnosession","errorCode":"errNoSession","errorMessage":"no-browser-session","messagePattern":"no-browser-session","errorType":"error_code","errorClass":null,"httpStatus":null,"severity":"info","filePath":"client/web/auth.go","lineNumber":76,"sourceCode":"\tcase s.isExpired(now):\n\t\treturn false // expired\n\t}\n\treturn true\n}\n\n// isExpired reports true if s is expired.\n// 2023-10-05: Sessions expire by default 30 days after creation.\nfunc (s *browserSession) isExpired(now time.Time) bool {\n\treturn !s.Created.IsZero() && now.After(s.expires())\n}\n\n// expires reports when the given session expires.\nfunc (s *browserSession) expires() time.Time {\n\treturn s.Created.Add(sessionCookieExpiry)\n}\n\nvar (\n\terrNoSession          = errors.New(\"no-browser-session\")\n\terrNotUsingTailscale  = errors.New(\"not-using-tailscale\")\n\terrTaggedRemoteSource = errors.New(\"tagged-remote-source\")\n\terrTaggedLocalSource  = errors.New(\"tagged-local-source\")\n\terrNotOwner           = errors.New(\"not-owner\")\n)\n\n// getSession retrieves the browser session associated with the request,\n// if one exists.\n//\n// An error is returned in any of the following cases:\n//\n//   - (errNotUsingTailscale) The request was not made over tailscale.\n//\n//   - (errNoSession) The request does not have a session.\n//\n//   - (errTaggedRemoteSource) The source is remote (another node) and tagged.\n//     Users must use their own user-owned devices to manage other nodes'\n//     web clients.","sourceCodeStart":58,"sourceCodeEnd":94,"githubUrl":"https://github.com/tailscale/tailscale/blob/6e0912f97994f927632b34ae9e63b53d6516a6ac/client/web/auth.go#L58-L94","documentation":"errNoSession is a sentinel error from the Tailscale web client (client/web/auth.go) meaning the request carries no usable browser session. getSession returns it when the request has no session cookie at all, when the cookie value is not in the in-memory browserSessions map, when the session was created by a different node/user, or when the session expired (30 days by default). It is the normal 'not logged in yet' signal that drives the web client to its login flow.","triggerScenarios":"Calling a web client handler before completing the browser-based login; cookie deleted or not sent (different browser, incognito, cross-origin fetch without credentials); the tailscale/web client process restarted so the in-memory sync.Map session store is empty; the session hit sessionCookieExpiry; or the source node/user changed since login (machine logged out and back in as a different identity).","commonSituations":"First visit to the web UI; long-lived tab whose 30-day session expired; server restart wiping sessions while the browser still holds the cookie; automated clients that hit web client routes without ever performing the login handshake.","solutions":["Complete the web client login flow to obtain a fresh session cookie before calling protected endpoints","Ensure every request sends the session cookie (same browser profile, credentials: 'include' for fetch/XHR)","If the web client process restarted, log in again - sessions are in-memory only","If the node was re-logged-in as a different identity, log in again so the session is re-bound to the current node/user"],"exampleFix":"// before\nhttp.HandleFunc(\"/api/\", s.authed(func(w, r) {...})) // 500 on first visit\n\n// after\nhttp.HandleFunc(\"/api/\", func(w http.ResponseWriter, r *http.Request) {\n\t_, _, _, err := s.getSession(r)\n\tif errors.Is(err, errNoSession) {\n\t\thttp.Redirect(w, r, \"/login\", http.StatusTemporaryRedirect)\n\t\treturn\n\t}\n\t// proceed\n})","handlingStrategy":"try-catch","validationCode":"// before hitting protected web-client routes, check the cookie exists\nif _, err := r.Cookie(\"tailscale-webclient-session\"); errors.Is(err, http.ErrNoCookie) {\n    http.Redirect(w, r, \"/login\", http.StatusTemporaryRedirect)\n    return\n}","typeGuard":"func isNoSession(err error) bool {\n    return err != nil && errors.Is(err, errNoSession) // within package web; externally: err.Error() == \"no-browser-session\"\n}","tryCatchPattern":"sess, whois, status, err := s.getSession(r)\nswitch {\ncase errors.Is(err, errNoSession):\n    // expected state: send the user through the login flow, do not log as 5xx\n    http.Redirect(w, r, \"/login\", http.StatusTemporaryRedirect)\n    return\ncase err != nil:\n    http.Error(w, err.Error(), http.StatusInternalServerError)\n    return\n}","preventionTips":["Treat errNoSession as a redirect trigger, not a server error","Ensure front-end fetch/XHR calls include credentials so the session cookie travels","Expect all sessions to be lost on web-client restart (in-memory store) and re-authenticate","Handle the 30-day sessionCookieExpiry in long-lived dashboards by re-login on 401-equivalent"],"tags":["go","tailscale","web-client","authentication","session-cookie"],"backgroundTag":"missing-session-cookie","analyzedSha":"6e0912f97994f927632b34ae9e63b53d6516a6ac","analyzedAt":"2026-08-18T08:17:25.280Z","contentChangedAt":"2026-08-18T08:17:25.280Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}