Skip to content

Commit

Permalink
fix: node runtime json fetch handles connection errors properly
Browse files Browse the repository at this point in the history
  • Loading branch information
panva committed Feb 10, 2021
1 parent c037e59 commit fc584b2
Show file tree
Hide file tree
Showing 2 changed files with 73 additions and 25 deletions.
52 changes: 30 additions & 22 deletions src/runtime/node/fetch.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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<any>

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
46 changes: 43 additions & 3 deletions test/jwks/remote.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 = [
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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) => {
Expand Down

0 comments on commit fc584b2

Please sign in to comment.