mirror of
https://github.com/csunny/DB-GPT.git
synced 2026-07-16 17:15:22 +00:00
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 <noreply@anthropic.com>
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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": ["*"],
|
||||
}
|
||||
|
||||
43
packages/dbgpt-core/src/dbgpt/util/tests/test_fastapi.py
Normal file
43
packages/dbgpt-core/src/dbgpt/util/tests/test_fastapi.py
Normal file
@@ -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
|
||||
@@ -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<PromptBotProps> = ({ submit, chat_scene }) => {
|
||||
},
|
||||
{
|
||||
refreshDeps: [current, chat_scene],
|
||||
onError: err => {
|
||||
message.error(err?.message);
|
||||
},
|
||||
manual: true,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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, any>): 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<null, any>(url, body)
|
||||
.then(res => res)
|
||||
.catch(err => {
|
||||
message.error(err);
|
||||
Promise.reject(err);
|
||||
message.error(formatErrTip(err));
|
||||
return Promise.reject(err);
|
||||
});
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user