Skip to content

Commit 23da029

Browse files
Rafael ReisRafael Reis
authored andcommitted
fix(zulip): improve diagnostics for non-JSON API responses
1 parent f3987e4 commit 23da029

File tree

2 files changed

+26
-12
lines changed

2 files changed

+26
-12
lines changed

extensions/zulip/src/zulip/client.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ describe("parseJsonOrThrow", () => {
1313
headers: { "content-type": "text/html" },
1414
});
1515

16-
await expect(parseJsonOrThrow(res)).rejects.toThrow(/Non-JSON response from Zulip/i);
16+
await expect(parseJsonOrThrow(res)).rejects.toThrow(/received HTML instead of JSON/i);
1717
});
1818

1919
it("throws when payload.result != success", async () => {

extensions/zulip/src/zulip/client.ts

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -87,24 +87,38 @@ function withAuth(client: ZulipClient, init?: RequestInit): RequestInit {
8787
export async function parseJsonOrThrow(res: Response): Promise<unknown> {
8888
const text = await res.text();
8989

90-
// Zulip API endpoints should return JSON. If we get HTML (common with SSO/Cloudflare Access),
91-
// treat it as an auth error so we don't report false positives like "Sent".
90+
// Zulip API endpoints should return JSON.
91+
// If we get HTML, it's often an auth/SSO/proxy login page (Cloudflare Access, SSO, etc.),
92+
// but "non-JSON" can also be an upstream error page (502/503) or a misconfigured base URL.
9293
const contentType = (res.headers.get("content-type") || "").toLowerCase();
93-
const looksLikeHtml = /^\s*<!doctype html/i.test(text) || /^\s*<html/i.test(text);
94-
const expectsJson =
95-
contentType.includes("application/json") || contentType.includes("application/");
94+
const looksLikeHtml =
95+
/^\s*<!doctype html/i.test(text) ||
96+
/^\s*<html/i.test(text) ||
97+
/^\s*<head/i.test(text) ||
98+
/^\s*<meta\b/i.test(text);
9699

97100
let payload: Record<string, unknown>;
98101
try {
99102
payload = text ? JSON.parse(text) : {};
100103
} catch {
101-
if (looksLikeHtml || !expectsJson) {
102-
const hint =
103-
"Non-JSON response from Zulip. This usually means a reverse-proxy/SSO (e.g. Cloudflare Access) is blocking /api. " +
104-
"Allow bot access to /api/v1/* (service token / bypass policy) or expose an internal API URL.";
105-
throw new Error(`Zulip API error: ${hint}`);
104+
const snippet = text.trim().slice(0, 240).replace(/\s+/g, " ");
105+
106+
if (looksLikeHtml) {
107+
throw new Error(
108+
"Zulip API error: received HTML instead of JSON from /api. " +
109+
`HTTP ${res.status} (content-type: ${contentType || "unknown"}). ` +
110+
"This typically means an auth/SSO/proxy layer is intercepting API requests. " +
111+
"Allow bot access to /api/v1/* (service token / bypass policy) or use an internal API base URL. " +
112+
(snippet ? `Snippet: ${snippet}` : ""),
113+
);
106114
}
107-
throw new Error(`Zulip API error: invalid JSON response (HTTP ${res.status})`);
115+
116+
throw new Error(
117+
"Zulip API error: received non-JSON response from /api. " +
118+
`HTTP ${res.status} (content-type: ${contentType || "unknown"}). ` +
119+
"This can be caused by a proxy/load balancer error (502/503), a misconfigured base URL, or an auth layer. " +
120+
(snippet ? `Snippet: ${snippet}` : ""),
121+
);
108122
}
109123

110124
const msgField =

0 commit comments

Comments
 (0)