fix(gitea-client): 強化分頁回應的 JSON 與錯誤內容處理
- res.json() 以 try-catch 包裹,解析失敗時補上 URL 上下文 - res.ok 為真時嚴格要求陣列,非陣列回應改為拋錯而非靜默結束 - 改用迴圈逐一 push 取代展開運算子,避免大量項目的堆疊風險 - 錯誤訊息中的回應內容經 sanitize(移除控制字元)防止 log 注入 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
1623408d02
commit
0f4354bd01
+37
-10
@@ -4,6 +4,18 @@
|
||||
// 單一 HTTP 請求的逾時(毫秒)。避免 API 緩慢或掛起時容器永久卡死。
|
||||
const REQUEST_TIMEOUT_MS = 30000
|
||||
|
||||
/**
|
||||
* 將回應內容整理成可安全寫入錯誤訊息的片段:移除控制字元(避免換行等造成的 log 注入)並限制長度。
|
||||
* @param {string} text 原始回應文字
|
||||
* @returns {string} 清理後、最長 200 字元的片段
|
||||
*/
|
||||
function sanitizeBody(text) {
|
||||
return (text || '')
|
||||
.replace(/[\u0000-\u001F\u007F]+/g, ' ')
|
||||
.trim()
|
||||
.slice(0, 200)
|
||||
}
|
||||
|
||||
/**
|
||||
* 與 Gitea REST API 溝通的輕量 HTTP 客戶端,負責帶上認證標頭、分頁讀取清單與發出刪除請求。
|
||||
* 取代原 bash 版本以 `curl`/`jq` 進行的 API 操作。
|
||||
@@ -22,14 +34,15 @@ export class GiteaClient {
|
||||
}
|
||||
|
||||
/**
|
||||
* 逐頁讀取分頁式清單 API(每頁以 `?page=N` 由 1 遞增),直到某頁回傳空陣列(或非陣列)為止,
|
||||
* 逐頁讀取分頁式清單 API(每頁以 `?page=N` 由 1 遞增),直到某頁回傳空陣列為止,
|
||||
* 將所有頁面項目合併成單一陣列回傳。
|
||||
*
|
||||
* 注意:終止條件依賴「空陣列代表最後一頁」的假設;若 API 不以空陣列結尾則迴圈不會自然停止。
|
||||
* 在 `res.ok` 為真的前提下嚴格要求回應為 JSON 陣列:非 JSON、JSON 解析失敗或非陣列內容
|
||||
* 都會拋出帶 URL 上下文的錯誤,而非被誤判為最後一頁而靜默結束。
|
||||
*
|
||||
* @param {string} baseUrl 不含 query string 的 API 位址(本方法會自行附加 `?page=N`)
|
||||
* @returns {Promise<any[]>} 所有頁面合併後的項目陣列;無資料時為空陣列
|
||||
* @throws {Error} 任一頁回應 HTTP 非 2xx(`!res.ok`)時拋出,訊息含 URL 與狀態碼
|
||||
* @throws {Error} HTTP 非 2xx、回應非 JSON、JSON 無法解析或非陣列、或請求逾時(AbortError)時拋出
|
||||
*/
|
||||
async fetchAllPages(baseUrl) {
|
||||
const all = []
|
||||
@@ -43,27 +56,41 @@ export class GiteaClient {
|
||||
})
|
||||
|
||||
if (!res.ok) {
|
||||
const body = await res.text().catch(() => '')
|
||||
const body = sanitizeBody(await res.text().catch(() => ''))
|
||||
throw new Error(
|
||||
`GET ${url} failed: HTTP ${res.status}${body ? ` - ${body.slice(0, 200)}` : ''}`,
|
||||
`GET ${url} failed: HTTP ${res.status}${body ? ` - ${body}` : ''}`,
|
||||
)
|
||||
}
|
||||
|
||||
// 確認回應確實是 JSON,避免 API 回傳 HTML 錯誤頁時 res.json() 拋出難以理解的 SyntaxError。
|
||||
const contentType = res.headers.get('content-type') || ''
|
||||
if (!contentType.includes('application/json')) {
|
||||
const body = await res.text().catch(() => '')
|
||||
const body = sanitizeBody(await res.text().catch(() => ''))
|
||||
throw new Error(
|
||||
`GET ${url} returned non-JSON content-type "${contentType}": ${body.slice(0, 200)}`,
|
||||
`GET ${url} returned non-JSON content-type "${contentType}"${body ? `: ${body}` : ''}`,
|
||||
)
|
||||
}
|
||||
|
||||
const items = await res.json()
|
||||
if (!Array.isArray(items) || items.length === 0) {
|
||||
// 即使 content-type 正確,內容仍可能格式錯誤;明確攔截 SyntaxError 並補上 URL 上下文。
|
||||
let items
|
||||
try {
|
||||
items = await res.json()
|
||||
} catch (error) {
|
||||
throw new Error(`GET ${url} returned invalid JSON: ${error.message}`)
|
||||
}
|
||||
|
||||
// res.ok 為真時嚴格要求陣列:非陣列代表非預期回應(如錯誤物件),應報錯而非視為最後一頁。
|
||||
if (!Array.isArray(items)) {
|
||||
throw new Error(`GET ${url} returned a non-array JSON payload`)
|
||||
}
|
||||
if (items.length === 0) {
|
||||
break
|
||||
}
|
||||
|
||||
all.push(...items)
|
||||
// 逐一加入(不使用展開運算子),避免大量項目時觸發呼叫堆疊上限。
|
||||
for (const item of items) {
|
||||
all.push(item)
|
||||
}
|
||||
page += 1
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user