From 898fc65ed8e84b3bb95a373a23a691c41ede4fa0 Mon Sep 17 00:00:00 2001 From: Brian Geihsler Date: Wed, 25 Feb 2026 23:35:12 +0000 Subject: [PATCH] fix: handle non-numeric error codes from Microsoft Graph in OAuth callback Microsoft Graph API returns string error codes (e.g. "TooManyPendingRequests") in JSON error responses, unlike Google APIs which return numeric codes. Passing these strings to Boom.boomify() as statusCode causes an AssertError crash since Boom requires a numeric value >= 400. Extract resolveOAuthErrorStatus() helper into lib/tools.js to check if response.error.code is a valid numeric HTTP status code before using it, falling back to the HTTP status already captured on err.statusCode or err.oauthRequest.status. --- lib/tools.js | 11 +++++- test/oauth-error-status-test.js | 59 +++++++++++++++++++++++++++++++++ workers/api.js | 9 ++--- 3 files changed, 74 insertions(+), 5 deletions(-) create mode 100644 test/oauth-error-status-test.js diff --git a/lib/tools.js b/lib/tools.js index 2cf72b74..fe648ea0 100644 --- a/lib/tools.js +++ b/lib/tools.js @@ -241,6 +241,13 @@ function formatTokenError(provider, tokenRequest) { return parts.join(': '); } +function resolveOAuthErrorStatus(responseError, err) { + if (typeof responseError.code === 'number' && responseError.code >= 400) { + return responseError.code; + } + return (err && err.statusCode) || (err && err.oauthRequest && err.oauthRequest.status) || 500; +} + module.exports = { /** * Helper function to set specific bit in a buffer @@ -1963,7 +1970,9 @@ MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEV3QUiYsp13nD9suD1/ZkEXnuMoSg normalizeHashKeys, - formatTokenError + formatTokenError, + + resolveOAuthErrorStatus }; function msgpackDecode(buf) { diff --git a/test/oauth-error-status-test.js b/test/oauth-error-status-test.js new file mode 100644 index 00000000..c5c99747 --- /dev/null +++ b/test/oauth-error-status-test.js @@ -0,0 +1,59 @@ +'use strict'; + +const test = require('node:test'); +const assert = require('node:assert').strict; + +const { resolveOAuthErrorStatus } = require('../lib/tools'); + +test('resolveOAuthErrorStatus tests', async t => { + await t.test('returns numeric error code when it is a valid HTTP status', async () => { + assert.strictEqual(resolveOAuthErrorStatus({ code: 400 }, {}), 400); + assert.strictEqual(resolveOAuthErrorStatus({ code: 403 }, {}), 403); + assert.strictEqual(resolveOAuthErrorStatus({ code: 404 }, {}), 404); + assert.strictEqual(resolveOAuthErrorStatus({ code: 429 }, {}), 429); + assert.strictEqual(resolveOAuthErrorStatus({ code: 500 }, {}), 500); + assert.strictEqual(resolveOAuthErrorStatus({ code: 503 }, {}), 503); + }); + + await t.test('falls back to err.statusCode when error code is a string', async () => { + let err = { statusCode: 429 }; + assert.strictEqual(resolveOAuthErrorStatus({ code: 'TooManyPendingRequests' }, err), 429); + assert.strictEqual(resolveOAuthErrorStatus({ code: 'TooManyRequests' }, err), 429); + + err = { statusCode: 400 }; + assert.strictEqual(resolveOAuthErrorStatus({ code: 'BadRequest' }, err), 400); + }); + + await t.test('falls back to err.oauthRequest.status when err.statusCode is missing', async () => { + let err = { oauthRequest: { status: 429 } }; + assert.strictEqual(resolveOAuthErrorStatus({ code: 'TooManyPendingRequests' }, err), 429); + + err = { oauthRequest: { status: 503 } }; + assert.strictEqual(resolveOAuthErrorStatus({ code: 'ServiceUnavailable' }, err), 503); + }); + + await t.test('returns 500 when no fallback status is available', async () => { + assert.strictEqual(resolveOAuthErrorStatus({ code: 'UnknownError' }, {}), 500); + assert.strictEqual(resolveOAuthErrorStatus({ code: 'TooManyPendingRequests' }, {}), 500); + }); + + await t.test('rejects numeric codes below 400', async () => { + let err = { statusCode: 429 }; + assert.strictEqual(resolveOAuthErrorStatus({ code: 200 }, err), 429); + assert.strictEqual(resolveOAuthErrorStatus({ code: 301 }, err), 429); + assert.strictEqual(resolveOAuthErrorStatus({ code: 0 }, err), 429); + }); + + await t.test('handles undefined and null error codes', async () => { + let err = { statusCode: 502 }; + assert.strictEqual(resolveOAuthErrorStatus({ code: undefined }, err), 502); + assert.strictEqual(resolveOAuthErrorStatus({ code: null }, err), 502); + assert.strictEqual(resolveOAuthErrorStatus({}, err), 502); + }); + + await t.test('handles missing err parameter', async () => { + assert.strictEqual(resolveOAuthErrorStatus({ code: 403 }, null), 403); + assert.strictEqual(resolveOAuthErrorStatus({ code: 'BadRequest' }, null), 500); + assert.strictEqual(resolveOAuthErrorStatus({ code: 'BadRequest' }, undefined), 500); + }); +}); diff --git a/workers/api.js b/workers/api.js index 11761122..aa933fb8 100644 --- a/workers/api.js +++ b/workers/api.js @@ -40,7 +40,8 @@ const { getBoolean, loadTlsConfig, httpAgent, - reloadHttpProxyAgent + reloadHttpProxyAgent, + resolveOAuthErrorStatus } = require('../lib/tools'); const { matchIp, detectAutomatedRequest } = require('../lib/utils/network'); @@ -2145,7 +2146,7 @@ Include your token in requests using one of these methods: message = response.error.message; } - let error = Boom.boomify(new Error(message), { statusCode: response.error.code }); + let error = Boom.boomify(new Error(message), { statusCode: resolveOAuthErrorStatus(response.error, err) }); throw error; } throw err; @@ -2239,7 +2240,7 @@ Include your token in requests using one of these methods: let response = err.oauthRequest && err.oauthRequest.response; if (response && response.error) { let message = response.error.message; - let error = Boom.boomify(new Error(message), { statusCode: response.error.code }); + let error = Boom.boomify(new Error(message), { statusCode: resolveOAuthErrorStatus(response.error, err) }); throw error; } throw err; @@ -2328,7 +2329,7 @@ Include your token in requests using one of these methods: let response = err.oauthRequest && err.oauthRequest.response; if (response && response.error) { let message = response.error.message; - let error = Boom.boomify(new Error(message), { statusCode: response.error.code }); + let error = Boom.boomify(new Error(message), { statusCode: resolveOAuthErrorStatus(response.error, err) }); throw error; } throw err;