@dongdev/fca-unofficial 3.0.6 → 3.0.8
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.
- package/CHANGELOG.md +6 -0
- package/package.json +1 -1
- package/src/api/threads/getThreadList.js +49 -4
- package/src/utils/client.js +34 -2
- package/src/utils/headers.js +63 -10
- package/src/utils/request.js +74 -2
package/CHANGELOG.md
CHANGED
package/package.json
CHANGED
|
@@ -226,14 +226,59 @@ module.exports = function(defaultFuncs, api, ctx) {
|
|
|
226
226
|
.post("https://www.facebook.com/api/graphqlbatch/", ctx.jar, form)
|
|
227
227
|
.then(parseAndCheckLogin(ctx, defaultFuncs))
|
|
228
228
|
.then(resData => {
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
229
|
+
// Validate resData is an array and has elements
|
|
230
|
+
if (!resData || !Array.isArray(resData) || resData.length === 0) {
|
|
231
|
+
throw {
|
|
232
|
+
error: "getThreadList: Invalid response data - resData is not a valid array",
|
|
233
|
+
res: resData
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Validate last element exists and has required properties
|
|
238
|
+
const lastElement = resData[resData.length - 1];
|
|
239
|
+
if (!lastElement || typeof lastElement !== "object") {
|
|
240
|
+
throw {
|
|
241
|
+
error: "getThreadList: Invalid response data - last element is missing or invalid",
|
|
242
|
+
res: resData
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (lastElement.error_results > 0) {
|
|
247
|
+
// Check if first element and o0 exist before accessing errors
|
|
248
|
+
if (resData[0] && resData[0].o0 && resData[0].o0.errors) {
|
|
249
|
+
throw resData[0].o0.errors;
|
|
250
|
+
} else {
|
|
251
|
+
throw {
|
|
252
|
+
error: "getThreadList: Error results > 0 but error details not available",
|
|
253
|
+
res: resData
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (lastElement.successful_results === 0) {
|
|
232
259
|
throw {
|
|
233
260
|
error: "getThreadList: there was no successful_results",
|
|
234
261
|
res: resData
|
|
235
262
|
};
|
|
236
|
-
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Validate first element and nested data structure
|
|
266
|
+
if (!resData[0] || !resData[0].o0 || !resData[0].o0.data ||
|
|
267
|
+
!resData[0].o0.data.viewer || !resData[0].o0.data.viewer.message_threads ||
|
|
268
|
+
!Array.isArray(resData[0].o0.data.viewer.message_threads.nodes)) {
|
|
269
|
+
throw {
|
|
270
|
+
error: "getThreadList: Invalid response data structure - missing required fields",
|
|
271
|
+
res: resData
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (timestamp) {
|
|
276
|
+
const nodes = resData[0].o0.data.viewer.message_threads.nodes;
|
|
277
|
+
if (Array.isArray(nodes) && nodes.length > 0) {
|
|
278
|
+
nodes.shift();
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
237
282
|
callback(
|
|
238
283
|
null,
|
|
239
284
|
formatThreadList(resData[0].o0.data.viewer.message_threads.nodes)
|
package/src/utils/client.js
CHANGED
|
@@ -131,6 +131,15 @@ function parseAndCheckLogin(ctx, http, retryCount = 0) {
|
|
|
131
131
|
// Retry parsing with the new response
|
|
132
132
|
return await parseAndCheckLogin(ctx, http, retryCount)(newData);
|
|
133
133
|
} catch (retryErr) {
|
|
134
|
+
// Handle ERR_INVALID_CHAR - don't retry, return error immediately
|
|
135
|
+
if (retryErr?.code === "ERR_INVALID_CHAR" || (retryErr?.message && retryErr.message.includes("Invalid character in header"))) {
|
|
136
|
+
logger(`Auto login retry failed: Invalid header detected. Error: ${retryErr.message}`, "error");
|
|
137
|
+
const e = new Error("Not logged in. Auto login retry failed due to invalid header.");
|
|
138
|
+
e.error = "Not logged in.";
|
|
139
|
+
e.res = resData;
|
|
140
|
+
e.originalError = retryErr;
|
|
141
|
+
throw e;
|
|
142
|
+
}
|
|
134
143
|
logger(`Auto login retry failed: ${retryErr && retryErr.message ? retryErr.message : String(retryErr)}`, "error");
|
|
135
144
|
const e = new Error("Not logged in. Auto login retry failed.");
|
|
136
145
|
e.error = "Not logged in.";
|
|
@@ -179,8 +188,12 @@ function parseAndCheckLogin(ctx, http, retryCount = 0) {
|
|
|
179
188
|
throw err;
|
|
180
189
|
}
|
|
181
190
|
// Exponential backoff with jitter
|
|
191
|
+
// First retry: ~1507ms (1500ms base + small jitter)
|
|
192
|
+
// Subsequent retries: exponential backoff
|
|
193
|
+
const baseDelay = retryCount === 0 ? 1500 : 1000 * Math.pow(2, retryCount);
|
|
194
|
+
const jitter = Math.floor(Math.random() * 200); // 0-199ms jitter
|
|
182
195
|
const retryTime = Math.min(
|
|
183
|
-
|
|
196
|
+
baseDelay + jitter,
|
|
184
197
|
10000 // Max 10 seconds
|
|
185
198
|
);
|
|
186
199
|
logger(`parseAndCheckLogin: Retrying request (attempt ${retryCount + 1}/5) after ${retryTime}ms for status ${status}`, "warn");
|
|
@@ -205,7 +218,26 @@ function parseAndCheckLogin(ctx, http, retryCount = 0) {
|
|
|
205
218
|
return await parseAndCheckLogin(ctx, http, retryCount)(newData);
|
|
206
219
|
}
|
|
207
220
|
} catch (retryErr) {
|
|
208
|
-
|
|
221
|
+
// Handle ERR_INVALID_CHAR - don't retry, return error immediately
|
|
222
|
+
if (retryErr?.code === "ERR_INVALID_CHAR" || (retryErr?.message && retryErr.message.includes("Invalid character in header"))) {
|
|
223
|
+
logger(`parseAndCheckLogin: Invalid header detected, aborting retry. Error: ${retryErr.message}`, "error");
|
|
224
|
+
const err = new Error("Invalid header content detected. Request aborted to prevent crash.");
|
|
225
|
+
err.error = "Invalid header content";
|
|
226
|
+
err.statusCode = status;
|
|
227
|
+
err.res = res?.data;
|
|
228
|
+
err.originalError = retryErr;
|
|
229
|
+
throw err;
|
|
230
|
+
}
|
|
231
|
+
// If max retries reached, return error instead of throwing to prevent crash
|
|
232
|
+
if (retryCount >= 5) {
|
|
233
|
+
logger(`parseAndCheckLogin: Max retries reached, returning error instead of crashing`, "error");
|
|
234
|
+
const err = new Error("Request retry failed after 5 attempts. Check the `res` and `statusCode` property on this error.");
|
|
235
|
+
err.statusCode = status;
|
|
236
|
+
err.res = res?.data;
|
|
237
|
+
err.error = "Request retry failed after 5 attempts";
|
|
238
|
+
err.originalError = retryErr;
|
|
239
|
+
throw err;
|
|
240
|
+
}
|
|
209
241
|
// Continue retry loop
|
|
210
242
|
return await parseAndCheckLogin(ctx, http, retryCount)(res);
|
|
211
243
|
}
|
package/src/utils/headers.js
CHANGED
|
@@ -1,5 +1,42 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
+
// Sanitize header value to remove invalid characters
|
|
4
|
+
function sanitizeHeaderValue(value) {
|
|
5
|
+
if (value === null || value === undefined) return "";
|
|
6
|
+
let str = String(value);
|
|
7
|
+
|
|
8
|
+
// Remove array-like strings (e.g., "["performAutoLogin"]")
|
|
9
|
+
// This handles cases where arrays were accidentally stringified
|
|
10
|
+
if (str.trim().startsWith("[") && str.trim().endsWith("]")) {
|
|
11
|
+
// Try to detect if it's a stringified array and remove it
|
|
12
|
+
try {
|
|
13
|
+
const parsed = JSON.parse(str);
|
|
14
|
+
if (Array.isArray(parsed)) {
|
|
15
|
+
// If it's an array, return empty string (invalid header value)
|
|
16
|
+
return "";
|
|
17
|
+
}
|
|
18
|
+
} catch {
|
|
19
|
+
// Not valid JSON, continue with normal sanitization
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Remove invalid characters for HTTP headers:
|
|
24
|
+
// - Control characters (0x00-0x1F, except HTAB 0x09)
|
|
25
|
+
// - DEL character (0x7F)
|
|
26
|
+
// - Newlines and carriage returns
|
|
27
|
+
// - Square brackets (often indicate array stringification issues)
|
|
28
|
+
str = str.replace(/[\x00-\x08\x0B-\x0C\x0E-\x1F\x7F\r\n\[\]]/g, "").trim();
|
|
29
|
+
|
|
30
|
+
return str;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Sanitize header name to ensure it's valid
|
|
34
|
+
function sanitizeHeaderName(name) {
|
|
35
|
+
if (!name || typeof name !== "string") return "";
|
|
36
|
+
// Remove invalid characters for HTTP header names
|
|
37
|
+
return name.replace(/[^\x21-\x7E]/g, "").trim();
|
|
38
|
+
}
|
|
39
|
+
|
|
3
40
|
function getHeaders(url, options, ctx, customHeader) {
|
|
4
41
|
const u = new URL(url);
|
|
5
42
|
const ua = options?.userAgent || "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36";
|
|
@@ -8,14 +45,14 @@ function getHeaders(url, options, ctx, customHeader) {
|
|
|
8
45
|
const contentType = options?.contentType || "application/x-www-form-urlencoded";
|
|
9
46
|
const acceptLang = options?.acceptLanguage || "en-US,en;q=0.9,vi;q=0.8";
|
|
10
47
|
const headers = {
|
|
11
|
-
Host: u.host,
|
|
12
|
-
Origin: origin,
|
|
13
|
-
Referer: referer,
|
|
14
|
-
"User-Agent": ua,
|
|
48
|
+
Host: sanitizeHeaderValue(u.host),
|
|
49
|
+
Origin: sanitizeHeaderValue(origin),
|
|
50
|
+
Referer: sanitizeHeaderValue(referer),
|
|
51
|
+
"User-Agent": sanitizeHeaderValue(ua),
|
|
15
52
|
Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,application/json;q=0.8,*/*;q=0.7",
|
|
16
|
-
"Accept-Language": acceptLang,
|
|
53
|
+
"Accept-Language": sanitizeHeaderValue(acceptLang),
|
|
17
54
|
"Accept-Encoding": "gzip, deflate, br",
|
|
18
|
-
"Content-Type": contentType,
|
|
55
|
+
"Content-Type": sanitizeHeaderValue(contentType),
|
|
19
56
|
Connection: "keep-alive",
|
|
20
57
|
DNT: "1",
|
|
21
58
|
"Upgrade-Insecure-Requests": "1",
|
|
@@ -33,7 +70,10 @@ function getHeaders(url, options, ctx, customHeader) {
|
|
|
33
70
|
Pragma: "no-cache",
|
|
34
71
|
"Cache-Control": "no-cache"
|
|
35
72
|
};
|
|
36
|
-
if (ctx?.region)
|
|
73
|
+
if (ctx?.region) {
|
|
74
|
+
const regionValue = sanitizeHeaderValue(ctx.region);
|
|
75
|
+
if (regionValue) headers["X-MSGR-Region"] = regionValue;
|
|
76
|
+
}
|
|
37
77
|
if (customHeader && typeof customHeader === "object") {
|
|
38
78
|
// Filter customHeader to only include valid HTTP header values (strings, numbers, booleans)
|
|
39
79
|
// Exclude functions, objects, arrays, and other non-serializable values
|
|
@@ -50,13 +90,26 @@ function getHeaders(url, options, ctx, customHeader) {
|
|
|
50
90
|
// Skip plain objects (but allow null which is already handled above)
|
|
51
91
|
continue;
|
|
52
92
|
}
|
|
53
|
-
// Only allow strings, numbers, and booleans - convert to string
|
|
93
|
+
// Only allow strings, numbers, and booleans - convert to string and sanitize
|
|
54
94
|
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
55
|
-
|
|
95
|
+
const sanitizedKey = sanitizeHeaderName(key);
|
|
96
|
+
const sanitizedValue = sanitizeHeaderValue(value);
|
|
97
|
+
if (sanitizedKey && sanitizedValue !== "") {
|
|
98
|
+
headers[sanitizedKey] = sanitizedValue;
|
|
99
|
+
}
|
|
56
100
|
}
|
|
57
101
|
}
|
|
58
102
|
}
|
|
59
|
-
|
|
103
|
+
// Final pass: sanitize all header values to ensure no invalid characters
|
|
104
|
+
const sanitizedHeaders = {};
|
|
105
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
106
|
+
const sanitizedKey = sanitizeHeaderName(key);
|
|
107
|
+
const sanitizedValue = sanitizeHeaderValue(value);
|
|
108
|
+
if (sanitizedKey && sanitizedValue !== "") {
|
|
109
|
+
sanitizedHeaders[sanitizedKey] = sanitizedValue;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return sanitizedHeaders;
|
|
60
113
|
}
|
|
61
114
|
|
|
62
115
|
module.exports = { getHeaders };
|
package/src/utils/request.js
CHANGED
|
@@ -12,6 +12,66 @@ const getType = formatMod.getType || formatMod;
|
|
|
12
12
|
const constMod = require("./constants");
|
|
13
13
|
const getFrom = constMod.getFrom || constMod;
|
|
14
14
|
|
|
15
|
+
// Sanitize header value to remove invalid characters
|
|
16
|
+
function sanitizeHeaderValue(value) {
|
|
17
|
+
if (value === null || value === undefined) return "";
|
|
18
|
+
const str = String(value);
|
|
19
|
+
// Remove invalid characters for HTTP headers:
|
|
20
|
+
// - Control characters (0x00-0x1F, except HTAB 0x09)
|
|
21
|
+
// - DEL character (0x7F)
|
|
22
|
+
// - Newlines and carriage returns
|
|
23
|
+
return str.replace(/[\x00-\x08\x0B-\x0C\x0E-\x1F\x7F\r\n]/g, "").trim();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Sanitize header name to ensure it's valid
|
|
27
|
+
function sanitizeHeaderName(name) {
|
|
28
|
+
if (!name || typeof name !== "string") return "";
|
|
29
|
+
// Remove invalid characters for HTTP header names
|
|
30
|
+
return name.replace(/[^\x21-\x7E]/g, "").trim();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Sanitize all headers in an object
|
|
34
|
+
function sanitizeHeaders(headers) {
|
|
35
|
+
if (!headers || typeof headers !== "object") return {};
|
|
36
|
+
const sanitized = {};
|
|
37
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
38
|
+
const sanitizedKey = sanitizeHeaderName(key);
|
|
39
|
+
if (!sanitizedKey) continue;
|
|
40
|
+
|
|
41
|
+
// Handle arrays - skip them entirely
|
|
42
|
+
if (Array.isArray(value)) continue;
|
|
43
|
+
|
|
44
|
+
// Handle objects - skip them
|
|
45
|
+
if (value !== null && typeof value === "object") continue;
|
|
46
|
+
|
|
47
|
+
// Handle functions - skip them
|
|
48
|
+
if (typeof value === "function") continue;
|
|
49
|
+
|
|
50
|
+
// Check if string value looks like a stringified array (e.g., "["performAutoLogin"]")
|
|
51
|
+
if (typeof value === "string") {
|
|
52
|
+
const trimmed = value.trim();
|
|
53
|
+
if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
|
|
54
|
+
// Try to parse as JSON array - if successful, skip this header
|
|
55
|
+
try {
|
|
56
|
+
const parsed = JSON.parse(trimmed);
|
|
57
|
+
if (Array.isArray(parsed)) {
|
|
58
|
+
continue; // Skip stringified arrays
|
|
59
|
+
}
|
|
60
|
+
} catch {
|
|
61
|
+
// Not valid JSON array, continue with normal sanitization
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Sanitize the value
|
|
67
|
+
const sanitizedValue = sanitizeHeaderValue(value);
|
|
68
|
+
if (sanitizedValue !== "") {
|
|
69
|
+
sanitized[sanitizedKey] = sanitizedValue;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return sanitized;
|
|
73
|
+
}
|
|
74
|
+
|
|
15
75
|
const jar = new CookieJar();
|
|
16
76
|
const client = wrapper(axios.create({
|
|
17
77
|
jar,
|
|
@@ -32,6 +92,16 @@ async function requestWithRetry(fn, retries = 3, baseDelay = 1000) {
|
|
|
32
92
|
return await fn();
|
|
33
93
|
} catch (e) {
|
|
34
94
|
lastError = e;
|
|
95
|
+
|
|
96
|
+
// Handle ERR_INVALID_CHAR and other header errors - don't retry, return error immediately
|
|
97
|
+
if (e?.code === "ERR_INVALID_CHAR" || (e?.message && e.message.includes("Invalid character in header"))) {
|
|
98
|
+
const err = new Error("Invalid header content detected. Request aborted to prevent crash.");
|
|
99
|
+
err.error = "Invalid header content";
|
|
100
|
+
err.originalError = e;
|
|
101
|
+
err.code = "ERR_INVALID_CHAR";
|
|
102
|
+
return Promise.reject(err);
|
|
103
|
+
}
|
|
104
|
+
|
|
35
105
|
// Don't retry on client errors (4xx) except 429 (rate limit)
|
|
36
106
|
const status = e?.response?.status || e?.statusCode || 0;
|
|
37
107
|
if (status >= 400 && status < 500 && status !== 429) {
|
|
@@ -49,13 +119,15 @@ async function requestWithRetry(fn, retries = 3, baseDelay = 1000) {
|
|
|
49
119
|
await delay(backoffDelay);
|
|
50
120
|
}
|
|
51
121
|
}
|
|
52
|
-
|
|
122
|
+
// Return error instead of throwing to prevent uncaught exception
|
|
123
|
+
const finalError = lastError || new Error("Request failed after retries");
|
|
124
|
+
return Promise.reject(finalError);
|
|
53
125
|
}
|
|
54
126
|
|
|
55
127
|
function cfg(base = {}) {
|
|
56
128
|
const { reqJar, headers, params, agent, timeout } = base;
|
|
57
129
|
return {
|
|
58
|
-
headers,
|
|
130
|
+
headers: sanitizeHeaders(headers),
|
|
59
131
|
params,
|
|
60
132
|
jar: reqJar || jar,
|
|
61
133
|
withCredentials: true,
|