Supabase Studio 错误处理模式:数据层分类 + ErrorMatcher 排障组件的完整实现
Supabase Studio(apps/studio)针对失败 API 请求采用了一套「数据层分类、展示层只读」的错误处理模式:正则匹配只发生在数据层的 handleError 中,UI 组件 ErrorMatcher 拿到的是已经被分类成具体错误类的对象,直接渲染对应的排障步骤(重启项目、排障文档、AI 调试)。读完本文,你可以掌握该模式的完整调用链、每个关键文件的职责,以及如何按仓库约定为一种新的错误类型接入自动排障能力。
模式总览:分类发生在数据层
这套模式的核心约定是:组件永远不做正则匹配。整个错误流转链路如下:
handleError() → 抛出 ConnectionTimeoutError → React Query 捕获
→ ErrorMatcher 读取错误类实例 → 渲染对应的 troubleshooting 组件
- 分类由数据层的
handleError(位于 apps/studio/data/fetchers.ts)完成:它把错误消息与ERROR_PATTERNS中的正则逐一比对,命中则抛出对应的错误子类(例如ConnectionTimeoutError extends ResponseError)。 - 展示层的 ErrorMatcher 只对错误实例做
instanceof查找,从ERROR_MAPPINGS中取出映射,O(1) 级别完成,绝不触碰正则。 - 同一错误类型在不同页面可以有不同的标题(
title永远由调用方提供),错误映射本身不感知页面上下文。
关键文件与职责
| 文件 | 职责 |
|---|---|
| apps/studio/data/error-patterns.ts | { pattern, ErrorClass } 数组——所有正则只住在这里 |
| apps/studio/types/api-errors.ts | 错误类、KnownErrorType 联合类型、ClassifiedError 类型 |
| apps/studio/components/interfaces/ErrorHandling/ErrorMatcher.tsx | 展示组件——查找错误映射并渲染排障步骤 |
| apps/studio/components/interfaces/ErrorHandling/ErrorMatcher.utils.ts | getMappingForError():对错误实例做类匹配,返回映射或 null |
| apps/studio/components/interfaces/ErrorHandling/error-mappings.tsx | Map<错误类, { id, Troubleshooting: ComponentType }>——错误类到排障组件的注册表 |
| apps/studio/components/interfaces/ErrorHandling/errorMappings/ConnectionTimeout.tsx | 参考级排障组件示例 |
| apps/studio/components/interfaces/ErrorHandling/TroubleshootingSections.tsx | 可复用的手风琴步骤组件(重启数据库 / 排障文档 / AI 调试) |
| apps/studio/components/interfaces/ErrorHandling/TroubleshootingAccordion.tsx | 带遥测(telemetry)的手风琴包装器 |
数据层:handleError 如何分类错误
正则注册表:用 Map 杜绝重复
apps/studio/data/error-patterns.ts 定义了类型与注册表:
type ErrorConstructor = new (
...args: ConstructorParameters<typeof ResponseError>
) => ClassifiedError
export interface ErrorPattern {
pattern: RegExp
ErrorClass: ErrorConstructor
}
/**
* 用 Map 保证每个错误类只能出现一次——重复在结构上就不可能,
* 而不只是靠测试来兜底。
*/
const ERROR_PATTERN_MAP = new Map<ErrorConstructor, RegExp>([
[ConnectionTimeoutError, /CONNECTION\s+TERMINATED\s+TO\s+CONNECTION\s+TIMEOUT/i],
])
export const ERROR_PATTERNS: ErrorPattern[] = Array.from(ERROR_PATTERN_MAP.entries()).map(
([ErrorClass, pattern]) => ({ ErrorClass, pattern })
)
两个值得注意的细节:
- 用
Map<ErrorClass, RegExp>作为内部存储,对外再展开为ErrorPattern[]。以类为 key 意味着同一个错误类不可能注册两条正则,注释里明确写了这是「by construction(结构性)而非靠测试」的约束。配套测试 apps/studio/data/error-patterns.test.ts 进一步验证了每个模式确实能匹配到样例消息、且一个消息最多命中一个模式。 - 正则大小写不敏感且允许空白变体,例如当前的连接超时模式
CONNECTION\s+TERMINATED\s+DUE\s+TO\s+CONNECTION\s+TIMEOUT(带/i),能覆盖数据库返回消息中空格数量的差异。
错误类:携带完整响应上下文的 ResponseError 子类
apps/studio/types/api-errors.ts 中,每个已知错误类都是 ResponseError 的子类,并带一个 as const 的 errorType 字面量:
export type KnownErrorType = 'connection-timeout'
export class ConnectionTimeoutError extends ResponseError {
readonly errorType = 'connection-timeout' as const
constructor(
message: string | undefined,
code?: number,
requestId?: string,
retryAfter?: number,
requestPathname?: string,
metadata?: ErrorMetadata,
formattedError?: string
) {
super(message, code, requestId, retryAfter, requestPathname, metadata, formattedError)
}
}
// 未命中任何模式的 API 错误统一归入此类
export class UnknownAPIResponseError extends ResponseError {
readonly errorType = 'unknown' as const
// ...同样的构造参数
}
export type ClassifiedError = ConnectionTimeoutError | UnknownAPIResponseError
从源码结构看,ResponseError(apps/studio/types/base)统一承载了 HTTP 层的 code、requestId、retryAfter、requestPathname、metadata 与 formattedError——这些字段正是后续「联系支持」工单能带上完整上下文的基础。
handleError 的匹配与抛出逻辑
apps/studio/data/fetchers.ts 中 handleError 在解析出 errorMessage 后(约 L210-L231):
if (errorMessage) {
const matched = ERROR_PATTERNS.find(({ pattern }) => pattern.test(errorMessage))
throw matched
? new matched.ErrorClass(
errorMessage, code, requestId, retryAfter, requestPathname, metadata, formattedError
)
: new UnknownAPIResponseError(
errorMessage, code, requestId, retryAfter, requestPathname, metadata, formattedError
)
}
要点:
- 命中模式 → 抛出对应的具体错误类实例;未命中 → 抛出
UnknownAPIResponseError兜底,保证进入 UI 的 API 错误始终是ResponseError的已知子类。 - 在
new matched.ErrorClass(...)之前,handleError会逐个校验error.code、error.requestId、error.retryAfter、error.requestPathname、error.metadata、error.formattedError的类型(L192-L208),确保抛出的实例字段干净、类型正确。
展示层:ErrorMatcher 如何渲染
组件接口与行为
ErrorMatcher.tsx 的 props(比文档示例多一个 fallback):
interface ErrorMatcherProps {
title: string // 显示在错误卡片头部,由调用方提供
error: string | { message: string } // 传 React Query 的完整 error 对象,而不是 .message
supportFormParams?: SupportFormParams // 支持表单 URL 的强类型参数(projectRef、category 等)
className?: string
/** 错误未被分类、没有专属排障步骤时显示的兜底节点 */
fallback?: ReactNode
}
内部逻辑只有三步:
const message = typeof error === 'string' ? error : error.message
const mapping = getMappingForError(error)
const Troubleshooting = mapping?.Troubleshooting
然后渲染 ui-patterns 包中的 ErrorDisplay(错误卡片 + 支持表单入口),把 Troubleshooting 组件或 fallback 作为子内容传入。
匹配实现:instanceof,而非正则
ErrorMatcher.utils.ts 全文只有几行:
export function getMappingForError(error: unknown): ErrorMapping | null {
const isResponseError = error instanceof ResponseError
if (!isResponseError) return null
for (const [ErrorClass, mapping] of ERROR_MAPPINGS) {
if (error instanceof ErrorClass) return mapping
}
return null
}
- 非
ResponseError(比如普通的TypeError)直接返回 null,ErrorMatcher会走通用错误卡片(或fallback); ERROR_MAPPINGS(error-mappings.tsx)是Map<ErrorConstructor, ErrorMapping>,当前只注册了ConnectionTimeoutError → { id: 'connection-timeout', Troubleshooting: ConnectionTimeoutTroubleshooting }一条。因为 key 是错误类本身,映射的 id 与KnownErrorType一一对应。
内建遥测
ErrorMatcher 与排障组件自带埋点,事件名与触发时机:
| 事件 | 触发点 | 携带信息 |
|---|---|---|
dashboard_error_created |
错误卡片渲染时(onRender),且通过 isDashboardErrorSampled() 采样 |
source: 'error_display'、errorType、hasTroubleshooting |
inline_error_troubleshooter_exposed |
命中映射、排障组件曝光时 | errorType |
inline_error_troubleshooter_step_clicked |
TroubleshootingAccordion 步骤展开/收起 | errorType、step、stepTitle、expanded |
inline_error_troubleshooter_action_clicked |
点击卡片支持入口 / 重启 / 文档 / AI 调试按钮 | errorType、ctaType(contact_support、restart_db、troubleshooting_guide、ask_ai) |
TroubleshootingAccordion 通过 stepTitles prop(步骤号 → 标题)把标题写进埋点,步骤 id 采用 step-${number} 命名,并支持 defaultExpandedStep 指定默认展开项。
参考实现:ConnectionTimeout 排障组件
errorMappings/ConnectionTimeout.tsx 是仓库中的参考样板,三步排障:
const ERROR_TYPE = 'connection-timeout'
const BUILD_PROMPT = () =>
`The user is encountering connection timeout errors. The error message is:
"CONNECTION TERMINATED DUE TO CONNECTION TIMEOUT". What are the most likely
causes of this issue and how can the user resolve it?`
export function ConnectionTimeoutTroubleshooting() {
const { openSidebar } = useSidebarManagerSnapshot()
const aiSnap = useAiAssistantStateSnapshot()
return (
<TroubleshootingAccordion
errorType={ERROR_TYPE}
stepTitles={{
1: 'Try restarting your project',
2: 'Try our troubleshooting guide',
3: 'Debug with AI',
}}
>
<RestartDatabaseTroubleshootingSection number={1} errorType={ERROR_TYPE} />
<TroubleshootingGuideSection
number={2}
errorType={ERROR_TYPE}
href={`${DOCS_URL}/guides/troubleshooting/failed-to-run-sql-query-connection-terminated-due-to-connection-timeout`}
description="Follow step-by-step instructions for diagnosing connection timeout issues."
/>
<FixWithAITroubleshootingSection
number={3}
errorType={ERROR_TYPE}
onDebugWithAI={(prompt) => {
openSidebar(SIDEBAR_KEYS.AI_ASSISTANT)
aiSnap.newChat({ initialMessage: prompt })
}}
buildPrompt={BUILD_PROMPT}
/>
</TroubleshootingAccordion>
)
}
其中两个关键设计:
- 重启步骤默认自带对话框。TroubleshootingSections.tsx 里的
RestartDatabaseTroubleshootingSection未提供onRestartProject时,内部维护showDialog状态并渲染 RestartProjectDialog(restartType="database"),排障组件因此不需要外部接线。 - AI 调试走 zustand snapshot。组件内部直接调用
useAiAssistantStateSnapshot()与useSidebarManagerSnapshot()打开 AI 助手侧栏并预填buildPrompt()生成的消息,而不是把onDebugWithAI之类的回调 prop 暴露给上层调用者。
调用方使用方式
在页面组件中,把 React Query 的完整 error 对象传给 ErrorMatcher:
{
isError && (
<ErrorMatcher
title="Failed to load tables"
error={error}
supportFormParams={{ projectRef }}
/>
)
}
title由调用方给出——同一错误类型在不同页面标题不同;supportFormParams类型为Partial<SupportFormUrlKeys>,自动补全支持projectRef、orgSlug、category、subject、message、error、sid等字段;支持表单 URL 由SupportForm.utils.tsx的createSupportFormUrl()统一构建。
新增一种错误映射:四步接入
官方流程(完整文档见 apps/studio/components/interfaces/ErrorHandling/README.md),假设新增 your-error 类型:
第 1 步:在 types/api-errors.ts 添加错误类
export type KnownErrorType = 'connection-timeout' | 'your-error'
export class YourError extends ResponseError {
readonly errorType = 'your-error' as const
}
export type ClassifiedError = ConnectionTimeoutError | FailedToRetrieveProjectsError | YourError
第 2 步:在 data/error-patterns.ts 注册正则
import { YourError } from 'types/api-errors'
// 加入 ERROR_PATTERN_MAP:
[YourError, /YOUR_ERROR_PATTERN/i],
handleError 会自动拾取——此后任何匹配该模式的 API 错误都会以 YourError 实例的形式抛出。
第 3 步:创建 errorMappings/YourError.tsx,参照 ConnectionTimeout 样板:
import { TroubleshootingAccordion } from '../TroubleshootingAccordion'
import {
FixWithAITroubleshootingSection,
TroubleshootingGuideSection,
} from '../TroubleshootingSections'
import { DOCS_URL } from '@/lib/constants'
const ERROR_TYPE = 'your-error'
const BUILD_PROMPT = () => `Describe the issue for the AI assistant.`
export function YourErrorTroubleshooting() {
return (
<TroubleshootingAccordion
errorType={ERROR_TYPE}
stepTitles={{ 1: 'Troubleshooting guide', 2: 'Debug with AI' }}
>
<TroubleshootingGuideSection
number={1}
errorType={ERROR_TYPE}
href={`${DOCS_URL}/guides/...`}
/>
<FixWithAITroubleshootingSection
number={2}
errorType={ERROR_TYPE}
buildPrompt={BUILD_PROMPT}
// onDebugWithAI: 通过 state/ai-assistant-state 的 snapshot 打开侧栏并 newChat
/>
</TroubleshootingAccordion>
)
}
第 4 步:注册到 error-mappings.tsx
import { YourErrorTroubleshooting } from './errorMappings/YourError'
import { YourError } from '@/types/api-errors'
export const ERROR_MAPPINGS = new Map<ErrorConstructor, ErrorMapping>([
// ...existing
[YourError, { id: 'your-error', Troubleshooting: YourErrorTroubleshooting }],
])
至此完成——ErrorMatcher 会自动拾取新映射,无需改动组件本身。
可复用的排障步骤组件
| 组件 | Props |
|---|---|
RestartDatabaseTroubleshootingSection |
number、errorType、onRestartProject?(不传则内部弹出重启对话框) |
TroubleshootingGuideSection |
number、errorType、href、title?、description? |
FixWithAITroubleshootingSection |
number、errorType、buildPrompt、onDebugWithAI? |
三者都是 AccordionItem 形式,value 为 step-${number},与外层 TroubleshootingAccordion 的 type="single" collapsible 手风琴行为配合,保证任意时刻只有一个步骤展开,且每次展开/收起都会上报埋点。
常见反模式(What NOT to do)
Skill 文档 .claude/skills/studio-error-handling/SKILL.md 明确列出了六条红线,逐条对应到上面的实现约束:
- 不要把
error.message传给ErrorMatcher——必须传完整error对象,否则错误类信息丢失,getMappingForError的instanceof判断必然失败,永远落到通用错误卡片。 - 不要在
error-mappings.tsx里放正则——正则只属于 data/error-patterns.ts,这是「分类在数据层」这一约定的边界。 - 不要用
Object.assign给 error 对象打errorType标记——应抛出正确的ResponseError子类,让类型系统(ClassifiedError联合)和instanceof匹配自然生效。 - 不要用裸 URL 字符串做支持入口——应传
supportFormParams={{ projectRef }},由createSupportFormUrl()生成带完整上下文的表单链接。 - 不要把页面标题放进错误映射——标题属于
<ErrorMatcher>调用方,错误映射应保持与页面无关、可跨页面复用。 - 不要给排障组件加
onDebugWithAI、onRestartProject等回调 prop(例外是RestartDatabaseTroubleshootingSection的可选覆盖)——AI 助手、侧栏等能力应在组件内部通过 hook(useSidebarManagerSnapshot、useAiAssistantStateSnapshot)接线,保持调用方零接线。
小结
这套模式的分工可以概括为三句话:正则只在数据层(error-patterns.ts + handleError)、类型即契约(KnownErrorType / ClassifiedError / errorType 字面量贯穿三层)、展示层只做查找与渲染(ErrorMatcher + 注册表 ERROR_MAPPINGS)。新增一种错误类型只需四处小改动,且每一步都有 TypeScript 类型与 Map 键约束兜底,避免了 UI 代码里散落正则的常见腐化路径。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0622
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00