describeNonJsonBody.mjs

import get from 'lodash-es/get.js'
import isestr from 'wsemi/src/isestr.mjs'


// describeNonJsonBody.mjs — REST轉接器「HTTP 200但本體非JSON」之錯誤訊息(單一來源, 三個REST轉接器與遞補層共用)
//
// 【為何需要(2026-09-24安裝方建議C2)】舊版各REST轉接器之JSON.parse失敗被吞掉, 與「是JSON但缺欄位」回同一句
//   INVALID_RESPONSE(如missing choices[0].message.content), stderr又只有解碼後之前500字——本體若是壓縮位元組
//   只剩亂碼, 安裝方為此直接重打才查出是brotli漏標Content-Encoding。本訊息附原始位元組數、前16位元組之hex
//   (gzip以1f8b開頭等特徵可一眼辨認)、content-encoding與content-type, 令此類故障不必重打即可定位。
//
// 【分流】本體非JSON屬傳輸或閘道狀態, 同一請求換金鑰必然再敗一次, 遞補層以BODY_NOT_JSON前綴判為與金鑰無關
//   而整組跳過(安裝方實例: 換第二把金鑰只是以剩餘預算重打至逾時); 「JSON缺欄位」則維持換金鑰——
//   部分閘道以200回帳號層級之錯誤JSON, 換金鑰可能有效。errorType一律仍為invalid-response(字彙不增)。


//錯誤訊息前綴, dispatchAiFallback據此判定整組跳過
let BODY_NOT_JSON = 'INVALID_RESPONSE: body is not JSON'


/**
 * 組出「HTTP 200但本體非JSON」之錯誤訊息
 *
 * @param {Uint8Array} bytes 輸入回應本體之原始位元組(未解碼)
 * @param {Headers|Object} headers 輸入回應標頭(fetch之Headers,或具get方法之物件)
 * @returns {String} 回傳錯誤訊息字串,以BODY_NOT_JSON開頭,附位元組數、前16位元組hex、content-encoding與content-type
 * @example
 *
 * import describeNonJsonBody from './src/describeNonJsonBody.mjs'
 *
 * let h = new Headers({ 'content-type': 'application/json' })
 * console.log(describeNonJsonBody(new Uint8Array([0x8b, 0xef, 0x02]), h))
 * // => 'INVALID_RESPONSE: body is not JSON (3 bytes, first bytes 8bef02, content-encoding=none, content-type=application/json)'
 *
 */
function describeNonJsonBody(bytes, headers) {
    let u8 = (bytes instanceof Uint8Array) ? bytes : new Uint8Array(0)
    let hex = Array.from(u8.subarray(0, 16)).map((b) => b.toString(16).padStart(2, '0')).join('')
    let getH = (k) => {
        let fn = get(headers, 'get', null)
        let v = (typeof fn === 'function') ? headers.get(k) : null
        return isestr(v) ? v : 'none'
    }
    let more = u8.length > 16 ? '...' : ''
    return `${BODY_NOT_JSON} (${u8.length} bytes, first bytes ${hex || '(none)'}${more}, content-encoding=${getH('content-encoding')}, content-type=${getH('content-type')})`
}


export default describeNonJsonBody
export { BODY_NOT_JSON }