usememos/memos · error
16
16
Error message
authentication required
What it means
This is the gRPC-Gateway HTTP middleware wrapper in server/router/api/v1/v1.go: it authenticates the request (Authorization header via the authorizer), resolves the procedure, and calls CheckAccess. When access is denied — no valid credential for a protected procedure — it responds 401 with a gRPC-style JSON body {"code": 16, "message": "authentication required"}; code 16 is gRPC UNAUTHENTICATED. Per the comment, unresolved paths fail closed: anonymous callers are refused so a routing gap cannot become an access-control gap.
Source
Thrown at server/router/api/v1/v1.go:120
routeResolver, err := newGatewayRouteResolver()
if err != nil {
return errors.Wrap(err, "failed to build gateway route resolver")
}
gatewayAuthMiddleware := func(next runtime.HandlerFunc) runtime.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request, pathParams map[string]string) {
ctx := r.Context()
authHeader := r.Header.Get("Authorization")
result := authorizer.Authenticate(ctx, authHeader)
// An unresolved path yields an empty procedure, which CheckAccess
// treats as protected: authenticated callers pass and anonymous ones
// are refused. Failing closed keeps a routing gap from becoming an
// access-control gap.
procedure, _ := routeResolver.resolveRequest(r)
if err := authorizer.CheckAccess(ctx, procedure, result); err != nil {
http.Error(w, `{"code": 16, "message": "authentication required"}`, http.StatusUnauthorized)
return
}
// Apply the identity to the context (no-op for permitted anonymous requests).
if result != nil {
r = r.WithContext(auth.ApplyToContext(ctx, result))
}
next(w, r, pathParams)
}
}
// Create gRPC-Gateway mux with auth middleware.
gwMux := runtime.NewServeMux(
runtime.WithMarshalerOption(runtime.MIMEWildcard, newGatewayMarshaler()),
runtime.WithMiddlewares(gatewayAuthMiddleware),
)
if err := v1pb.RegisterInstanceServiceHandlerServer(ctx, gwMux, s); err != nil {View on GitHub (pinned to 14d757ce1f)
Solutions
- Attach a valid credential: "Authorization: Bearer <access_token>" from SignIn/refresh, or a personal access token in the header the authorizer accepts.
- If the token expired, run the refresh flow (the web client's auth interceptor) or sign in again to obtain a new access token.
- Verify the token is still active (not revoked in user settings) and that no intermediary strips the Authorization header.
- If you hit this on a route you believe is public, check server/router/api/v1/acl_config.go — unauthenticated access must be declared there, and the path must resolve in the route resolver.
Example fix
// before
const res = await fetch("/memos.api.v1.MemoService/ListMemos", {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify({}),
}); // 401 {"code":16,...}
// after
const res = await fetch("/memos.api.v1.MemoService/ListMemos", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${await getValidAccessToken()}`,
},
body: JSON.stringify({}),
}); Defensive patterns
Strategy: validation
Validate before calling
async function ensureAuth(url: string, getToken: () => string | null) {
const token = getToken();
if (!token) throw new Error("sign in before calling " + url);
return new Request(url, { headers: { Authorization: `Bearer ${token}` } });
}
// build every protected request through this helper so the header is never omitted Type guard
function isUnauthenticated(resp: Response, body: { code?: number }): boolean {
return resp.status === 401 || body.code === 16;
} Try / catch
try {
await client.listMemos({});
} catch (e: any) {
if (e?.code === 16 /* UNAUTHENTICATED */) {
await refreshAccessToken(); // or redirect to sign-in
return client.listMemos({}); // retry once with the new token
}
throw e;
} Prevention
- Use the Connect clients in web/src/connect.ts, whose interceptor refreshes expired access tokens automatically.
- Send tokens as "Authorization: Bearer <token>"; verify with a whoami/profile call before long-running scripts.
- Handle 401/code 16 uniformly by refreshing once, then re-prompting for sign-in if refresh fails.
- For routes that must work anonymously, confirm they are declared public in server/router/api/v1/acl_config.go and covered by the route resolver.
When it happens
Trigger: Calling any protected REST/gateway endpoint (memo CRUD, user settings, attachments, etc.) without an Authorization header, with an expired/revoked access token and no valid refresh flow, or with a malformed token ("Authorization: xyz" instead of "Bearer <jwt>"). Also when a request path fails procedure resolution and the caller is anonymous.
Common situations: Access tokens expiring while the client skips the refresh interceptor (web/src/connect.ts handles this for the SPA); PATs revoked or disabled; API scripts hardcoding a stale token; proxies stripping the Authorization header; hitting a newly added route whose path is not yet in the resolver map while unauthenticated.
Understand the failure class
- Authentication and authorization failures — expired tokens, bad credentials, and missing scopes.
Related errors
- Failed to link account. Please sign in to Memos again and re
- too many redirects
- not a HTML page
- wrong image mediatype
- CodeUnauthenticated
AI-assisted analysis of usememos/memos@14d757ce1f (2026-08-15).
Data as JSON: /api/errors/f11e2107601c9d6e.
Report an issue: GitHub.