Skip to main content
Module

x/ky/test/hooks.ts

🌳 Tiny & elegant JavaScript HTTP client based on the browser Fetch API
Go to Latest
File
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675
import test from 'ava';import delay from 'delay';import ky, {HTTPError} from '../source/index.js';import {Options} from '../source/types/options.js';import {createHttpTestServer} from './helpers/create-http-test-server.js';
test('hooks can be async', async t => { const server = await createHttpTestServer(); server.post('/', async (request, response) => { response.json(request.body); });
const json = { foo: true, };
const responseJson = await ky .post(server.url, { json, hooks: { beforeRequest: [ async (request, options) => { await delay(100); const bodyJson = JSON.parse(options.body as string); bodyJson.foo = false; return new Request(request, {body: JSON.stringify(bodyJson)}); }, ], }, }) .json<typeof json>();
t.false(responseJson.foo);
await server.close();});
test('hooks can be empty object', async t => { const expectedResponse = 'empty hook'; const server = await createHttpTestServer();
server.get('/', (_request, response) => { response.end(expectedResponse); });
const response = await ky.get(server.url, {hooks: {}}).text();
t.is(response, expectedResponse);
await server.close();});
test('beforeRequest hook allows modifications', async t => { const server = await createHttpTestServer(); server.post('/', async (request, response) => { response.json(request.body); });
const json = { foo: true, };
const responseJson = await ky .post(server.url, { json, hooks: { beforeRequest: [ (request, options) => { const bodyJson = JSON.parse(options.body as string); bodyJson.foo = false; return new Request(request, {body: JSON.stringify(bodyJson)}); }, ], }, }) .json<typeof json>();
t.false(responseJson.foo);
await server.close();});
test('afterResponse hook accept success response', async t => { const server = await createHttpTestServer(); server.post('/', async (request, response) => { response.json(request.body); });
const json = { foo: true, };
await t.notThrowsAsync( ky .post(server.url, { json, hooks: { afterResponse: [ async (_input, _options, response) => { t.is(response.status, 200); t.deepEqual(await response.json(), json); }, ], }, }) .json(), );
await server.close();});
test('afterResponse hook accept fail response', async t => { const server = await createHttpTestServer(); server.post('/', async (request, response) => { response.status(500).send(request.body); });
const json = { foo: true, };
await t.throwsAsync( ky .post(server.url, { json, hooks: { afterResponse: [ async (_input, _options, response) => { t.is(response.status, 500); t.deepEqual(await response.json(), json); }, ], }, }) .json(), );
await server.close();});
test('afterResponse hook can change response instance by sequence', async t => { const server = await createHttpTestServer(); server.post('/', (_request, response) => { response.status(500).send(); });
const modifiedBody1 = 'hello ky'; const modifiedStatus1 = 400; const modifiedBody2 = 'hello ky again'; const modifiedStatus2 = 200;
await t.notThrowsAsync(async () => { const responseBody = await ky .post(server.url, { hooks: { afterResponse: [ () => new Response(modifiedBody1, { status: modifiedStatus1, }), async (_input, _options, response) => { t.is(response.status, modifiedStatus1); t.is(await response.text(), modifiedBody1);
return new Response(modifiedBody2, { status: modifiedStatus2, }); }, ], }, }) .text();
t.is(responseBody, modifiedBody2); });
await server.close();});
test('afterResponse hook can throw error to reject the request promise', async t => { const server = await createHttpTestServer(); server.get('/', (_request, response) => { response.status(200).send(); });
const expectError = new Error('Error from `afterResponse` hook');
// Sync hook function await t.throwsAsync( ky .get(server.url, { hooks: { afterResponse: [ () => { throw expectError; }, ], }, }) .text(), { is: expectError, }, );
// Async hook function await t.throwsAsync( ky .get(server.url, { hooks: { afterResponse: [ async () => { throw expectError; }, ], }, }) .text(), { is: expectError, }, );
await server.close();});
test('`afterResponse` hook gets called even if using body shortcuts', async t => { const server = await createHttpTestServer(); server.get('/', (_request, response) => { response.json({}); });
let called = false; await ky .get(server.url, { hooks: { afterResponse: [ (_input, _options, response) => { called = true; return response; }, ], }, }) .json();
t.true(called);
await server.close();});
test('`afterResponse` hook is called with request, normalized options, and response which can be used to retry', async t => { const server = await createHttpTestServer(); server.post('/', async (request, response) => { const json = request.body; if (json.token === 'valid:token') { response.json(json); } else { response.sendStatus(403); } });
const json = { foo: true, token: 'invalid:token', };
t.deepEqual( await ky .post(server.url, { json, hooks: { afterResponse: [ async (request, options, response) => { if (response.status === 403) { // Retry request with valid token return ky(request, { ...options, json: { ...(options as Options).json as Record<string, unknown>, token: 'valid:token', }, }); }
return undefined; }, ], }, }) .json(), { foo: true, token: 'valid:token', }, );
await server.close();});
test('afterResponse hook with parseJson and response.json()', async t => { t.plan(5);
const server = await createHttpTestServer(); server.get('/', async (_request, response) => { response.end('text'); });
const json = await ky .get(server.url, { parseJson(text) { t.is(text, 'text'); return {awesome: true}; }, hooks: { afterResponse: [ async (_request, _options, response) => { t.true(response instanceof Response); t.deepEqual(await response.json(), {awesome: true}); }, ], }, }) .json();
t.deepEqual(json, {awesome: true});
await server.close();});
test('beforeRetry hook is never called for the initial request', async t => { const fixture = 'fixture'; const server = await createHttpTestServer(); server.get('/', async (request, response) => { response.end(request.headers['unicorn']); });
t.not( await ky .get(server.url, { hooks: { beforeRetry: [ ({options}) => { (options.headers as Headers | undefined)?.set('unicorn', fixture); }, ], }, }) .text(), fixture, );
await server.close();});
test('beforeRetry hook allows modifications of non initial requests', async t => { let requestCount = 0;
const fixture = 'fixture'; const server = await createHttpTestServer(); server.get('/', async (request, response) => { requestCount++;
if (requestCount > 1) { response.end(request.headers['unicorn']); } else { response.sendStatus(408); } });
t.is( await ky .get(server.url, { hooks: { beforeRetry: [ ({request}) => { request.headers.set('unicorn', fixture); }, ], }, }) .text(), fixture, );
await server.close();});
test('beforeRetry hook is called with error and retryCount', async t => { let requestCount = 0;
const server = await createHttpTestServer(); server.get('/', async (request, response) => { requestCount++;
if (requestCount > 1) { response.end(request.headers['unicorn']); } else { response.sendStatus(408); } });
await ky.get(server.url, { hooks: { beforeRetry: [ ({error, retryCount}) => { t.true(error instanceof HTTPError); t.true(retryCount >= 1); }, ], }, });
await server.close();});
test('beforeRetry hook is called even if the error has no response', async t => { t.plan(6);
let requestCount = 0;
const server = await createHttpTestServer(); server.get('/', async (_request, response) => { requestCount++; response.end('unicorn'); });
const text = await ky .get(server.url, { retry: 2, async fetch(request) { if (requestCount === 0) { requestCount++; throw new Error('simulated network failure'); }
return globalThis.fetch(request); }, hooks: { beforeRetry: [ ({error, retryCount}) => { t.is(error.message, 'simulated network failure'); // @ts-expect-error t.is(error.response, undefined); t.is(retryCount, 1); t.is(requestCount, 1); }, ], }, }) .text();
t.is(text, 'unicorn'); t.is(requestCount, 2);
await server.close();});
test('beforeRetry hook with parseJson and error.response.json()', async t => { t.plan(10);
let requestCount = 0;
const server = await createHttpTestServer(); server.get('/', async (_request, response) => { requestCount++; if (requestCount === 1) { response.status(502).end('text'); } else { response.end('text'); } });
const json = await ky .get(server.url, { retry: 2, parseJson(text) { t.is(text, 'text'); return {awesome: true}; }, hooks: { beforeRetry: [ async ({error, retryCount}) => { t.true(error instanceof HTTPError); t.is(error.message, 'Request failed with status code 502 Bad Gateway'); t.true((error as HTTPError).response instanceof Response); t.deepEqual(await (error as HTTPError).response.json(), {awesome: true}); t.is(retryCount, 1); t.is(requestCount, 1); }, ], }, }) .json();
t.deepEqual(json, {awesome: true}); t.is(requestCount, 2);
await server.close();});
test('beforeRetry hook can cancel retries by returning `stop`', async t => { let requestCount = 0;
const server = await createHttpTestServer(); server.get('/', async (request, response) => { requestCount++;
if (requestCount > 2) { response.end(request.headers['unicorn']); } else { response.sendStatus(408); } });
await ky.get(server.url, { hooks: { beforeRetry: [ ({error, retryCount}) => { t.truthy(error); t.is(retryCount, 1);
return ky.stop; }, ], }, });
t.is(requestCount, 1);
await server.close();});
test('catches beforeRetry thrown errors', async t => { let requestCount = 0;
const server = await createHttpTestServer(); server.get('/', async (request, response) => { requestCount++;
if (requestCount > 1) { response.end(request.headers['unicorn']); } else { response.sendStatus(408); } });
const errorString = 'oops'; const error = new Error(errorString);
await t.throwsAsync( ky.get(server.url, { hooks: { beforeRetry: [ () => { throw error; }, ], }, }), {message: errorString}, );});
test('catches beforeRetry promise rejections', async t => { let requestCount = 0;
const server = await createHttpTestServer(); server.get('/', async (request, response) => { requestCount++;
if (requestCount > 1) { response.end(request.headers['unicorn']); } else { response.sendStatus(408); } });
const errorString = 'oops'; const error = new Error(errorString);
await t.throwsAsync( ky.get(server.url, { hooks: { beforeRetry: [ async () => { throw error; }, ], }, }), {message: errorString}, );});
test('hooks beforeRequest returning Response skips HTTP Request', async t => { const expectedResponse = 'empty hook';
const response = await ky .get('server.url', { hooks: { beforeRequest: [() => new Response(expectedResponse, {status: 200, statusText: 'OK'})], }, }) .text();
t.is(response, expectedResponse);});
test('runs beforeError before throwing HTTPError', async t => { const server = await createHttpTestServer(); server.post('/', (_request, response) => { response.status(500).send(); });
await t.throwsAsync( ky.post(server.url, { hooks: { beforeError: [ (error: HTTPError) => { const {response} = error;
if (response?.body) { error.name = 'GitHubError'; error.message = `${response.statusText} --- (${response.status})`.trim(); }
return error; }, ], }, }), { name: 'GitHubError', message: 'Internal Server Error --- (500)', }, );
await server.close();});
test('beforeError can return promise which resolves to HTTPError', async t => { const server = await createHttpTestServer(); const responseBody = {reason: 'github down'}; server.post('/', (_request, response) => { response.status(500).send(responseBody); });
await t.throwsAsync( ky.post(server.url, { hooks: { beforeError: [ async (error: HTTPError) => { const {response} = error; const body = await response.json() as {reason: string};
if (response?.body) { error.name = 'GitHubError'; error.message = `${body.reason} --- (${response.status})`.trim(); }
return error; }, ], }, }), { name: 'GitHubError', message: `${responseBody.reason} --- (500)`, }, );
await server.close();});