Skip to content

Commit

Permalink
Fix long-poll request cancellation (#434)
Browse files Browse the repository at this point in the history
fix(subscribe): fix long-poll request cancellation

Fix long-poll request cancellation caused by APM packages monkey patching 'fetch' and try to use
'native' implementation instead of patched.
  • Loading branch information
parfeon authored Jan 31, 2025
1 parent 0d424f9 commit a204205
Show file tree
Hide file tree
Showing 13 changed files with 152 additions and 21 deletions.
11 changes: 8 additions & 3 deletions .pubnub.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
---
changelog:
- date: 2025-01-31
version: v8.7.1
changes:
- type: bug
text: "Fix long-poll request cancellation caused by APM packages monkey patching 'fetch' and try to use 'native' implementation instead of patched."
- date: 2025-01-30
version: v8.7.0
changes:
Expand Down Expand Up @@ -1118,7 +1123,7 @@ supported-platforms:
- 'Ubuntu 14.04 and up'
- 'Windows 7 and up'
version: 'Pubnub Javascript for Node'
version: '8.7.0'
version: '8.7.1'
sdks:
- full-name: PubNub Javascript SDK
short-name: Javascript
Expand All @@ -1134,7 +1139,7 @@ sdks:
- distribution-type: source
distribution-repository: GitHub release
package-name: pubnub.js
location: https://github.com/pubnub/javascript/archive/refs/tags/v8.7.0.zip
location: https://github.com/pubnub/javascript/archive/refs/tags/v8.7.1.zip
requires:
- name: 'agentkeepalive'
min-version: '3.5.2'
Expand Down Expand Up @@ -1805,7 +1810,7 @@ sdks:
- distribution-type: library
distribution-repository: GitHub release
package-name: pubnub.js
location: https://github.com/pubnub/javascript/releases/download/v8.7.0/pubnub.8.7.0.js
location: https://github.com/pubnub/javascript/releases/download/v8.7.1/pubnub.8.7.1.js
requires:
- name: 'agentkeepalive'
min-version: '3.5.2'
Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
## v8.7.1
January 31 2025

#### Fixed
- Fix long-poll request cancellation caused by APM packages monkey patching 'fetch' and try to use 'native' implementation instead of patched.

## v8.7.0
January 30 2025

Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ Watch [Getting Started with PubNub JS SDK](https://app.dashcam.io/replay/64ee0d2
npm install pubnub
```
* or download one of our builds from our CDN:
* https://cdn.pubnub.com/sdk/javascript/pubnub.8.7.0.js
* https://cdn.pubnub.com/sdk/javascript/pubnub.8.7.0.min.js
* https://cdn.pubnub.com/sdk/javascript/pubnub.8.7.1.js
* https://cdn.pubnub.com/sdk/javascript/pubnub.8.7.1.min.js
2. Configure your keys:
Expand Down
46 changes: 42 additions & 4 deletions dist/web/pubnub.js
Original file line number Diff line number Diff line change
Expand Up @@ -3370,14 +3370,24 @@
/**
* Create and configure transport provider for Web and Rect environments.
*
* @param originalFetch - Pointer to the original (not monkey patched) `fetch` implementation.
* @param [keepAlive] - Whether client should try to keep connections open for reuse or not.
* @param logVerbosity - Whether verbose logs should be printed or not.
*
* @internal
*/
constructor(keepAlive = false, logVerbosity) {
constructor(originalFetch, keepAlive = false, logVerbosity = false) {
this.keepAlive = keepAlive;
this.logVerbosity = logVerbosity;
WebReactNativeTransport.originalFetch = originalFetch;
// Check whether `fetch` has been monkey patched or not.
if (logVerbosity && this.isFetchMonkeyPatched()) {
console.warn("[PubNub] Native Web Fetch API 'fetch' function monkey patched.");
if (!this.isFetchMonkeyPatched(WebReactNativeTransport.originalFetch))
console.info("[PubNub] Use native Web Fetch API 'fetch' implementation from iframe as APM workaround.");
else
console.warn('[PubNub] Unable receive native Web Fetch API. There can be issues with subscribe long-poll cancellation');
}
}
makeSendable(req) {
let controller;
Expand Down Expand Up @@ -3407,7 +3417,11 @@
}, req.timeout * 1000);
});
return Promise.race([
fetch(request, { signal: abortController === null || abortController === void 0 ? void 0 : abortController.signal, credentials: 'omit', cache: 'no-cache' }),
WebReactNativeTransport.originalFetch(request, {
signal: abortController === null || abortController === void 0 ? void 0 : abortController.signal,
credentials: 'omit',
cache: 'no-cache',
}),
requestTimeout,
])
.then((response) => response.arrayBuffer().then((arrayBuffer) => [response, arrayBuffer]))
Expand Down Expand Up @@ -3522,6 +3536,17 @@
console.log('-----');
}
}
/**
* Check whether original `fetch` has been monkey patched or not.
*
* @returns `true` if original `fetch` has been patched.
*
* @internal
*/
isFetchMonkeyPatched(oFetch) {
const fetchString = (oFetch !== null && oFetch !== void 0 ? oFetch : fetch).toString();
return !fetchString.includes('[native code]') && fetch.name !== 'fetch';
}
}
/**
* Service {@link ArrayBuffer} response decoder.
Expand Down Expand Up @@ -3975,7 +4000,7 @@
return base.PubNubFile;
},
get version() {
return '8.7.0';
return '8.7.1';
},
getVersion() {
return this.version;
Expand Down Expand Up @@ -14544,7 +14569,7 @@
let cryptography;
cryptography = new WebCryptography();
// Setup transport provider.
let transport = new WebReactNativeTransport(clientConfiguration.keepAlive, clientConfiguration.logVerbosity);
let transport = new WebReactNativeTransport(PubNub.originalFetch(), clientConfiguration.keepAlive, clientConfiguration.logVerbosity);
{
if (configurationCopy.subscriptionWorkerUrl) {
// Inject subscription worker into transport provider stack.
Expand Down Expand Up @@ -14593,6 +14618,19 @@
this.listenerManager.announceNetworkUp();
this.reconnect();
}
static originalFetch() {
let iframe = document.querySelector('iframe[name="pubnub-context-unpatched-fetch"]');
if (!iframe) {
iframe = document.createElement('iframe');
iframe.style.display = 'none';
iframe.name = 'pubnub-context-unpatched-fetch';
iframe.src = 'about:blank';
document.body.appendChild(iframe);
}
if (iframe.contentWindow)
return iframe.contentWindow.fetch.bind(iframe.contentWindow);
return fetch;
}
}
/**
* Data encryption / decryption module constructor.
Expand Down
4 changes: 2 additions & 2 deletions dist/web/pubnub.min.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion lib/core/components/configuration.js
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ const makeConfiguration = (base, setupCryptoModule) => {
return base.PubNubFile;
},
get version() {
return '8.7.0';
return '8.7.1';
},
getVersion() {
return this.version;
Expand Down
2 changes: 1 addition & 1 deletion lib/react_native/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ class PubNub extends pubnub_common_1.PubNubCore {
const transportMiddleware = new middleware_1.PubNubMiddleware({
clientConfiguration,
tokenManager,
transport: new web_react_native_transport_1.WebReactNativeTransport(clientConfiguration.keepAlive, clientConfiguration.logVerbosity),
transport: new web_react_native_transport_1.WebReactNativeTransport(fetch, clientConfiguration.keepAlive, clientConfiguration.logVerbosity),
});
super({
configuration: clientConfiguration,
Expand Down
29 changes: 27 additions & 2 deletions lib/transport/web-react-native-transport.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,24 @@ class WebReactNativeTransport {
/**
* Create and configure transport provider for Web and Rect environments.
*
* @param originalFetch - Pointer to the original (not monkey patched) `fetch` implementation.
* @param [keepAlive] - Whether client should try to keep connections open for reuse or not.
* @param logVerbosity - Whether verbose logs should be printed or not.
*
* @internal
*/
constructor(keepAlive = false, logVerbosity) {
constructor(originalFetch, keepAlive = false, logVerbosity = false) {
this.keepAlive = keepAlive;
this.logVerbosity = logVerbosity;
WebReactNativeTransport.originalFetch = originalFetch;
// Check whether `fetch` has been monkey patched or not.
if (logVerbosity && this.isFetchMonkeyPatched()) {
console.warn("[PubNub] Native Web Fetch API 'fetch' function monkey patched.");
if (!this.isFetchMonkeyPatched(WebReactNativeTransport.originalFetch))
console.info("[PubNub] Use native Web Fetch API 'fetch' implementation from iframe as APM workaround.");
else
console.warn('[PubNub] Unable receive native Web Fetch API. There can be issues with subscribe long-poll cancellation');
}
}
makeSendable(req) {
let controller;
Expand Down Expand Up @@ -63,7 +73,11 @@ class WebReactNativeTransport {
}, req.timeout * 1000);
});
return Promise.race([
fetch(request, { signal: abortController === null || abortController === void 0 ? void 0 : abortController.signal, credentials: 'omit', cache: 'no-cache' }),
WebReactNativeTransport.originalFetch(request, {
signal: abortController === null || abortController === void 0 ? void 0 : abortController.signal,
credentials: 'omit',
cache: 'no-cache',
}),
requestTimeout,
])
.then((response) => response.arrayBuffer().then((arrayBuffer) => [response, arrayBuffer]))
Expand Down Expand Up @@ -178,6 +192,17 @@ class WebReactNativeTransport {
console.log('-----');
}
}
/**
* Check whether original `fetch` has been monkey patched or not.
*
* @returns `true` if original `fetch` has been patched.
*
* @internal
*/
isFetchMonkeyPatched(oFetch) {
const fetchString = (oFetch !== null && oFetch !== void 0 ? oFetch : fetch).toString();
return !fetchString.includes('[native code]') && fetch.name !== 'fetch';
}
}
exports.WebReactNativeTransport = WebReactNativeTransport;
/**
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "pubnub",
"version": "8.7.0",
"version": "8.7.1",
"author": "PubNub <[email protected]>",
"description": "Publish & Subscribe Real-time Messaging with PubNub",
"scripts": {
Expand Down
2 changes: 1 addition & 1 deletion src/core/components/configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ export const makeConfiguration = (
return base.PubNubFile;
},
get version(): string {
return '8.7.0';
return '8.7.1';
},
getVersion(): string {
return this.version;
Expand Down
2 changes: 1 addition & 1 deletion src/react_native/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ export default class PubNub extends PubNubCore<null, PubNubFileParameters> {
const transportMiddleware = new PubNubMiddleware({
clientConfiguration,
tokenManager,
transport: new WebReactNativeTransport(clientConfiguration.keepAlive, clientConfiguration.logVerbosity!),
transport: new WebReactNativeTransport(fetch, clientConfiguration.keepAlive, clientConfiguration.logVerbosity!),
});

super({
Expand Down
47 changes: 44 additions & 3 deletions src/transport/web-react-native-transport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,15 @@ import { queryStringFromObject } from '../core/utils';
* @internal
*/
export class WebReactNativeTransport implements Transport {
/**
* Pointer to the "clean" `fetch` function.
*
* This protects against APM which overload implementation and may break crucial functionality.
*
* @internal
*/
private static originalFetch: typeof fetch;

/**
* Service {@link ArrayBuffer} response decoder.
*
Expand All @@ -27,15 +36,30 @@ export class WebReactNativeTransport implements Transport {
/**
* Create and configure transport provider for Web and Rect environments.
*
* @param originalFetch - Pointer to the original (not monkey patched) `fetch` implementation.
* @param [keepAlive] - Whether client should try to keep connections open for reuse or not.
* @param logVerbosity - Whether verbose logs should be printed or not.
*
* @internal
*/
constructor(
originalFetch: unknown,
private keepAlive: boolean = false,
private readonly logVerbosity: boolean,
) {}
private readonly logVerbosity: boolean = false,
) {
WebReactNativeTransport.originalFetch = originalFetch as typeof fetch;

// Check whether `fetch` has been monkey patched or not.
if (logVerbosity && this.isFetchMonkeyPatched()) {
console.warn("[PubNub] Native Web Fetch API 'fetch' function monkey patched.");
if (!this.isFetchMonkeyPatched(WebReactNativeTransport.originalFetch))
console.info("[PubNub] Use native Web Fetch API 'fetch' implementation from iframe as APM workaround.");
else
console.warn(
'[PubNub] Unable receive native Web Fetch API. There can be issues with subscribe long-poll cancellation',
);
}
}

makeSendable(req: TransportRequest): [Promise<TransportResponse>, CancellationController | undefined] {
let controller: CancellationController | undefined;
Expand Down Expand Up @@ -71,7 +95,11 @@ export class WebReactNativeTransport implements Transport {
});

return Promise.race([
fetch(request, { signal: abortController?.signal, credentials: 'omit', cache: 'no-cache' }),
WebReactNativeTransport.originalFetch(request, {
signal: abortController?.signal,
credentials: 'omit',
cache: 'no-cache',
}),
requestTimeout,
])
.then((response): Promise<[Response, ArrayBuffer]> | [Response, ArrayBuffer] =>
Expand Down Expand Up @@ -198,4 +226,17 @@ export class WebReactNativeTransport implements Transport {
console.log('-----');
}
}

/**
* Check whether original `fetch` has been monkey patched or not.
*
* @returns `true` if original `fetch` has been patched.
*
* @internal
*/
private isFetchMonkeyPatched(oFetch?: typeof fetch): boolean {
const fetchString = (oFetch ?? fetch).toString();

return !fetchString.includes('[native code]') && fetch.name !== 'fetch';
}
}
16 changes: 16 additions & 0 deletions src/web/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ export default class PubNub extends PubNubCore<ArrayBuffer | string, PubNubFileP

// Setup transport provider.
let transport: Transport = new WebReactNativeTransport(
PubNub.originalFetch(),
clientConfiguration.keepAlive,
clientConfiguration.logVerbosity!,
);
Expand Down Expand Up @@ -150,4 +151,19 @@ export default class PubNub extends PubNubCore<ArrayBuffer | string, PubNubFileP
this.listenerManager.announceNetworkUp();
this.reconnect();
}

private static originalFetch(): typeof fetch {
let iframe = document.querySelector<HTMLIFrameElement>('iframe[name="pubnub-context-unpatched-fetch"]');

if (!iframe) {
iframe = document.createElement('iframe');
iframe.style.display = 'none';
iframe.name = 'pubnub-context-unpatched-fetch';
iframe.src = 'about:blank';
document.body.appendChild(iframe);
}

if (iframe.contentWindow) return iframe.contentWindow.fetch.bind(iframe.contentWindow);
return fetch;
}
}

0 comments on commit a204205

Please sign in to comment.