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

  1. Attach a valid credential: "Authorization: Bearer <access_token>" from SignIn/refresh, or a personal access token in the header the authorizer accepts.
  2. If the token expired, run the refresh flow (the web client's auth interceptor) or sign in again to obtain a new access token.
  3. Verify the token is still active (not revoked in user settings) and that no intermediary strips the Authorization header.
  4. 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

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

Related errors


AI-assisted analysis of usememos/memos@14d757ce1f (2026-08-15). Data as JSON: /api/errors/f11e2107601c9d6e. Report an issue: GitHub.