Skip to content

Commit 4f69e53

Browse files
authored
fix: improve diagnostics for failures in outbound HTTP requests (#221)
1 parent 3bcd790 commit 4f69e53

File tree

5 files changed

+138
-3
lines changed

5 files changed

+138
-3
lines changed

src/log-hook.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -380,6 +380,26 @@ export function hookLoggerToMongoLogWriter(
380380
);
381381
});
382382

383+
emitter.on('mongodb-oidc-plugin:outbound-http-request-completed', (ev) => {
384+
log.debug?.(
385+
'OIDC-PLUGIN',
386+
mongoLogId(1_002_000_032),
387+
`${contextPrefix}-oidc`,
388+
'Outbound HTTP request completed',
389+
{ ...ev, url: redactUrl(ev.url) }
390+
);
391+
});
392+
393+
emitter.on('mongodb-oidc-plugin:outbound-http-request-failed', (ev) => {
394+
log.debug?.(
395+
'OIDC-PLUGIN',
396+
mongoLogId(1_002_000_033),
397+
`${contextPrefix}-oidc`,
398+
'Outbound HTTP request failed',
399+
{ ...ev, url: redactUrl(ev.url) }
400+
);
401+
});
402+
383403
emitter.on('mongodb-oidc-plugin:state-updated', (ev) => {
384404
log.info(
385405
'OIDC-PLUGIN',

src/plugin.spec.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1492,5 +1492,45 @@ describe('OIDC plugin (mock OIDC provider)', function () {
14921492
expect(result.accessToken).to.be.a('string');
14931493
expect(customFetch).to.have.been.called;
14941494
});
1495+
1496+
it('logs helpful error messages', async function () {
1497+
const outboundHTTPRequestsCompleted: any[] = [];
1498+
1499+
getTokenPayload = () => Promise.reject(new Error('test failure'));
1500+
const plugin = createMongoDBOIDCPlugin({
1501+
openBrowserTimeout: 60_000,
1502+
openBrowser: fetchBrowser,
1503+
allowedFlows: ['auth-code'],
1504+
redirectURI: 'http://localhost:0/callback',
1505+
});
1506+
1507+
plugin.logger.on(
1508+
'mongodb-oidc-plugin:outbound-http-request-completed',
1509+
(ev) => outboundHTTPRequestsCompleted.push(ev)
1510+
);
1511+
1512+
try {
1513+
await requestToken(plugin, {
1514+
issuer: provider.issuer,
1515+
clientId: 'mockclientid',
1516+
requestScopes: [],
1517+
});
1518+
expect.fail('missed exception');
1519+
} catch (err: any) {
1520+
expect(err.message).to.equal(
1521+
'unexpected HTTP response status code: caused by HTTP response 500 (Internal Server Error): test failure'
1522+
);
1523+
}
1524+
expect(outboundHTTPRequestsCompleted).to.deep.include({
1525+
url: `${provider.issuer}/.well-known/openid-configuration`,
1526+
status: 200,
1527+
statusText: 'OK',
1528+
});
1529+
expect(outboundHTTPRequestsCompleted).to.deep.include({
1530+
url: `${provider.issuer}/token`,
1531+
status: 500,
1532+
statusText: 'Internal Server Error',
1533+
});
1534+
});
14951535
});
14961536
});

src/plugin.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { MongoDBOIDCError } from './types';
1010
import {
1111
errorString,
1212
getRefreshTokenId,
13+
improveHTTPResponseBasedError,
1314
messageFromError,
1415
normalizeObject,
1516
throwIfAborted,
@@ -400,9 +401,27 @@ export class MongoDBOIDCPluginImpl implements MongoDBOIDCPlugin {
400401
});
401402
}
402403

403-
fetch: CustomFetch = async (url, init) => {
404+
private fetch: CustomFetch = async (url, init) => {
404405
this.logger.emit('mongodb-oidc-plugin:outbound-http-request', { url });
405406

407+
try {
408+
const response = await this.doFetch(url, init);
409+
this.logger.emit('mongodb-oidc-plugin:outbound-http-request-completed', {
410+
url,
411+
status: response.status,
412+
statusText: response.statusText,
413+
});
414+
return response;
415+
} catch (err) {
416+
this.logger.emit('mongodb-oidc-plugin:outbound-http-request-failed', {
417+
url,
418+
error: errorString(err),
419+
});
420+
throw err;
421+
}
422+
};
423+
424+
private doFetch: CustomFetch = async (url, init) => {
406425
if (this.options.customFetch) {
407426
return await this.options.customFetch(url, init);
408427
}
@@ -1022,7 +1041,7 @@ export class MongoDBOIDCPluginImpl implements MongoDBOIDCPlugin {
10221041
authStateId: state.id,
10231042
error: errorString(err),
10241043
});
1025-
throw err;
1044+
throw await improveHTTPResponseBasedError(err);
10261045
} finally {
10271046
this.options.signal?.removeEventListener('abort', optionsAbortCb);
10281047
driverAbortSignal?.removeEventListener('abort', driverAbortCb);

src/types.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,15 @@ export interface MongoDBOIDCLogEventsMap {
127127
'mongodb-oidc-plugin:missing-id-token': () => void;
128128
'mongodb-oidc-plugin:outbound-http-request': (event: { url: string }) => void;
129129
'mongodb-oidc-plugin:inbound-http-request': (event: { url: string }) => void;
130+
'mongodb-oidc-plugin:outbound-http-request-failed': (event: {
131+
url: string;
132+
error: string;
133+
}) => void;
134+
'mongodb-oidc-plugin:outbound-http-request-completed': (event: {
135+
url: string;
136+
status: number;
137+
statusText: string;
138+
}) => void;
130139
'mongodb-oidc-plugin:received-server-params': (event: {
131140
params: OIDCCallbackParams;
132141
}) => void;

src/util.ts

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import type {
44
TokenEndpointResponse,
55
TokenEndpointResponseHelpers,
66
} from 'openid-client';
7-
import type { OIDCAbortSignal } from './types';
7+
import { MongoDBOIDCError, type OIDCAbortSignal } from './types';
88
import { createHash, randomBytes } from 'crypto';
99

1010
class AbortError extends Error {
@@ -235,3 +235,50 @@ export class TokenSet {
235235
.digest('hex');
236236
}
237237
}
238+
239+
// [email protected] has reduced error messages for HTTP errors significantly, reducing e.g.
240+
// an HTTP error to just a simple 'unexpect HTTP response status code' message, without
241+
// further diagnostic information. So if the `cause` of an `err` object is a fetch `Response`
242+
// object, we try to throw a more helpful error.
243+
export async function improveHTTPResponseBasedError<T>(
244+
err: T
245+
): Promise<T | MongoDBOIDCError> {
246+
if (
247+
err &&
248+
typeof err === 'object' &&
249+
'cause' in err &&
250+
err.cause &&
251+
typeof err.cause === 'object' &&
252+
'status' in err.cause &&
253+
'statusText' in err.cause &&
254+
'text' in err.cause &&
255+
typeof err.cause.text === 'function'
256+
) {
257+
try {
258+
let body = '';
259+
try {
260+
body = await err.cause.text();
261+
} catch {
262+
// ignore
263+
}
264+
let errorMessageFromBody = '';
265+
try {
266+
const parsed = JSON.parse(body);
267+
errorMessageFromBody =
268+
': ' + String(parsed.error_description || parsed.error || '');
269+
} catch {
270+
// ignore
271+
}
272+
if (!errorMessageFromBody) errorMessageFromBody = `: ${body}`;
273+
return new MongoDBOIDCError(
274+
`${errorString(err)}: caused by HTTP response ${String(
275+
err.cause.status
276+
)} (${String(err.cause.statusText)})${errorMessageFromBody}`,
277+
{ codeName: 'HTTPResponseError', cause: err }
278+
);
279+
} catch {
280+
return err;
281+
}
282+
}
283+
return err;
284+
}

0 commit comments

Comments
 (0)