From 949a09c7e2105a6b64adafdd446e5ad6a698edba Mon Sep 17 00:00:00 2001 From: "rajeshwaran.r" Date: Thu, 21 May 2026 19:46:49 +0530 Subject: [PATCH 1/3] health check API added --- docker-compose.yml | 16 ++++++++++++++ src/server.ts | 54 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+) create mode 100644 docker-compose.yml diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 000000000..824f4698a --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,16 @@ +version: "3.8" + +services: + copilot-api: + build: . + ports: + - "4141:4141" + restart: unless-stopped + # Health check validates Copilot API connection + # Container restarts automatically if connection fails + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:4141/health"] + interval: 30s # Check every 30 seconds + timeout: 10s # 10 second timeout per check + retries: 3 # Restart after 3 consecutive failures (~90 seconds) + start_period: 30s # Wait 30 seconds before first check diff --git a/src/server.ts b/src/server.ts index 462a278f3..da762c3c5 100644 --- a/src/server.ts +++ b/src/server.ts @@ -8,6 +8,9 @@ import { messageRoutes } from "./routes/messages/route" import { modelRoutes } from "./routes/models/route" import { tokenRoute } from "./routes/token/route" import { usageRoute } from "./routes/usage/route" +import { state } from "./lib/state" +import { getCopilotToken } from "./services/github/get-copilot-token" +import { HTTPError } from "./lib/error" export const server = new Hono() @@ -16,6 +19,57 @@ server.use(cors()) server.get("/", (c) => c.text("Server running")) +// Health check endpoint - verifies Copilot API connection +server.get("/health", async (c) => { + try { + // Verify that we have required tokens + if (!state.copilotToken || !state.githubToken) { + return c.json( + { + status: "unhealthy", + reason: "Missing authentication tokens", + timestamp: new Date().toISOString(), + }, + 503, + ) + } + + // Verify that we can reach the Copilot API + await getCopilotToken() + + return c.json({ + status: "healthy", + timestamp: new Date().toISOString(), + copilotConnected: true, + }) + } catch (error) { + const errorMessage = + error instanceof HTTPError + ? `Copilot API error: ${error.message}` + : error instanceof Error + ? error.message + : "Unknown error" + + return c.json( + { + status: "unhealthy", + reason: errorMessage, + timestamp: new Date().toISOString(), + copilotConnected: false, + }, + 503, + ) + } +}) + +// Liveness probe for Kubernetes - lightweight check +server.get("/healthz", (c) => + c.json({ + status: "alive", + timestamp: new Date().toISOString(), + }), +) + server.route("/chat/completions", completionRoutes) server.route("/models", modelRoutes) server.route("/embeddings", embeddingRoutes) From b6fc7f759947696e1244cb8bd8cce64268dbf24c Mon Sep 17 00:00:00 2001 From: "rajeshwaran.r" Date: Thu, 21 May 2026 20:17:04 +0530 Subject: [PATCH 2/3] fix: improve health check with timeout and curl support - Install curl in Docker image for health check execution - Add 5-second timeout to health check endpoint to prevent socket hanging - Increase health check start_period to 30s for proper initialization - Improve error handling and logging in health check - Configure health check to validate Copilot API connection - Returns HTTP 503 on connection failure for auto-restart Co-Authored-By: Claude Haiku 4.5 --- Dockerfile | 7 +++++-- docker-compose.yml | 8 +++++++- src/server.ts | 33 +++++++++++++++++++++++---------- 3 files changed, 35 insertions(+), 13 deletions(-) diff --git a/Dockerfile b/Dockerfile index 1265220ef..4cd9bf70d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,6 +10,9 @@ RUN bun run build FROM oven/bun:1.2.19-alpine AS runner WORKDIR /app +# Install curl for health checks +RUN apk add --no-cache curl + COPY ./package.json ./bun.lock ./ RUN bun install --frozen-lockfile --production --ignore-scripts --no-cache @@ -17,8 +20,8 @@ COPY --from=builder /app/dist ./dist EXPOSE 4141 -HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ - CMD wget --spider -q http://localhost:4141/ || exit 1 +HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \ + CMD curl -f http://localhost:4141/health || exit 1 COPY entrypoint.sh /entrypoint.sh RUN chmod +x /entrypoint.sh diff --git a/docker-compose.yml b/docker-compose.yml index 824f4698a..f4b887126 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,8 +9,14 @@ services: # Health check validates Copilot API connection # Container restarts automatically if connection fails healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:4141/health"] + test: ["CMD", "curl", "-s", "-f", "http://localhost:4141/health"] interval: 30s # Check every 30 seconds timeout: 10s # 10 second timeout per check retries: 3 # Restart after 3 consecutive failures (~90 seconds) start_period: 30s # Wait 30 seconds before first check + deploy: + restart_policy: + condition: on-failure + delay: 5s + max_attempts: 3 + window: 120s diff --git a/src/server.ts b/src/server.ts index da762c3c5..8afc55659 100644 --- a/src/server.ts +++ b/src/server.ts @@ -19,10 +19,10 @@ server.use(cors()) server.get("/", (c) => c.text("Server running")) -// Health check endpoint - verifies Copilot API connection +// Health check endpoint - verifies Copilot API connection with timeout server.get("/health", async (c) => { try { - // Verify that we have required tokens + // Quick check: Verify that we have required tokens if (!state.copilotToken || !state.githubToken) { return c.json( { @@ -34,8 +34,17 @@ server.get("/health", async (c) => { ) } - // Verify that we can reach the Copilot API - await getCopilotToken() + // Verify that we can reach the Copilot API with timeout + const controller = new AbortController() + const timeout = setTimeout(() => controller.abort(), 5000) // 5 second timeout + + try { + await getCopilotToken() + clearTimeout(timeout) + } catch (timeoutError) { + clearTimeout(timeout) + throw timeoutError + } return c.json({ status: "healthy", @@ -43,12 +52,16 @@ server.get("/health", async (c) => { copilotConnected: true, }) } catch (error) { - const errorMessage = - error instanceof HTTPError - ? `Copilot API error: ${error.message}` - : error instanceof Error - ? error.message - : "Unknown error" + let errorMessage = "Unknown error" + + if (error instanceof HTTPError) { + errorMessage = `Copilot API error: ${error.message}` + } else if (error instanceof Error) { + errorMessage = error.message + } + + // Log the error for debugging + console.error("[Health Check] Error:", errorMessage) return c.json( { From 16fe2359760bd08693d7873a6914bb7b8a232b02 Mon Sep 17 00:00:00 2001 From: "rajeshwaran.r" Date: Thu, 21 May 2026 21:21:16 +0530 Subject: [PATCH 3/3] fix: resolve 10-second request timeout by using Bun.serve directly with 60s idle timeout --- docker-compose.yml | 2 -- src/server.ts | 33 ++++++++++----------------------- src/start.ts | 6 +++--- 3 files changed, 13 insertions(+), 28 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index f4b887126..d995dc20f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,5 +1,3 @@ -version: "3.8" - services: copilot-api: build: . diff --git a/src/server.ts b/src/server.ts index 8afc55659..da762c3c5 100644 --- a/src/server.ts +++ b/src/server.ts @@ -19,10 +19,10 @@ server.use(cors()) server.get("/", (c) => c.text("Server running")) -// Health check endpoint - verifies Copilot API connection with timeout +// Health check endpoint - verifies Copilot API connection server.get("/health", async (c) => { try { - // Quick check: Verify that we have required tokens + // Verify that we have required tokens if (!state.copilotToken || !state.githubToken) { return c.json( { @@ -34,17 +34,8 @@ server.get("/health", async (c) => { ) } - // Verify that we can reach the Copilot API with timeout - const controller = new AbortController() - const timeout = setTimeout(() => controller.abort(), 5000) // 5 second timeout - - try { - await getCopilotToken() - clearTimeout(timeout) - } catch (timeoutError) { - clearTimeout(timeout) - throw timeoutError - } + // Verify that we can reach the Copilot API + await getCopilotToken() return c.json({ status: "healthy", @@ -52,16 +43,12 @@ server.get("/health", async (c) => { copilotConnected: true, }) } catch (error) { - let errorMessage = "Unknown error" - - if (error instanceof HTTPError) { - errorMessage = `Copilot API error: ${error.message}` - } else if (error instanceof Error) { - errorMessage = error.message - } - - // Log the error for debugging - console.error("[Health Check] Error:", errorMessage) + const errorMessage = + error instanceof HTTPError + ? `Copilot API error: ${error.message}` + : error instanceof Error + ? error.message + : "Unknown error" return c.json( { diff --git a/src/start.ts b/src/start.ts index 14abbbdff..752730095 100644 --- a/src/start.ts +++ b/src/start.ts @@ -3,7 +3,6 @@ import { defineCommand } from "citty" import clipboard from "clipboardy" import consola from "consola" -import { serve, type ServerHandler } from "srvx" import invariant from "tiny-invariant" import { ensurePaths } from "./lib/paths" @@ -114,9 +113,10 @@ export async function runServer(options: RunServerOptions): Promise { `🌐 Usage Viewer: https://ericc-ch.github.io/copilot-api?endpoint=${serverUrl}/usage`, ) - serve({ - fetch: server.fetch as ServerHandler, + Bun.serve({ + fetch: server.fetch, port: options.port, + idleTimeout: 200, // 200 seconds }) }