UNPKG

@ministryofjustice/hmpps-digital-prison-reporting-frontend

Version:

The Digital Prison Reporting Frontend contains templates and code to help display data effectively in UI applications.

184 lines (181 loc) 7 kB
import superagent from 'superagent'; import Agent, { HttpsAgent } from 'agentkeepalive'; import http from 'http'; import https from 'https'; import { URL } from 'url'; import { pipeline } from 'stream'; import logger from '../utils/logger.js'; import sanitise from '../utils/sanitisedError.js'; class RestClient { name; config; agent; constructor(name, config) { this.name = name; this.config = config; this.agent = config.url.startsWith('https') ? new HttpsAgent(config.agent) : new Agent(config.agent); } apiUrl() { return this.config.url; } timeoutConfig() { return this.config.agent.timeout; } async get(request) { return this.getWithHeaders(request).then(result => result.data); } async getStream({ path = '', query = {}, headers = {}, token }, res) { const url = new URL(`${this.apiUrl()}${path}`); Object.entries(query).forEach(([key, value]) => { if (value === undefined) return; if (Array.isArray(value)) { value.forEach(v => url.searchParams.append(key, String(v))); } else { url.searchParams.append(key, String(value)); } }); const client = url.protocol === 'https:' ? https : http; const req = client.request(url, { method: 'GET', headers: { ...headers, Authorization: token ? `Bearer ${token}` : undefined, }, agent: this.agent, }, upstream => { // Forward status res.status(upstream.statusCode || 500); // Forward headers Object.entries(upstream.headers).forEach(([key, value]) => { if (value !== undefined) { res.setHeader(key, value); } }); res.flushHeaders(); res.on('close', () => { req.destroy(); }); pipeline(upstream, res, err => { if (err) { res.destroy(err); } }); }); req.setTimeout(this.timeoutConfig(), () => { req.destroy(new Error('Upstream request timed out.')); }); req.on('error', err => { logger.warn({ err }, `Error streaming from ${this.name}, path: '${path}'`); if (!res.headersSent) { res.status(502).end('Download request failed'); } else { res.destroy(err); } }); req.end(); } async requestWithBody(method, { path, query = {}, headers = {}, responseType = '', data = {}, raw = false, retry = false }, token) { logger.info(`${this.name} ${method.toUpperCase()}: ${path}`); logger.info(`info about request: ${method} | ${path} | ${JSON.stringify(data)} | ${JSON.stringify(query)}`); try { const result = await superagent[method](`${this.apiUrl()}${path}`) .query(query) .send(data) .agent(this.agent) .retry(2, err => { if (retry === false) { return false; } if (err) logger.info(`Retry handler found API error with ${err.code} ${err.message}`); return undefined; // retry handler only for logging retries, not to influence retry logic }) .auth(token, { type: 'bearer' }) .set(headers) .responseType(responseType) .timeout(this.timeoutConfig()); return raw ? result : result.body; } catch (error) { if (!(error instanceof Error)) { throw error; } const sanitisedError = sanitise(error); logger.warn({ ...sanitisedError }, `Error calling ${this.name}, path: '${path}', verb: '${method.toUpperCase()}'`); throw sanitisedError; } } async post(request, token) { return this.requestWithBody('post', request, token); } async getWithHeaders({ path = '', query = {}, headers = {}, responseType = '', raw = false, token, }) { const loggerData = { path: `${this.config.url}${path}`, ...(Object.keys(query).length && { query }), }; logger.info(`${this.name}: ${JSON.stringify(loggerData)}`); try { const result = await superagent .get(`${this.apiUrl()}${path}`) .agent(this.agent) .retry(2, err => { if (err) logger.info(`Retry handler found API error with ${err.code} ${err.message}`); return undefined; // retry handler only for logging retries, not to influence retry logic }) .query(query) .auth(token, { type: 'bearer' }) .set(headers) .responseType(responseType) .timeout(this.timeoutConfig()); return { data: raw ? result : result.body, headers: result.headers, }; } catch (error) { const sanitisedError = sanitise(error); logger.warn({ ...sanitisedError, query }, `Error calling ${this.name}, path: '${path}', verb: 'GET'`); throw sanitisedError; } } async deleteWithHeaders({ path = '', query = {}, headers = {}, responseType = '', raw = false, token, }) { const loggerData = { path: `${this.config.url}${path}`, query, }; logger.info(`${this.name}: ${JSON.stringify(loggerData)}`); try { const result = await superagent .delete(`${this.apiUrl()}${path}`) .agent(this.agent) .retry(2, err => { if (err) logger.info(`Retry handler found API error with ${err.code} ${err.message}`); return undefined; // retry handler only for logging retries, not to influence retry logic }) .query(query) .auth(token, { type: 'bearer' }) .set(headers) .responseType(responseType) .timeout(this.timeoutConfig()); return { data: raw ? result : result.body, headers: result.headers, }; } catch (error) { const sanitisedError = sanitise(error); logger.warn({ ...sanitisedError, query }, `Error calling ${this.name}, path: '${path}', verb: 'GET'`); throw sanitisedError; } } async delete(request) { return this.deleteWithHeaders(request).then(result => result.data); } } export { RestClient, RestClient as default }; //# sourceMappingURL=restClient.js.map