Skip to content

Commit 7999304

Browse files
committed
Merge branch 'fix/cli-401-hint' into 'main'
fix(cli): append 'postgresai auth' remediation hint to 401 errors See merge request postgres-ai/postgresai!320
2 parents 684a206 + 6415233 commit 7999304

3 files changed

Lines changed: 58 additions & 3 deletions

File tree

cli/lib/util.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,19 +22,27 @@ function isHtmlContent(text: string): boolean {
2222
return trimmed.startsWith("<!DOCTYPE") || trimmed.startsWith("<html") || trimmed.startsWith("<HTML");
2323
}
2424

25+
/**
26+
* Remediation hint appended to 401 errors so both humans and AI agents
27+
* (MCP tool callers) know how to recover from an invalid/stale API key.
28+
*/
29+
const AUTH_REMEDIATION_HINT = "Run 'postgresai auth' to (re)authenticate, or set/update PGAI_API_KEY.";
30+
2531
/**
2632
* Format an HTTP error response into a clean, developer-friendly message.
2733
* Handles HTML error pages (e.g., from Cloudflare) by showing just the status code and message.
34+
* For 401 responses, appends a remediation hint pointing at `postgresai auth`.
2835
*/
2936
export function formatHttpError(operation: string, status: number, responseBody?: string): string {
3037
const statusMessage = HTTP_STATUS_MESSAGES[status] || "Request failed";
3138
let errMsg = `${operation}: HTTP ${status} - ${statusMessage}`;
39+
const remediation = status === 401 ? `\n${AUTH_REMEDIATION_HINT}` : "";
3240

3341
if (responseBody) {
3442
// If it's HTML (like Cloudflare error pages), don't dump the raw HTML
3543
if (isHtmlContent(responseBody)) {
3644
// Just use the status message, don't append HTML
37-
return errMsg;
45+
return errMsg + remediation;
3846
}
3947

4048
// Try to parse as JSON for structured error info
@@ -56,7 +64,7 @@ export function formatHttpError(operation: string, status: number, responseBody?
5664
}
5765
}
5866

59-
return errMsg;
67+
return errMsg + remediation;
6068
}
6169

6270
export function maskSecret(secret: string): string {
@@ -124,4 +132,3 @@ export function resolveBaseUrls(
124132
storageBaseUrl: normalizeBaseUrl(storageCandidate),
125133
};
126134
}
127-

cli/test/mcp-server.test.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,8 @@ describe("MCP Server", () => {
238238

239239
expect(response.isError).toBe(true);
240240
expect(getResponseText(response)).toContain("401");
241+
// Agent-facing remediation: invalid key must point at re-auth, not dead-end
242+
expect(getResponseText(response)).toContain("Run 'postgresai auth'");
241243

242244
readConfigSpy.mockRestore();
243245
});
@@ -1678,6 +1680,8 @@ describe("MCP Server", () => {
16781680

16791681
expect(response.isError).toBe(true);
16801682
expect(getResponseText(response)).toContain("401");
1683+
// Agent-facing remediation: invalid key must point at re-auth, not dead-end
1684+
expect(getResponseText(response)).toContain("Run 'postgresai auth'");
16811685

16821686
readConfigSpy.mockRestore();
16831687
});

cli/test/util.test.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { describe, expect, test } from "bun:test";
2+
3+
import { formatHttpError } from "../lib/util";
4+
5+
describe("formatHttpError", () => {
6+
test("appends auth remediation hint on 401 with JSON body", () => {
7+
const msg = formatHttpError(
8+
"Failed to fetch issues",
9+
401,
10+
'{"hint": "Please check validity of token", "details": "Invalid token"}'
11+
);
12+
13+
expect(msg).toContain("HTTP 401");
14+
expect(msg).toContain("Run 'postgresai auth'");
15+
expect(msg).toContain("PGAI_API_KEY");
16+
});
17+
18+
test("appends auth remediation hint on 401 with HTML body (early-return path)", () => {
19+
const msg = formatHttpError("Failed to fetch issues", 401, "<html><body>Unauthorized</body></html>");
20+
21+
expect(msg).toContain("HTTP 401");
22+
expect(msg).toContain("Run 'postgresai auth'");
23+
});
24+
25+
test("appends auth remediation hint on 401 without body", () => {
26+
const msg = formatHttpError("Failed to fetch issues", 401);
27+
28+
expect(msg).toContain("Run 'postgresai auth'");
29+
});
30+
31+
test("does not append auth hint for non-401 statuses", () => {
32+
expect(formatHttpError("Failed to fetch issues", 403, '{"message": "denied"}')).not.toContain(
33+
"Run 'postgresai auth'"
34+
);
35+
expect(formatHttpError("Failed to fetch issues", 500)).not.toContain("Run 'postgresai auth'");
36+
});
37+
38+
test("keeps structured JSON error details before the hint", () => {
39+
const msg = formatHttpError("Failed to fetch issues", 401, '{"message": "Invalid token"}');
40+
41+
expect(msg.indexOf("Invalid token")).toBeGreaterThan(-1);
42+
expect(msg.indexOf("Invalid token")).toBeLessThan(msg.indexOf("Run 'postgresai auth'"));
43+
});
44+
});

0 commit comments

Comments
 (0)