Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
47 changes: 39 additions & 8 deletions bin/cli/commands/doctor.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -380,18 +380,49 @@ function resolveLivenessUrl(options = {}) {
return `http://${formatHostForUrl(host || "127.0.0.1")}:${dashboardPort}/api/health/degradation`;
}

async function checkServerLiveness(options = {}) {
const url = resolveLivenessUrl(options);

async function probeUrl(url) {
try {
const response = await fetchWithTimeout(url);
if (!response.ok) {
return warn("Server liveness", `Server responded with HTTP ${response.status}`, { url });
}
return ok("Server liveness", "Server health endpoint is reachable", { url });
return { ok: response.ok, status: response.status };
} catch {
return warn("Server liveness", "Server health endpoint is not reachable", { url });
return { ok: false, status: 0 };
}
}

async function checkServerLiveness(options = {}) {
const url = resolveLivenessUrl(options);

// First attempt: configured health endpoint (may require auth token).
const primary = await probeUrl(url);
if (primary.ok) {
return ok("Server liveness", "Server health endpoint is reachable", { url, status: primary.status });
}

// #6162: /api/health and /api/health/degradation require a management token.
// When unauthenticated, fall back to probing a publicly served static asset
// (favicon.ico) to confirm the Next.js server is alive and reachable.
const port = parsePort(process.env.PORT || "20128", 20128);
const dashboardPort = parsePort(process.env.DASHBOARD_PORT || String(port), port);
const host = String(options.livenessHost || process.env.OMNIROUTE_DOCTOR_HOST || "127.0.0.1")
.trim()
.replace(/^https?:\/\//, "")
.replace(/\/.*$/, "");
const fallbackUrl = `http://${formatHostForUrl(host || "127.0.0.1")}:${dashboardPort}/favicon.ico`;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

When a custom liveness URL is configured via options.livenessUrl or process.env.OMNIROUTE_DOCTOR_LIVENESS_URL, the fallback probe currently ignores it and reconstructs the fallback URL using the default host and port (http://127.0.0.1:20128/favicon.ico). This can lead to false-positive warnings on custom host/port/protocol configurations.

Using new URL(url) to derive the fallback URL is more robust as it preserves the configured protocol, host, port, and subpaths automatically.

  let fallbackUrl;
  let dashboardPort;
  try {
    const parsedUrl = new URL(url);
    dashboardPort = parsedUrl.port || (parsedUrl.protocol === "https:" ? "443" : "80");
    parsedUrl.pathname = "/favicon.ico";
    parsedUrl.search = "";
    parsedUrl.hash = "";
    fallbackUrl = parsedUrl.toString();
  } catch {
    const port = parsePort(process.env.PORT || "20128", 20128);
    dashboardPort = parsePort(process.env.DASHBOARD_PORT || String(port), port);
    const host = String(options.livenessHost || process.env.OMNIROUTE_DOCTOR_HOST || "127.0.0.1")
      .trim()
      .replace(/^https?:\/\//, "")
      .replace(/\/.*$/, "");
    fallbackUrl = "http://" + formatHostForUrl(host || "127.0.0.1") + ":" + dashboardPort + "/favicon.ico";
  }

const fallback = await probeUrl(fallbackUrl);

if (fallback.ok) {
return ok(
"Server liveness",
`Server reachable on :${dashboardPort} (health endpoint returned ${primary.status}, likely requires MANAGEMENT_TOKEN)`,
{ primaryUrl: url, primaryStatus: primary.status, fallbackUrl, fallbackStatus: fallback.status }
);
}

return warn(
"Server liveness",
`Server health endpoint returned HTTP ${primary.status || "no-response"} and fallback probe failed`,
{ primaryUrl: url, primaryStatus: primary.status, fallbackUrl, fallbackStatus: fallback.status }
);
}

export async function collectDoctorChecks(context = {}, options = {}) {
Expand Down
6 changes: 3 additions & 3 deletions src/lib/cli-helper/claudeProfileAutoSync.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import path from "node:path";
import { ensureCliConfigWriteAllowed, getCliConfigPaths } from "@/shared/services/cliRuntime";
import { getModelSyncInternalBaseUrl } from "@/shared/services/modelSyncScheduler";
import { isFeatureFlagEnabled } from "@/shared/utils/featureFlags";
import { ensureCliConfigWriteAllowed, getCliConfigPaths } from "../../shared/services/cliRuntime";
import { getModelSyncInternalBaseUrl } from "../../shared/services/modelSyncScheduler";
import { isFeatureFlagEnabled } from "../../shared/utils/featureFlags";

type SyncResult =
| {
Expand Down
6 changes: 3 additions & 3 deletions src/lib/cli-helper/codexProfileAutoSync.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import path from "node:path";
import { ensureCliConfigWriteAllowed, getCliConfigPaths } from "@/shared/services/cliRuntime";
import { getModelSyncInternalBaseUrl } from "@/shared/services/modelSyncScheduler";
import { isFeatureFlagEnabled } from "@/shared/utils/featureFlags";
import { ensureCliConfigWriteAllowed, getCliConfigPaths } from "../../shared/services/cliRuntime";
import { getModelSyncInternalBaseUrl } from "../../shared/services/modelSyncScheduler";
import { isFeatureFlagEnabled } from "../../shared/utils/featureFlags";

type SyncResult =
| {
Expand Down
2 changes: 1 addition & 1 deletion src/lib/cli-helper/config-generator/opencode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
parseOutboundUrl,
isCloudMetadataHost,
OutboundUrlGuardError,
} from "@/shared/network/outboundUrlGuard";
} from "../../../shared/network/outboundUrlGuard";

const CONFIG_PATH = path.join(os.homedir(), ".config", "opencode", "opencode.json");

Expand Down
2 changes: 1 addition & 1 deletion src/lib/cli-helper/tool-detector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import path from "node:path";
import { execFile } from "node:child_process";
import { promisify } from "node:util";
import { getCurrentHermesAgentRoles } from "./config-generator/hermes-agent";
import { getCachedLoginShellPath, mergeShellPath } from "@/shared/services/loginShellPath";
import { getCachedLoginShellPath, mergeShellPath } from "../../shared/services/loginShellPath";

const execFileAsync = promisify(execFile);
let execFileImpl = execFileAsync;
Expand Down
Loading