diff --git a/packages/twenty-sdk/src/cli/utilities/auth/__tests__/callback-server.test.ts b/packages/twenty-sdk/src/cli/utilities/auth/__tests__/callback-server.test.ts index 2159793af1..44792fcc57 100644 --- a/packages/twenty-sdk/src/cli/utilities/auth/__tests__/callback-server.test.ts +++ b/packages/twenty-sdk/src/cli/utilities/auth/__tests__/callback-server.test.ts @@ -2,14 +2,26 @@ import http from 'node:http'; import { startCallbackServer } from '../callback-server'; -const httpGet = (url: string): Promise<{ status: number; body: string }> => +const httpGet = ( + url: string, +): Promise<{ + status: number; + body: string; + headers: http.IncomingHttpHeaders; +}> => new Promise((resolve, reject) => { http .get(url, (res) => { let body = ''; res.on('data', (chunk: string) => (body += chunk)); - res.on('end', () => resolve({ status: res.statusCode ?? 0, body })); + res.on('end', () => + resolve({ + status: res.statusCode ?? 0, + body, + headers: res.headers, + }), + ); }) .on('error', reject); }); @@ -44,6 +56,46 @@ describe('startCallbackServer', () => { } }); + it('should send the success page with an explicit Content-Length (not chunked)', async () => { + const server = await startCallbackServer(); + + try { + const waitPromise = server.waitForCallback(); + + const response = await httpGet( + `${server.callbackUrl}?code=test-auth-code`, + ); + + await waitPromise; + + expect(response.status).toBe(200); + expect(response.headers['transfer-encoding']).toBeUndefined(); + expect(response.headers['content-length']).toBe( + String(Buffer.byteLength(response.body)), + ); + expect(response.body).toContain('Authentication successful'); + } finally { + server.close(); + } + }); + + it('should deliver the full page even when the server is closed right after the callback resolves', async () => { + const server = await startCallbackServer(); + + const responsePromise = httpGet( + `${server.callbackUrl}?code=test-auth-code`, + ); + + await server.waitForCallback(); + server.close(); + + const response = await responsePromise; + + expect(response.status).toBe(200); + expect(response.body).toContain('Authentication successful'); + expect(response.body).toContain(''); + }); + it('should resolve with error when callback contains an error', async () => { const server = await startCallbackServer(); diff --git a/packages/twenty-sdk/src/cli/utilities/auth/callback-server.ts b/packages/twenty-sdk/src/cli/utilities/auth/callback-server.ts index d1b3e8402e..c2e7e69636 100644 --- a/packages/twenty-sdk/src/cli/utilities/auth/callback-server.ts +++ b/packages/twenty-sdk/src/cli/utilities/auth/callback-server.ts @@ -195,7 +195,7 @@ export const startCallbackServer = (options?: { const url = new URL(req.url ?? '/', `http://127.0.0.1`); if (url.pathname !== '/callback') { - res.writeHead(404); + res.writeHead(404, { Connection: 'close' }); res.end('Not found'); return; @@ -205,23 +205,28 @@ export const startCallbackServer = (options?: { const error = url.searchParams.get('error'); const isDarkMode = url.searchParams.get('theme') === 'dark'; - const headers = { + const result: CallbackResult = code + ? { success: true, code } + : { + success: false, + error: + error ?? + url.searchParams.get('error_description') ?? + 'Unknown error', + }; + + const body = result.success + ? successHtml(isDarkMode) + : errorHtml(result.error, isDarkMode); + + res.writeHead(200, { 'Content-Type': 'text/html', + 'Content-Length': Buffer.byteLength(body), Connection: 'close', - }; + }); - if (code) { - res.writeHead(200, headers); - res.end(successHtml(isDarkMode)); - callbackResolve({ success: true, code }); - } else { - const errorMessage = - error ?? url.searchParams.get('error_description') ?? 'Unknown error'; - - res.writeHead(200, headers); - res.end(errorHtml(errorMessage, isDarkMode)); - callbackResolve({ success: false, error: errorMessage }); - } + res.on('close', () => callbackResolve(result)); + res.end(body); }); server.listen(0, '127.0.0.1', () => { @@ -252,8 +257,8 @@ export const startCallbackServer = (options?: { }, close: () => { clearTimeout(timeoutHandle); - server.closeAllConnections(); server.close(); + server.closeIdleConnections(); }, }); });