httpClient.js 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244
  1. import { HttpsProxyAgent } from 'https-proxy-agent'
  2. import { Config } from '../config/simpleEnv.js'
  3. import { logger } from './logger.js'
  4. /**
  5. * 统一的HTTP客户端,支持proxy和重试
  6. */
  7. export class HttpClient {
  8. constructor() {
  9. this.defaultTimeout = 30000
  10. this.defaultRetries = 3
  11. this.defaultRetryDelay = 1000
  12. this.initializeProxy()
  13. }
  14. static getInstance() {
  15. if (!HttpClient.instance) {
  16. HttpClient.instance = new HttpClient()
  17. }
  18. return HttpClient.instance
  19. }
  20. initializeProxy() {
  21. if (Config.proxy.isAnyConfigured()) {
  22. logger.info('HTTP代理系统已初始化')
  23. if (Config.proxy.isConfigured()) {
  24. logger.info(`全局代理: ${Config.proxy.protocol()}://${Config.proxy.host()}:${Config.proxy.port()}`)
  25. }
  26. if (Config.proxy.aster.isConfigured()) {
  27. logger.info(
  28. `Aster专用代理: ${Config.proxy.aster.protocol()}://${Config.proxy.aster.host()}:${Config.proxy.aster.port()}`,
  29. )
  30. }
  31. } else {
  32. logger.info('HTTP代理未配置,使用直连')
  33. }
  34. }
  35. /**
  36. * 重新初始化代理 (当环境变量发生变化时调用)
  37. */
  38. reinitializeProxy() {
  39. this.proxyAgent = undefined
  40. this.initializeProxy()
  41. }
  42. /**
  43. * 检查HTTP状态码是否应该重试
  44. */
  45. shouldRetry(status) {
  46. return status >= 500 || status === 429 || status === 408 || status === 0
  47. }
  48. /**
  49. * 等待指定时间
  50. */
  51. sleep(ms) {
  52. return new Promise(resolve => setTimeout(resolve, ms))
  53. }
  54. /**
  55. * 发送HTTP请求
  56. */
  57. async request(url, options = {}) {
  58. const {
  59. method = 'GET',
  60. headers = {},
  61. body,
  62. timeout = this.defaultTimeout,
  63. signal,
  64. retries = this.defaultRetries,
  65. retryDelay = this.defaultRetryDelay,
  66. useProxy = true,
  67. exchange,
  68. } = options
  69. const maxRetries = Math.max(0, retries)
  70. let lastError
  71. // 请求配置
  72. const fetchOptions = {
  73. method,
  74. headers: {
  75. 'User-Agent': 'Binance-Multi-Exchange-Client/1.0',
  76. ...headers,
  77. },
  78. signal,
  79. }
  80. // 添加请求体
  81. if (body && method !== 'GET') {
  82. if (typeof body === 'object') {
  83. fetchOptions.body = JSON.stringify(body)
  84. fetchOptions.headers = {
  85. 'Content-Type': 'application/json',
  86. ...fetchOptions.headers,
  87. }
  88. } else {
  89. fetchOptions.body = body
  90. }
  91. }
  92. // 添加代理(仅在Node.js环境中)
  93. if (useProxy && typeof global !== 'undefined') {
  94. const proxyUrl = Config.proxy.getUrl(exchange)
  95. if (proxyUrl) {
  96. const proxyAgent = new HttpsProxyAgent(proxyUrl)
  97. fetchOptions.agent = proxyAgent
  98. if (Config.isDev()) {
  99. logger.debug(`使用代理发送HTTP请求`, {
  100. exchange: exchange || 'global',
  101. proxyHost: exchange === 'aster' ? Config.proxy.aster.host() : Config.proxy.host(),
  102. method,
  103. url: url.length > 100 ? url.substring(0, 100) + '...' : url,
  104. })
  105. }
  106. }
  107. }
  108. for (let attempt = 0; attempt <= maxRetries; attempt++) {
  109. try {
  110. // 创建超时控制
  111. const controller = new AbortController()
  112. const timeoutId = setTimeout(() => controller.abort(), timeout)
  113. const requestSignal = signal || controller.signal
  114. const startTime = Date.now()
  115. const response = await fetch(url, {
  116. ...fetchOptions,
  117. signal: requestSignal,
  118. })
  119. clearTimeout(timeoutId)
  120. const duration = Date.now() - startTime
  121. // 读取响应数据
  122. const contentType = response.headers.get('content-type') || ''
  123. let data
  124. if (contentType.includes('application/json')) {
  125. data = await response.json()
  126. } else {
  127. data = await response.text()
  128. }
  129. // 记录请求日志
  130. if (Config.isDev()) {
  131. logger.debug(`HTTP ${method} ${url}`, {
  132. status: response.status,
  133. duration: `${duration}ms`,
  134. attempt: attempt + 1,
  135. proxy: useProxy && this.proxyAgent ? 'enabled' : 'disabled',
  136. })
  137. }
  138. // 检查是否需要重试
  139. if (!response.ok && this.shouldRetry(response.status) && attempt < maxRetries) {
  140. const jitter = Math.random() * 500 // 0-500ms随机延迟
  141. const delay = retryDelay * Math.pow(2, attempt) + jitter
  142. logger.warn(`HTTP请求失败,将在${Math.round(delay)}ms后重试`, {
  143. url,
  144. status: response.status,
  145. attempt: attempt + 1,
  146. maxRetries: maxRetries + 1,
  147. })
  148. await this.sleep(delay)
  149. continue
  150. }
  151. return {
  152. status: response.status,
  153. statusText: response.statusText,
  154. ok: response.ok,
  155. data,
  156. headers: response.headers,
  157. }
  158. } catch (error) {
  159. lastError = error
  160. const isTimeout = error.name === 'AbortError'
  161. const isNetworkError = error.message?.includes('fetch')
  162. // 记录错误日志
  163. logger.error(`HTTP请求异常`, {
  164. url,
  165. method,
  166. attempt: attempt + 1,
  167. error: error.message,
  168. isTimeout,
  169. isNetworkError,
  170. })
  171. // 检查是否需要重试
  172. if ((isTimeout || isNetworkError) && attempt < maxRetries) {
  173. const jitter = Math.random() * 500
  174. const delay = retryDelay * Math.pow(2, attempt) + jitter
  175. logger.warn(`网络错误,将在${Math.round(delay)}ms后重试`, {
  176. url,
  177. attempt: attempt + 1,
  178. maxRetries: maxRetries + 1,
  179. })
  180. await this.sleep(delay)
  181. continue
  182. }
  183. // 不能重试,抛出错误
  184. throw error
  185. }
  186. }
  187. // 所有重试都失败了
  188. throw lastError || new Error(`HTTP请求失败,已重试${maxRetries}次`)
  189. }
  190. /**
  191. * GET 请求
  192. */
  193. async get(url, options = {}) {
  194. return this.request(url, { ...options, method: 'GET' })
  195. }
  196. /**
  197. * POST 请求
  198. */
  199. async post(url, body, options = {}) {
  200. return this.request(url, { ...options, method: 'POST', body })
  201. }
  202. /**
  203. * PUT 请求
  204. */
  205. async put(url, body, options = {}) {
  206. return this.request(url, { ...options, method: 'PUT', body })
  207. }
  208. /**
  209. * DELETE 请求
  210. */
  211. async delete(url, options = {}) {
  212. return this.request(url, { ...options, method: 'DELETE' })
  213. }
  214. }
  215. /**
  216. * 全局HTTP客户端实例
  217. */
  218. export const httpClient = HttpClient.getInstance()
  219. /**
  220. * 便捷方法:使用代理的fetch
  221. */
  222. export async function fetchWithProxy(url, options = {}) {
  223. return httpClient.request(url, options)
  224. }
  225. /**
  226. * 便捷方法:GET请求with proxy
  227. */
  228. export async function getWithProxy(url, options = {}) {
  229. const response = await httpClient.get(url, options)
  230. if (!response.ok) {
  231. throw new Error(`HTTP ${response.status}: ${response.statusText}`)
  232. }
  233. return response.data
  234. }
  235. /**
  236. * 便捷方法:POST请求with proxy
  237. */
  238. export async function postWithProxy(url, body, options = {}) {
  239. const response = await httpClient.post(url, body, options)
  240. if (!response.ok) {
  241. throw new Error(`HTTP ${response.status}: ${response.statusText}`)
  242. }
  243. return response.data
  244. }