@dongdev/fca-unofficial 2.0.31 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -12,11 +12,20 @@ module.exports = function createListenMqtt(deps) {
12
12
  function scheduleReconnect(delayMs) {
13
13
  const d = (ctx._mqttOpt && ctx._mqttOpt.reconnectDelayMs) || 2000;
14
14
  const ms = typeof delayMs === "number" ? delayMs : d;
15
- if (ctx._reconnectTimer) return; // debounce
15
+ if (ctx._reconnectTimer) {
16
+ logger("mqtt reconnect already scheduled", "warn");
17
+ return; // debounce
18
+ }
19
+ if (ctx._ending) {
20
+ logger("mqtt reconnect skipped - ending", "warn");
21
+ return;
22
+ }
16
23
  logger(`mqtt will reconnect in ${ms}ms`, "warn");
17
24
  ctx._reconnectTimer = setTimeout(() => {
18
25
  ctx._reconnectTimer = null;
19
- listenMqtt(defaultFuncs, api, ctx, globalCallback);
26
+ if (!ctx._ending) {
27
+ listenMqtt(defaultFuncs, api, ctx, globalCallback);
28
+ }
20
29
  }, ms);
21
30
  }
22
31
  function isEndingLikeError(msg) {
@@ -88,20 +97,29 @@ module.exports = function createListenMqtt(deps) {
88
97
  }
89
98
 
90
99
  if (/Not logged in|Not logged in.|blocked the login|401|403/i.test(msg)) {
91
- try { mqttClient.end(true); } catch (_) { }
100
+ try {
101
+ if (mqttClient && mqttClient.connected) {
102
+ mqttClient.end(true);
103
+ }
104
+ } catch (_) { }
92
105
  return emitAuth(ctx, api, globalCallback,
93
106
  /blocked/i.test(msg) ? "login_blocked" : "not_logged_in",
94
107
  msg
95
108
  );
96
109
  }
97
110
  logger(`mqtt error: ${msg}`, "error");
98
- try { mqttClient.end(true); } catch (_) { }
111
+ try {
112
+ if (mqttClient && mqttClient.connected) {
113
+ mqttClient.end(true);
114
+ }
115
+ } catch (_) { }
99
116
  if (ctx._ending || ctx._cycling) return;
100
117
 
101
- if (ctx.globalOptions.autoReconnect) {
118
+ if (ctx.globalOptions.autoReconnect && !ctx._ending) {
102
119
  const d = (ctx._mqttOpt && ctx._mqttOpt.reconnectDelayMs) || 2000;
103
120
  logger(`mqtt autoReconnect listenMqtt() in ${d}ms`, "warn");
104
- setTimeout(() => listenMqtt(defaultFuncs, api, ctx, globalCallback), d);
121
+ // Use scheduleReconnect to prevent multiple reconnections
122
+ scheduleReconnect(d);
105
123
  } else {
106
124
  globalCallback({ type: "stop_listen", error: msg || "Connection refused" }, null);
107
125
  }
@@ -128,8 +146,16 @@ module.exports = function createListenMqtt(deps) {
128
146
  mqttClient.publish("/set_client_settings", JSON.stringify({ make_user_available_when_in_foreground: true }), { qos: 1 });
129
147
  const d = (ctx._mqttOpt && ctx._mqttOpt.reconnectDelayMs) || 2000;
130
148
  const rTimeout = setTimeout(function () {
149
+ if (ctx._ending) {
150
+ logger("mqtt t_ms timeout skipped - ending", "warn");
151
+ return;
152
+ }
131
153
  logger(`mqtt t_ms timeout, cycling in ${d}ms`, "warn");
132
- try { mqttClient.end(true); } catch (_) { }
154
+ try {
155
+ if (mqttClient && mqttClient.connected) {
156
+ mqttClient.end(true);
157
+ }
158
+ } catch (_) { }
133
159
  scheduleReconnect(d);
134
160
  }, 5000);
135
161
 
@@ -141,9 +167,15 @@ module.exports = function createListenMqtt(deps) {
141
167
  });
142
168
 
143
169
  mqttClient.on("message", function (topic, message) {
170
+ if (ctx._ending) return; // Ignore messages if ending
144
171
  try {
145
172
  let jsonMessage = Buffer.isBuffer(message) ? Buffer.from(message).toString() : message;
146
- try { jsonMessage = JSON.parse(jsonMessage); } catch (_) { jsonMessage = {}; }
173
+ try {
174
+ jsonMessage = JSON.parse(jsonMessage);
175
+ } catch (parseErr) {
176
+ logger(`mqtt message parse error for topic ${topic}: ${parseErr && parseErr.message ? parseErr.message : String(parseErr)}`, "warn");
177
+ jsonMessage = {};
178
+ }
147
179
 
148
180
  if (jsonMessage.type === "jewel_requests_add") {
149
181
  globalCallback(null, { type: "friend_request_received", actorFbId: jsonMessage.from.toString(), timestamp: Date.now().toString() });
@@ -186,11 +218,26 @@ module.exports = function createListenMqtt(deps) {
186
218
  }
187
219
  }
188
220
  } catch (ex) {
189
- logger(`mqtt message parse error: ${ex && ex.message ? ex.message : ex}`, "error");
221
+ const errMsg = ex && ex.message ? ex.message : String(ex || "Unknown error");
222
+ logger(`mqtt message handler error: ${errMsg}`, "error");
223
+ // Don't crash on message parsing errors, just log and continue
190
224
  }
191
225
  });
192
226
 
193
- mqttClient.on("close", function () { });
194
- mqttClient.on("disconnect", () => { });
227
+ mqttClient.on("close", function () {
228
+ if (ctx._ending || ctx._cycling) {
229
+ logger("mqtt close expected", "info");
230
+ return;
231
+ }
232
+ logger("mqtt connection closed", "warn");
233
+ });
234
+
235
+ mqttClient.on("disconnect", () => {
236
+ if (ctx._ending || ctx._cycling) {
237
+ logger("mqtt disconnect expected", "info");
238
+ return;
239
+ }
240
+ logger("mqtt disconnected", "warn");
241
+ });
195
242
  };
196
243
  };
@@ -1,9 +1,35 @@
1
1
  "use strict";
2
2
  module.exports = function createEmitAuth({ logger }) {
3
3
  return function emitAuth(ctx, api, globalCallback, reason, detail) {
4
- try { if (ctx._autoCycleTimer) clearInterval(ctx._autoCycleTimer); } catch (_) { }
5
- try { ctx._ending = true; } catch (_) { }
6
- try { if (ctx.mqttClient) ctx.mqttClient.end(true); } catch (_) { }
4
+ // Clean up all timers
5
+ try {
6
+ if (ctx._autoCycleTimer) {
7
+ clearInterval(ctx._autoCycleTimer);
8
+ ctx._autoCycleTimer = null;
9
+ }
10
+ } catch (_) { }
11
+ try {
12
+ if (ctx._reconnectTimer) {
13
+ clearTimeout(ctx._reconnectTimer);
14
+ ctx._reconnectTimer = null;
15
+ }
16
+ } catch (_) { }
17
+
18
+ try {
19
+ ctx._ending = true;
20
+ ctx._cycling = false;
21
+ } catch (_) { }
22
+
23
+ // Clean up MQTT client
24
+ try {
25
+ if (ctx.mqttClient) {
26
+ ctx.mqttClient.removeAllListeners();
27
+ if (ctx.mqttClient.connected) {
28
+ ctx.mqttClient.end(true);
29
+ }
30
+ }
31
+ } catch (_) { }
32
+
7
33
  ctx.mqttClient = undefined;
8
34
  ctx.loggedIn = false;
9
35
 
@@ -11,12 +37,16 @@ module.exports = function createEmitAuth({ logger }) {
11
37
  logger(`auth change -> ${reason}: ${msg}`, "error");
12
38
 
13
39
  if (typeof globalCallback === "function") {
14
- globalCallback({
15
- type: "account_inactive",
16
- reason,
17
- error: msg,
18
- timestamp: Date.now()
19
- }, null);
40
+ try {
41
+ globalCallback({
42
+ type: "account_inactive",
43
+ reason,
44
+ error: msg,
45
+ timestamp: Date.now()
46
+ }, null);
47
+ } catch (cbErr) {
48
+ logger(`emitAuth callback error: ${cbErr && cbErr.message ? cbErr.message : String(cbErr)}`, "error");
49
+ }
20
50
  }
21
51
  };
22
52
  };
@@ -1,5 +1,6 @@
1
1
  "use strict";
2
2
  const { formatDeltaEvent, formatMessage, _formatAttachment, formatDeltaMessage, formatDeltaReadReceipt, formatID, getType, decodeClientPayload } = require("../../../utils/format");
3
+ const logger = require("../../../func/logger");
3
4
  module.exports = function createParseDelta(deps) {
4
5
  const { markDelivery, parseAndCheckLogin } = deps;
5
6
  return function parseDelta(defaultFuncs, api, ctx, globalCallback, { delta }) {
@@ -21,9 +22,11 @@ module.exports = function createParseDelta(deps) {
21
22
  }
22
23
  } else {
23
24
  const attachment = delta.attachments[i];
24
- if (attachment.mercury.attach_type === "photo") {
25
+ if (attachment && attachment.mercury && attachment.mercury.attach_type === "photo") {
25
26
  api.resolvePhotoUrl(attachment.fbid, (err, url) => {
26
- if (!err) attachment.mercury.metadata.url = url;
27
+ if (!err && attachment.mercury && attachment.mercury.metadata) {
28
+ attachment.mercury.metadata.url = url;
29
+ }
27
30
  resolveAttachmentUrl(i + 1);
28
31
  });
29
32
  } else {
@@ -166,7 +169,10 @@ module.exports = function createParseDelta(deps) {
166
169
  mentions: mobj,
167
170
  timestamp: parseInt(fetchData.timestamp_precise)
168
171
  };
169
- }).catch(err => {}).finally(() => {
172
+ }).catch(err => {
173
+ const errMsg = err && err.message ? err.message : String(err || "Unknown error");
174
+ logger(`parseDelta message_reply fetch error: ${errMsg}`, "warn");
175
+ }).finally(() => {
170
176
  if (ctx.globalOptions.autoMarkDelivery) {
171
177
  markDelivery(ctx, api, callbackToReturn.threadID, callbackToReturn.messageID);
172
178
  }
@@ -313,7 +319,10 @@ module.exports = function createParseDelta(deps) {
313
319
  } else {
314
320
  return;
315
321
  }
316
- }).catch(err => {});
322
+ }).catch(err => {
323
+ const errMsg = err && err.message ? err.message : String(err || "Unknown error");
324
+ logger(`parseDelta ForcedFetch error: ${errMsg}`, "warn");
325
+ });
317
326
  }
318
327
  break;
319
328
  }
@@ -61,6 +61,10 @@ module.exports = function (defaultFuncs, api, ctx, opts) {
61
61
  let conf = mqttConf(ctx, opts);
62
62
 
63
63
  function getSeqIDWrapper() {
64
+ if (ctx._ending && !ctx._cycling) {
65
+ logger("mqtt getSeqID skipped - ending", "warn");
66
+ return Promise.resolve();
67
+ }
64
68
  const form = {
65
69
  av: ctx.globalOptions.pageID,
66
70
  queries: JSON.stringify({
@@ -75,8 +79,25 @@ module.exports = function (defaultFuncs, api, ctx, opts) {
75
79
  };
76
80
  logger("mqtt getSeqID call", "info");
77
81
  return getSeqIDFactory(defaultFuncs, api, ctx, globalCallback, form)
78
- .then(() => { logger("mqtt getSeqID done", "info"); ctx._cycling = false; })
79
- .catch(e => { logger(`mqtt getSeqID error: ${e && e.message ? e.message : e}`, "error"); });
82
+ .then(() => {
83
+ logger("mqtt getSeqID done", "info");
84
+ ctx._cycling = false;
85
+ })
86
+ .catch(e => {
87
+ ctx._cycling = false;
88
+ const errMsg = e && e.message ? e.message : String(e || "Unknown error");
89
+ logger(`mqtt getSeqID error: ${errMsg}`, "error");
90
+ // Don't reconnect if we're ending
91
+ if (ctx._ending) return;
92
+ // Retry after delay if autoReconnect is enabled
93
+ if (ctx.globalOptions.autoReconnect) {
94
+ const d = conf.reconnectDelayMs;
95
+ logger(`mqtt getSeqID will retry in ${d}ms`, "warn");
96
+ setTimeout(() => {
97
+ if (!ctx._ending) getSeqIDWrapper();
98
+ }, d);
99
+ }
100
+ });
80
101
  }
81
102
 
82
103
  function isConnected() {
@@ -84,30 +105,70 @@ module.exports = function (defaultFuncs, api, ctx, opts) {
84
105
  }
85
106
 
86
107
  function unsubAll(cb) {
87
- if (!isConnected()) return cb && cb();
108
+ if (!isConnected()) {
109
+ if (cb) setTimeout(cb, 0);
110
+ return;
111
+ }
88
112
  let pending = topics.length;
89
- if (!pending) return cb && cb();
113
+ if (!pending) {
114
+ if (cb) setTimeout(cb, 0);
115
+ return;
116
+ }
90
117
  let fired = false;
118
+ const timeout = setTimeout(() => {
119
+ if (!fired) {
120
+ fired = true;
121
+ logger("unsubAll timeout, proceeding anyway", "warn");
122
+ if (cb) cb();
123
+ }
124
+ }, 5000); // 5 second timeout
125
+
91
126
  topics.forEach(t => {
92
- ctx.mqttClient.unsubscribe(t, () => {
93
- if (--pending === 0 && !fired) { fired = true; cb && cb(); }
94
- });
127
+ try {
128
+ ctx.mqttClient.unsubscribe(t, () => {
129
+ if (--pending === 0 && !fired) {
130
+ clearTimeout(timeout);
131
+ fired = true;
132
+ if (cb) cb();
133
+ }
134
+ });
135
+ } catch (err) {
136
+ logger(`unsubAll error for topic ${t}: ${err && err.message ? err.message : String(err)}`, "warn");
137
+ if (--pending === 0 && !fired) {
138
+ clearTimeout(timeout);
139
+ fired = true;
140
+ if (cb) cb();
141
+ }
142
+ }
95
143
  });
96
144
  }
97
145
 
98
146
  function endQuietly(next) {
99
147
  const finish = () => {
100
- try { ctx.mqttClient && ctx.mqttClient.removeAllListeners(); } catch (_) { }
148
+ try {
149
+ if (ctx.mqttClient) {
150
+ ctx.mqttClient.removeAllListeners();
151
+ }
152
+ } catch (_) { }
101
153
  ctx.mqttClient = undefined;
102
154
  ctx.lastSeqId = null;
103
155
  ctx.syncToken = undefined;
104
156
  ctx.t_mqttCalled = false;
105
157
  ctx._ending = false;
158
+ ctx._cycling = false;
159
+ if (ctx._reconnectTimer) {
160
+ clearTimeout(ctx._reconnectTimer);
161
+ ctx._reconnectTimer = null;
162
+ }
106
163
  next && next();
107
164
  };
108
165
  try {
109
166
  if (ctx.mqttClient) {
110
- if (isConnected()) { try { ctx.mqttClient.publish("/browser_close", "{}"); } catch (_) { } }
167
+ if (isConnected()) {
168
+ try {
169
+ ctx.mqttClient.publish("/browser_close", "{}", { qos: 0 });
170
+ } catch (_) { }
171
+ }
111
172
  ctx.mqttClient.end(true, finish);
112
173
  } else finish();
113
174
  } catch (_) { finish(); }
@@ -120,7 +181,10 @@ module.exports = function (defaultFuncs, api, ctx, opts) {
120
181
  }
121
182
 
122
183
  function forceCycle() {
123
- if (ctx._cycling) return;
184
+ if (ctx._cycling) {
185
+ logger("mqtt force cycle already in progress", "warn");
186
+ return;
187
+ }
124
188
  ctx._cycling = true;
125
189
  ctx._ending = true;
126
190
  logger("mqtt force cycle begin", "warn");
@@ -140,6 +204,11 @@ module.exports = function (defaultFuncs, api, ctx, opts) {
140
204
  logger("mqtt auto-cycle cleared", "info");
141
205
  }
142
206
 
207
+ if (ctx._reconnectTimer) {
208
+ clearTimeout(ctx._reconnectTimer);
209
+ ctx._reconnectTimer = null;
210
+ }
211
+
143
212
  ctx._ending = true;
144
213
  unsubAll(() => endQuietly(() => {
145
214
  logger("mqtt stopped", "info");
@@ -85,31 +85,85 @@ function parseAndCheckLogin(ctx, http, retryCount = 0) {
85
85
  return `${n}=${v}; Domain=.${service}.com; Path=/; Secure`;
86
86
  };
87
87
 
88
- const maybeAutoLogin = async (resData) => {
88
+ const maybeAutoLogin = async (resData, resConfig) => {
89
+ // Prevent infinite loop if auto login is already in progress
89
90
  if (ctx.auto_login) {
90
- const e = new Error("Not logged in.");
91
+ const e = new Error("Not logged in. Auto login already in progress.");
91
92
  e.error = "Not logged in.";
92
93
  e.res = resData;
93
94
  throw e;
94
95
  }
96
+ // Check if performAutoLogin function exists
95
97
  if (typeof ctx.performAutoLogin !== "function") {
96
- const e = new Error("Not logged in.");
98
+ const e = new Error("Not logged in. Auto login function not available.");
97
99
  e.error = "Not logged in.";
98
100
  e.res = resData;
99
101
  throw e;
100
102
  }
103
+ // Set flag to prevent concurrent auto login attempts
101
104
  ctx.auto_login = true;
102
- logger("Login session expired", "warn");
103
- const ok = await ctx.performAutoLogin();
104
- if (ok) {
105
- logger("Auto login successful! Restarting...");
106
- ctx.auto_login = false;
107
- process.exit(1);
108
- } else {
105
+ logger("Login session expired, attempting auto login...", "warn");
106
+
107
+ try {
108
+ const ok = await ctx.performAutoLogin();
109
+ if (ok) {
110
+ logger("Auto login successful! Retrying request...", "info");
111
+ ctx.auto_login = false;
112
+
113
+ // After successful auto login, retry the original request
114
+ if (resConfig) {
115
+ const url = buildUrl(resConfig);
116
+ const method = String(resConfig?.method || "GET").toUpperCase();
117
+ const ctype = String(headerOf(resConfig?.headers, "content-type") || "").toLowerCase();
118
+ const isMultipart = ctype.includes("multipart/form-data");
119
+ const payload = resConfig?.data;
120
+ const params = resConfig?.params;
121
+
122
+ try {
123
+ let newData;
124
+ if (method === "GET") {
125
+ newData = await http.get(url, ctx.jar, params || null, ctx.globalOptions, ctx);
126
+ } else if (isMultipart) {
127
+ newData = await http.postFormData(url, ctx.jar, payload, params, ctx.globalOptions, ctx);
128
+ } else {
129
+ newData = await http.post(url, ctx.jar, payload, ctx.globalOptions, ctx);
130
+ }
131
+ // Retry parsing with the new response
132
+ return await parseAndCheckLogin(ctx, http, retryCount)(newData);
133
+ } catch (retryErr) {
134
+ logger(`Auto login retry failed: ${retryErr && retryErr.message ? retryErr.message : String(retryErr)}`, "error");
135
+ const e = new Error("Not logged in. Auto login retry failed.");
136
+ e.error = "Not logged in.";
137
+ e.res = resData;
138
+ e.originalError = retryErr;
139
+ throw e;
140
+ }
141
+ } else {
142
+ // No config available, can't retry
143
+ const e = new Error("Not logged in. Auto login successful but cannot retry request.");
144
+ e.error = "Not logged in.";
145
+ e.res = resData;
146
+ throw e;
147
+ }
148
+ } else {
149
+ ctx.auto_login = false;
150
+ const e = new Error("Not logged in. Auto login failed.");
151
+ e.error = "Not logged in.";
152
+ e.res = resData;
153
+ throw e;
154
+ }
155
+ } catch (autoLoginErr) {
109
156
  ctx.auto_login = false;
110
- const e = new Error("Not logged in.");
157
+ // If error already has the right format, rethrow it
158
+ if (autoLoginErr.error === "Not logged in.") {
159
+ throw autoLoginErr;
160
+ }
161
+ // Otherwise, wrap it
162
+ logger(`Auto login error: ${autoLoginErr && autoLoginErr.message ? autoLoginErr.message : String(autoLoginErr)}`, "error");
163
+ const e = new Error("Not logged in. Auto login error.");
111
164
  e.error = "Not logged in.";
112
165
  e.res = resData;
166
+ e.originalError = autoLoginErr;
113
167
  throw e;
114
168
  }
115
169
  };
@@ -121,9 +175,15 @@ function parseAndCheckLogin(ctx, http, retryCount = 0) {
121
175
  err.statusCode = status;
122
176
  err.res = res?.data;
123
177
  err.error = "Request retry failed. Check the `res` and `statusCode` property on this error.";
178
+ logger(`parseAndCheckLogin: Max retries (5) reached for status ${status}`, "error");
124
179
  throw err;
125
180
  }
126
- const retryTime = Math.floor(Math.random() * 5000);
181
+ // Exponential backoff with jitter
182
+ const retryTime = Math.min(
183
+ Math.floor(Math.random() * (1000 * Math.pow(2, retryCount))) + 1000,
184
+ 10000 // Max 10 seconds
185
+ );
186
+ logger(`parseAndCheckLogin: Retrying request (attempt ${retryCount + 1}/5) after ${retryTime}ms for status ${status}`, "warn");
127
187
  await delay(retryTime);
128
188
  const url = buildUrl(res?.config);
129
189
  const method = String(res?.config?.method || "GET").toUpperCase();
@@ -132,16 +192,22 @@ function parseAndCheckLogin(ctx, http, retryCount = 0) {
132
192
  const payload = res?.config?.data;
133
193
  const params = res?.config?.params;
134
194
  retryCount += 1;
135
- if (method === "GET") {
136
- const newData = await http.get(url, ctx.jar, params || null, ctx.globalOptions, ctx);
137
- return await parseAndCheckLogin(ctx, http, retryCount)(newData);
138
- }
139
- if (isMultipart) {
140
- const newData = await http.postFormData(url, ctx.jar, payload, params, ctx.globalOptions, ctx);
141
- return await parseAndCheckLogin(ctx, http, retryCount)(newData);
142
- } else {
143
- const newData = await http.post(url, ctx.jar, payload, ctx.globalOptions, ctx);
144
- return await parseAndCheckLogin(ctx, http, retryCount)(newData);
195
+ try {
196
+ if (method === "GET") {
197
+ const newData = await http.get(url, ctx.jar, params || null, ctx.globalOptions, ctx);
198
+ return await parseAndCheckLogin(ctx, http, retryCount)(newData);
199
+ }
200
+ if (isMultipart) {
201
+ const newData = await http.postFormData(url, ctx.jar, payload, params, ctx.globalOptions, ctx);
202
+ return await parseAndCheckLogin(ctx, http, retryCount)(newData);
203
+ } else {
204
+ const newData = await http.post(url, ctx.jar, payload, ctx.globalOptions, ctx);
205
+ return await parseAndCheckLogin(ctx, http, retryCount)(newData);
206
+ }
207
+ } catch (retryErr) {
208
+ if (retryCount >= 5) throw retryErr;
209
+ // Continue retry loop
210
+ return await parseAndCheckLogin(ctx, http, retryCount)(res);
145
211
  }
146
212
  }
147
213
  if (status === 404) return;
@@ -192,17 +258,24 @@ function parseAndCheckLogin(ctx, http, retryCount = 0) {
192
258
  const resData = parsed;
193
259
  const resStr = JSON.stringify(resData);
194
260
  if (resStr.includes("XCheckpointFBScrapingWarningController") || resStr.includes("601051028565049")) {
195
- await maybeAutoLogin(resData);
261
+ return await maybeAutoLogin(resData, res?.config);
196
262
  }
197
263
  if (resStr.includes("https://www.facebook.com/login.php?") || String(parsed?.redirect || "").includes("login.php?")) {
198
- await maybeAutoLogin(resData);
264
+ return await maybeAutoLogin(resData, res?.config);
199
265
  }
200
266
  if (resStr.includes("1501092823525282")) {
201
267
  logger("Bot checkpoint 282 detected, please check the account!", "error");
202
- process.exit(0);
268
+ const err = new Error("Checkpoint 282 detected");
269
+ err.error = "checkpoint_282";
270
+ err.res = resData;
271
+ throw err;
203
272
  }
204
273
  if (resStr.includes("828281030927956")) {
205
274
  logger("Bot checkpoint 956 detected, please check the account!", "error");
275
+ const err = new Error("Checkpoint 956 detected");
276
+ err.error = "checkpoint_956";
277
+ err.res = resData;
278
+ throw err;
206
279
  }
207
280
  return parsed;
208
281
  };
@@ -17,21 +17,39 @@ const client = wrapper(axios.create({
17
17
  jar,
18
18
  withCredentials: true,
19
19
  timeout: 60000,
20
- validateStatus: s => s >= 200 && s < 600
20
+ validateStatus: s => s >= 200 && s < 600,
21
+ maxRedirects: 5,
22
+ maxContentLength: Infinity,
23
+ maxBodyLength: Infinity
21
24
  }));
22
25
 
23
26
  const delay = ms => new Promise(r => setTimeout(r, ms));
24
27
 
25
- async function requestWithRetry(fn, retries = 3) {
26
- let err;
28
+ async function requestWithRetry(fn, retries = 3, baseDelay = 1000) {
29
+ let lastError;
27
30
  for (let i = 0; i < retries; i++) {
28
- try { return await fn(); } catch (e) {
29
- err = e;
30
- if (i === retries - 1) return e.response ? e.response : Promise.reject(e);
31
- await delay((1 << i) * 1000 + Math.floor(Math.random() * 200));
31
+ try {
32
+ return await fn();
33
+ } catch (e) {
34
+ lastError = e;
35
+ // Don't retry on client errors (4xx) except 429 (rate limit)
36
+ const status = e?.response?.status || e?.statusCode || 0;
37
+ if (status >= 400 && status < 500 && status !== 429) {
38
+ return e.response || Promise.reject(e);
39
+ }
40
+ // Don't retry on last attempt
41
+ if (i === retries - 1) {
42
+ return e.response || Promise.reject(e);
43
+ }
44
+ // Exponential backoff with jitter
45
+ const backoffDelay = Math.min(
46
+ baseDelay * Math.pow(2, i) + Math.floor(Math.random() * 200),
47
+ 30000 // Max 30 seconds
48
+ );
49
+ await delay(backoffDelay);
32
50
  }
33
51
  }
34
- throw err;
52
+ throw lastError || new Error("Request failed after retries");
35
53
  }
36
54
 
37
55
  function cfg(base = {}) {
@@ -69,12 +87,12 @@ function isPairArrayList(arr) {
69
87
  }
70
88
 
71
89
  function cleanGet(url) {
72
- return requestWithRetry(() => client.get(url, cfg()));
90
+ return requestWithRetry(() => client.get(url, cfg()), 3, 1000);
73
91
  }
74
92
 
75
93
  function get(url, reqJar, qs, options, ctx, customHeader) {
76
94
  const headers = getHeaders(url, options, ctx, customHeader);
77
- return requestWithRetry(() => client.get(url, cfg({ reqJar, headers, params: qs })));
95
+ return requestWithRetry(() => client.get(url, cfg({ reqJar, headers, params: qs })), 3, 1000);
78
96
  }
79
97
 
80
98
  function post(url, reqJar, form, options, ctx, customHeader) {
@@ -107,7 +125,7 @@ function post(url, reqJar, form, options, ctx, customHeader) {
107
125
  data = p.toString();
108
126
  headers["Content-Type"] = "application/x-www-form-urlencoded";
109
127
  }
110
- return requestWithRetry(() => client.post(url, data, cfg({ reqJar, headers })));
128
+ return requestWithRetry(() => client.post(url, data, cfg({ reqJar, headers })), 3, 1000);
111
129
  }
112
130
 
113
131
  async function postFormData(url, reqJar, form, qs, options, ctx) {
@@ -160,7 +178,7 @@ async function postFormData(url, reqJar, form, qs, options, ctx) {
160
178
  }
161
179
  }
162
180
  const headers = { ...getHeaders(url, options, ctx), ...fd.getHeaders() };
163
- return requestWithRetry(() => client.post(url, fd, cfg({ reqJar, headers, params: qs })));
181
+ return requestWithRetry(() => client.post(url, fd, cfg({ reqJar, headers, params: qs })), 3, 1000);
164
182
  }
165
183
 
166
184
  function makeDefaults(html, userID, ctx) {