Skip to content

Commit af673f5

Browse files
authored
Parse azd JSON output for cleaner AzureDeveloperCliCredential error messages (Azure#37268)
1 parent 2cf94a9 commit af673f5

File tree

3 files changed

+126
-2
lines changed

3 files changed

+126
-2
lines changed

sdk/identity/identity/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
### Bugs Fixed
1212

13+
- Fixed an issue where `AzureDeveloperCliCredential` error messages included raw JSON output from `azd auth token` instead of clean, user-friendly messages. The credential now parses the JSON stderr output to extract and display only the error message. [#37268](https://github.com/Azure/azure-sdk-for-js/pull/37268)
1314
- Fixed an issue where `IdentityClient` does not pass response in expected format for MSAL in empty response situations with additional logging. [#36906](https://github.com/Azure/azure-sdk-for-js/pull/36906)
1415

1516
### Other Changes

sdk/identity/identity/src/credentials/azureDeveloperCliCredential.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,28 @@ export const azureDeveloperCliPublicErrorMessages = {
3535
* @internal
3636
*/
3737
export const developerCliCredentialInternals = {
38+
/**
39+
* Parses azd stderr JSON output to extract the error message.
40+
* azd outputs JSON like: \{"type":"consoleMessage","timestamp":"...","data":\{"message":"ERROR: ..."\}\}
41+
* If parsing succeeds and .data.message exists, returns the trimmed message.
42+
* Otherwise, returns the raw stderr.
43+
* @param stderr - The stderr output from azd command
44+
* @returns The parsed error message or raw stderr
45+
* @internal
46+
*/
47+
parseAzdStderr(stderr: string): string {
48+
try {
49+
const parsed = JSON.parse(stderr);
50+
const message = parsed?.data?.message;
51+
if (typeof message === "string" && message.trim().length > 0) {
52+
return message.trim();
53+
}
54+
} catch {
55+
// If JSON parsing fails, fall through to return raw stderr
56+
}
57+
return stderr;
58+
},
59+
3860
/**
3961
* @internal
4062
*/
@@ -241,7 +263,8 @@ export class AzureDeveloperCliCredential implements TokenCredential {
241263
} as AccessToken;
242264
} catch (e: any) {
243265
if (obj.stderr) {
244-
throw new CredentialUnavailableError(obj.stderr);
266+
const errorMessage = developerCliCredentialInternals.parseAzdStderr(obj.stderr);
267+
throw new CredentialUnavailableError(errorMessage);
245268
}
246269
throw e;
247270
}

sdk/identity/identity/test/internal/node/azureDeveloperCliCredential.spec.ts

Lines changed: 101 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT License.
33
import { AzureDeveloperCliCredential } from "@azure/identity";
4-
import { azureDeveloperCliPublicErrorMessages } from "$internal/credentials/azureDeveloperCliCredential.js";
4+
import {
5+
azureDeveloperCliPublicErrorMessages,
6+
developerCliCredentialInternals,
7+
} from "$internal/credentials/azureDeveloperCliCredential.js";
58
import type { GetTokenOptions } from "@azure/core-auth";
69
import child_process, { type ChildProcess } from "node:child_process";
710
import { describe, it, assert, expect, vi, beforeEach, afterEach } from "vitest";
@@ -272,4 +275,101 @@ describe("AzureDeveloperCliCredential (internal)", function () {
272275
);
273276
});
274277
}
278+
279+
describe("parseAzdStderr", () => {
280+
it("parses valid JSON with data.message", () => {
281+
const json = JSON.stringify({
282+
type: "consoleMessage",
283+
timestamp: "2024-01-01T00:00:00Z",
284+
data: { message: "\nERROR: fetching token: authentication failed\n" },
285+
});
286+
const result = developerCliCredentialInternals.parseAzdStderr(json);
287+
assert.equal(result, "ERROR: fetching token: authentication failed");
288+
});
289+
290+
it("trims whitespace from data.message", () => {
291+
const json = JSON.stringify({
292+
type: "consoleMessage",
293+
timestamp: "2024-01-01T00:00:00Z",
294+
data: { message: " \n ERROR: test error \n " },
295+
});
296+
const result = developerCliCredentialInternals.parseAzdStderr(json);
297+
assert.equal(result, "ERROR: test error");
298+
});
299+
300+
it("returns raw stderr when JSON parsing fails", () => {
301+
const invalidJson = "not valid json";
302+
const result = developerCliCredentialInternals.parseAzdStderr(invalidJson);
303+
assert.equal(result, "not valid json");
304+
});
305+
306+
it("returns raw stderr when data.message is missing", () => {
307+
const json = JSON.stringify({
308+
type: "consoleMessage",
309+
timestamp: "2024-01-01T00:00:00Z",
310+
data: {},
311+
});
312+
const result = developerCliCredentialInternals.parseAzdStderr(json);
313+
assert.equal(result, json);
314+
});
315+
316+
it("returns raw stderr when data.message is empty", () => {
317+
const json = JSON.stringify({
318+
type: "consoleMessage",
319+
timestamp: "2024-01-01T00:00:00Z",
320+
data: { message: "" },
321+
});
322+
const result = developerCliCredentialInternals.parseAzdStderr(json);
323+
assert.equal(result, json);
324+
});
325+
326+
it("returns raw stderr when data.message is only whitespace", () => {
327+
const json = JSON.stringify({
328+
type: "consoleMessage",
329+
timestamp: "2024-01-01T00:00:00Z",
330+
data: { message: " \n \n " },
331+
});
332+
const result = developerCliCredentialInternals.parseAzdStderr(json);
333+
assert.equal(result, json);
334+
});
335+
336+
it("returns raw stderr when data is missing", () => {
337+
const json = JSON.stringify({
338+
type: "consoleMessage",
339+
timestamp: "2024-01-01T00:00:00Z",
340+
});
341+
const result = developerCliCredentialInternals.parseAzdStderr(json);
342+
assert.equal(result, json);
343+
});
344+
});
345+
346+
describe("error message parsing integration", () => {
347+
it("parses JSON error message from stderr", async () => {
348+
stdout = "";
349+
stderr = JSON.stringify({
350+
type: "consoleMessage",
351+
timestamp: "2024-01-01T00:00:00Z",
352+
data: { message: "\nERROR: fetching token: authentication failed\n" },
353+
});
354+
const credential = new AzureDeveloperCliCredential();
355+
try {
356+
await credential.getToken("https://service/.default");
357+
assert.fail("Expected error to be thrown");
358+
} catch (error: any) {
359+
assert.equal(error.message, "ERROR: fetching token: authentication failed");
360+
}
361+
});
362+
363+
it("uses raw stderr when JSON parsing fails", async () => {
364+
stdout = "";
365+
stderr = "plain text error message";
366+
const credential = new AzureDeveloperCliCredential();
367+
try {
368+
await credential.getToken("https://service/.default");
369+
assert.fail("Expected error to be thrown");
370+
} catch (error: any) {
371+
assert.equal(error.message, "plain text error message");
372+
}
373+
});
374+
});
275375
});

0 commit comments

Comments
 (0)