feat: Add configurable proxy bypass setting for S3 backups

Co-authored-by: GeorgeDong32 <98630204+GeorgeDong32@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot] 2025-11-03 06:07:33 +00:00 committed by GeorgeDong32
parent 97ab200657
commit ea9e7fccda
7 changed files with 50 additions and 19 deletions

View File

@ -39,7 +39,7 @@ export default class S3Storage {
private root: string
constructor(config: S3Config) {
const { endpoint, region, accessKeyId, secretAccessKey, bucket, root } = config
const { endpoint, region, accessKeyId, secretAccessKey, bucket, root, bypassProxy = true } = config
const usePathStyle = (() => {
if (!endpoint) return false
@ -59,22 +59,24 @@ export default class S3Storage {
}
})()
// Fix for S3 backup failure when using proxy
// Issue: When proxy is enabled, S3 uploads can fail with incomplete writes
// Error: "Io error: put_object write size < data.size(), w_size=15728640, data.size=16396159"
// Root cause: AWS SDK uses global fetch/undici dispatcher which routes through proxy,
// causing data corruption or incomplete transfers for large files
// Solution: Configure S3Client with a direct dispatcher that bypasses the global proxy
const directDispatcher = new UndiciAgent({
connect: {
timeout: 60000 // 60 second connection timeout
}
})
// Conditionally bypass proxy for S3 requests based on user configuration
// When bypassProxy is true (default), S3 requests use a direct dispatcher to avoid
// proxy interference with large file uploads that can cause incomplete transfers
// Error example: "Io error: put_object write size < data.size(), w_size=15728640, data.size=16396159"
let requestHandler: FetchHttpHandler | undefined
const requestHandler = new FetchHttpHandler({
requestTimeout: 300000, // 5 minute request timeout for large files
dispatcher: directDispatcher
})
if (bypassProxy) {
const directDispatcher = new UndiciAgent({
connect: {
timeout: 60000 // 60 second connection timeout
}
})
requestHandler = new FetchHttpHandler({
requestTimeout: 300000, // 5 minute request timeout for large files
dispatcher: directDispatcher
})
}
this.client = new S3Client({
region,
@ -84,7 +86,7 @@ export default class S3Storage {
secretAccessKey: secretAccessKey
},
forcePathStyle: usePathStyle,
requestHandler
...(requestHandler && { requestHandler })
})
this.bucket = bucket

View File

@ -3419,6 +3419,10 @@
"help": "When enabled, file data will be skipped during backup, only configuration information will be backed up, significantly reducing backup file size",
"label": "Lightweight Backup"
},
"bypassProxy": {
"help": "When enabled, S3 requests bypass the application proxy to prevent data corruption during large file uploads. Recommended to keep enabled unless your S3 endpoint requires proxy access.",
"label": "Bypass Proxy"
},
"syncStatus": {
"error": "Sync error: {{message}}",
"label": "Sync Status",

View File

@ -3419,6 +3419,10 @@
"help": "开启后备份时将跳过文件数据,仅备份配置信息,显著减小备份文件体积",
"label": "精简备份"
},
"bypassProxy": {
"help": "开启后S3 请求将绕过应用程序代理,以防止大文件上传时的数据损坏。建议保持开启,除非您的 S3 端点需要通过代理访问。",
"label": "绕过代理"
},
"syncStatus": {
"error": "同步错误: {{message}}",
"label": "同步状态",

View File

@ -3419,6 +3419,10 @@
"help": "開啟後備份時將跳過檔案資料,僅備份設定資訊,顯著減小備份檔案體積",
"label": "精簡備份"
},
"bypassProxy": {
"help": "開啟後S3 請求將繞過應用程式代理,以防止大檔案上傳時的資料損壞。建議保持開啟,除非您的 S3 端點需要透過代理存取。",
"label": "繞過代理"
},
"syncStatus": {
"error": "同步錯誤: {{message}}",
"label": "同步狀態",

View File

@ -31,7 +31,8 @@ const S3Settings: FC = () => {
root: s3RootInit = '',
syncInterval: s3SyncIntervalInit = 0,
maxBackups: s3MaxBackupsInit = 5,
skipBackupFile: s3SkipBackupFileInit = false
skipBackupFile: s3SkipBackupFileInit = false,
bypassProxy: s3BypassProxyInit = true
} = s3
const [endpoint, setEndpoint] = useState<string | undefined>(s3EndpointInit)
@ -41,6 +42,7 @@ const S3Settings: FC = () => {
const [secretAccessKey, setSecretAccessKey] = useState<string | undefined>(s3SecretAccessKeyInit)
const [root, setRoot] = useState<string | undefined>(s3RootInit)
const [skipBackupFile, setSkipBackupFile] = useState<boolean>(s3SkipBackupFileInit)
const [bypassProxy, setBypassProxy] = useState<boolean>(s3BypassProxyInit)
const [backupManagerVisible, setBackupManagerVisible] = useState(false)
const [syncInterval, setSyncInterval] = useState<number>(s3SyncIntervalInit)
@ -82,6 +84,11 @@ const S3Settings: FC = () => {
dispatch(setS3Partial({ skipBackupFile: value }))
}
const onBypassProxyChange = (value: boolean) => {
setBypassProxy(value)
dispatch(setS3Partial({ bypassProxy: value }))
}
const renderSyncStatus = () => {
if (!endpoint) return null
@ -261,6 +268,14 @@ const S3Settings: FC = () => {
<SettingRow>
<SettingHelpText>{t('settings.data.s3.skipBackupFile.help')}</SettingHelpText>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.data.s3.bypassProxy.label')}</SettingRowTitle>
<Switch checked={bypassProxy} onChange={onBypassProxyChange} />
</SettingRow>
<SettingRow>
<SettingHelpText>{t('settings.data.s3.bypassProxy.help')}</SettingHelpText>
</SettingRow>
{syncInterval > 0 && (
<>
<SettingDivider />

View File

@ -401,7 +401,8 @@ export const initialState: SettingsState = {
autoSync: false,
syncInterval: 0,
maxBackups: 0,
skipBackupFile: false
skipBackupFile: false,
bypassProxy: true
},
// Developer mode

View File

@ -883,6 +883,7 @@ export type S3Config = {
autoSync: boolean
syncInterval: number
maxBackups: number
bypassProxy?: boolean // Whether to bypass proxy for S3 requests (default: true)
}
export type { Message } from './newMessage'