diff --git a/src/runtime/node/fetch.ts b/src/runtime/node/fetch.ts index f961c6f347..cbf2862567 100644 --- a/src/runtime/node/fetch.ts +++ b/src/runtime/node/fetch.ts @@ -1,5 +1,8 @@ -import { get as http, ClientRequest } from 'http' -import { get as https, RequestOptions } from 'https' +import { get as http } from 'http' +import { get as https } from 'https' +import { once } from 'events' +import type { ClientRequest, IncomingMessage } from 'http' +import type { RequestOptions } from 'https' import type { FetchFunction } from '../interfaces.d' import { JOSEError } from '../../util/errors.js' @@ -16,26 +19,31 @@ const fetch: FetchFunction = async (url: URL, timeout: number, options: Accepted if (protocols[url.protocol] === undefined) { throw new TypeError('Unsupported URL protocol.') } - return new Promise((resolve, reject) => { - const { agent } = options - protocols[url.protocol](url.href, { agent, timeout }, async (response) => { - if (response.statusCode !== 200) { - reject(new JOSEError('Expected 200 OK from the JSON Web Key Set HTTP response')) - } else { - const parts = [] - // eslint-disable-next-line no-restricted-syntax - for await (const part of response) { - parts.push(part) - } - - try { - resolve(JSON.parse(decoder.decode(concat(...parts)))) - } catch (err) { - reject(new JOSEError('Failed to parse the JSON Web Key Set HTTP response as JSON')) - } - } - }).on('error', reject) - }) as Promise + + const { agent } = options + const req = protocols[url.protocol](url.href, { + agent, + timeout, + }) + + // eslint-disable-next-line @typescript-eslint/keyword-spacing + const [response] = <[IncomingMessage]>await once(req, 'response') + + if (response.statusCode !== 200) { + throw new JOSEError('Expected 200 OK from the JSON Web Key Set HTTP response') + } + + const parts = [] + // eslint-disable-next-line no-restricted-syntax + for await (const part of response) { + parts.push(part) + } + + try { + return JSON.parse(decoder.decode(concat(...parts))) + } catch (err) { + throw new JOSEError('Failed to parse the JSON Web Key Set HTTP response as JSON') + } } export default fetch diff --git a/test/jwks/remote.test.mjs b/test/jwks/remote.test.mjs index 7451d4a45c..8c7e233bd3 100644 --- a/test/jwks/remote.test.mjs +++ b/test/jwks/remote.test.mjs @@ -2,6 +2,8 @@ import test from 'ava'; import nock from 'nock'; import timekeeper from 'timekeeper'; +import { createServer } from 'http'; +import { once } from 'events'; let root; let keyRoot; @@ -29,16 +31,25 @@ Promise.all([ ]) => { const now = 1604416038; - test.beforeEach(() => { - timekeeper.freeze(now * 1000); + test.before(async (t) => { + nock.disableNetConnect(); + t.context.server = createServer().listen(3000); + t.context.server.removeAllListeners('request'); + await once(t.context.server, 'listening'); + }); + + test.after(async (t) => { + nock.enableNetConnect(); + await new Promise((resolve) => t.context.server.close(resolve)); }); test.afterEach((t) => { + t.context.server.removeAllListeners('request'); t.true(nock.isDone()); nock.cleanAll(); }); - test.afterEach(timekeeper.reset); + test.afterEach(() => timekeeper.reset()); test.serial('RemoteJWKSet', async (t) => { const keys = [ @@ -174,6 +185,7 @@ Promise.all([ }); test.serial('refreshes the JWKS once off cooldown', async (t) => { + timekeeper.freeze(now * 1000); let jwk = { crv: 'P-256', x: 'fqCXPnWs3sSfwztvwYU9SthmRdoT4WCXxS8eD8icF6U', @@ -263,6 +275,34 @@ Promise.all([ message: 'Failed to parse the JSON Web Key Set HTTP response as JSON', }); }); + + test.serial('handles ENOTFOUND', async (t) => { + nock.enableNetConnect(); + const url = new URL('https://op.example.com/jwks'); + const JWKS = createRemoteJWKSet(url); + await t.throwsAsync(JWKS({ alg: 'RS256' }), { + code: 'ENOTFOUND', + }); + }); + + test.serial('handles ECONNREFUSED', async (t) => { + const url = new URL('http://localhost:3001/jwks'); + const JWKS = createRemoteJWKSet(url); + await t.throwsAsync(JWKS({ alg: 'RS256' }), { + code: 'ECONNREFUSED', + }); + }); + + test.serial('handles ECONNRESET', async (t) => { + const url = new URL('http://localhost:3000/jwks'); + t.context.server.once('connection', (socket) => { + socket.destroy(); + }); + const JWKS = createRemoteJWKSet(url); + await t.throwsAsync(JWKS({ alg: 'RS256' }), { + code: 'ECONNRESET', + }); + }); }, (err) => { test.serial('failed to import', (t) => {