Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,16 @@ module.exports = {
{
name: 'atob',
message:
"'atob' unavailable in React Native 0.72. Use 'decodeBase64' helper in src/obfuscation.ts instead",
"'atob' unavailable in React Native's Hermes JS engine. Use 'decodeBase64' helper in src/obfuscation.ts instead",
},
{
name: 'btoa',
message:
"'btoa' unavailable in React Native 0.72. Use 'encodeBase64' helper in src/obfuscation.ts instead",
"'btoa' unavailable in React Native's Hermes JS engine. Use 'encodeBase64' helper in src/obfuscation.ts instead",
},
{
name: 'URL',
message: "URL is improperly implemented in React Native's Hermes JS engine.",
},
{
name: 'Buffer',
Expand Down
21 changes: 12 additions & 9 deletions src/api-endpoints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,23 +17,26 @@ export default class ApiEndpoints {
this.params.baseUrl = params.baseUrl ?? DEFAULT_BASE_URL;
}

endpoint(resource: string): URL {
const url = new URL(this.params.baseUrl + resource);
Object.entries(this.params.queryParams ?? {}).forEach(([key, value]) =>
url.searchParams.append(key, value),
);
return url;
endpoint(resource: string): string {
const endpointUrl = `${this.params.baseUrl}${resource}`;
const queryParams = this.params.queryParams;
if (!queryParams) {
return endpointUrl;
}
const urlSearchParams = new URLSearchParams();
Object.entries(queryParams).forEach(([key, value]) => urlSearchParams.append(key, value));
return `${endpointUrl}?${urlSearchParams}`;
}

ufcEndpoint(): URL {
ufcEndpoint(): string {
return this.endpoint(UFC_ENDPOINT);
}

banditParametersEndpoint(): URL {
banditParametersEndpoint(): string {
return this.endpoint(BANDIT_ENDPOINT);
}

precomputedFlagsEndpoint(): URL {
precomputedFlagsEndpoint(): string {
return this.endpoint(PRECOMPUTED_FLAGS_ENDPOINT);
}
}
24 changes: 6 additions & 18 deletions src/http-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,26 +46,14 @@ export interface IBanditParametersResponse {
bandits: Record<string, BanditParameters>;
}

/**
* Fixes issues with url.toString() in older React Native versions
* that leaves a trailing slash in pathname
* @param url URL to stringify
* @returns stringified url
*/
const urlWithNoTrailingSlash = (url: URL) => {
// Note: URL.pathname does not exist in some React Native JS runtime
// so we have to do string manipulation on the stringified URL
return url.toString().replace(/\/\?/, '?').replace(/\/$/, '');
};
Comment on lines -55 to -59
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removing the trailing-slash band-aid fix. It worked for this situation, but doesn't account for future usages of URL.


export interface IHttpClient {
getUniversalFlagConfiguration(): Promise<IUniversalFlagConfigResponse | undefined>;
getBanditParameters(): Promise<IBanditParametersResponse | undefined>;
getPrecomputedFlags(
payload: PrecomputedFlagsPayload,
): Promise<IObfuscatedPrecomputedConfigurationResponse | undefined>;
rawGet<T>(url: URL): Promise<T | undefined>;
rawPost<T, P>(url: URL, payload: P): Promise<T | undefined>;
rawGet<T>(url: string): Promise<T | undefined>;
rawPost<T, P>(url: string, payload: P): Promise<T | undefined>;
}

export default class FetchHttpClient implements IHttpClient {
Expand Down Expand Up @@ -94,14 +82,14 @@ export default class FetchHttpClient implements IHttpClient {
);
}

async rawGet<T>(url: URL): Promise<T | undefined> {
async rawGet<T>(url: string): Promise<T | undefined> {
try {
// Canonical implementation of abortable fetch for interrupting when request takes longer than desired.
// https://developer.chrome.com/blog/abortable-fetch/#reacting_to_an_aborted_fetch
const controller = new AbortController();
const signal = controller.signal;
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
const response = await fetch(urlWithNoTrailingSlash(url), { signal });
const response = await fetch(url, { signal });
// Clear timeout when response is received within the budget.
clearTimeout(timeoutId);

Expand All @@ -120,13 +108,13 @@ export default class FetchHttpClient implements IHttpClient {
}
}

async rawPost<T, P>(url: URL, payload: P): Promise<T | undefined> {
async rawPost<T, P>(url: string, payload: P): Promise<T | undefined> {
try {
const controller = new AbortController();
const signal = controller.signal;
const timeoutId = setTimeout(() => controller.abort(), this.timeout);

const response = await fetch(urlWithNoTrailingSlash(url), {
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Expand Down
Loading