Skip to content

Commit 92ed5ed

Browse files
Merge pull request #369 from splitio/javascript_client_issue_847
Sanitize `SplitSDKMachineName` header value to avoid HTTP/S request exceptions.
2 parents 36ae03e + 603680d commit 92ed5ed

File tree

11 files changed

+44
-64
lines changed

11 files changed

+44
-64
lines changed

CHANGES.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
2.0.2 (December 3, 2024)
2+
- Updated the factory `init` and `destroy` methods to support re-initialization after destruction. This update ensures compatibility of the React SDK with React Strict Mode, where the factory's `init` and `destroy` effects are executed an extra time to validate proper resource cleanup.
3+
- Bugfixing - Sanitize the `SplitSDKMachineName` header value to avoid exceptions on HTTP/S requests when it contains non ISO-8859-1 characters (Related to issue https://github.com/splitio/javascript-client/issues/847).
4+
15
2.0.1 (November 25, 2024)
26
- Bugfixing - Fixed an issue with the SDK_UPDATE event on server-side, where it was not being emitted if there was an empty segment and the SDK received a feature flag update notification.
37

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@splitsoftware/splitio-commons",
3-
"version": "2.0.1",
3+
"version": "2.0.2",
44
"description": "Split JavaScript SDK common components",
55
"main": "cjs/index.js",
66
"module": "esm/index.js",

src/readiness/readinessManager.ts

Lines changed: 15 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,9 @@ function segmentsEventEmitterFactory(EventEmitter: new () => SplitIO.IEventEmitt
3737
export function readinessManagerFactory(
3838
EventEmitter: new () => SplitIO.IEventEmitter,
3939
settings: ISettings,
40-
splits: ISplitsEventEmitter = splitsEventEmitterFactory(EventEmitter)): IReadinessManager {
40+
splits: ISplitsEventEmitter = splitsEventEmitterFactory(EventEmitter),
41+
isShared?: boolean
42+
): IReadinessManager {
4143

4244
const readyTimeout = settings.startup.readyTimeout;
4345

@@ -66,23 +68,26 @@ export function readinessManagerFactory(
6668
gate.emit(SDK_READY_TIMED_OUT, 'Split SDK emitted SDK_READY_TIMED_OUT event.');
6769
}
6870

69-
let readyTimeoutId: ReturnType<typeof setTimeout>;
70-
if (readyTimeout > 0) {
71-
if (splits.hasInit) readyTimeoutId = setTimeout(timeout, readyTimeout);
72-
else splits.initCallbacks.push(() => { readyTimeoutId = setTimeout(timeout, readyTimeout); });
73-
}
7471

7572
// emit SDK_READY and SDK_UPDATE
7673
let isReady = false;
7774
splits.on(SDK_SPLITS_ARRIVED, checkIsReadyOrUpdate);
7875
segments.on(SDK_SEGMENTS_ARRIVED, checkIsReadyOrUpdate);
7976

8077
let isDestroyed = false;
78+
let readyTimeoutId: ReturnType<typeof setTimeout>;
79+
function __init() {
80+
isDestroyed = false;
81+
if (readyTimeout > 0 && !isReady) readyTimeoutId = setTimeout(timeout, readyTimeout);
82+
}
83+
84+
splits.initCallbacks.push(__init);
85+
if (splits.hasInit) __init();
8186

8287
function checkIsReadyFromCache() {
8388
isReadyFromCache = true;
8489
// Don't emit SDK_READY_FROM_CACHE if SDK_READY has been emitted
85-
if (!isReady) {
90+
if (!isReady && !isDestroyed) {
8691
try {
8792
syncLastUpdate();
8893
gate.emit(SDK_READY_FROM_CACHE);
@@ -94,6 +99,7 @@ export function readinessManagerFactory(
9499
}
95100

96101
function checkIsReadyOrUpdate(diff: any) {
102+
if (isDestroyed) return;
97103
if (isReady) {
98104
try {
99105
syncLastUpdate();
@@ -117,16 +123,13 @@ export function readinessManagerFactory(
117123
}
118124
}
119125

120-
let refCount = 1;
121-
122126
return {
123127
splits,
124128
segments,
125129
gate,
126130

127131
shared() {
128-
refCount++;
129-
return readinessManagerFactory(EventEmitter, settings, splits);
132+
return readinessManagerFactory(EventEmitter, settings, splits, true);
130133
},
131134

132135
// @TODO review/remove next methods when non-recoverable errors are reworked
@@ -145,13 +148,9 @@ export function readinessManagerFactory(
145148
destroy() {
146149
isDestroyed = true;
147150
syncLastUpdate();
148-
149-
segments.removeAllListeners();
150-
gate.removeAllListeners();
151151
clearTimeout(readyTimeoutId);
152152

153-
if (refCount > 0) refCount--;
154-
if (refCount === 0) splits.removeAllListeners();
153+
if (!isShared) splits.hasInit = false;
155154
},
156155

157156
isReady() { return isReady; },

src/sdkFactory/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ export function sdkFactory(params: ISdkFactoryParams): SplitIO.ISDK | SplitIO.IA
126126
settings,
127127

128128
destroy() {
129+
hasInit = false;
129130
return Promise.all(Object.keys(clients).map(key => clients[key].destroy())).then(() => { });
130131
}
131132
}, extraProps && extraProps(ctx), lazyInit ? { init } : init());

src/services/__tests__/decorateHeaders.spec.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { ISettings } from '../../types';
2-
import { decorateHeaders } from '../decorateHeaders';
2+
import { decorateHeaders, removeNonISO88591 } from '../decorateHeaders';
33

44
const HEADERS = {
55
Authorization: 'Bearer SDK-KEY',
@@ -48,3 +48,10 @@ describe('decorateHeaders', () => {
4848
expect(settings.log.error).toHaveBeenCalledWith('Problem adding custom headers to request decorator: Error: Unexpected error');
4949
});
5050
});
51+
52+
test('removeNonISO88591', () => {
53+
expect(removeNonISO88591('')).toBe('');
54+
expect(removeNonISO88591('This is a test')).toBe('This is a test');
55+
expect(removeNonISO88591('This is a test ó \u0FFF 你')).toBe('This is a test ó ');
56+
expect(removeNonISO88591('Emiliano’s-MacBook-Pro')).toBe('Emilianos-MacBook-Pro');
57+
});

src/services/decorateHeaders.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,8 @@ export function decorateHeaders(settings: ISettings, headers: Record<string, str
3030
}
3131
return headers;
3232
}
33+
34+
export function removeNonISO88591(input: string) {
35+
// eslint-disable-next-line no-control-regex
36+
return input.replace(/[^\x00-\xFF]/g, '');
37+
}

src/services/splitHttpClient.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { objectAssign } from '../utils/lang/objectAssign';
33
import { ERROR_HTTP, ERROR_CLIENT_CANNOT_GET_READY } from '../logger/constants';
44
import { ISettings } from '../types';
55
import { IPlatform } from '../sdkFactory/types';
6-
import { decorateHeaders } from './decorateHeaders';
6+
import { decorateHeaders, removeNonISO88591 } from './decorateHeaders';
77

88
const messageNoFetch = 'Global fetch API is not available.';
99

@@ -30,7 +30,7 @@ export function splitHttpClientFactory(settings: ISettings, { getOptions, getFet
3030
};
3131

3232
if (ip) commonHeaders['SplitSDKMachineIP'] = ip;
33-
if (hostname) commonHeaders['SplitSDKMachineName'] = hostname;
33+
if (hostname) commonHeaders['SplitSDKMachineName'] = removeNonISO88591(hostname);
3434

3535
return function httpClient(url: string, reqOpts: IRequestOptions = {}, latencyTracker: (error?: NetworkError) => void = () => { }, logErrorsAsInfo: boolean = false): Promise<IResponse> {
3636

src/storages/inLocalStorage/index.ts

Lines changed: 3 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,6 @@ import { KeyBuilderCS, myLargeSegmentsKeyBuilder } from '../KeyBuilderCS';
77
import { isLocalStorageAvailable } from '../../utils/env/isLocalStorageAvailable';
88
import { SplitsCacheInLocal } from './SplitsCacheInLocal';
99
import { MySegmentsCacheInLocal } from './MySegmentsCacheInLocal';
10-
import { MySegmentsCacheInMemory } from '../inMemory/MySegmentsCacheInMemory';
11-
import { SplitsCacheInMemory } from '../inMemory/SplitsCacheInMemory';
1210
import { DEFAULT_CACHE_EXPIRATION_IN_MILLIS } from '../../utils/constants/browser';
1311
import { InMemoryStorageCSFactory } from '../inMemory/InMemoryStorageCS';
1412
import { LOG_PREFIX } from './constants';
@@ -36,7 +34,7 @@ export function InLocalStorage(options: InLocalStorageOptions = {}): IStorageSyn
3634
return InMemoryStorageCSFactory(params);
3735
}
3836

39-
const { settings, settings: { log, scheduler: { impressionsQueueSize, eventsQueueSize, }, sync: { impressionsMode, __splitFiltersValidation } } } = params;
37+
const { settings, settings: { log, scheduler: { impressionsQueueSize, eventsQueueSize, }, sync: { impressionsMode } } } = params;
4038
const matchingKey = getMatching(settings.core.key);
4139
const keys = new KeyBuilderCS(prefix, matchingKey);
4240
const expirationTimestamp = Date.now() - DEFAULT_CACHE_EXPIRATION_IN_MILLIS;
@@ -55,15 +53,7 @@ export function InLocalStorage(options: InLocalStorageOptions = {}): IStorageSyn
5553
telemetry: shouldRecordTelemetry(params) ? new TelemetryCacheInMemory(splits, segments) : undefined,
5654
uniqueKeys: impressionsMode === NONE ? new UniqueKeysCacheInMemoryCS() : undefined,
5755

58-
destroy() {
59-
this.splits = new SplitsCacheInMemory(__splitFiltersValidation);
60-
this.segments = new MySegmentsCacheInMemory();
61-
this.largeSegments = new MySegmentsCacheInMemory();
62-
this.impressions.clear();
63-
this.impressionCounts && this.impressionCounts.clear();
64-
this.events.clear();
65-
this.uniqueKeys?.clear();
66-
},
56+
destroy() { },
6757

6858
// When using shared instantiation with MEMORY we reuse everything but segments (they are customer per key).
6959
shared(matchingKey: string) {
@@ -77,11 +67,7 @@ export function InLocalStorage(options: InLocalStorageOptions = {}): IStorageSyn
7767
events: this.events,
7868
telemetry: this.telemetry,
7969

80-
destroy() {
81-
this.splits = new SplitsCacheInMemory(__splitFiltersValidation);
82-
this.segments = new MySegmentsCacheInMemory();
83-
this.largeSegments = new MySegmentsCacheInMemory();
84-
}
70+
destroy() { }
8571
};
8672
},
8773
};

src/storages/inMemory/InMemoryStorage.ts

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -28,15 +28,7 @@ export function InMemoryStorageFactory(params: IStorageFactoryParams): IStorageS
2828
telemetry: shouldRecordTelemetry(params) ? new TelemetryCacheInMemory(splits, segments) : undefined,
2929
uniqueKeys: impressionsMode === NONE ? new UniqueKeysCacheInMemory() : undefined,
3030

31-
// When using MEMORY we should clean all the caches to leave them empty
32-
destroy() {
33-
this.splits.clear();
34-
this.segments.clear();
35-
this.impressions.clear();
36-
this.impressionCounts && this.impressionCounts.clear();
37-
this.events.clear();
38-
this.uniqueKeys && this.uniqueKeys.clear();
39-
}
31+
destroy() { }
4032
};
4133

4234
// @TODO revisit storage logic in localhost mode

0 commit comments

Comments
 (0)