From 641704de40e56201aa86ea12715db750505628b3 Mon Sep 17 00:00:00 2001 From: "alan.cl" <1165243776@qq.com> Date: Wed, 1 Jul 2026 19:56:47 +0800 Subject: [PATCH] fix(cors): configure allowed origins and request errors Add shared CORS configuration for the web and model API servers, avoid wildcard credentials, and show safe frontend request error messages. Co-Authored-By: Claude Opus 4.8 --- configs/dbgpt-app-config.example.toml | 2 + configs/dbgpt-proxy-openai.toml | 2 + configs/dbgpt-proxy-siliconflow-mysql.toml | 2 + configs/dbgpt-proxy-siliconflow.toml | 2 + configs/dbgpt-proxy-tongyi.toml | 2 + packages/dbgpt-app/src/dbgpt_app/config.py | 11 +++++ .../dbgpt-app/src/dbgpt_app/dbgpt_server.py | 7 +-- .../src/dbgpt/model/cluster/apiserver/api.py | 7 +-- .../dbgpt-core/src/dbgpt/model/parameter.py | 10 +++++ packages/dbgpt-core/src/dbgpt/util/fastapi.py | 35 +++++++++++++++ .../src/dbgpt/util/tests/test_fastapi.py | 43 ++++++++++++++++++ web/components/common/prompt-bot.tsx | 5 +-- web/locales/en/common.ts | 5 +++ web/locales/zh/common.ts | 5 +++ web/utils/request.ts | 45 ++++++++++++++----- 15 files changed, 159 insertions(+), 24 deletions(-) create mode 100644 packages/dbgpt-core/src/dbgpt/util/tests/test_fastapi.py diff --git a/configs/dbgpt-app-config.example.toml b/configs/dbgpt-app-config.example.toml index a8fe64877..2c1d04378 100644 --- a/configs/dbgpt-app-config.example.toml +++ b/configs/dbgpt-app-config.example.toml @@ -9,6 +9,8 @@ encrypt_key = "your_secret_key" [service.web] host = "0.0.0.0" port = 5670 +# CORS allowed origins: '*' allows all; set comma-separated origins to restrict. +cors_allowed_origins = "${env:DBGPT_CORS_ALLOWED_ORIGINS:-*}" [service.web.database] type = "sqlite" diff --git a/configs/dbgpt-proxy-openai.toml b/configs/dbgpt-proxy-openai.toml index ad7e2eb69..9d96be846 100644 --- a/configs/dbgpt-proxy-openai.toml +++ b/configs/dbgpt-proxy-openai.toml @@ -8,6 +8,8 @@ encrypt_key = "your_secret_key" [service.web] host = "0.0.0.0" port = 5670 +# CORS allowed origins: '*' allows all; set comma-separated origins to restrict. +cors_allowed_origins = "${env:DBGPT_CORS_ALLOWED_ORIGINS:-*}" [service.web.database] type = "sqlite" diff --git a/configs/dbgpt-proxy-siliconflow-mysql.toml b/configs/dbgpt-proxy-siliconflow-mysql.toml index 1e166e6f8..47ffec21a 100644 --- a/configs/dbgpt-proxy-siliconflow-mysql.toml +++ b/configs/dbgpt-proxy-siliconflow-mysql.toml @@ -8,6 +8,8 @@ encrypt_key = "your_secret_key" [service.web] host = "0.0.0.0" port = 5670 +# CORS allowed origins: '*' allows all; set comma-separated origins to restrict. +cors_allowed_origins = "${env:DBGPT_CORS_ALLOWED_ORIGINS:-*}" [service.web.database] type = "mysql" diff --git a/configs/dbgpt-proxy-siliconflow.toml b/configs/dbgpt-proxy-siliconflow.toml index fbaf8af12..ce5a9a561 100644 --- a/configs/dbgpt-proxy-siliconflow.toml +++ b/configs/dbgpt-proxy-siliconflow.toml @@ -8,6 +8,8 @@ encrypt_key = "your_secret_key" [service.web] host = "0.0.0.0" port = 5670 +# CORS allowed origins: '*' allows all; set comma-separated origins to restrict. +cors_allowed_origins = "${env:DBGPT_CORS_ALLOWED_ORIGINS:-*}" [service.web.agent_context] # Agent context-window budget. Set max_context_tokens to 0 to auto-detect from diff --git a/configs/dbgpt-proxy-tongyi.toml b/configs/dbgpt-proxy-tongyi.toml index 4c29893a6..80587ed56 100644 --- a/configs/dbgpt-proxy-tongyi.toml +++ b/configs/dbgpt-proxy-tongyi.toml @@ -8,6 +8,8 @@ encrypt_key = "your_secret_key" [service.web] host = "0.0.0.0" port = 5670 +# CORS allowed origins: '*' allows all; set comma-separated origins to restrict. +cors_allowed_origins = "${env:DBGPT_CORS_ALLOWED_ORIGINS:-*}" [service.web.agent_context] # Agent context-window budget. Set max_context_tokens to 0 to auto-detect from diff --git a/packages/dbgpt-app/src/dbgpt_app/config.py b/packages/dbgpt-app/src/dbgpt_app/config.py index c7afda253..be24616ed 100644 --- a/packages/dbgpt-app/src/dbgpt_app/config.py +++ b/packages/dbgpt-app/src/dbgpt_app/config.py @@ -378,6 +378,17 @@ class ServiceWebParameters(BaseParameters): default_factory=AgentContextParameters, metadata={"help": _("Agent context-window management configuration")}, ) + cors_allowed_origins: str = field( + default="*", + metadata={ + "help": _( + "Comma-separated allowed CORS origins. Default '*' allows all " + "(any website can read API responses cross-origin). Set to " + "explicit origins to restrict, e.g. " + "http://localhost:3000,https://your-app.com." + ) + }, + ) @dataclass diff --git a/packages/dbgpt-app/src/dbgpt_app/dbgpt_server.py b/packages/dbgpt-app/src/dbgpt_app/dbgpt_server.py index 09ae592e5..95eb0a74c 100644 --- a/packages/dbgpt-app/src/dbgpt_app/dbgpt_server.py +++ b/packages/dbgpt-app/src/dbgpt_app/dbgpt_server.py @@ -15,7 +15,7 @@ from dbgpt.configs.model_config import ( LOGDIR, STATIC_MESSAGE_IMG_PATH, ) -from dbgpt.util.fastapi import create_app, replace_router +from dbgpt.util.fastapi import build_cors_config, create_app, replace_router from dbgpt.util.i18n_utils import _, set_default_language from dbgpt.util.parameter_utils import _get_dict_from_obj from dbgpt.util.system_utils import get_system_info @@ -263,10 +263,7 @@ def run_uvicorn(param: ServiceWebParameters): # https://github.com/encode/starlette/issues/617 cors_app = CORSMiddleware( app=app, - allow_origins=["*"], - allow_credentials=True, - allow_methods=["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"], - allow_headers=["*"], + **build_cors_config(param.cors_allowed_origins), ) log_level = "info" if param.log: diff --git a/packages/dbgpt-core/src/dbgpt/model/cluster/apiserver/api.py b/packages/dbgpt-core/src/dbgpt/model/cluster/apiserver/api.py index cc0a6aed1..a9969368f 100644 --- a/packages/dbgpt-core/src/dbgpt/model/cluster/apiserver/api.py +++ b/packages/dbgpt-core/src/dbgpt/model/cluster/apiserver/api.py @@ -50,7 +50,7 @@ from dbgpt.model.cluster.manager_base import WorkerManager, WorkerManagerFactory from dbgpt.model.cluster.registry import ModelRegistry from dbgpt.model.parameter import ModelAPIServerParameters, WorkerType from dbgpt.util.chat_util import transform_to_sse -from dbgpt.util.fastapi import create_app +from dbgpt.util.fastapi import build_cors_config, create_app from dbgpt.util.tracer import initialize_tracer, root_tracer, trace from dbgpt.util.tracer.tracer_impl import TracerParameters from dbgpt.util.utils import ( @@ -899,10 +899,7 @@ def initialize_apiserver( # https://github.com/encode/starlette/issues/617 cors_app = CORSMiddleware( app=app, - allow_origins=["*"], - allow_credentials=True, - allow_methods=["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"], - allow_headers=["*"], + **build_cors_config(apiserver_params.cors_allowed_origins), ) log_level = "info" if log_config: diff --git a/packages/dbgpt-core/src/dbgpt/model/parameter.py b/packages/dbgpt-core/src/dbgpt/model/parameter.py index c21d10272..fcafa651e 100644 --- a/packages/dbgpt-core/src/dbgpt/model/parameter.py +++ b/packages/dbgpt-core/src/dbgpt/model/parameter.py @@ -144,6 +144,16 @@ class ModelAPIServerParameters(BaseServerParameters): ignore_stop_exceeds_error: Optional[bool] = field( default=False, metadata={"help": _("Ignore exceeds stop words error")} ) + cors_allowed_origins: str = field( + default="*", + metadata={ + "help": _( + "Comma-separated allowed CORS origins for the standalone model " + "API server. Default '*' allows all. Set explicit origins " + "(e.g. http://localhost:3000) to restrict." + ) + }, + ) @dataclass diff --git a/packages/dbgpt-core/src/dbgpt/util/fastapi.py b/packages/dbgpt-core/src/dbgpt/util/fastapi.py index 0ed4cc97a..5a1c7a7ca 100644 --- a/packages/dbgpt-core/src/dbgpt/util/fastapi.py +++ b/packages/dbgpt-core/src/dbgpt/util/fastapi.py @@ -128,3 +128,38 @@ def replace_router(app: FastAPI, router: Optional[APIRouter] = None): app.router = router app.setup() return app + + +def build_cors_config(allow_origins: Optional[str]) -> Dict[str, Any]: + """Build CORS middleware kwargs from a comma-separated origins string. + + Shared by the webserver (``dbgpt-app``) and the model apiserver + (``dbgpt-core``) so both honor the same ``cors_allowed_origins`` setting + without coupling to each other's parameter classes. + + Args: + allow_origins (Optional[str]): Comma-separated allowed origins. ``"*"`` + (or empty/None) allows all origins. Otherwise a literal origin list, + e.g. ``"http://localhost:3000,https://your-app.com"``. + + Returns: + Dict[str, Any]: Keyword arguments for ``CORSMiddleware`` (without + ``app``; the caller supplies ``app``). + + Notes: + - ``"*"`` is returned with ``allow_credentials=False`` because W3C + CORS forbids ``Access-Control-Allow-Origin: *`` together with + ``Access-Control-Allow-Credentials: true``. The previous hardcoded + ``["*"]`` + ``allow_credentials=True`` was an invalid combination. + - An explicit origin list is returned with ``allow_credentials=True`` + so cookie/credential-bearing cross-origin requests keep working. + """ + raw = (allow_origins or "*").strip() + origins = [o.strip() for o in raw.split(",") if o.strip()] + allow_all = not origins or "*" in origins + return { + "allow_origins": ["*"] if allow_all else origins, + "allow_credentials": not allow_all, + "allow_methods": ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"], + "allow_headers": ["*"], + } diff --git a/packages/dbgpt-core/src/dbgpt/util/tests/test_fastapi.py b/packages/dbgpt-core/src/dbgpt/util/tests/test_fastapi.py new file mode 100644 index 000000000..cebec9b82 --- /dev/null +++ b/packages/dbgpt-core/src/dbgpt/util/tests/test_fastapi.py @@ -0,0 +1,43 @@ +"""Tests for ``dbgpt.util.fastapi`` helpers.""" + +from dbgpt.util.fastapi import build_cors_config + +_DEFAULT_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"] + + +def test_wildcard_disables_credentials(): + """``"*"`` must return all origins with credentials disabled (W3C).""" + cfg = build_cors_config("*") + assert cfg["allow_origins"] == ["*"] + assert cfg["allow_credentials"] is False + assert cfg["allow_methods"] == _DEFAULT_METHODS + assert cfg["allow_headers"] == ["*"] + + +def test_empty_or_none_treated_as_wildcard(): + """Empty string or None should fall back to ``"*"`` (safe default).""" + assert build_cors_config("")["allow_origins"] == ["*"] + assert build_cors_config("")["allow_credentials"] is False + assert build_cors_config(None)["allow_origins"] == ["*"] + assert build_cors_config(None)["allow_credentials"] is False + + +def test_explicit_origins_enable_credentials_and_trim(): + """Explicit origin list enables credentials and trims whitespace/blanks.""" + cfg = build_cors_config(" http://localhost:3000 , https://your-app.com , ,") + assert cfg["allow_origins"] == ["http://localhost:3000", "https://your-app.com"] + assert cfg["allow_credentials"] is True + + +def test_mixed_wildcard_treated_as_wildcard(): + """Any wildcard entry disables credentials to keep CORS headers valid.""" + cfg = build_cors_config("*, https://your-app.com") + assert cfg["allow_origins"] == ["*"] + assert cfg["allow_credentials"] is False + + +def test_single_explicit_origin(): + """A single explicit origin still enables credentials.""" + cfg = build_cors_config("https://your-app.com") + assert cfg["allow_origins"] == ["https://your-app.com"] + assert cfg["allow_credentials"] is True diff --git a/web/components/common/prompt-bot.tsx b/web/components/common/prompt-bot.tsx index 977096824..281f39a46 100644 --- a/web/components/common/prompt-bot.tsx +++ b/web/components/common/prompt-bot.tsx @@ -1,6 +1,6 @@ import { sendSpacePostRequest } from '@/utils/request'; import { useRequest } from 'ahooks'; -import { ConfigProvider, FloatButton, Form, List, Popover, Select, Tooltip, message } from 'antd'; +import { ConfigProvider, FloatButton, Form, List, Popover, Select, Tooltip } from 'antd'; import React, { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; @@ -73,9 +73,6 @@ const PromptBot: React.FC = ({ submit, chat_scene }) => { }, { refreshDeps: [current, chat_scene], - onError: err => { - message.error(err?.message); - }, manual: true, }, ); diff --git a/web/locales/en/common.ts b/web/locales/en/common.ts index 143382cce..92f8a784d 100644 --- a/web/locales/en/common.ts +++ b/web/locales/en/common.ts @@ -739,4 +739,9 @@ export const CommonEn = { // ── FromTaskBanner ── 'scheduled.banner.autoGenerated': 'This conversation was auto-generated by a scheduled task', 'scheduled.banner.backToDetail': 'Back to task details', + // ── request.ts error tips ── + 'request.error.network': + 'Cannot connect to DB-GPT service: network unreachable (possibly CORS restriction or service not running). Please check cors_allowed_origins config and whether the service is up.', + 'request.error.timeout': 'Request timed out, please try again later.', + 'request.error.default': 'Request failed (HTTP {{status}}).', } as const; diff --git a/web/locales/zh/common.ts b/web/locales/zh/common.ts index b84f27b86..8ef599456 100644 --- a/web/locales/zh/common.ts +++ b/web/locales/zh/common.ts @@ -738,4 +738,9 @@ export const CommonZh: Resources['translation'] = { // ── FromTaskBanner ── 'scheduled.banner.autoGenerated': '此对话由定时任务自动生成', 'scheduled.banner.backToDetail': '返回任务详情', + // ── request.ts 错误提示 ── + 'request.error.network': + '无法连接 DB-GPT 服务:网络不可达(可能是 CORS 跨域限制或服务未启动),请检查 cors_allowed_origins 配置及服务是否运行。', + 'request.error.timeout': '请求超时,请稍后重试。', + 'request.error.default': '请求失败(HTTP {{status}})。', } as const; diff --git a/web/utils/request.ts b/web/utils/request.ts index 2932fb462..9b55fc99c 100644 --- a/web/utils/request.ts +++ b/web/utils/request.ts @@ -1,3 +1,4 @@ +import i18n from '@/app/i18n'; import { getUserId } from '@/utils'; import { message } from 'antd'; import { isPlainObject } from 'lodash'; @@ -22,6 +23,30 @@ const sanitizeBody = (obj: Record): string => { return JSON.stringify(resObj); }; +// 从 axios 错误中提取友好提示文案(走 i18n)。 +// 网络层失败(err.response 不存在)按 err.code 细分:超时 / 其他网络错误。 +// 浏览器对 CORS 拦截 / 连接拒绝 / 断网 都只给 ERR_NETWORK,JS 无法进一步区分, +// 只能给方向性提示。传给 message.error 的必须是 string/ReactNode,不能是原始 +// axios 对象(antd 渲染对象会抛异常 → 触发 Next.js 错误页)。 +const formatErrTip = (err: any): string => { + if (!err) return i18n.t('request.error.default', { status: '?' }); + // 网络层失败:浏览器拦截或服务不可达,无 response + if (!err.response) { + if (err.code === 'ECONNABORTED') { + return i18n.t('request.error.timeout'); + } + return i18n.t('request.error.network'); + } + // HTTP 错误:优先用服务端返回的 err_msg / message + const data = err.response.data; + if (data) { + if (typeof data === 'string') return data; + if (typeof data.err_msg === 'string') return data.err_msg; + if (typeof data.message === 'string') return data.message; + } + return i18n.t('request.error.default', { status: err.response.status || '?' }); +}; + export const sendGetRequest = (url: string, qs?: { [key: string]: any }) => { if (qs) { const str = Object.keys(qs) @@ -38,8 +63,8 @@ export const sendGetRequest = (url: string, qs?: { [key: string]: any }) => { }) .then(res => res) .catch(err => { - message.error(err); - Promise.reject(err); + message.error(formatErrTip(err)); + return Promise.reject(err); }); }; @@ -59,8 +84,8 @@ export const sendSpaceGetRequest = (url: string, qs?: { [key: string]: any }) => }) .then(res => res) .catch(err => { - message.error(err); - Promise.reject(err); + message.error(formatErrTip(err)); + return Promise.reject(err); }); }; @@ -73,8 +98,8 @@ export const sendPostRequest = (url: string, body?: any) => { }) .then(res => res) .catch(err => { - message.error(err); - Promise.reject(err); + message.error(formatErrTip(err)); + return Promise.reject(err); }); }; @@ -85,8 +110,8 @@ export const sendSpacePostRequest = (url: string, body?: any) => { }) .then(res => res) .catch(err => { - message.error(err); - Promise.reject(err); + message.error(formatErrTip(err)); + return Promise.reject(err); }); }; @@ -95,7 +120,7 @@ export const sendSpaceUploadPostRequest = (url: string, body?: any) => { .post(url, body) .then(res => res) .catch(err => { - message.error(err); - Promise.reject(err); + message.error(formatErrTip(err)); + return Promise.reject(err); }); };