@dongdev/fca-unofficial 3.0.6 → 3.0.7

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 CHANGED
@@ -146,3 +146,6 @@ Too lazy to write changelog, sorry! (will write changelog in the next release, t
146
146
 
147
147
  ## v3.0.5 - 2025-11-27
148
148
  - Hotfix / auto bump
149
+
150
+ ## v3.0.6 - 2025-11-27
151
+ - Hotfix / auto bump
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dongdev/fca-unofficial",
3
- "version": "3.0.6",
3
+ "version": "3.0.7",
4
4
  "description": "Unofficial Facebook Chat API for Node.js - Interact with Facebook Messenger programmatically",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -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
- Math.floor(Math.random() * (1000 * Math.pow(2, retryCount))) + 1000,
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
- if (retryCount >= 5) throw retryErr;
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
  }
@@ -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) headers["X-MSGR-Region"] = 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
- headers[key] = String(value);
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
- return headers;
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 };
@@ -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
- throw lastError || new Error("Request failed after retries");
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,