You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Supabase多用户并发访问触发RLS 42501错误求助

问题描述

基于Python(>=3.9) + FastAPI开发,通过Vercel部署,用Supabase实现数据读写。单用户访问正常,多用户并发时,除一名用户外其余均返回RLS权限错误:

{
    "error": "Failed to get history: {'code': '42501', 'details': None, 'hint': None, 'message': 'new row violates row-level security policy for table \"chat_history\"'}"
}

表结构与RLS策略

CREATE TABLE chat_history (
  chat_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
  user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
  messages JSONB NOT NULL DEFAULT '[]',
  updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX idx_chat_history_user_id ON chat_history(user_id);


CREATE POLICY "Users can view their own chat history"
ON public.chat_history
FOR SELECT
TO authenticated
USING (
  user_id = auth.uid()
);


CREATE POLICY "Users can insert their own chat history"
ON public.chat_history
FOR INSERT
TO authenticated
WITH CHECK (
  user_id = auth.uid()
);


CREATE POLICY "Users can update their own chat history"
ON public.chat_history
FOR UPDATE
TO authenticated
USING (
  user_id = auth.uid()
)
WITH CHECK (
  user_id = auth.uid()
);


CREATE POLICY "Users can delete their own chat history"
ON public.chat_history
FOR DELETE
TO authenticated
USING (
  user_id = auth.uid()
);

核心Python代码

获取/创建聊天记录函数

async def _get_or_create_chat_record(self, user_id: str):
    """Get or create a chat history record for the user"""
    # Check if a chat record already exists
    try:
        response = supabase.table("chat_history").select("*").eq("user_id", user_id).execute()
        logger.info(f"Checking chat history for user {user_id}: {response}")
        if response.data and len(response.data) > 0:
            # Chat record exists, return it
            return response.data[0]
        else:
            # Create new chat record
            insert_response = supabase.table("chat_history").insert({
                "user_id": user_id,
                "messages": []  # Empty array of messages
            }).execute()
            
            if not insert_response.data:
                logger.error(f"Failed to create chat history record: {insert_response}")
                raise Exception("Failed to create chat history record")
            
            logger.info(f"Insert response {insert_response.data[0]} for user {user_id}")
            return insert_response.data[0]
    except Exception as e:
        logger.error(f"Error getting or creating chat record for user {user_id}: {str(e)}")
        raise

Auth依赖函数

async def get_user_supabase(request: Request):
    """
    Dependency to extract and validate Supabase Auth user from Authorization header.
    """
    auth_header = request.headers.get("Authorization")
    if not auth_header or not auth_header.startswith("Bearer "):
        raise HTTPException(status_code=401, detail="Missing or invalid Authorization header")
    access_token = auth_header.split(" ")[1]
    try:
        user_response = supabase.auth.get_user(jwt=access_token)
        if not user_response or not user_response.user:
            raise HTTPException(status_code=401, detail="Invalid or expired token")
        return {
            "supabase": supabase,
            "user": user_response.user
        }
    except Exception as e:
        raise HTTPException(status_code=401, detail=f"Authentication error: {str(e)}")

API路由

@app.post("/chat", response_model=ChatResponse)
async def chat(request: ChatRequest, supabase_client = Depends(get_user_supabase)):

补充信息

关闭RLS后多用户并发正常,说明问题出在RLS策略的上下文传递逻辑上。


解决方案

1. 为每个请求创建独立的Supabase客户端

Supabase Python客户端非线程安全,并发请求共享同一客户端时,Auth token会被覆盖,导致RLS验证时auth.uid()拿到其他用户的ID,触发权限错误。修改get_user_supabase函数,为每个验证通过的用户创建独立客户端实例:

from supabase import create_client, Client
import os

# 全局读取Supabase配置(从环境变量获取)
SUPABASE_URL = os.getenv("SUPABASE_URL")
SUPABASE_KEY = os.getenv("SUPABASE_SERVICE_ROLE_KEY")

async def get_user_supabase(request: Request):
    auth_header = request.headers.get("Authorization")
    if not auth_header or not auth_header.startswith("Bearer "):
        raise HTTPException(status_code=401, detail="Missing or invalid Authorization header")
    access_token = auth_header.split(" ")[1]
    
    try:
        # 为当前用户创建独立的Supabase客户端
        user_supabase: Client = create_client(SUPABASE_URL, SUPABASE_KEY)
        # 绑定当前用户的access token
        user_supabase.auth.set_session(access_token)
        
        user_response = user_supabase.auth.get_user()
        if not user_response or not user_response.user:
            raise HTTPException(status_code=401, detail="Invalid or expired token")
        
        return {
            "supabase": user_supabase,
            "user": user_response.user
        }
    except Exception as e:
        raise HTTPException(status_code=401, detail=f"Authentication error: {str(e)}")

2. 修复并发竞态问题

原_get_or_create_chat_record的"查询-插入"逻辑存在竞态:并发时多个请求可能同时查到无记录,重复插入导致RLS错误。改用Supabase的upsert原子操作完成逻辑:

async def _get_or_create_chat_record(self, user_id: str, supabase: Client):
    """Get or create a chat history record for the user"""
    try:
        # 原子化upsert操作,避免竞态
        response = supabase.table("chat_history")\
            .upsert({
                "user_id": user_id,
                "messages": []
            }, on_conflict="user_id")\
            .select("*")\
            .execute()
        
        if not response.data:
            logger.error(f"Failed to get or create chat history for user {user_id}: {response}")
            raise Exception("Failed to get or create chat history record")
        
        logger.info(f"Got chat history for user {user_id}: {response.data[0]}")
        return response.data[0]
    except Exception as e:
        logger.error(f"Error getting or creating chat record for user {user_id}: {str(e)}")
        raise

需先给user_id添加唯一约束:

ALTER TABLE chat_history ADD CONSTRAINT unique_user_id UNIQUE (user_id);

3. 验证上下文传递

确保每个请求的Supabase客户端都正确绑定当前用户的token,这样auth.uid()才能返回正确的用户ID,匹配RLS策略中的user_id = auth.uid()条件。

内容的提问来源于stack exchange,提问作者Ajin T

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.13 02:50:54