{"record":{"id":"f11e2107601c9d6e","repo":"usememos/memos","slug":"16","errorCode":"16","errorMessage":"authentication required","messagePattern":"authentication required","errorType":"http","errorClass":null,"httpStatus":401,"severity":"error","filePath":"server/router/api/v1/v1.go","lineNumber":120,"sourceCode":"\trouteResolver, err := newGatewayRouteResolver()\n\tif err != nil {\n\t\treturn errors.Wrap(err, \"failed to build gateway route resolver\")\n\t}\n\n\tgatewayAuthMiddleware := func(next runtime.HandlerFunc) runtime.HandlerFunc {\n\t\treturn func(w http.ResponseWriter, r *http.Request, pathParams map[string]string) {\n\t\t\tctx := r.Context()\n\n\t\t\tauthHeader := r.Header.Get(\"Authorization\")\n\t\t\tresult := authorizer.Authenticate(ctx, authHeader)\n\n\t\t\t// An unresolved path yields an empty procedure, which CheckAccess\n\t\t\t// treats as protected: authenticated callers pass and anonymous ones\n\t\t\t// are refused. Failing closed keeps a routing gap from becoming an\n\t\t\t// access-control gap.\n\t\t\tprocedure, _ := routeResolver.resolveRequest(r)\n\t\t\tif err := authorizer.CheckAccess(ctx, procedure, result); err != nil {\n\t\t\t\thttp.Error(w, `{\"code\": 16, \"message\": \"authentication required\"}`, http.StatusUnauthorized)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\t// Apply the identity to the context (no-op for permitted anonymous requests).\n\t\t\tif result != nil {\n\t\t\t\tr = r.WithContext(auth.ApplyToContext(ctx, result))\n\t\t\t}\n\n\t\t\tnext(w, r, pathParams)\n\t\t}\n\t}\n\n\t// Create gRPC-Gateway mux with auth middleware.\n\tgwMux := runtime.NewServeMux(\n\t\truntime.WithMarshalerOption(runtime.MIMEWildcard, newGatewayMarshaler()),\n\t\truntime.WithMiddlewares(gatewayAuthMiddleware),\n\t)\n\tif err := v1pb.RegisterInstanceServiceHandlerServer(ctx, gwMux, s); err != nil {","sourceCodeStart":102,"sourceCodeEnd":138,"githubUrl":"https://github.com/usememos/memos/blob/14d757ce1fb31c78590f374bc042f8dbedbc20d7/server/router/api/v1/v1.go#L102-L138","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nconst res = await fetch(\"/memos.api.v1.MemoService/ListMemos\", {\n  method: \"POST\",\n  headers: {\"Content-Type\": \"application/json\"},\n  body: JSON.stringify({}),\n}); // 401 {\"code\":16,...}\n\n// after\nconst res = await fetch(\"/memos.api.v1.MemoService/ListMemos\", {\n  method: \"POST\",\n  headers: {\n    \"Content-Type\": \"application/json\",\n    \"Authorization\": `Bearer ${await getValidAccessToken()}`,\n  },\n  body: JSON.stringify({}),\n});","handlingStrategy":"validation","validationCode":"async function ensureAuth(url: string, getToken: () => string | null) {\n  const token = getToken();\n  if (!token) throw new Error(\"sign in before calling \" + url);\n  return new Request(url, { headers: { Authorization: `Bearer ${token}` } });\n}\n// build every protected request through this helper so the header is never omitted","typeGuard":"function isUnauthenticated(resp: Response, body: { code?: number }): boolean {\n  return resp.status === 401 || body.code === 16;\n}","tryCatchPattern":"try {\n  await client.listMemos({});\n} catch (e: any) {\n  if (e?.code === 16 /* UNAUTHENTICATED */) {\n    await refreshAccessToken(); // or redirect to sign-in\n    return client.listMemos({}); // retry once with the new token\n  }\n  throw e;\n}","preventionTips":["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."],"tags":["auth","http","grpc-gateway","unauthenticated","acl"],"backgroundTag":null,"analyzedSha":"14d757ce1fb31c78590f374bc042f8dbedbc20d7","analyzedAt":"2026-08-15T09:27:36.538Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}