From 2d35f2d498f0e0988bf072ee72ca1a1c3e821266 Mon Sep 17 00:00:00 2001 From: Danny Date: Tue, 20 Jan 2026 12:18:09 -0600 Subject: [PATCH] feat: implement server-side proxy with google-auth-library Rewrite server.js to use google-auth-library for generating Google ID tokens when authenticating requests to the backend Cloud Run service. Key changes: - Replace metadata server token fetch with GoogleAuth client - Implement proper SSE streaming with raw byte passthrough - Add health check endpoint for Cloud Run monitoring - Update server to listen on 0.0.0.0:8080 for Cloud Run compatibility - Add environment variable validation at startup - Improve error handling and logging throughout request lifecycle Update Dockerfile to install google-auth-library dependency and expose port 8080 instead of port 80. --- Dockerfile | 4 +-- server.js | 78 +++++++++++++++++++++++++++++++++++++++++------------- 2 files changed, 62 insertions(+), 20 deletions(-) diff --git a/Dockerfile b/Dockerfile index 30a5283..250f26f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,6 +11,6 @@ FROM node:20-alpine WORKDIR /app COPY --from=build /app/dist ./dist COPY server.js . -RUN npm install express@4 -EXPOSE 80 +RUN npm install express@4 google-auth-library +EXPOSE 8080 CMD ["node", "server.js"] diff --git a/server.js b/server.js index 02ada4e..27be14d 100644 --- a/server.js +++ b/server.js @@ -1,63 +1,105 @@ const express = require('express'); const path = require('path'); +const { GoogleAuth } = require('google-auth-library'); const app = express(); -const PORT = process.env.PORT || 80; +const port = process.env.PORT || 8080; const BACKEND_URL = process.env.BACKEND_URL; +// Validate required environment variable at startup +if (!BACKEND_URL) { + console.error('ERROR: BACKEND_URL environment variable is required'); + process.exit(1); +} + +// Initialize Google Auth client (auto-detects credentials on Cloud Run) +const auth = new GoogleAuth(); + app.use(express.json()); // Serve static React build app.use(express.static(path.join(__dirname, 'dist'))); -// Proxy endpoint - fetches identity token and forwards to backend +// Proxy endpoint - generates ID token and forwards to backend app.post('/api/chat/stream', async (req, res) => { + console.log('Proxy request received for /api/chat/stream'); + try { - // Fetch identity token from GCP metadata server - const tokenResponse = await fetch( - `http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/identity?audience=${BACKEND_URL}`, - { headers: { 'Metadata-Flavor': 'Google' } } - ); - const identityToken = await tokenResponse.text(); + // Get ID token client with backend URL as audience + const client = await auth.getIdTokenClient(BACKEND_URL); + const headers = await client.getRequestHeaders(); + + console.log('Generated ID token, forwarding to backend...'); // Forward request to backend with auth const backendResponse = await fetch(`${BACKEND_URL}/chat/stream`, { method: 'POST', headers: { 'Content-Type': 'application/json', - 'Authorization': `Bearer ${identityToken}`, + ...headers, }, body: JSON.stringify(req.body), }); - // Stream response back to client + // Check if backend returned an error + if (!backendResponse.ok) { + const errorText = await backendResponse.text(); + console.error(`Backend error: ${backendResponse.status} - ${errorText}`); + res.status(backendResponse.status).json({ + error: 'Backend error', + status: backendResponse.status, + message: errorText, + }); + return; + } + + // Set SSE headers and flush immediately res.setHeader('Content-Type', 'text/event-stream'); res.setHeader('Cache-Control', 'no-cache'); res.setHeader('Connection', 'keep-alive'); + res.flushHeaders?.(); + // Pipe raw SSE stream from backend to browser (no parsing) const reader = backendResponse.body.getReader(); - const pump = async () => { + + // Handle client disconnect + req.on('close', () => { + console.log('Client disconnected'); + reader.cancel(); + }); + + // Stream loop + while (true) { const { done, value } = await reader.read(); if (done) { + console.log('Stream complete'); res.end(); return; } res.write(value); - pump(); - }; - pump(); - + } } catch (error) { console.error('Proxy error:', error); - res.status(500).json({ error: 'Proxy error' }); + if (!res.headersSent) { + res.status(500).json({ error: 'Proxy error', message: error.message }); + } else { + res.end(); + } } }); +// Health check endpoint for Cloud Run +app.get('/health', (req, res) => { + res.status(200).json({ status: 'healthy' }); +}); + // SPA fallback - all other routes serve index.html app.get('*', (req, res) => { res.sendFile(path.join(__dirname, 'dist', 'index.html')); }); -app.listen(PORT, () => { - console.log(`Server running on port ${PORT}`); +// Listen on 0.0.0.0 for Cloud Run +app.listen(port, '0.0.0.0', () => { + console.log(`Server listening on ${port}`); + console.log(`Backend URL: ${BACKEND_URL}`); });