Sign in before the first call, not after a 401 that never comes

Logging in failed on both the Web App and the Phone App with PocketBase's
{"data":{},"message":"The requested resource wasn't found.","status":404}.
The login itself succeeded; the profile fetch right after it — GET /api/me
— was what 404ed, and the web app renders the relayed body on the login
form, so it read as a rejected sign-in.

PocketBase answers a record read it will not allow with 404 rather than
401: it hides the record instead of refusing the credentials. The pb
client only re-authenticated on a 401, so with no cached token the first
request went out carrying no Authorization header at all, came back 404,
and the retry never fired. Nothing ever tried again — the server kept
404ing long after PocketBase was healthy.

The cache is empty in exactly the two cases that matter: a startup where
the up-front Authenticate failed because PocketBase wasn't up yet, which
main.go treats as non-fatal on purpose so a superadmin can still log in
and fix the connection; and a Reconfigure from the panel, which clears
the token so the new credentials get used.

So acquire the token before the first attempt rather than hoping for a
401 to prompt it, across all four superuser paths. Bad credentials now
surface as the authentication failure they are instead of masquerading
as a missing record. With no service account configured there is nothing
to acquire and the call proceeds as before, since the endpoints that need
superuser access already answer 503 on their own.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
tajniak81andClaude Opus 5 committed 2026-08-29 10:33:33 +02:00
1 parent 12ec10a797
commit fa569c4030
2 files changed
+228 -2

No files matched your search

+36 -2
View File
@@ -134,9 +134,34 @@ func (c *Client) currentToken() string {
return c.token
}
// do executes a request, attaching the auth token. On a 401 it re-authenticates
// once and retries.
// ensureToken acquires a superuser token when none is cached yet, so that a
// superuser call never goes out unauthenticated.
//
// It matters because PocketBase answers a record read it will not allow with
// 404, not 401 — it hides the record rather than refusing the credentials. So a
// request sent with no Authorization header comes back "The requested resource
// wasn't found", the 401 retries below never fire, and the caller is told the
// record is missing when the truth is that this client never signed in. The
// cache is empty in exactly the cases that matter: a startup where Authenticate
// failed because PocketBase wasn't up yet (deliberately non-fatal), and a
// Reconfigure, which clears the token so the new credentials are used.
//
// With no service account configured there is nothing to acquire, so the call
// proceeds as before — the endpoints that need superuser access answer 503 on
// their own.
func (c *Client) ensureToken(ctx context.Context) error {
if c.currentToken() != "" || !c.Configured() {
return nil
}
return c.Authenticate(ctx)
}
// do executes a request, attaching the auth token — signing in first if there
// isn't one yet. On a 401 it re-authenticates once and retries.
func (c *Client) do(ctx context.Context, method, path string, payload any) ([]byte, error) {
if err := c.ensureToken(ctx); err != nil {
return nil, err
}
raw, status, err := c.attempt(ctx, method, path, payload)
if err != nil {
return nil, err
@@ -191,6 +216,9 @@ func (c *Client) attempt(ctx context.Context, method, path string, payload any)
// PocketBase's own validation errors to the client verbatim (user/organization
// management) use this; handlers that want Go errors use the typed CRUD helpers.
func (c *Client) Raw(ctx context.Context, method, path string, payload any) ([]byte, int, error) {
if err := c.ensureToken(ctx); err != nil {
return nil, 0, err
}
raw, status, err := c.attempt(ctx, method, path, payload)
if err != nil {
return nil, 0, err
@@ -396,6 +424,9 @@ func (c *Client) Delete(ctx context.Context, collection, id string) error {
// superuser (this project's collections have no public access rules).
func (c *Client) GetFile(ctx context.Context, collection, recordID, filename string) ([]byte, string, error) {
path := "/api/files/" + collection + "/" + recordID + "/" + filename
if err := c.ensureToken(ctx); err != nil {
return nil, "", err
}
raw, ctype, status, err := c.attemptGetFile(ctx, path)
if err != nil {
return nil, "", err
@@ -448,6 +479,9 @@ func (c *Client) RequestVerification(ctx context.Context, collection, email stri
// fields (e.g. the users.avatar upload), which PocketBase doesn't accept as
// plain JSON. Pass fileField == "" to send only the plain fields.
func (c *Client) UpdateMultipart(ctx context.Context, collection, id string, fields map[string]string, fileField, filename string, fileContent []byte) error {
if err := c.ensureToken(ctx); err != nil {
return err
}
status, err := c.attemptMultipart(ctx, collection, id, fields, fileField, filename, fileContent)
if status == http.StatusUnauthorized {
if aerr := c.Authenticate(ctx); aerr != nil {