Data Formulator 统一错误处理规范:前后端 API 错误协议、错误码与日志脱敏实战指南 📅 发布时间:2026/9/13 22:50:57 👁 浏览次数: Data Formulator 统一错误处理规范前后端 API 错误协议、错误码与日志脱敏实战指南【免费下载链接】data-formulator Data Formulator is an interactive AI-powered data analysis system makes it easy to connect, explore and visualize data.项目地址: https://gitcode.com/GitHub_Trending/da/data-formulator导读Data FormulatorDF作为一款交互式 AI 数据分析系统其后端同时提供普通 JSON API 与 NDJSON 流式 API前端涉及 Agent 聊天、数据加载、图表生成等多条调用链路。为了让业务错误与基础设施故障在前后端、监控与代理层之间语义一致项目沉淀了一套统一错误处理系统业务/校验错误一律返回 HTTP 200 并在 body 中通过status: error传递仅认证/授权错误使用401/403配套结构化错误码、request_id追踪与日志脱敏。本文以 .cursor/skills/error-handling/SKILL.md 为主干结合 统一错误处理开发规范 与核心源码系统讲解这套协议的契约、后端/前端落地模板、错误码扩展流程、错误分类工具与敏感信息防护读完即可在新增端点或修改错误处理时直接套用。设计哲学为什么业务错误要返回 HTTP 200这是理解整套系统的第一原则也是与常见 REST 实践最大的差异点。层级规范HTTP 状态码业务/校验错误 →200认证/授权错误 →401/403不可控错误 →404/413/500应用层 body非流式/流预检成功 →status: success失败 →status: error结构化错误必须使用error: { code, message, retry }成功数据必须包裹在data字段中{status: success, data: {...}}流内事件已建立的 NDJSON 流使用type区分事件fatal error →{type: error, error: {...}}这样设计有三个核心理由源码注释中亦有明确说明见 errors.py与流式 API 保持一致——NDJSON 流式端点一旦建立连接就始终是 HTTP 200非流式 API 采用相同策略后前后端判错逻辑统一避免代理/WAF/监控误判——业务错误如表不存在、校验失败如果返回 4xx/5xx会被基础设施层误当作故障告警前端判错路径单一——非流式调用通过body.status error检测错误流式调用通过 NDJSONtype error检测流内错误。禁止事项新代码必须遵守不要用 HTTP400/422表达业务校验错误不要把已建立的 NDJSON 流中错误改成{status: error, ...}——流事件必须靠type区分不要在响应体中暴露str(exc)、secret、token、连接串、文件系统敏感路径或堆栈。架构总览前后端各司其职从 SKILL.md 的架构图可以清晰看到两端的分工Frontend Backend ──────── ─────── apiClient.ts errors.py ├── apiRequest() ←── JSON ──── ├── ErrorCode (enum) ├── streamRequest() ←── NDJSON ── └── AppError (exception) └── parseStreamLine() error_handler.py errorCodes.ts ├── register_error_handlers(app) └── getErrorMessage() ├── classify_and_wrap_llm_error() └── stream_error_event() errorHandler.ts └── handleApiError() security/sanitize.py └── classify_llm_error() (internal) MessageSnackbar ← dfSlice.messages后端errors.py定义机器可读的ErrorCode与统一业务异常AppErrorerror_handler.py注册全局 handlerregister_error_handlers、提供 LLM 异常分类器与流式错误事件格式化security/sanitize.py内部提供classify_llm_error与sanitize_error_message前端apiClient.ts提供apiRequest()/streamRequest()/parseStreamLine()errorHandler.ts的handleApiError()统一兜底errorCodes.ts将后端错误码映射到 i18n 文案最终经MessageSnackbar展示给用户。协议快照所有新 API 必须遵守的契约开发任何新建或重构的 DF API先按响应类型选择协议不要混用场景HTTP响应格式非流式成功200{status: success, data: ...}非流式业务/校验错误200{status: error, error: {code, message, retry}}非流式认证/授权错误401/403同样的结构化错误 body流式预检错误200application/json{status: error, error: ...}流式运行中 fatal 错误200NDJSON 行{type: error, error: ...}无 Flask route / 请求体过大 / 未捕获崩溃404/413/500传输层错误两条硬性约束新代码禁止用 HTTP400/422表达业务校验错误流已建立后禁止把 NDJSON 错误转成status: error——此时事件type才是协议判别符。响应工具速查所有工具定义在 error_handler.py开发新 route 时对照选择你想做什么用这个一句话说明返回成功 JSONjson_ok(data)统一成功信封{status:success,data:...}HTTP 200抛出业务/校验错误raise AppError(ErrorCode.XXX, msg)全局 handler 自动捕获HTTP 200认证错误 401/403流建立前校验失败stream_preflight_error(AppError(...))返回application/jsonerror bodyHTTP 200流运行中 fatal erroryield stream_error_event(error)输出一行 NDJSON{type:error,error:{...}}流终止流运行中非致命警告generator 内yield stream_warning_event(msg)输出一行 NDJSON{type:warning,warning:{...}}流继续流运行中非致命警告helper/深层函数内collect_stream_warning(msg)攒到flask.ggenerator 用flush_stream_warnings()统一输出LLM / 外部 API 异常分类classify_and_wrap_llm_error(exc)把原始异常转成安全的AppError后端新增 API 端点HTTP 状态码策略业务和校验错误返回 HTTP 200仅在 body 中用status: error标记。只有以下场景使用非 200401/403—— 认证错误AUTH_REQUIRED、AUTH_EXPIRED、ACCESS_DENIED404—— 无匹配 Flask route413—— WSGI body 超限500—— 未捕获异常程序 bug这些映射定义在 errors.py 的ERROR_CODE_HTTP_STATUS目前仅包含三个认证错误码ERROR_CODE_HTTP_STATUS: dict[str, int] { ErrorCode.AUTH_REQUIRED: 401, ErrorCode.AUTH_EXPIRED: 401, ErrorCode.ACCESS_DENIED: 403, }不在映射中的任何自定义ErrorCode都默认 HTTP 200由AppError.get_http_status()保证见 errors.py。非流式端点标准模板from data_formulator.errors import AppError, ErrorCode from data_formulator.error_handler import json_ok bp.route(/my-endpoint, methods[POST]) def my_endpoint(): content request.get_json() if not content.get(required_field): raise AppError(ErrorCode.INVALID_REQUEST, Missing required_field) try: result do_work(content) except SomeBusinessError as e: raise AppError(ErrorCode.DATA_LOAD_ERROR, Failed to load data) from e except Exception as e: from data_formulator.error_handler import classify_and_wrap_llm_error raise classify_and_wrap_llm_error(e) from e return json_ok(result) # 全局 handler 返回HTTP 200 {status: error, error: {code, message, retry}} # 认证错误AUTH_REQUIRED/AUTH_EXPIRED/ACCESS_DENIED返回 401/403全局 handler 在 error_handler.py 的_handle_app_error中实现根据get_http_status()决定状态码body 统一为{status: error, error: {code, message, retry, request_id}}。实际错误响应形如{ status: error, error: { code: INVALID_REQUEST, message: Missing required_field, retry: false, request_id: 6f8c2a1e-... } }历史格式警示{status: error, message: ...}、error_message、裸{error}、status: ok都是历史格式见 dev-guide 2.4 节。迁移后的apiRequest()/parseApiResponse()不再兼容这些旧格式新代码不要为它们添加兼容分支——先把 route 迁移到json_ok()/AppError再让前端通过apiRequest()消费。流式端点标准模板流式端点的关键约束校验必须在 generator 之外完成。校验失败返回 200 JSON而非 NDJSON因为流一旦建立HTTP 状态码和整体 body 都不可再更改。from data_formulator.errors import AppError, ErrorCode from data_formulator.error_handler import ( classify_and_wrap_llm_error, stream_error_event, stream_preflight_error, ) bp.route(/my-stream, methods[POST]) def my_stream(): if not request.is_json: return stream_preflight_error( AppError(ErrorCode.INVALID_REQUEST, Invalid request) ) content request.get_json() client get_client(content[model]) def generate(): try: for event in agent.run(...): yield json.dumps(event, ensure_asciiFalse) \n except Exception as e: yield stream_error_event(classify_and_wrap_llm_error(e)) return Response(stream_with_context(generate()), mimetypeapplication/x-ndjson)流运行中的错误故意使用{type: error, error: ...}——不能使用顶层status信封因为 HTTP 响应和 NDJSON 事件流已经开始。stream_error_event在 error_handler.py 中实现AppError会序列化为结构化对象非AppError异常则记录服务端日志并返回INTERNAL_ERROR兜底。流内非致命警告警告用于可继续但需要通知用户的场景如某张表不可用、降级到缓存。前端以 toast / snackbar 展示不中断流。在 generator 内直接yield stream_warning_event(...)在无法yield的 helper/深层函数内用collect_stream_warning()攒到flask.g再由 generator 通过flush_stream_warnings()统一取出输出实现见 error_handler.py。前端消费 API 的统一方式非流式调用import { apiRequest } from ../app/apiClient; import { handleApiError } from ../app/errorHandler; try { const { data } await apiRequestResponseType(getUrls().MY_ENDPOINT, { method: POST, body: JSON.stringify(payload), headers: { Content-Type: application/json }, }); } catch (e) { handleApiError(e, MyComponent); }apiRequest()使用双层错误检测见 apiClient.tsHTTP 层!response.ok非 2xx→ 抛出ApiRequestError(code: HTTP_ERROR)仅在认证错误401/403或不可控传输错误时触发Body 层body.status error→ 抛出携带结构化错误信息的ApiRequestError。这是业务错误的主要检测路径——因为大部分应用错误返回 HTTP 200。ApiRequestError还提供了两个派生属性见 apiClient.tsisRetryableerror.retry true和isAuthErrorcode 属于三个认证码供上层决策。流式调用import { streamRequest } from ../app/apiClient; import { handleApiError } from ../app/errorHandler; try { for await (const event of streamRequest(url, options, abortController.signal)) { switch (event.type) { case text_delta: break; case error: // Error arrived mid-stream — show inline in component. break; case done: break; } } } catch (e) { handleApiError(e, MyComponent); }streamRequest()apiClient.ts处理两类预流错误真正的传输层错误500/413 等抛ApiRequestError后端返回200 application/json而非 NDJSON的预检校验错误通过检测content-type: application/json识别后同样抛出ApiRequestError。带回调的高级用法handleApiError(e, MyComponent, { onAuth: () redirectToLogin(), // AUTH_REQUIRED / AUTH_EXPIRED onRetryable: () retryOperation(), // LLM_RATE_LIMIT / LLM_TIMEOUT silent: true, // dont show Snackbar (component handles display) });handleApiError的行为见 errorHandler.tsAbortError直接忽略ApiRequestError优先触发onAuth/onRetryable回调否则经getErrorMessage()提取文案并 dispatch 到dfSlice.messages由MessageSnackbar展示silent: true时组件自行处理显示。加载状态必须显式建模不要用!data/data null推断 loading——失败请求可能合法地让数据为空但加载已结束继续显示 spinner 会造成 UI 假死。请求状态必须显式区分idle、loading、success、empty、error。组件内局部状态优先使用src/app/loadableState.ts提供的LoadableState、errorLoadable、loadingLoadable、successLoadableUI 渲染基于state.status分支详见 dev-guide 4.6 节。迁移与特例边界DF API 消费者应使用apiRequest()/streamRequest()和handleApiError()。直接fetchWithIdentity()仅保留给底层 client helper 和明确的协议例外场景文件下载、blob/CSV 响应、OIDC 重定向、SPA fallback、第三方 URL。以下场景不能机械套用普通 JSON API 规范评审时先确认具体协议见 dev-guide 4.7 节场景规范文件下载 / CSV streaming成功响应是文件流错误响应仍尽量用安全的结构化 body避免str(exc)暴露SPA fallback非/api/路径无匹配 Flask route 时继续返回前端入口OIDC redirect flow部分错误通过 redirect query param 传回前端展示外部 URL fetch第三方传输语义不适用 DF API 约定已建立的流式响应只能通过 NDJSONtype: error事件传递不能再改 HTTP 状态码新增错误码后端 前端 i18n 三步新增一个结构化错误码需要同步修改三处仓库中已有完整示例可对照 errors.py、errorCodes.ts 与 errors.json1. 后端—— 添加到 errors.py 的ErrorCodeMY_NEW_ERROR MY_NEW_ERROR无需添加 HTTP 映射——默认 HTTP 200。只有认证错误码才需要加入ERROR_CODE_HTTP_STATUS。2. 前端映射—— 添加到 errorCodes.ts 的ERROR_CODE_I18N_MAPMY_NEW_ERROR: errors.myNewError,3. 翻译—— 同时添加到两个 locale 文件en/errors.jsonmyNewError: English messagezh/errors.jsonmyNewError: 中文消息前端通过getErrorMessage(apiError)errorCodes.ts优先使用本地 i18n 翻译缺失时回退到后端英文message。错误分类工具DB、Connector、LLM 各司其职系统为不同错误来源提供了专门分类器避免在每个 endpoint 里手写字符串匹配。数据库/工作区错误tables.py表 CRUD 端点使用 classify_and_raise_db_errorfrom data_formulator.routes.tables import classify_and_raise_db_error tables_bp.route(/my-table-op, methods[POST]) def my_table_op(): try: result workspace.do_something() return jsonify({status: success, data: result}) except Exception as e: classify_and_raise_db_error(e)它将常见 DB 错误映射为对应的AppErrorcode由全局 handler 以 HTTP 200 返回ACCESS_DENIED例外为 403原始错误特征ErrorCodeHTTPTable does not existTABLE_NOT_FOUND200Table already existsINVALID_REQUEST200Permission deniedACCESS_DENIED403其他CONNECTOR_ERROR200安全规则该方法抛出的 message 绝不从str(error)派生只用预定义的、人工编写的安全文案完整异常仅记录在服务端日志logger.error(..., exc_infoerror)。Connector 错误data_connector.py连接器端点使用classify_and_raise_connector_errorfrom data_formulator.data_connector import classify_and_raise_connector_error except Exception as e: classify_and_raise_connector_error(e, operationpreview)Connector/DataLoader 分类刻意保持简单实现在 connector_errors.py映射到一组稳定的小集合INVALID_REQUEST、CONNECTOR_AUTH_FAILED、AUTH_EXPIRED、ACCESS_DENIED、DB_CONNECTION_FAILED、DB_QUERY_ERROR、DATA_LOAD_ERROR、CONNECTOR_ERROR。除非分类器确实无法覆盖某类别否则不要在 endpoint 内新增局部字符串匹配。LLM / 外部 API 异常分类classify_and_wrap_llm_errorerror_handler.py复用sanitize.py的classify_llm_error生成安全文案再通过正则模式表_LLM_CODE_PATTERNS匹配错误文本以确定ErrorCode与retry标志。典型映射错误特征ErrorCoderetry401/ unauthorized / invalid api keyLLM_AUTH_FAILEDfalse429/ rate limit / quotaLLM_RATE_LIMITtruecontext length / too many tokensLLM_CONTEXT_TOO_LONGfalsemodel not found / decommissionedLLM_MODEL_NOT_FOUNDfalsetimeout / connection refusedLLM_TIMEOUTtrue50x/ bad gateway / service unavailableLLM_SERVICE_ERRORtruecontent filter / responsible ai / safetyLLM_CONTENT_FILTEREDfalse403/ forbidden / access deniedACCESS_DENIEDfalse原始异常文本只保存在detail中用于服务端日志绝不出现在面向客户端的message中。测试用例覆盖了上述映射见 test_error_handler.py。request_id全局兜底与追踪所有AppError、404、413、未捕获500的 JSON 错误体都包含error.request_id同时响应头带X-Request-Id由register_error_handlers注册的before_request/after_request钩子注入见 error_handler.py。用户反馈后端故障时可把这个 ID 提供给运维定位服务端日志。生产环境不返回未捕获异常的原始文本、traceback 或连接串即使在debug 模式下AppError.detail返回客户端前也会经过sanitize_error_message()清洗——剥离文件路径、凭据和完整堆栈帧只保留可操作的错误摘要如ValueError: invalid literal完整日志始终通过logger.exception()写入服务端。已迁移端点参考所有流式端点已统一到该协议EndpointFormatNotes/data-agent-streamingNDJSON stream_error_event()顶层type事件错误用{type:error, error:{...}}/get-recommendation-questionsNDJSON stream_error_event()曾是error: {json}前缀/generate-report-chat纯 NDJSON stream_error_event()曾是 SSEdata: {json}前缀/data-loading-chatNDJSON stream_error_event()已移除str(e)/clean-data-streamNDJSON stream_error_event()曾是\n{json}\n格式非流式端点Endpoint错误格式Notes/chart-insightAppError→ HTTP 200 {status:error, error:{...}}已完全迁移前端用fetchChartInsightrejected reducer所有已迁移端点AppError→ HTTP 200 统一错误体credentials、knowledge、sessions、tables、agents/derive-data、/refine-data、/sort-data、/process-data-on-load、/test-modeljson_ok()/AppError已迁移到新格式协议合约测试见 test_api_error_protocol_contract.py覆盖了流预检 JSON 信封、分析师流式顶层type事件、DB 错误分类默认 HTTP 200 等场景。空 catch 策略哪些.catch(() {})是合法的并非所有空 catch 都是 bug但必须能解释清楚。使用以下决策树用户主动操作删除、刷新、提交→ 必须通知用addMessages或handleApiError()后台 best-effort 加载挂载时的 connector 列表、session 列表→ 可以静默但必须加注释说明为何可忽略RTK thunks→ 必须加.rejectedhandler用addMessagesAbortError→ 用if (action.error?.name ! AbortError)过滤掉RTK 序列化边界易踩坑RTKcreateAsyncThunk内部抛出的错误会经miniSerializeError()序列化为普通 JS 对象{name, message, stack}丢失 class 类型和自定义属性如apiError。调用.unwrap()时.catch(error)拿到的是普通对象而非Error实例因此String(error)或${error}会输出[object Object]。import { extractErrorMessage } from ../app/errorHandler; // ✅ GOOD — 正确提取 message dispatch(loadTable(...)).unwrap() .catch((error) { const msg extractErrorMessage(error); }); // ✅ GOOD — 统一处理 dispatch(loadTable(...)).unwrap() .catch((error) handleApiError(error, my-component)); // ❌ BAD — 普通对象无法正确 String() .catch((error) Failed: ${error}) // → Failed: [object Object]extractErrorMessage()和handleApiError()都已处理 RTK 序列化对象isSerializedError类型守卫见 errorHandler.ts会正确提取.message属性。调试错误传播六步排查法当错误没有到达前端时按顺序检查后端日志—— 错误是否被记录logger.warning/logger.exception响应格式—— 非流式应为{status: error, error: {code: ..., message: ...}}流式应为一行{type: error, error: {code: ..., message: ...}}Content-Type—— 流式必须是application/x-ndjson不是application/json或text/event-stream前端解析器—— 消费者是否在查找data.type error全局 handler—— 确认 app.py 中调用了register_error_handlers(app)蓝图 handler—— 注意 blueprint 级别的errorhandler(Exception)优先级高于全局 handler遗留的message/error_messagebody 在已迁移 API 路径上属于协议违规。日志脱敏两层防御防止敏感数据泄漏服务端日志绝不能泄漏密码、token、API key 或连接串。项目采用纵深防御的两层方案实现见 log_sanitizer.py。第一层显式工具调用点主动脱敏from data_formulator.security.log_sanitizer import ( sanitize_url, sanitize_params, redact_token, ) # 含凭据的 dict → sanitize_params() log.info(Connecting with: %s, sanitize_params(params)) # 可能内嵌凭据的 URL → sanitize_url() logger.info(Issuer: %s, sanitize_url(issuer_url)) # Token/API key → redact_token() logger.debug(Token: %s, redact_token(token))第二层SensitiveDataFilter全局安全网在 app.py 的configure_logging()中注册自动脱敏URL 内嵌凭据://user:passhostBearertokenpasswordxxx、api_keyxxx、secretxxx模式JWT 样式的 base64 字符串eyJ开头 28 位 base64url 字符含敏感 key 的 Python dict reprpassword: value仅在本地调试时可通过LOG_SANITIZEfalse关闭。何时使用何种工具数据工具为什么不能只靠全局 filter含 password key 的 dictsanitize_params()filter 无法识别 dict repr 中任意的 password 值来自配置/env 的 URLsanitize_url()显式更清晰filter 只是兜底Token/key 值redact_token()显式更清晰filter 只是兜底普通文本什么都不用交给 filter 处理边界情况新模块检查清单新增处理凭据或外部服务的模块时审计所有logger.*()调用排查凭据/URL/token 日志dict 用sanitize_params()、URL 用sanitize_url()、token 用redact_token()warning 级日志优先用type(exc).__name__而非str(exc)引入新的凭据 key 名时加入 log_sanitizer.py 的SENSITIVE_KEYS关键文件索引文件用途errors.pyErrorCode枚举 AppError异常 ERROR_CODE_HTTP_STATUS映射error_handler.py全局 handlers、json_ok、classify_and_wrap_llm_error、stream_error_event、stream_warning_event、request_id 中间件log_sanitizer.pysanitize_url、sanitize_params、redact_token、SensitiveDataFiltertables.pyclassify_and_raise_db_error数据库/工作区错误分类data_connector.pyclassify_and_raise_connector_error连接器错误分类入口connector_errors.pyDataLoader/connector 简单错误分类sanitize.pyclassify_llm_error内部、sanitize_error_messageapiClient.tsapiRequest、streamRequest、parseStreamLine、ApiRequestError、parseApiResponseerrorHandler.tshandleApiError、extractErrorMessageerrorCodes.tsERROR_CODE_I18N_MAP、getErrorMessageerrors.json错误消息翻译en/zh 各一份test_error_handler.pyLLM 分类、流错误事件、全局 handler 的单元测试test_api_error_protocol_contract.py协议合约测试预检信封、顶层 type 事件、DB 错误分类测试要求速览后端测试需覆盖非认证AppError断言 HTTP 200 status errorerror.code匹配认证错误断言 401/403404/413/500 保持非 200JSON 错误响应断言error.request_id与响应头X-Request-Id对齐流式运行中错误断言 NDJSONtype: error错误响应不得包含原始 secret、token、连接串或内部异常文本。前端测试需覆盖parseApiResponse对status: success、结构化错误、legacy 格式拒绝streamRequest对预检错误与 NDJSON error 事件handleApiError对 AbortError、auth/retry 回调、silent 模式errorCodes.ts对已知 code 翻译和未知 code fallback。最后在改动任何 API 错误行为前先通读 统一错误处理开发规范涉及日志、凭据、外部服务或 DataLoader 时再参考 日志脱敏开发规范。如果引入了新的错误处理模式或约定记得回填更新本 skill 文件与相关 dev-guides。【免费下载链接】data-formulator Data Formulator is an interactive AI-powered data analysis system makes it easy to connect, explore and visualize data.项目地址: https://gitcode.com/GitHub_Trending/da/data-formulator创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考