Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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/three-balloons-collect.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@firebase/functions": patch
---

Fixed issue where Firebase Functions SDK caused CORS errors when connected to emulators in Firebase Studio
63 changes: 63 additions & 0 deletions packages/functions/src/callable.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -523,8 +523,71 @@ describe('Firebase Functions > Stream', () => {
const [_, options] = mockFetch.firstCall.args;
expect(options.headers['Authorization']).to.equal('Bearer auth-token');
expect(options.headers['Content-Type']).to.equal('application/json');
expect(options.credentials).to.equal(undefined);
expect(options.headers['Accept']).to.equal('text/event-stream');
});
it('calls cloud workstations with credentials', async () => {
const authMock: FirebaseAuthInternal = {
getToken: async () => ({ accessToken: 'auth-token' })
} as unknown as FirebaseAuthInternal;
const authProvider = new Provider<FirebaseAuthInternalName>(
'auth-internal',
new ComponentContainer('test')
);
authProvider.setComponent(
new Component('auth-internal', () => authMock, ComponentType.PRIVATE)
);
const appCheckMock: FirebaseAppCheckInternal = {
getToken: async () => ({ token: 'app-check-token' })
} as unknown as FirebaseAppCheckInternal;
const appCheckProvider = new Provider<AppCheckInternalComponentName>(
'app-check-internal',
new ComponentContainer('test')
);
appCheckProvider.setComponent(
new Component(
'app-check-internal',
() => appCheckMock,
ComponentType.PRIVATE
)
);

const functions = createTestService(
app,
region,
authProvider,
undefined,
appCheckProvider
);
functions.emulatorOrigin = 'test.cloudworkstations.dev';
const mockFetch = sinon.stub(functions, 'fetchImpl' as any);

const mockResponse = new ReadableStream({
start(controller) {
controller.enqueue(
new TextEncoder().encode('data: {"result":"Success"}\n')
);
controller.close();
}
});

mockFetch.resolves({
body: mockResponse,
headers: new Headers({ 'Content-Type': 'text/event-stream' }),
status: 200,
statusText: 'OK'
} as Response);

const func = httpsCallable<Record<string, any>, string, string>(
functions,
'stream'
);
await func.stream({});

expect(mockFetch.calledOnce).to.be.true;
const [_, options] = mockFetch.firstCall.args;
expect(options.credentials).to.equal('include');
});

it('aborts during initial fetch', async () => {
const controller = new AbortController();
Expand Down
9 changes: 7 additions & 2 deletions packages/functions/src/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ export function connectFunctionsEmulator(
}://${host}:${port}`;
// Workaround to get cookies in Firebase Studio
if (useSsl) {
void pingServer(functionsInstance.emulatorOrigin);
void pingServer(functionsInstance.emulatorOrigin + '/backends');
updateEmulatorBanner('Functions', true);
}
}
Expand Down Expand Up @@ -439,7 +439,12 @@ async function streamAtURL(
method: 'POST',
body: JSON.stringify(body),
headers,
signal: options?.signal
signal: options?.signal,
credentials:
functionsInstance.emulatorOrigin &&
isCloudWorkstation(functionsInstance.emulatorOrigin)
? 'include'
: undefined
});
} catch (e) {
if (e instanceof Error && e.name === 'AbortError') {
Expand Down
Loading