request.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332
  1. // Create axios instance
  2. import { APP_GAME_APK, APP_LOGIN, APP_LOG, BASE_API } from "./config";
  3. import axios from "axios";
  4. import { getHttpMsg, getUrlParam, isDebugEnv, localStorageGet, localStorageSet } from "@utils/helpers";
  5. import { LOCAL_ACTIVITY_C, LOCAL_IS_LOGIN, LOCAL_SIGN, LOCAL_WHEEL_INVITE_CODE } from "@constants/localkey";
  6. import { toast } from "react-toastify";
  7. import LZString from "lz-string";
  8. import { isStandalone, isTWA } from "@utils/UAUtils";
  9. import { normalizeClientError, reportClientLog, sanitizeUrlForLog } from "@utils/clientLog";
  10. const SENSITIVE_LOG_KEY = /(pass(word)?|sign|token|secret|authorization|cookie|phone|email|cpf|cashapp|paypal)/i;
  11. const stringifyForDebug = (value) => {
  12. try {
  13. return JSON.stringify(value, (key, item) => SENSITIVE_LOG_KEY.test(key) ? "[REDACTED]" : item);
  14. } catch (e) {
  15. return "[unserializable]";
  16. }
  17. };
  18. const getSign = () => {
  19. const sign = localStorageGet(LOCAL_SIGN, "");
  20. return sign;
  21. }
  22. const getC = () => {
  23. let c = localStorageGet(LOCAL_ACTIVITY_C, "");
  24. //容错为空时候再检测下url
  25. if (!c){
  26. let ccode = getUrlParam("c");
  27. if (ccode != null) {
  28. localStorageSet(LOCAL_ACTIVITY_C, ccode);
  29. c=ccode;
  30. }
  31. }
  32. return c;
  33. }
  34. const getComParams = () => {
  35. let params = {};
  36. if (global.BFP != null) params.bfp = global.BFP;
  37. if (global.FF != null) params.ff = global.FF;
  38. if(window.themeConfig)params.regionid=window.themeConfig.product;
  39. else params.regionid = window.location.hostname.split('.')[window.location.hostname.startsWith("www")?1:0];
  40. let sign = getSign();
  41. if (!!sign) params.sign = sign;
  42. let c = getC();
  43. if (!!c) params.c = c;
  44. params._d = global.device;
  45. params._t = Date.now() + "_" + global.width + "_" + global.height;
  46. if (isStandalone() || isTWA()) {
  47. params.pwa = 1;
  48. }
  49. let act = localStorageGet(LOCAL_WHEEL_INVITE_CODE);
  50. if (act != null && act !== "") {
  51. params.act = act;
  52. }
  53. return params;
  54. }
  55. const checkResponse = (data) => {
  56. if (data) {
  57. //登录失效
  58. if (data.code === 404) {
  59. let msg = getHttpMsg(data);
  60. toast.error(msg, {
  61. onClose: () => {
  62. window.location.replace(window.location.origin)
  63. }, toastId: "checkResponse"
  64. });
  65. }
  66. else if (data.code === 777) {
  67. let action = {
  68. type: "main/updateOpenPay",
  69. payload: 1,
  70. }
  71. setTimeout(() => {
  72. if (global.dispatch) global.dispatch(action);
  73. }, 1000)
  74. }
  75. }
  76. }
  77. const isLogRequest = (url = "") => {
  78. return String(url).includes(APP_LOG);
  79. }
  80. const service = axios.create({
  81. baseURL: BASE_API,
  82. withCredentials: true,
  83. timeout: 10000, // Request timeout
  84. retry: 2,//重复5次
  85. retryDelay: 1000,
  86. compress: !isDebugEnv()
  87. });
  88. //出错重试 retry
  89. const checkRetryRequest = (error) => {
  90. let config = error.config;
  91. if (!config || !config.retry) return Promise.reject(error);
  92. config.__retryCount = config.__retryCount || 0;
  93. if (config.__retryCount >= config.retry) {
  94. // Reject with the error
  95. return Promise.reject(error);
  96. }
  97. config.__retryCount += 1;
  98. // Create new promise to handle exponential backoff
  99. let backoff = new Promise(function (resolve) {
  100. setTimeout(function () {
  101. resolve();
  102. }, config.retryDelay || 1);
  103. });
  104. // Return the promise in which recalls axios to retry the request
  105. return backoff.then(function () {
  106. return service(config);
  107. });
  108. }
  109. // Request intercepter
  110. service.interceptors.request.use(
  111. config => {
  112. if (config.compress) {
  113. let data = config.ori_data;
  114. if (data == null) data = config.data;
  115. if (data) {
  116. let compressData = LZString.compressToBase64(JSON.stringify(data));
  117. config.data = {
  118. lz: compressData
  119. }
  120. config.ori_data = data;
  121. }
  122. let params = config.ori_params;
  123. if (params == null) params = config.params;
  124. if (params) {
  125. let compressParams = LZString.compressToBase64(JSON.stringify(params));
  126. config.params = {
  127. lz: compressParams
  128. }
  129. config.ori_params = params;
  130. }
  131. }
  132. return config;
  133. },
  134. error => {
  135. // Do something with request error
  136. return Promise.reject(error);
  137. }
  138. );
  139. // response pre-processing
  140. service.interceptors.response.use(
  141. response => {
  142. let data = response.data;
  143. if (typeof data === "string") {
  144. const decompressed = LZString.decompressFromBase64(data);
  145. const candidates = [decompressed, data];
  146. for (const candidate of candidates) {
  147. if (typeof candidate !== "string" || candidate === "") continue;
  148. try {
  149. response.data = JSON.parse(candidate);
  150. data = response.data;
  151. break;
  152. } catch (error) {
  153. // Try the next representation; preserve the original string if neither parses.
  154. }
  155. }
  156. }
  157. if (data && typeof data === "object" && typeof data.code === "string") {
  158. const code = data.code.trim();
  159. if (/^-?\d+$/.test(code)) data.code = Number(code);
  160. }
  161. if (isDebugEnv()) {
  162. const responseUrl = response.request?.responseURL || response.config?.url || "";
  163. console.debug(sanitizeUrlForLog(responseUrl), stringifyForDebug(response.data));
  164. }
  165. const requestUrl = response.config && response.config.url;
  166. if (!isLogRequest(requestUrl) && data && data.code != null && data.code !== 200) {
  167. reportClientLog("api_response_error", {
  168. url: requestUrl,
  169. method_type: response.config && response.config.method,
  170. code: data.code,
  171. msg: data.msg || data.message,
  172. response: data
  173. }, {
  174. throttleKey: `api_response_error:${requestUrl}:${data.code}:${data.msg || data.message || ""}`,
  175. throttleMs: 5000
  176. });
  177. }
  178. checkResponse(response.data);
  179. return response.data;
  180. },
  181. error => {
  182. const config = error.config || {};
  183. const retry = config.retry || 0;
  184. const retryCount = config.__retryCount || 0;
  185. const willRetry = retry && retryCount < retry;
  186. if (!willRetry && !isLogRequest(config.url)) {
  187. reportClientLog("api_request_error", {
  188. url: config.url,
  189. method_type: config.method,
  190. status: error.response && error.response.status,
  191. message: error.message,
  192. error: normalizeClientError(error),
  193. response: error.response && error.response.data
  194. }, {
  195. throttleKey: `api_request_error:${config.url}:${error.message || ""}`,
  196. throttleMs: 5000
  197. });
  198. }
  199. return checkRetryRequest(error);
  200. }
  201. );
  202. let _guestLoginPromise = null;
  203. const ensureSign = async (requestUrl) => {
  204. if (getSign()) return;
  205. const isLogin = localStorageGet(LOCAL_IS_LOGIN, "") === "1" || !!global.user;
  206. if (!isLogin) return;
  207. const isLoginUrl = requestUrl && (requestUrl.includes("/login") || requestUrl.includes("/register") || requestUrl.includes("/routes"));
  208. if (isLoginUrl) return;
  209. if (!_guestLoginPromise) {
  210. _guestLoginPromise = (async () => {
  211. try {
  212. const comParams = getComParams();
  213. const loginData = { type: "guest", ...comParams };
  214. const resp = await service({ method: "post", url: APP_LOGIN, data: loginData });
  215. if (resp && resp.code === 200 && resp.data) {
  216. const sign = resp.data.sign;
  217. if (sign) {
  218. localStorageSet(LOCAL_SIGN, sign);
  219. global.user = resp.data;
  220. }
  221. }
  222. } catch (e) {
  223. console.error("ensureSign guest login error", e);
  224. } finally {
  225. _guestLoginPromise = null;
  226. }
  227. })();
  228. }
  229. await _guestLoginPromise;
  230. };
  231. const get = async (params = {}) => {
  232. await ensureSign(params.url);
  233. const requestParams = {
  234. ...params,
  235. params: { ...(params.params || {}), ...getComParams() }
  236. };
  237. return service({
  238. method: 'get',
  239. ...requestParams
  240. });
  241. }
  242. const post = async (params = {}) => {
  243. await ensureSign(params.url);
  244. const requestParams = {
  245. ...params,
  246. data: { ...(params.data || {}), ...getComParams() }
  247. };
  248. if (isDebugEnv()) console.debug(`post params: ${stringifyForDebug(requestParams.data)} url:${params.url}`);
  249. return service({
  250. method: 'post',
  251. ...requestParams
  252. });
  253. }
  254. const put = async (params = {}) => {
  255. await ensureSign(params.url);
  256. const requestParams = {
  257. ...params,
  258. data: { ...(params.data || {}), ...getComParams() }
  259. };
  260. return service({
  261. method: 'put',
  262. ...requestParams
  263. });
  264. }
  265. const adelete = async (params = {}) => {
  266. await ensureSign(params.url);
  267. const requestParams = {
  268. ...params,
  269. data: { ...(params.data || {}), ...getComParams() }
  270. };
  271. return service({
  272. method: 'delete',
  273. ...requestParams
  274. });
  275. }
  276. const buildQuery = (param) => {
  277. if (param == null || typeof param === "string") return param;
  278. const query = new URLSearchParams();
  279. for (const key in param) {
  280. let value = param[key];
  281. if (value != null && typeof value === "object") value = JSON.stringify(value);
  282. query.append(key, value == null ? "" : String(value));
  283. }
  284. return query.toString();
  285. }
  286. const getAPKUrl = (apkUrl = null) => {
  287. let params = getComParams();
  288. let url = apkUrl == null ? BASE_API + APP_GAME_APK : apkUrl;
  289. const separator = url.includes("?") ? "&" : "?";
  290. if (!isDebugEnv()) {
  291. url += separator + "lz=" + encodeURIComponent(LZString.compressToBase64(JSON.stringify(params)));
  292. }
  293. else {
  294. url += separator + buildQuery(params);
  295. }
  296. return url;
  297. }
  298. /** 解析 quickSave 生成的 path:/s_k_c 或 /s_k_c/xxx => { s_k, c },用于 routes 恢复环境与 basename */
  299. export const parseQuickSavePath = () => {
  300. const pathname = window.location.pathname || "";
  301. const m = pathname.match(/^\/([a-zA-Z0-9]+)_(\d+)(?:\/.*)?$/);
  302. return m ? { s_k: m[1], c: m[2] } : null;
  303. };
  304. /** 当前 pathname(去掉 s_k_c 前缀),用于与 "/"、ROUTE_PATH 等比较时与首页一视同仁 */
  305. export const getLogicalPathname = () => {
  306. const pathname = window.location.pathname || "";
  307. const q = parseQuickSavePath();
  308. if (!q) return pathname;
  309. const prefix = `/${q.s_k}_${q.c}`;
  310. if (pathname === prefix || pathname === prefix + "/") return "/";
  311. if (pathname.startsWith(prefix + "/")) return pathname.slice(prefix.length) || "/";
  312. return pathname;
  313. };
  314. const Http = {
  315. get,
  316. post,
  317. put,
  318. delete: adelete,
  319. getAPKUrl
  320. }
  321. export default Http;