diff --git a/src/core/server/services/axios.ts/axios.ts b/src/core/server/services/axios.ts/axios.ts new file mode 100644 index 000000000..a87a4e317 --- /dev/null +++ b/src/core/server/services/axios.ts/axios.ts @@ -0,0 +1,79 @@ +import axios, { Method } from "axios"; +import http from "http"; +import https from "https"; +import { capitalize } from "lodash"; + +import { version } from "coral-common/version"; + +export type Axios = ( + url: string, + options?: AxiosOptions, + timeout?: number +) => Promise; + +export interface AxiosResponse { + ok: boolean; + status: number; + data: any; +} + +export interface CreateAxiosOptions { + /** + * name is the string that is attached to the `User-Agent` header as: + * + * `Coral ${name}/${version}` + */ + name: string; +} + +export interface AxiosOptions { + method: Method; + headers: any; + body: any; +} + +export const createAxios = ({ name }: CreateAxiosOptions): Axios => { + // defaultHeaders are the headers attached to each request (unless they are + // overridden). + const defaultHeaders = { + "User-Agent": `Coral ${capitalize(name)}/${version}`, + }; + + // Create HTTP agents to improve connection performance. + const agents = { + https: new https.Agent({ + keepAlive: true, + }), + http: new http.Agent({ + keepAlive: true, + }), + }; + + return async ( + url: string, + { method = "GET", headers = {}, body = {} }: AxiosOptions, + timeout?: number + ) => { + const response = await axios.request({ + method, + url, + headers: { + ...defaultHeaders, + ...headers, + }, + data: body, + timeout, + timeoutErrorMessage: "axios request timed out", + httpAgent: agents.http, + httpsAgent: agents.https, + }); + + const ok = response.status >= 200 || response.status <= 299; + + return { + ok, + status: response.status, + data: response.data, + }; + }; +}; diff --git a/src/core/server/services/axios.ts/index.ts b/src/core/server/services/axios.ts/index.ts new file mode 100644 index 000000000..07041166c --- /dev/null +++ b/src/core/server/services/axios.ts/index.ts @@ -0,0 +1 @@ +export * from "./axios"; diff --git a/src/core/server/services/perspective/perspective.ts b/src/core/server/services/perspective/perspective.ts index 70750ba76..f0f8efc03 100644 --- a/src/core/server/services/perspective/perspective.ts +++ b/src/core/server/services/perspective/perspective.ts @@ -1,11 +1,15 @@ -import axios from "axios"; import path from "path"; import { URL } from "url"; import { TOXICITY_ENDPOINT_DEFAULT } from "coral-common/constants"; import { LanguageCode } from "coral-common/helpers"; +import logger from "coral-server/logger"; import { getURLWithCommentID } from "coral-server/models/story"; +import { createAxios } from "../axios.ts"; + +const axios = createAxios({ name: "perspective" }); + /** * Language is the language key that is supported by the Perspective API in the * ISO 631-1 format. @@ -135,28 +139,33 @@ export async function sendToPerspective( try { // Create the request and send it. - const res = await axios.post(url.toString(), body, { - headers: { - "Content-Type": "application/json", + + const res = await axios( + url.toString(), + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body, }, - timeout, - }); + timeout + ); // Non-successful response - if (res.status < 200 || res.status > 299) { + if (!res.ok) { return { - ok: false, + ok: res.ok, status: res.status, data: null, }; } // Parse the JSON body and send back the result! - const data = res.data; return { ok: true, status: res.status, - data, + data: res.data, }; } catch (err) { // Ensure that the API key doesn't get leaked to the logs by accident. @@ -167,6 +176,10 @@ export async function sendToPerspective( ); } + if (err.message && err.message.startsWith("axios request timed out")) { + logger.error("perspective request timed out"); + } + // Rethrow the error. throw err; }