From 8ec79250e626a93b597826a6ac465db897ff5c23 Mon Sep 17 00:00:00 2001
From: shamoon <4887959+shamoon@users.noreply.github.com>
Date: Mon, 5 Oct 2026 20:56:01 -0700
Subject: [PATCH] Chore: handle cloudflare breaking API change dropping
connections (#7217)
---
src/widgets/cloudflared/component.jsx | 11 ++-
src/widgets/cloudflared/component.test.jsx | 100 ++++++++++++++-------
src/widgets/cloudflared/widget.js | 8 +-
src/widgets/cloudflared/widget.test.js | 14 ++-
4 files changed, 93 insertions(+), 40 deletions(-)
diff --git a/src/widgets/cloudflared/component.jsx b/src/widgets/cloudflared/component.jsx
index abae61bb0..89a66ca12 100644
--- a/src/widgets/cloudflared/component.jsx
+++ b/src/widgets/cloudflared/component.jsx
@@ -6,9 +6,10 @@ export default function Component({ service }) {
const { widget } = service;
const { data: statsData, error: statsError } = useWidgetAPI(widget, "cfd_tunnel");
+ const { data: connectionsData, error: connectionsError } = useWidgetAPI(widget, "connections");
- if (statsError) {
- return ;
+ if (statsError || connectionsError) {
+ return ;
}
if (!statsData) {
@@ -20,7 +21,9 @@ export default function Component({ service }) {
);
}
- const originIP = statsData.result.connections?.origin_ip ?? statsData.result.connections[0]?.origin_ip;
+ const originIP = connectionsData?.result
+ .flatMap((connector) => connector.conns ?? [])
+ .find((connection) => connection.origin_ip)?.origin_ip;
return (
@@ -28,7 +31,7 @@ export default function Component({ service }) {
label="cloudflared.status"
value={statsData.result.status.charAt(0).toUpperCase() + statsData.result.status.slice(1)}
/>
-
+
);
}
diff --git a/src/widgets/cloudflared/component.test.jsx b/src/widgets/cloudflared/component.test.jsx
index 9a81a0835..c6f279242 100644
--- a/src/widgets/cloudflared/component.test.jsx
+++ b/src/widgets/cloudflared/component.test.jsx
@@ -4,59 +4,93 @@ import { screen } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { renderWithProviders } from "test-utils/render-with-providers";
-import { expectBlockValue } from "test-utils/widget-assertions";
+import { expectBlockValue, findServiceBlockByLabel } from "test-utils/widget-assertions";
const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() }));
-
vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI }));
import Component from "./component";
describe("widgets/cloudflared/component", () => {
- beforeEach(() => {
- vi.clearAllMocks();
- });
+ beforeEach(() => vi.resetAllMocks());
- it("renders placeholders while loading", () => {
- useWidgetAPI.mockReturnValue({ data: undefined, error: undefined });
+ function mockResponses(stats, connections) {
+ useWidgetAPI.mockImplementation((_widget, endpoint) => {
+ if (endpoint === "cfd_tunnel") return stats;
+ if (endpoint === "connections") return connections;
+ throw new Error(`Unexpected endpoint: ${endpoint}`);
+ });
+ }
- const { container } = renderWithProviders(, {
+ function renderWidget() {
+ return renderWithProviders(, {
settings: { hideErrors: false },
});
+ }
- expect(container.querySelectorAll(".service-block")).toHaveLength(2);
+ it("renders placeholders while both requests are loading", () => {
+ mockResponses({ data: undefined }, { data: undefined });
+ const { container } = renderWidget();
+ expect(container.querySelectorAll(".service-block.animate-pulse")).toHaveLength(2);
expect(screen.getByText("cloudflared.status")).toBeInTheDocument();
expect(screen.getByText("cloudflared.origin_ip")).toBeInTheDocument();
});
- it("renders status capitalization and origin_ip from nested connections", () => {
- useWidgetAPI.mockReturnValue({
- data: {
- result: { status: "healthy", connections: { origin_ip: "1.2.3.4" } },
- },
- error: undefined,
- });
-
- const { container } = renderWithProviders(, {
- settings: { hideErrors: false },
- });
-
+ it("renders status without the retired connections field and reads connector IPs", () => {
+ mockResponses(
+ { data: { result: { status: "healthy" } } },
+ { data: { result: [{ conns: [{ origin_ip: "1.2.3.4" }] }] } },
+ );
+ const { container } = renderWidget();
expectBlockValue(container, "cloudflared.status", "Healthy");
expectBlockValue(container, "cloudflared.origin_ip", "1.2.3.4");
});
- it("falls back to origin_ip from first connection entry", () => {
- useWidgetAPI.mockReturnValue({
- data: {
- result: { status: "down", connections: [{ origin_ip: "5.6.7.8" }] },
- },
- error: undefined,
- });
-
- const { container } = renderWithProviders(, {
- settings: { hideErrors: false },
- });
-
+ it("finds the first available IP across connectors and connections", () => {
+ mockResponses(
+ { data: { result: { status: "degraded", connections: [{ origin_ip: "old-ip" }] } } },
+ { data: { result: [{}, { conns: [] }, { conns: [{}, { origin_ip: "5.6.7.8" }, { origin_ip: "9.10.11.12" }] }] } },
+ );
+ const { container } = renderWidget();
+ expectBlockValue(container, "cloudflared.status", "Degraded");
expectBlockValue(container, "cloudflared.origin_ip", "5.6.7.8");
+ expect(container.textContent).not.toContain("old-ip");
+ });
+
+ it.each([
+ { connectors: [] },
+ { connectors: [{}] },
+ { connectors: [{ conns: [] }] },
+ { connectors: [{ conns: [{}] }] },
+ ])("renders a settled placeholder when no connector has an IP ($connectors)", ({ connectors }) => {
+ mockResponses({ data: { result: { status: "down" } } }, { data: { result: connectors } });
+ const { container } = renderWidget();
+ expectBlockValue(container, "cloudflared.status", "Down");
+ expectBlockValue(container, "cloudflared.origin_ip", "-");
+ expect(findServiceBlockByLabel(container, "cloudflared.origin_ip")).not.toHaveClass("animate-pulse");
+ });
+
+ it("renders status while connector details are loading", () => {
+ mockResponses({ data: { result: { status: "healthy" } } }, { data: undefined });
+ const { container } = renderWidget();
+ expectBlockValue(container, "cloudflared.status", "Healthy");
+ expect(findServiceBlockByLabel(container, "cloudflared.origin_ip")).toHaveClass("animate-pulse");
+ });
+
+ it("keeps placeholders when connectors load before tunnel status", () => {
+ mockResponses({ data: undefined }, { data: { result: [{ conns: [{ origin_ip: "1.2.3.4" }] }] } });
+ const { container } = renderWidget();
+ expect(container.querySelectorAll(".service-block.animate-pulse")).toHaveLength(2);
+ });
+
+ it.each(["cfd_tunnel", "connections"])("shows errors from the %s request", (failedEndpoint) => {
+ useWidgetAPI.mockImplementation((_widget, endpoint) =>
+ endpoint === failedEndpoint
+ ? { error: { message: "Cloudflare request failed" } }
+ : { data: endpoint === "cfd_tunnel" ? { result: { status: "healthy" } } : { result: [] } },
+ );
+ const { container } = renderWidget();
+ expect(container.querySelectorAll(".service-block")).toHaveLength(0);
+ expect(screen.getByText(/Cloudflare request failed/)).toBeInTheDocument();
});
});
diff --git a/src/widgets/cloudflared/widget.js b/src/widgets/cloudflared/widget.js
index b2019cff0..f9c19ae1a 100644
--- a/src/widgets/cloudflared/widget.js
+++ b/src/widgets/cloudflared/widget.js
@@ -1,12 +1,16 @@
import credentialedProxyHandler from "utils/proxy/handlers/credentialed";
const widget = {
- api: "https://api.cloudflare.com/client/v4/accounts/{accountid}/{endpoint}/{tunnelid}",
+ api: "https://api.cloudflare.com/client/v4/accounts/{accountid}/{endpoint}",
proxyHandler: credentialedProxyHandler,
mappings: {
cfd_tunnel: {
- endpoint: "cfd_tunnel",
+ endpoint: "cfd_tunnel/{tunnelid}",
+ validate: ["success", "result"],
+ },
+ connections: {
+ endpoint: "cfd_tunnel/{tunnelid}/connections",
validate: ["success", "result"],
},
},
diff --git a/src/widgets/cloudflared/widget.test.js b/src/widgets/cloudflared/widget.test.js
index cd2f38327..14008dcd4 100644
--- a/src/widgets/cloudflared/widget.test.js
+++ b/src/widgets/cloudflared/widget.test.js
@@ -1,6 +1,7 @@
-import { describe, it } from "vitest";
+import { describe, expect, it } from "vitest";
import { expectWidgetConfigShape } from "test-utils/widget-config";
+import { formatApiCall } from "utils/proxy/api-helpers";
import widget from "./widget";
@@ -8,4 +9,15 @@ describe("cloudflared widget config", () => {
it("exports a valid widget config", () => {
expectWidgetConfigShape(widget);
});
+
+ it.each([
+ ["cfd_tunnel", "cfd_tunnel/tunnel-id"],
+ ["connections", "cfd_tunnel/tunnel-id/connections"],
+ ])("routes %s to the account and tunnel", (endpoint, path) => {
+ const mapping = widget.mappings[endpoint];
+ expect(
+ formatApiCall(widget.api, { accountid: "account-id", tunnelid: "tunnel-id", endpoint: mapping.endpoint }),
+ ).toBe(`https://api.cloudflare.com/client/v4/accounts/account-id/${path}`);
+ expect(mapping.validate).toEqual(["success", "result"]);
+ });
});