Skip to content

Switched firebase/database to use the fetch() api #9046

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
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
5 changes: 5 additions & 0 deletions .changeset/little-worms-burn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@firebase/database': patch
---

Switched out usage of XMLHttpRequest for fetch()
214 changes: 88 additions & 126 deletions packages/database/src/core/ReadonlyRestClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,7 @@
* limitations under the License.
*/

import {
assert,
jsonEval,
safeGet,
querystring,
Deferred
} from '@firebase/util';
import { assert, safeGet } from '@firebase/util';

import { AppCheckTokenProvider } from './AppCheckTokenProvider';
import { AuthTokenProvider } from './AuthTokenProvider';
Expand Down Expand Up @@ -81,7 +75,7 @@ export class ReadonlyRestClient extends ServerActions {
}

/** @inheritDoc */
listen(
async listen(
query: QueryContext,
currentHashFn: () => string,
tag: number | null,
Expand All @@ -99,35 +93,34 @@ export class ReadonlyRestClient extends ServerActions {
query._queryParams
);

this.restRequest_(
let [response, data] = await this.restRequest_(
pathString + '.json',
queryStringParameters,
(error, result) => {
let data = result;

if (error === 404) {
data = null;
error = null;
}

if (error === null) {
this.onDataUpdate_(pathString, data, /*isMerge=*/ false, tag);
}

if (safeGet(this.listens_, listenId) === thisListen) {
let status;
if (!error) {
status = 'ok';
} else if (error === 401) {
status = 'permission_denied';
} else {
status = 'rest_error:' + error;
}

onComplete(status, null);
}
}
queryStringParameters
);

let error = response.status;

if (error === 404) {
data = null;
error = null;
}

if (error === null) {
this.onDataUpdate_(pathString, data, /*isMerge=*/ false, tag);
}

if (safeGet(this.listens_, listenId) === thisListen) {
let status;
if (!error) {
status = 'ok';
} else if (error === 401) {
status = 'permission_denied';
} else {
status = 'rest_error:' + error;
}

onComplete(status, null);
}
}

/** @inheritDoc */
Expand All @@ -136,40 +129,26 @@ export class ReadonlyRestClient extends ServerActions {
delete this.listens_[listenId];
}

get(query: QueryContext): Promise<string> {
async get(query: QueryContext): Promise<string> {
const queryStringParameters = queryParamsToRestQueryStringParameters(
query._queryParams
);

const pathString = query._path.toString();

const deferred = new Deferred<string>();

this.restRequest_(
let [response, data] = await this.restRequest_(
pathString + '.json',
queryStringParameters,
(error, result) => {
let data = result;

if (error === 404) {
data = null;
error = null;
}

if (error === null) {
this.onDataUpdate_(
pathString,
data,
/*isMerge=*/ false,
/*tag=*/ null
);
deferred.resolve(data as string);
} else {
deferred.reject(new Error(data as string));
}
}
queryStringParameters
);
return deferred.promise;

if (response.status === 404) {
data = null;
} else if (!response.ok) {
throw new Error(data as string);
}

this.onDataUpdate_(pathString, data, /*isMerge=*/ false, /*tag=*/ null);
return data as string;
}

/** @inheritDoc */
Expand All @@ -181,74 +160,57 @@ export class ReadonlyRestClient extends ServerActions {
* Performs a REST request to the given path, with the provided query string parameters,
* and any auth credentials we have.
*/
private restRequest_(
private async restRequest_<T = unknown>(
pathString: string,
queryStringParameters: { [k: string]: string | number } = {},
callback: ((a: number | null, b?: unknown) => void) | null
) {
queryStringParameters['format'] = 'export';

return Promise.all([
queryStringParameters: Record<string, string | number> = {}
): Promise<[Response, T | null]> {
// Fetch tokens
const [authToken, appCheckToken] = await Promise.all([
this.authTokenProvider_.getToken(/*forceRefresh=*/ false),
this.appCheckTokenProvider_.getToken(/*forceRefresh=*/ false)
]).then(([authToken, appCheckToken]) => {
if (authToken && authToken.accessToken) {
queryStringParameters['auth'] = authToken.accessToken;
}
if (appCheckToken && appCheckToken.token) {
queryStringParameters['ac'] = appCheckToken.token;
}
]);

// Configure URL parameters
const searchParams = new URLSearchParams(
queryStringParameters as Record<string, string>
);
if (authToken && authToken.accessToken) {
searchParams.set('auth', authToken.accessToken);
}
if (appCheckToken && appCheckToken.token) {
searchParams.set('ac', appCheckToken.token);
}
searchParams.set('format', 'export');
searchParams.set('ns', this.repoInfo_.namespace);

// Build & send the request
const url =
(this.repoInfo_.secure ? 'https://' : 'http://') +
this.repoInfo_.host +
pathString +
'?' +
searchParams.toString();

this.log_('Sending REST request for ' + url);
const response = await fetch(url);
if (!response.ok) {
// Request was not successful, so throw an error
throw new Error(
`REST request at ${url} returned error: ${response.status}`
);
}

this.log_(
'REST Response for ' + url + ' received. status:',
response.status
);
let result: T | null = null;
try {
result = await response.json();
} catch (e) {
warn('Failed to parse server response as json.', e);
}

const url =
(this.repoInfo_.secure ? 'https://' : 'http://') +
this.repoInfo_.host +
pathString +
'?' +
'ns=' +
this.repoInfo_.namespace +
querystring(queryStringParameters);

this.log_('Sending REST request for ' + url);
const xhr = new XMLHttpRequest();
xhr.onreadystatechange = () => {
if (callback && xhr.readyState === 4) {
this.log_(
'REST Response for ' + url + ' received. status:',
xhr.status,
'response:',
xhr.responseText
);
let res = null;
if (xhr.status >= 200 && xhr.status < 300) {
try {
res = jsonEval(xhr.responseText);
} catch (e) {
warn(
'Failed to parse JSON response for ' +
url +
': ' +
xhr.responseText
);
}
callback(null, res);
} else {
// 401 and 404 are expected.
if (xhr.status !== 401 && xhr.status !== 404) {
warn(
'Got unsuccessful REST response for ' +
url +
' Status: ' +
xhr.status
);
}
callback(xhr.status);
}
callback = null;
}
};

xhr.open('GET', url, /*asynchronous=*/ true);
xhr.send();
});
return [response, result];
}
}