UNPKG

node-hue-api

Version:
233 lines (196 loc) 5.87 kB
import fetch, { Response } from 'node-fetch'; import {URLSearchParams} from 'url'; import { Agent as HttpsAgent } from 'https'; import { Agent as HttpAgent } from 'http'; import HttpError from './HttpError'; export type HTTPHeaders = { [key: string]: string } type HttpClientFetchConfig = { headers?: HTTPHeaders, baseURL?: string, httpAgent?: HttpAgent, httpsAgent?: HttpsAgent, timeout?: number, } type AgentConfig = { agent?: HttpAgent | HttpsAgent, httpAgent?: HttpAgent, httpsAgent?: HttpsAgent } export type RequestConfig = { json?: boolean data?: object | string url: string headers?: HTTPHeaders method: string timeout?: number params?: { [key: string]: string } validateStatus?: Function, // transformResponse?: Function, } & AgentConfig type FetchRequestConfig = { method?: string, headers?: HTTPHeaders, timeout?: number, body?: any, agent?: HttpsAgent | HttpAgent, } export type FetchResult = { status: number, data?: any config?: { [key: string]: any }, headers?: HTTPHeaders } export class HttpClientFetch { private _config: HttpClientFetchConfig; constructor(config?: HttpClientFetchConfig) { this._config = config || {}; } get headers(): HTTPHeaders { return this._config.headers || {}; } get baseURL(): string | undefined { return this._config.baseURL; } get hasBaseUrl(): boolean { return !!this.baseURL; } getTimeout(timeout: number | undefined): number { if (timeout !== undefined) { return timeout; } return this._config?.timeout || 0; } refreshAuthorizationHeader(token: string) { if (!this._config.headers) { this._config.headers = {}; } this._config.headers['Authorization'] = `Bearer ${token}`; } getAgent(url: string, config: RequestConfig): HttpsAgent | HttpAgent | undefined { const specifiedAgent = config.agent || config.httpsAgent || config.httpAgent || undefined; if (specifiedAgent) { return specifiedAgent; } return this._config.httpsAgent || this._config.httpAgent || undefined; } getUrl(url: string): string { if (!this.hasBaseUrl) { return url; } else if (/^http/.test(url)) { return url; } let path; if (url && url[0] === '/') { path = url; } else { path = `/${url}`; } return `${this.baseURL}${path}`; } request(req: RequestConfig): Promise<FetchResult> { const isJson = req.json === true , hasData = !!req.data , url = this.getUrl(req.url) , headers = this.headers , config: FetchRequestConfig = { method: req.method, headers: headers, timeout: this.getTimeout(req.timeout), } ; // We are setting the timeout on the HTTP(s) agent, but node-fetch does not appear to be respecting this setting // from the agent, so taking to explicitly extracting the timeout from the agent and setting it on the API call // if a timeout is not specified as part of the request. if (isJson) { headers['Content-Type'] = 'application/json'; headers['Accept'] = 'application/json'; if (hasData) { config.body = JSON.stringify(req.data); } } else { if (hasData) { config.body = req.data; } } if (req.headers) { const requestHeaders = req.headers; Object.keys(requestHeaders).forEach(header => { headers[header] = requestHeaders[header]; }); } if (req.params) { config.body = new URLSearchParams(req.params); headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8'; } config.agent = this.getAgent(url, req); return fetch(url, config) .then((res: Response) => { if (req.validateStatus) { if (req.validateStatus(res.status)) { return res; } } else if (res.ok) { return res; } // Process the result and then return the error object return resolveBodyPromise(res) .then(data => { throw new HttpError(res.status, res.url, res.headers.raw(), data); }); }) .then((res: Response) => { const result: FetchResult = { status: res.status, }; if (res.headers) { // @ts-ignore result.headers = res.headers.raw(); } return resolveBodyPromise(res) .then(data => { result.data = data; return result; }); }); } } function resolveBodyPromise(res: Response) { // The local bridge connection with nginx in front of it does not return a content-length header, unlike the remote API // so we cannot gate on this and prevent calls to res.json() from errorring on an empty string. // // This means we need to get it back as text and process it accordingly. // let promise; // const contentLength: string = res.headers.get('content-length'); // if (contentLength && parseInt(contentLength) > 0) { // const contentType = res.headers.get('content-type'); // // if (contentType.startsWith('application/json')) { // promise = res.json(); // } else { // promise = res.text(); // } // } else { // promise = Promise.resolve(); // } // return promise; return res.text() .then((data: string) => { const contentType = res.headers.get('content-type'); if (contentType && contentType.startsWith('application/json')) { try { return JSON.parse(data) } catch (err) { return data; } } return data; }); } export function create(config?: HttpClientFetchConfig): HttpClientFetch { return new HttpClientFetch(config); } export function request(req: RequestConfig): Promise<FetchResult> { return new HttpClientFetch().request(req); }