You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Error: SyntaxError: Unexpected token 'l', "lass FPClassifier {" is not valid JSON
Suspected cause
Pusher's chunked-XHR transport parses an incomplete chunk as JSON when the response looks like a truncated JS class definition (class FPClassifier {}), suggesting the client occasionally receives a different content type than expected mid-stream.
Reproduction
Unknown — see Sentry events linked from the Sentry issue.
Pusher did not send a truncated payload. A complete line of foreign JavaScript (class FPClassifier {) reached pusher-js inside an HTTP 200 response body. pusher-js then tried to parse it as a protocol frame.
When WebSockets fail or are slow, pusher-js falls back to xhr_streaming. That transport splits the response into lines and uses each line's first character as the frame type. A line that starts with c is treated as a "close" frame and passed to JSON.parse without a try/catch (http_socket.ts:109-111). The parse throws inside the XHR onprogress handler. Sentry wraps that handler, so the error is reported as auto.browser.browserapierrors.xhr.onprogress.
FPClassifier does not exist in App, its dependencies, or the service worker. The text most likely comes from the user's environment, such as an extension that overrides XMLHttpRequest or a proxy that rewrites responses. That fits the small number of affected users (20).
What changes do you think we should make in order to solve the problem?
Add a patch-package patch for pusher-js@8.3.0 in patches/pusher-js/, with a details.md like the other patches.
In HTTPSocket.onChunk, wrap only the JSON.parse call for the o, a, m, and c frames in a try/catch.
If the parse fails, skip the line, the same way the switch already ignores lines with unknown prefixes.
Do not wrap the onOpen, onEvent, or onClose calls. They run App's channel callbacks, and a broad catch would hide real App errors.
The patch must change dist/web/pusher-with-encryption.js, because App imports pusher-js/with-encryption. This matches how the WebSocket transport already handles bad messages (it catches the error and emits MessageParseError). Upstream pusher-js still has the same unguarded code, so upgrading will not fix this.
What alternative solutions did you explore? (Optional)
Sentry ignoreErrors / beforeSend filter: This hides the report, but the uncaught exception still happens. An /FPClassifier/ filter only matches this one string. The existing denyUrls and third-party filters in setupSentry.ts do not match it, because the throwing frame is pusher-js inside our own bundle.
Disable xhr_streaming / xhr_polling: Not recommended. These transports are the only fallback when WebSockets are blocked. xhr_polling also uses the same onChunk code.
Evidence and open questions
App uses the default transport strategy and sets only cluster, activityTimeout, pongTimeout, and the authorizer (src/libs/Pusher/index.ts:88-98).
pusher-js only passes complete, newline-terminated lines to onChunk, so the input was not cut off. Non-200 responses are dropped before parsing (http_socket.ts:81-84).
The class body lines start with whitespace, so pusher-js ignores them. Only the class … line throws. Injected lines that start with a, o, or m (for example, async …) would throw too and may appear as separate Sentry issues. The proposed patch covers them.
Not verified: what injects FPClassifier. The Sentry MCP connection failed during this run, so I could not check the browser or extension tags on the events. Checking those tags would confirm the source. The fix does not depend on it.
The /search/r/:reportID route is probably where affected users happen to be. Nothing in the code ties the error to that route.
Next Steps for Contributor+ team: To accept:@MelvinBot implement [this](https://github.com/Expensify/App/issues/102745) to create a draft PR. To refine:@MelvinBot <your feedback> To reject: Explain why you are rejecting Melvin's proposal.
Please re-state the problem that we are trying to solve in this issue.
Sentry issue APP-27B reports SyntaxError: Unexpected token 'l', "lass FPClassifier {" is not valid JSON thrown from pusher-js's XHR streaming transport (xhr.onprogress). This occurs on web clients when Pusher attempts to use HTTP chunked/streaming fallback transports and receives an unexpected HTML/JS response (such as proxy/firewall injection or bot detection like class FPClassifier) instead of a valid Pusher protocol chunk.
By default in pusher-js on web browsers, when enabledTransports is omitted, the library includes HTTP-based fallback transports: ['ws', 'wss', 'xhr_streaming', 'xhr_polling', 'sockjs'].
When a client experiences network fluctuations, corporate firewall restrictions, or captive portals where WebSockets fail to negotiate, pusher-js falls back to xhr_streaming.
In certain corporate or proxied network environments, the HTTP streaming endpoint is intercepted by middleboxes or security layers (which return JavaScript/HTML containing class FPClassifier { ... }).
pusher-js's internal XHR streaming transport blindly attempts JSON.parse(chunk.data.slice(1) || '[]') on incoming chunks, throwing an unhandled SyntaxError that bubbles up to Sentry.
Moreover, NewDot uses encrypted channels (pusher-js/with-encryption), which are designed specifically for secure WebSocket connections (ws/wss). HTTP streaming fallbacks are neither intended nor suitable for Expensify's encrypted real-time architecture.
What changes do you think we should make in order to solve the problem?
Solution 1 (Recommended: Restrict Pusher to WebSocket transports):
In src/libs/Pusher/index.ts:
Configure enabledTransports: ['ws', 'wss'] in the Pusher initialization options:
Solution 1 is strongly recommended as the primary fix because disabling HTTP fallback transports eliminates the underlying crash altogether.
What alternative solutions did you explore? (Optional)
Patching pusher-js via patch-package: Modifying node_modules/pusher-js to wrap JSON.parse in a try/catch. Maintaining a patch-package for a third-party dependency is unnecessary when configuring enabledTransports: ['ws', 'wss'] natively resolves the issue.
Skip lines that fail to parse without emitting anything. Do not emit error / MessageParseError, because PusherConnectionManager would send it to Log.alert.
In the c case, also skip the line if the parsed payload is not an array.
Add a Jest test that feeds the HTTP stream a class FPClassifier { line before the o and a frames. Check that:
onopen and onmessage still fire in the same progress event.
onclose fires when the response ends.
In details.md, say the injected text comes from Forcepoint DLP, which adds its script to Pusher's application/javascript stream, and that upstream pusher-js master has the same unguarded parse.
Activity
melvin-bot commentedon Oct 1, 2026
Triggered auto assignment to Contributor-plus team member for initial proposal review - @abzokhattab (
External)[-][Sentry: APP-27B] Pusher chunked JSON parse breaks on truncated payload[/-][+][$175] [Sentry: APP-27B] Pusher chunked JSON parse breaks on truncated payload[/+]melvin-bot commentedon Oct 1, 2026
Job added to Upwork: https://www.upwork.com/jobs/~022105599620635439941
MelvinBot commentedon Oct 1, 2026
Proposal
What is the root cause of that problem?
Pusher did not send a truncated payload. A complete line of foreign JavaScript (
class FPClassifier {) reached pusher-js inside an HTTP 200 response body. pusher-js then tried to parse it as a protocol frame.When WebSockets fail or are slow, pusher-js falls back to
xhr_streaming. That transport splits the response into lines and uses each line's first character as the frame type. A line that starts withcis treated as a "close" frame and passed toJSON.parsewithout atry/catch(http_socket.ts:109-111). The parse throws inside the XHRonprogresshandler. Sentry wraps that handler, so the error is reported asauto.browser.browserapierrors.xhr.onprogress.FPClassifierdoes not exist in App, its dependencies, or the service worker. The text most likely comes from the user's environment, such as an extension that overridesXMLHttpRequestor a proxy that rewrites responses. That fits the small number of affected users (20).What changes do you think we should make in order to solve the problem?
Add a patch-package patch for
pusher-js@8.3.0inpatches/pusher-js/, with adetails.mdlike the other patches.HTTPSocket.onChunk, wrap only theJSON.parsecall for theo,a,m, andcframes in atry/catch.switchalready ignores lines with unknown prefixes.onOpen,onEvent, oronClosecalls. They run App's channel callbacks, and a broad catch would hide real App errors.The patch must change
dist/web/pusher-with-encryption.js, because App importspusher-js/with-encryption. This matches how the WebSocket transport already handles bad messages (it catches the error and emitsMessageParseError). Upstream pusher-js still has the same unguarded code, so upgrading will not fix this.What alternative solutions did you explore? (Optional)
ignoreErrors/beforeSendfilter: This hides the report, but the uncaught exception still happens. An/FPClassifier/filter only matches this one string. The existingdenyUrlsand third-party filters insetupSentry.tsdo not match it, because the throwing frame is pusher-js inside our own bundle.xhr_streaming/xhr_polling: Not recommended. These transports are the only fallback when WebSockets are blocked.xhr_pollingalso uses the sameonChunkcode.Evidence and open questions
cluster,activityTimeout,pongTimeout, and the authorizer (src/libs/Pusher/index.ts:88-98).onChunk, so the input was not cut off. Non-200 responses are dropped before parsing (http_socket.ts:81-84).class …line throws. Injected lines that start witha,o, orm(for example,async …) would throw too and may appear as separate Sentry issues. The proposed patch covers them.FPClassifier. The Sentry MCP connection failed during this run, so I could not check the browser or extension tags on the events. Checking those tags would confirm the source. The fix does not depend on it./search/r/:reportIDroute is probably where affected users happen to be. Nothing in the code ties the error to that route.Possibly related: Audit Sentry error volume and filter noise to stop paying overages
Next Steps for Contributor+ team:
To accept:
@MelvinBot implement [this](https://github.com/Expensify/App/issues/102745)to create a draft PR.To refine:
@MelvinBot <your feedback>To reject: Explain why you are rejecting Melvin's proposal.
view run
camesenin commentedon Oct 1, 2026
Proposal
Please re-state the problem that we are trying to solve in this issue.
Sentry issue
APP-27BreportsSyntaxError: Unexpected token 'l', "lass FPClassifier {" is not valid JSONthrown frompusher-js's XHR streaming transport (xhr.onprogress). This occurs on web clients when Pusher attempts to use HTTP chunked/streaming fallback transports and receives an unexpected HTML/JS response (such as proxy/firewall injection or bot detection likeclass FPClassifier) instead of a valid Pusher protocol chunk.What is the root cause of that problem?
In
src/libs/Pusher/index.ts:optionsdoes not specifyenabledTransports.pusher-json web browsers, whenenabledTransportsis omitted, the library includes HTTP-based fallback transports:['ws', 'wss', 'xhr_streaming', 'xhr_polling', 'sockjs'].pusher-jsfalls back toxhr_streaming.class FPClassifier { ... }).pusher-js's internal XHR streaming transport blindly attemptsJSON.parse(chunk.data.slice(1) || '[]')on incoming chunks, throwing an unhandledSyntaxErrorthat bubbles up to Sentry.pusher-js/with-encryption), which are designed specifically for secure WebSocket connections (ws/wss). HTTP streaming fallbacks are neither intended nor suitable for Expensify's encrypted real-time architecture.What changes do you think we should make in order to solve the problem?
Solution 1 (Recommended: Restrict Pusher to WebSocket transports):
In
src/libs/Pusher/index.ts:Configure
enabledTransports: ['ws', 'wss']in the Pusher initialization options:const options: Options = { cluster: args.cluster, activityTimeout: CONST.PUSHER.ACTIVITY_TIMEOUT_MS, pongTimeout: CONST.PUSHER.PONG_TIMEOUT_MS, + enabledTransports: ['ws', 'wss'], };Why this is the cleanest solution:
xhr_streamingandxhr_pollingtransports where the unhandled parsing crash occurs.wss), which aligns withpusher-js/with-encryptionrequirements.Solution 2 (Add Sentry ignore rule as defensive measure):
In
src/libs/Log.tsor Sentry client configuration:Filter out
SyntaxErrororiginating frompusher-jsXHR streaming transport inbeforeSend:Solution 1 is strongly recommended as the primary fix because disabling HTTP fallback transports eliminates the underlying crash altogether.
What alternative solutions did you explore? (Optional)
pusher-jsvia patch-package: Modifyingnode_modules/pusher-jsto wrapJSON.parsein atry/catch. Maintaining a patch-package for a third-party dependency is unnecessary when configuringenabledTransports: ['ws', 'wss']natively resolves the issue.abzokhattab commentedon Oct 1, 2026
Melvin proposal looks good to me 🎀 👀 🎀 C+ reviewed
melvin-bot commentedon Oct 1, 2026
Current assignee @mountiny is eligible for the choreEngineerContributorManagement assigner, not assigning anyone new.
abzokhattab commentedon Oct 1, 2026
@MelvinBot implement this with these changes:
error/MessageParseError, becausePusherConnectionManagerwould send it toLog.alert.ccase, also skip the line if the parsed payload is not an array.class FPClassifier {line before theoandaframes. Check that:onopenandonmessagestill fire in the same progress event.onclosefires when the response ends.details.md, say the injected text comes from Forcepoint DLP, which adds its script to Pusher'sapplication/javascriptstream, and that upstream pusher-jsmasterhas the same unguarded parse.25 remaining items
abzokhattab commentedon Oct 10, 2026
Still on hold for the upstream library version bump
melvin-bot commentedon Oct 11, 2026
Triggered auto assignment to @mallenexpensify (
Awaiting Payment)melvin-bot commentedon Oct 11, 2026
Payment Summary
Resolving PRs:
Skip malformed lines in the pusher-js HTTP stream instead of throwing #102752
Reviewer: @abzokhattab owed $175 via NewDot
BugZero Checklist (@mallenexpensify)
abzokhattab commentedon Oct 12, 2026
waiting for the library version upgrade #102745 (comment)
[-][Due for payment 2026-10-12] [$175] [Sentry: APP-27B] Pusher chunked JSON parse breaks on truncated payload[/-][+][$175] [Sentry: APP-27B] Pusher chunked JSON parse breaks on truncated payload[/+]mallenexpensify commentedon Oct 14, 2026
Please comment and tag me if/when payment is due, thx