Repository navigation
GraphError has an empty message when the error response is not Graph JSON (text/plain, HTML or empty body) #2011
Description
Describe the bug
When Graph (or a gateway in front of it) answers a request with an error status and a body that is not the usual { "error": { ... } } JSON, the thrown GraphError has message === "" and code === null. Logging error.message or showing it to a user then yields nothing; only statusCode says what happened.
From reading the code (unchanged between 3.0.7 and dev @ e6a6221):
- On a non-2xx response,
GraphResponseHandler.getResponsethrows the parsed body itself. Fortext/plainthat is a string; fortext/htmlandapplication/xmlit is the rawReadableStreamwhenDOMParseris undefined, as in Node (L125, parseDocumentResponse); without aContent-Typeit is the stream as well (L147). GraphErrorHandler.getErroronly reads a message from a Graph JSON body or from anError. Anything else falls through tonew GraphError(statusCode)with no message (L108), and the constructor passesundefinedtosuper(GraphError.ts L63).
A plain-text body such as Service Unavailable ends up in error.body, but not in error.message.
Expected behavior
error.message is never empty for an HTTP error. For example, the status line when there is nothing better ("HTTP 503" or "503 Service Unavailable"), or the text body when the response is text/plain.
How to reproduce
A local server stands in for Graph and answers four paths with an error status. Run with Node 22 or bun:
import http from "node:http";
import { Client, GraphError } from "@microsoft/microsoft-graph-client";
import pkg from "@microsoft/microsoft-graph-client/package.json" with { type: "json" };
const cases = {
"/json-error": [403, "application/json", JSON.stringify({ error: { code: "Authorization_RequestDenied", message: "Insufficient privileges to complete the operation." } })],
"/html-502": [502, "text/html", "<html><body>Bad Gateway</body></html>"],
"/text-503": [503, "text/plain", "Service Unavailable"],
"/empty-500": [500, undefined, ""],
};
const server = await new Promise((r) => { const s = http.createServer((req, res) => {
const [status, type, body] = cases[new URL(req.url, "http://x").pathname];
res.writeHead(status, type ? { "content-type": type } : {}).end(body);
}).listen(0, "127.0.0.1", () => r(s)); });
const client = Client.initWithMiddleware({
baseUrl: `http://127.0.0.1:${server.address().port}`,
authProvider: { getAccessToken: async () => "example-token" },
});
console.log("@microsoft/microsoft-graph-client", pkg.version, "| runtime", typeof Bun !== "undefined" ? `bun ${Bun.version}` : `node ${process.version}`);
for (const path of Object.keys(cases)) {
try { await client.api(path).version("").get(); }
catch (e) {
console.log(`${path.padEnd(12)} GraphError: ${e instanceof GraphError} statusCode: ${e.statusCode} code: ${JSON.stringify(e.code)} message: ${JSON.stringify(e.message)} body: ${JSON.stringify(e.body)?.slice(0, 50)}`);
}
}
server.close();Output:
@microsoft/microsoft-graph-client 3.0.7 | runtime node v22.23.3
/json-error GraphError: true statusCode: 403 code: "Authorization_RequestDenied" message: "Insufficient privileges to complete the operation." body: "{\"code\":\"Authorization_RequestDenied\",\"messa
/html-502 GraphError: true statusCode: 502 code: null message: "" body: {}
/text-503 GraphError: true statusCode: 503 code: null message: "" body: "Service Unavailable"
/empty-500 GraphError: true statusCode: 500 code: null message: "" body: {}
The same output with bun 1.4.4.
SDK Version
3.0.7 (latest on npm). Code read on dev @ e6a6221 (package.json says 3.0.8, not published yet).
Latest version known to work for scenario above?
None known.
Known Workarounds
Fall back on the status code when the message is empty, e.g. error.message || `HTTP ${error.statusCode}` .
Debug output
Not applicable. The output above is the full repro output.
Configuration
- OS: Linux (WSL2), x64
- Node.js v22.23.3 and bun 1.4.4
- Not specific to this configuration, as far as I can tell from the code.
Other information
A possible fix is a fallback message in getError's last branch: the body when it is a non-empty string, otherwise a status line such as HTTP ${statusCode}.
Related: #353 fixed an empty error coming from the auth provider callback, which is a different path. #1534 (closed) logs a 401 GraphError whose first line has no message text, which looks like the same case.
Activity