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
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
*/

import {
createWebChannelTransport,
createWebChannelTransport as internalCreateWebChannelTransport,
ErrorCode,
EventType,
WebChannel,
Expand All @@ -27,7 +27,8 @@ import {
EventTarget,
StatEvent,
Event,
Stat
Stat,
WebChannelTransport
} from '@firebase/webchannel-wrapper/webchannel-blob';

import { Token } from '../../api/credentials';
Expand Down Expand Up @@ -181,7 +182,7 @@ export class WebChannelConnection extends RestConnection {
rpcName,
'/channel'
];
const webchannelTransport = createWebChannelTransport();
const webchannelTransport = this.createWebChannelTransport();
const requestStats = getStatEventTarget();
const request: WebChannelOptions = {
// Required for backend stickiness, routing behavior is based on this
Expand Down Expand Up @@ -460,4 +461,29 @@ export class WebChannelConnection extends RestConnection {
instance => instance === webChannel
);
}

/**
* Modifies the headers for a request, adding the api key if present,
* and then calling super.modifyHeadersForRequest
*/
protected modifyHeadersForRequest(
headers: StringMap,
authToken: Token | null,
appCheckToken: Token | null
): void {
super.modifyHeadersForRequest(headers, authToken, appCheckToken);

// For web channel streams, we want to send the api key in the headers.
if (this.databaseInfo.apiKey) {
headers['x-goog-api-key'] = this.databaseInfo.apiKey;
}
}

/**
* Wrapped for mocking.
* @protected
*/
protected createWebChannelTransport(): WebChannelTransport {
return internalCreateWebChannelTransport();
}
}
8 changes: 3 additions & 5 deletions packages/firestore/src/remote/rest_connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,15 +64,14 @@ export abstract class RestConnection implements Connection {
protected readonly baseUrl: string;
private readonly databasePath: string;
private readonly requestParams: string;
private readonly apiKey: string | undefined;

get shouldResourcePathBeIncludedInRequest(): boolean {
// Both `invokeRPC()` and `invokeStreamingRPC()` use their `path` arguments to determine
// where to run the query, and expect the `request` to NOT specify the "path".
return false;
}

constructor(private readonly databaseInfo: DatabaseInfo) {
constructor(protected readonly databaseInfo: DatabaseInfo) {
this.databaseId = databaseInfo.databaseId;
const proto = databaseInfo.ssl ? 'https' : 'http';
const projectId = encodeURIComponent(this.databaseId.projectId);
Expand All @@ -83,7 +82,6 @@ export abstract class RestConnection implements Connection {
this.databaseId.database === DEFAULT_DATABASE_NAME
? `project_id=${projectId}`
: `project_id=${projectId}&database_id=${databaseId}`;
this.apiKey = databaseInfo.apiKey;
}

invokeRPC<Req, Resp>(
Expand Down Expand Up @@ -203,8 +201,8 @@ export abstract class RestConnection implements Connection {
'Unknown REST mapping for: ' + rpcName
);
let url = `${this.baseUrl}/${RPC_URL_VERSION}/${path}:${urlRpcName}`;
if (this.apiKey) {
url = `${url}?key=${encodeURIComponent(this.apiKey)}`;
if (this.databaseInfo.apiKey) {
url = `${url}?key=${encodeURIComponent(this.databaseInfo.apiKey)}`;
}
return url;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/**
* @license
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { expect } from 'chai';

import { DatabaseId, DatabaseInfo } from '../../../src/core/database_info';
import { WebChannelConnection } from '../../../src/platform/browser/webchannel_connection';

Check failure on line 21 in packages/firestore/test/unit/remote/web_channel_connection.browser.test.ts

View workflow job for this annotation

GitHub Actions / Lint

There should be at least one empty line between import groups
import {

Check failure on line 22 in packages/firestore/test/unit/remote/web_channel_connection.browser.test.ts

View workflow job for this annotation

GitHub Actions / Lint

`@firebase/webchannel-wrapper` import should occur before import of `chai`
WebChannelOptions,
WebChannelTransport
} from '@firebase/webchannel-wrapper';

export class TestWebChannelConnection extends WebChannelConnection {
public transport: { lastOptions?: WebChannelOptions } & WebChannelTransport =

Check failure on line 28 in packages/firestore/test/unit/remote/web_channel_connection.browser.test.ts

View workflow job for this annotation

GitHub Actions / Lint

Public accessibility modifier on class property transport
{
lastOptions: undefined,
createWebChannel(url: string, options: WebChannelOptions): never {
this.lastOptions = options;

// Throw here so we don't have to mock out any more of Web Channel
throw new Error('Not implemented for test');
}
};
protected createWebChannelTransport(): WebChannelTransport {
return this.transport;
}
}

describe('WebChannelConnection', () => {
const testDatabaseInfo = new DatabaseInfo(
new DatabaseId('testproject'),
'test-app-id',
'persistenceKey',
'example.com',
/*ssl=*/ false,
/*forceLongPolling=*/ false,
/*autoDetectLongPolling=*/ false,
/*longPollingOptions=*/ {},
/*useFetchStreams=*/ false,
/*isUsingEmulator=*/ false,
'wc-connection-test-api-key'
);

it('Passes the API Key from DatabaseInfo to makeHeaders for openStream', async () => {
const connection = new TestWebChannelConnection(testDatabaseInfo);

expect(() => connection.openStream('mockRpc', null, null)).to.throw(
'Not implemented for test'
);

const headers = connection.transport.lastOptions
?.initMessageHeaders as unknown as { [key: string]: string };
expect(headers['x-goog-api-key']).to.deep.equal(
'wc-connection-test-api-key'
);
});
});
Loading