"Session expired" on a screen where nobody had signed in

The first Windows install reached the setup screen, typed an
installation code, and was told the session had expired. There was no
session. The code had been minted on a different head office, and the
server said so - 401 bad_token, "That installation code is not valid.
Ask for a new one." - and the client threw the message away, because it
mapped every 401 to the string "session expired".

A 401 on a call that carried a session is a session problem. A 401 on a
call that carried none is about the request, and the server's message is
the answer. The client now tells them apart by whether it sent a token.
Two tests, one for each side of the rule.

Also: a launcher for pointing a Windows PC at a head office on the LAN,
with the two settings that needs and a comment saying why neither is
acceptable outside a demo.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGcjxF1cNLcuwc3DAPcnfj
This commit is contained in:
2026-09-11 15:31:55 +05:30
parent 719ba2c7f5
commit 70c447873d
3 changed files with 70 additions and 1 deletions

View File

@@ -180,9 +180,16 @@ func (c *Client) send(ctx context.Context, method, path string, raw []byte, out
switch {
case resp.StatusCode == http.StatusUnauthorized && e.Error == "token_expired":
return errTokenExpired
case resp.StatusCode == http.StatusUnauthorized:
case resp.StatusCode == http.StatusUnauthorized && tok != "":
// A 401 on a call we sent a session with: the session is the problem.
return ErrUnauthorized
case resp.StatusCode >= 400:
// Every other 4xx/5xx - including a 401 on a call that carried NO
// session, such as redeeming an installation code - is about the
// request, and the server wrote its message for exactly this moment.
// Mapping those to "session expired" told an installer their session
// had lapsed on a screen where they had never signed in, and hid
// "That installation code is not valid" behind it.
msg := e.Message
if msg == "" {
msg = fmt.Sprintf("%s %s: %s", method, path, resp.Status)

View File

@@ -6,6 +6,7 @@ import (
"errors"
"net/http"
"net/http/httptest"
"strings"
"testing"
)
@@ -137,3 +138,43 @@ func TestVisitorIDIsPathEscaped(t *testing.T) {
t.Errorf("path = %q", got)
}
}
// Redeeming an installation code is the one call a fresh PC makes before it
// has any session. When the server refuses it - wrong code, wrong head office -
// it answers 401 with a message written for the installer. That message must
// reach them: "session expired" on a screen where nobody has signed in sent a
// real installer looking for a login problem that did not exist.
func TestARefusedInstallationCodeSaysWhyNotSessionExpired(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("Authorization") != "" {
t.Errorf("enrol must not carry a session, got %q", r.Header.Get("Authorization"))
}
fail(w, http.StatusUnauthorized, "bad_token",
"That installation code is not valid. Ask for a new one.")
}))
t.Cleanup(srv.Close)
c := New(srv.URL) // deliberately no session
_, err := c.Bootstrap(context.Background(), "KWFH5S-EH46LT-EE4X47-OSOH7D")
if err == nil {
t.Fatal("a refused code must be an error")
}
if errors.Is(err, ErrUnauthorized) {
t.Fatalf("a refused code is not a session problem, got %v", err)
}
if !strings.Contains(err.Error(), "installation code is not valid") {
t.Fatalf("the server's own words should reach the installer, got %v", err)
}
}
// The other side of the same rule: a 401 on a call that DID carry a session is
// a session problem, and must still read as one.
func TestARejectedSessionStillReadsAsSessionExpired(t *testing.T) {
c := serve(t, func(w http.ResponseWriter, r *http.Request) {
fail(w, http.StatusUnauthorized, "unauthorized", "Sign in again.")
})
err := c.do(context.Background(), http.MethodGet, "/api/auth/me", nil, nil)
if !errors.Is(err, ErrUnauthorized) {
t.Fatalf("a 401 with a session should be ErrUnauthorized, got %v", err)
}
}