From d6d7867c367ab9e09b5e14c6be0884aa3a46a0a9 Mon Sep 17 00:00:00 2001 From: Maneesh Tewani Date: Tue, 12 Aug 2025 10:29:24 -0700 Subject: [PATCH 1/6] Used valid path for pingserver --- packages/functions/src/service.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/functions/src/service.ts b/packages/functions/src/service.ts index 57504a4c7a4..3146dc491f7 100644 --- a/packages/functions/src/service.ts +++ b/packages/functions/src/service.ts @@ -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); } } @@ -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') { From df180caa051614d61dfa22db5b2559b6c327af91 Mon Sep 17 00:00:00 2001 From: Maneesh Tewani Date: Tue, 12 Aug 2025 11:21:44 -0700 Subject: [PATCH 2/6] Added test --- packages/functions/src/callable.test.ts | 63 +++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/packages/functions/src/callable.test.ts b/packages/functions/src/callable.test.ts index b969304c89e..d58d2bca4e0 100644 --- a/packages/functions/src/callable.test.ts +++ b/packages/functions/src/callable.test.ts @@ -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( + '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( + '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, 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(); From f7536090eb920d619b6355a01406ff93b822f0dc Mon Sep 17 00:00:00 2001 From: Maneesh Tewani Date: Tue, 12 Aug 2025 11:23:11 -0700 Subject: [PATCH 3/6] Create three-balloons-collect.md --- .changeset/three-balloons-collect.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/three-balloons-collect.md diff --git a/.changeset/three-balloons-collect.md b/.changeset/three-balloons-collect.md new file mode 100644 index 00000000000..5978b3a28ab --- /dev/null +++ b/.changeset/three-balloons-collect.md @@ -0,0 +1,5 @@ +--- +"@firebase/functions": patch +--- + +Fixed issue where Firebase Functions SDK caused CORS errors when connected to emulators in Firebase Studio From ba0198d18032e3e1dd56b1b8009ac6a9965e851e Mon Sep 17 00:00:00 2001 From: Maneesh Tewani Date: Wed, 13 Aug 2025 11:11:23 -0700 Subject: [PATCH 4/6] Fixed fromUrl --- packages/functions/src/callable.test.ts | 70 ++++++++++++++++++++++++- packages/functions/src/service.ts | 28 +++++++--- 2 files changed, 89 insertions(+), 9 deletions(-) diff --git a/packages/functions/src/callable.test.ts b/packages/functions/src/callable.test.ts index d58d2bca4e0..a1b9354606a 100644 --- a/packages/functions/src/callable.test.ts +++ b/packages/functions/src/callable.test.ts @@ -37,7 +37,11 @@ import { AppCheckInternalComponentName } from '@firebase/app-check-interop-types'; import { makeFakeApp, createTestService } from '../test/utils'; -import { FunctionsService, httpsCallable } from './service'; +import { + FunctionsService, + httpsCallable, + httpsCallableFromURL +} from './service'; import { FUNCTIONS_TYPE } from './constants'; import { FunctionsError } from './error'; @@ -526,6 +530,7 @@ describe('Firebase Functions > Stream', () => { 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' }) @@ -589,6 +594,69 @@ describe('Firebase Functions > Stream', () => { expect(options.credentials).to.equal('include'); }); + it.only('calls streamFromURL cloud workstations with credentials', async () => { + const authMock: FirebaseAuthInternal = { + getToken: async () => ({ accessToken: 'auth-token' }) + } as unknown as FirebaseAuthInternal; + const authProvider = new Provider( + '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( + '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 = httpsCallableFromURL, 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(); diff --git a/packages/functions/src/service.ts b/packages/functions/src/service.ts index 3146dc491f7..7aa02f0a69d 100644 --- a/packages/functions/src/service.ts +++ b/packages/functions/src/service.ts @@ -245,18 +245,27 @@ export function httpsCallableFromURL< return callable as HttpsCallable; } +function getCredentials(functionsInstance: FunctionsService) { + return functionsInstance.emulatorOrigin && + isCloudWorkstation(functionsInstance.emulatorOrigin) + ? 'include' + : undefined; +} + /** * Does an HTTP POST and returns the completed response. * @param url The url to post to. * @param body The JSON body of the post. * @param headers The HTTP headers to include in the request. + * @param functionsInstance functions instance that is calling postJSON * @return A Promise that will succeed when the request finishes. */ async function postJSON( url: string, body: unknown, headers: { [key: string]: string }, - fetchImpl: typeof fetch + fetchImpl: typeof fetch, + functionsInstance: FunctionsService ): Promise { headers['Content-Type'] = 'application/json'; @@ -265,7 +274,8 @@ async function postJSON( response = await fetchImpl(url, { method: 'POST', body: JSON.stringify(body), - headers + headers, + credentials: getCredentials(functionsInstance) }); } catch (e) { // This could be an unhandled error on the backend, or it could be a @@ -353,7 +363,13 @@ async function callAtURL( const failAfterHandle = failAfter(timeout); const response = await Promise.race([ - postJSON(url, body, headers, functionsInstance.fetchImpl), + postJSON( + url, + body, + headers, + functionsInstance.fetchImpl, + functionsInstance + ), failAfterHandle.promise, functionsInstance.cancelAllRequests ]); @@ -440,11 +456,7 @@ async function streamAtURL( body: JSON.stringify(body), headers, signal: options?.signal, - credentials: - functionsInstance.emulatorOrigin && - isCloudWorkstation(functionsInstance.emulatorOrigin) - ? 'include' - : undefined + credentials: getCredentials(functionsInstance) }); } catch (e) { if (e instanceof Error && e.name === 'AbortError') { From 556d1bed2b011989793953669060f2da682dc2ae Mon Sep 17 00:00:00 2001 From: Maneesh Tewani Date: Wed, 13 Aug 2025 11:17:32 -0700 Subject: [PATCH 5/6] Removed only --- packages/functions/src/callable.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/functions/src/callable.test.ts b/packages/functions/src/callable.test.ts index a1b9354606a..724efc39c92 100644 --- a/packages/functions/src/callable.test.ts +++ b/packages/functions/src/callable.test.ts @@ -594,7 +594,7 @@ describe('Firebase Functions > Stream', () => { expect(options.credentials).to.equal('include'); }); - it.only('calls streamFromURL cloud workstations with credentials', async () => { + it('calls streamFromURL cloud workstations with credentials', async () => { const authMock: FirebaseAuthInternal = { getToken: async () => ({ accessToken: 'auth-token' }) } as unknown as FirebaseAuthInternal; From 5a22855715069be8f9bdb19cfd97fc9c2f4efe85 Mon Sep 17 00:00:00 2001 From: Maneesh Tewani Date: Wed, 13 Aug 2025 11:21:35 -0700 Subject: [PATCH 6/6] Fixed typings --- packages/functions/src/service.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/functions/src/service.ts b/packages/functions/src/service.ts index 7aa02f0a69d..6e2eddda3a2 100644 --- a/packages/functions/src/service.ts +++ b/packages/functions/src/service.ts @@ -245,7 +245,9 @@ export function httpsCallableFromURL< return callable as HttpsCallable; } -function getCredentials(functionsInstance: FunctionsService) { +function getCredentials( + functionsInstance: FunctionsService +): 'include' | undefined { return functionsInstance.emulatorOrigin && isCloudWorkstation(functionsInstance.emulatorOrigin) ? 'include'