| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244 |
- import { HttpsProxyAgent } from 'https-proxy-agent'
- import { Config } from '../config/simpleEnv.js'
- import { logger } from './logger.js'
- /**
- * 统一的HTTP客户端,支持proxy和重试
- */
- export class HttpClient {
- constructor() {
- this.defaultTimeout = 30000
- this.defaultRetries = 3
- this.defaultRetryDelay = 1000
- this.initializeProxy()
- }
- static getInstance() {
- if (!HttpClient.instance) {
- HttpClient.instance = new HttpClient()
- }
- return HttpClient.instance
- }
- initializeProxy() {
- if (Config.proxy.isAnyConfigured()) {
- logger.info('HTTP代理系统已初始化')
- if (Config.proxy.isConfigured()) {
- logger.info(`全局代理: ${Config.proxy.protocol()}://${Config.proxy.host()}:${Config.proxy.port()}`)
- }
- if (Config.proxy.aster.isConfigured()) {
- logger.info(
- `Aster专用代理: ${Config.proxy.aster.protocol()}://${Config.proxy.aster.host()}:${Config.proxy.aster.port()}`,
- )
- }
- } else {
- logger.info('HTTP代理未配置,使用直连')
- }
- }
- /**
- * 重新初始化代理 (当环境变量发生变化时调用)
- */
- reinitializeProxy() {
- this.proxyAgent = undefined
- this.initializeProxy()
- }
- /**
- * 检查HTTP状态码是否应该重试
- */
- shouldRetry(status) {
- return status >= 500 || status === 429 || status === 408 || status === 0
- }
- /**
- * 等待指定时间
- */
- sleep(ms) {
- return new Promise(resolve => setTimeout(resolve, ms))
- }
- /**
- * 发送HTTP请求
- */
- async request(url, options = {}) {
- const {
- method = 'GET',
- headers = {},
- body,
- timeout = this.defaultTimeout,
- signal,
- retries = this.defaultRetries,
- retryDelay = this.defaultRetryDelay,
- useProxy = true,
- exchange,
- } = options
- const maxRetries = Math.max(0, retries)
- let lastError
- // 请求配置
- const fetchOptions = {
- method,
- headers: {
- 'User-Agent': 'Binance-Multi-Exchange-Client/1.0',
- ...headers,
- },
- signal,
- }
- // 添加请求体
- if (body && method !== 'GET') {
- if (typeof body === 'object') {
- fetchOptions.body = JSON.stringify(body)
- fetchOptions.headers = {
- 'Content-Type': 'application/json',
- ...fetchOptions.headers,
- }
- } else {
- fetchOptions.body = body
- }
- }
- // 添加代理(仅在Node.js环境中)
- if (useProxy && typeof global !== 'undefined') {
- const proxyUrl = Config.proxy.getUrl(exchange)
- if (proxyUrl) {
- const proxyAgent = new HttpsProxyAgent(proxyUrl)
- fetchOptions.agent = proxyAgent
- if (Config.isDev()) {
- logger.debug(`使用代理发送HTTP请求`, {
- exchange: exchange || 'global',
- proxyHost: exchange === 'aster' ? Config.proxy.aster.host() : Config.proxy.host(),
- method,
- url: url.length > 100 ? url.substring(0, 100) + '...' : url,
- })
- }
- }
- }
- for (let attempt = 0; attempt <= maxRetries; attempt++) {
- try {
- // 创建超时控制
- const controller = new AbortController()
- const timeoutId = setTimeout(() => controller.abort(), timeout)
- const requestSignal = signal || controller.signal
- const startTime = Date.now()
- const response = await fetch(url, {
- ...fetchOptions,
- signal: requestSignal,
- })
- clearTimeout(timeoutId)
- const duration = Date.now() - startTime
- // 读取响应数据
- const contentType = response.headers.get('content-type') || ''
- let data
- if (contentType.includes('application/json')) {
- data = await response.json()
- } else {
- data = await response.text()
- }
- // 记录请求日志
- if (Config.isDev()) {
- logger.debug(`HTTP ${method} ${url}`, {
- status: response.status,
- duration: `${duration}ms`,
- attempt: attempt + 1,
- proxy: useProxy && this.proxyAgent ? 'enabled' : 'disabled',
- })
- }
- // 检查是否需要重试
- if (!response.ok && this.shouldRetry(response.status) && attempt < maxRetries) {
- const jitter = Math.random() * 500 // 0-500ms随机延迟
- const delay = retryDelay * Math.pow(2, attempt) + jitter
- logger.warn(`HTTP请求失败,将在${Math.round(delay)}ms后重试`, {
- url,
- status: response.status,
- attempt: attempt + 1,
- maxRetries: maxRetries + 1,
- })
- await this.sleep(delay)
- continue
- }
- return {
- status: response.status,
- statusText: response.statusText,
- ok: response.ok,
- data,
- headers: response.headers,
- }
- } catch (error) {
- lastError = error
- const isTimeout = error.name === 'AbortError'
- const isNetworkError = error.message?.includes('fetch')
- // 记录错误日志
- logger.error(`HTTP请求异常`, {
- url,
- method,
- attempt: attempt + 1,
- error: error.message,
- isTimeout,
- isNetworkError,
- })
- // 检查是否需要重试
- if ((isTimeout || isNetworkError) && attempt < maxRetries) {
- const jitter = Math.random() * 500
- const delay = retryDelay * Math.pow(2, attempt) + jitter
- logger.warn(`网络错误,将在${Math.round(delay)}ms后重试`, {
- url,
- attempt: attempt + 1,
- maxRetries: maxRetries + 1,
- })
- await this.sleep(delay)
- continue
- }
- // 不能重试,抛出错误
- throw error
- }
- }
- // 所有重试都失败了
- throw lastError || new Error(`HTTP请求失败,已重试${maxRetries}次`)
- }
- /**
- * GET 请求
- */
- async get(url, options = {}) {
- return this.request(url, { ...options, method: 'GET' })
- }
- /**
- * POST 请求
- */
- async post(url, body, options = {}) {
- return this.request(url, { ...options, method: 'POST', body })
- }
- /**
- * PUT 请求
- */
- async put(url, body, options = {}) {
- return this.request(url, { ...options, method: 'PUT', body })
- }
- /**
- * DELETE 请求
- */
- async delete(url, options = {}) {
- return this.request(url, { ...options, method: 'DELETE' })
- }
- }
- /**
- * 全局HTTP客户端实例
- */
- export const httpClient = HttpClient.getInstance()
- /**
- * 便捷方法:使用代理的fetch
- */
- export async function fetchWithProxy(url, options = {}) {
- return httpClient.request(url, options)
- }
- /**
- * 便捷方法:GET请求with proxy
- */
- export async function getWithProxy(url, options = {}) {
- const response = await httpClient.get(url, options)
- if (!response.ok) {
- throw new Error(`HTTP ${response.status}: ${response.statusText}`)
- }
- return response.data
- }
- /**
- * 便捷方法:POST请求with proxy
- */
- export async function postWithProxy(url, body, options = {}) {
- const response = await httpClient.post(url, body, options)
- if (!response.ok) {
- throw new Error(`HTTP ${response.status}: ${response.statusText}`)
- }
- return response.data
- }
|