Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
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
8 changes: 4 additions & 4 deletions packages/matter-server/src/converter/ChipConfigData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/

import { LegacyFabricConfigData } from "@matter-server/ws-controller";
import { getErrorMessage, LegacyFabricConfigData } from "@matter-server/ws-controller";
import { Bytes, Key, StandardCrypto, type BinaryKeyPair } from "@matter/main";
import { CertificateAuthority, Icac, Noc, Rcac } from "@matter/main/protocol";
import { readFile, writeFile } from "node:fs/promises";
Expand Down Expand Up @@ -614,7 +614,7 @@ export class ChipConfigData {
result.rcacValid = true;
} catch (e) {
result.rcacValid = false;
return { ...result, error: `RCAC verification failed: ${e instanceof Error ? e.message : String(e)}` };
return { ...result, error: `RCAC verification failed: ${getErrorMessage(e)}` };
}

// Verify ICAC (if present)
Expand All @@ -624,7 +624,7 @@ export class ChipConfigData {
result.icacValid = true;
} catch (e) {
result.icacValid = false;
return { ...result, error: `ICAC verification failed: ${e instanceof Error ? e.message : String(e)}` };
return { ...result, error: `ICAC verification failed: ${getErrorMessage(e)}` };
}
}

Expand All @@ -634,7 +634,7 @@ export class ChipConfigData {
result.nocValid = true;
} catch (e) {
result.nocValid = false;
return { ...result, error: `NOC verification failed: ${e instanceof Error ? e.message : String(e)}` };
return { ...result, error: `NOC verification failed: ${getErrorMessage(e)}` };
}

result.valid = true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ import {
ServerError,
UpdateSource,
} from "../types/WebSocketMessageTypes.js";
import { getErrorMessage } from "../util/errorUtils.js";
import { formatNodeId } from "../util/formatNodeId.js";
import { pingIp } from "../util/network.js";
import { CustomClusterPoller } from "./CustomClusterPoller.js";
Expand Down Expand Up @@ -992,9 +993,8 @@ export class ControllerCommandHandler {
});
} catch (error) {
// Preserve the original error message with context
const originalMessage = error instanceof Error ? error.message : String(error);
throw ServerError.nodeCommissionFailed(
`Commission failed: ${originalMessage}`,
`Commission failed: ${getErrorMessage(error)}`,
error instanceof Error ? error : undefined,
);
}
Expand Down
1 change: 1 addition & 0 deletions packages/ws-controller/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export * from "./types/CommandHandler.js";
export * from "./types/WebSocketMessageTypes.js";

// Export utilities
export { getErrorMessage } from "./util/errorUtils.js";
export { formatNodeId } from "./util/formatNodeId.js";
export * from "./util/matterVersion.js";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
ServerInfoMessage,
SuccessResultMessage,
} from "../types/WebSocketMessageTypes.js";
import { getErrorMessage } from "../util/errorUtils.js";
import { formatNodeId } from "../util/formatNodeId.js";
import { MATTER_VERSION } from "../util/matterVersion.js";
import { ConfigStorage } from "./ConfigStorage.js";
Expand Down Expand Up @@ -509,7 +510,7 @@ export class WebSocketControllerHandler implements WebServerHandler {
response: {
message_id: messageId ?? "",
error_code: errorCode,
details: (err as Error).message,
details: getErrorMessage(err),
},
};
}
Expand Down
27 changes: 27 additions & 0 deletions packages/ws-controller/src/util/errorUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/**
* @license
* Copyright 2025-2026 Open Home Foundation
* SPDX-License-Identifier: Apache-2.0
*/

/**
* Extracts an error message from an unknown error value.
* Handles string errors, Error instances, Error-like objects, and
* falls back to JSON serialization or String() for other types.
*/
export function getErrorMessage(err: unknown): string {
if (typeof err === "string") return err;
if (err instanceof Error) return err.message;
Comment thread
markvp marked this conversation as resolved.
if (err !== null && typeof err === "object") {
const anyErr = err as { message?: unknown };
if (typeof anyErr.message === "string") return anyErr.message;
Comment thread
markvp marked this conversation as resolved.
if (anyErr.message instanceof Error) return anyErr.message.message;
if (anyErr.message != null) return String(anyErr.message);
try {
return JSON.stringify(err);
} catch {
return Object.prototype.toString.call(err);
}
}
return String(err);
}
18 changes: 18 additions & 0 deletions packages/ws-controller/test/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"extends": "../../tools/tsc/tsconfig.test.json",
"compilerOptions": {
"types": [
"node",
"mocha",
"@matter/testing"
]
},
"references": [
{
"path": "../../tools/src"
},
{
"path": "../src"
}
]
}
71 changes: 71 additions & 0 deletions packages/ws-controller/test/util/ErrorUtilsTest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/**
* @license
* Copyright 2025-2026 Open Home Foundation
* SPDX-License-Identifier: Apache-2.0
*/

import { getErrorMessage } from "../../src/util/errorUtils.js";

describe("getErrorMessage", () => {
it("returns the string directly for string errors", () => {
expect(getErrorMessage("something went wrong")).to.equal("something went wrong");
});

it("returns empty string for empty string errors", () => {
expect(getErrorMessage("")).to.equal("");
});

it("returns message from Error instances", () => {
expect(getErrorMessage(new Error("test error"))).to.equal("test error");
});

it("returns message from Error subclasses", () => {
expect(getErrorMessage(new TypeError("type error"))).to.equal("type error");
expect(getErrorMessage(new RangeError("range error"))).to.equal("range error");
});

it("returns message from Error-like objects with string message", () => {
expect(getErrorMessage({ message: "error-like" })).to.equal("error-like");
});

it("returns nested message from Error-like objects with Error message", () => {
expect(getErrorMessage({ message: new Error("nested") })).to.equal("nested");
});

it("returns stringified message for non-string, non-Error message properties", () => {
expect(getErrorMessage({ message: 42 })).to.equal("42");
expect(getErrorMessage({ message: true })).to.equal("true");
});

it("returns JSON stringified result for objects without message", () => {
expect(getErrorMessage({ code: 404, detail: "not found" })).to.equal(
'{"code":404,"detail":"not found"}',
);
});

it("falls back to Object.prototype.toString for non-serializable objects", () => {
const circular: Record<string, unknown> = {};
circular.self = circular;
expect(getErrorMessage(circular)).to.equal("[object Object]");
});

it("uses String() for numbers", () => {
expect(getErrorMessage(42)).to.equal("42");
expect(getErrorMessage(0)).to.equal("0");
expect(getErrorMessage(NaN)).to.equal("NaN");
});

it("uses String() for booleans", () => {
expect(getErrorMessage(true)).to.equal("true");
expect(getErrorMessage(false)).to.equal("false");
});

it("uses String() for null and undefined", () => {
expect(getErrorMessage(null)).to.equal("null");
expect(getErrorMessage(undefined)).to.equal("undefined");
});

it("handles Symbol values", () => {
expect(getErrorMessage(Symbol("test"))).to.equal("Symbol(test)");
});
});
2 changes: 1 addition & 1 deletion packages/ws-controller/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"compilerOptions": { "composite": true },
"files": [],
"references": [{ "path": "src" }]
"references": [{ "path": "src" }, { "path": "test" }]
}