/** * dnetwork.ts - Network Utility Functions for HTTP Requests * * Provides a set of async functions for GET/POST requests using axios, * with integrated logging, assertion, and error handling. * - All requests log activity via dlog * - All responses are checked for HTTP success * - JSON responses are parsed and returned as objects * - SimpleStore helpers for common API patterns * - Detailed error handling for network issues */ import axios, { AxiosError, AxiosRequestConfig } from "axios"; import { dassert, DError } from "./dassert"; import { dlog } from "./dlog"; import { TObject } from "./dtypes"; let count = 0; // Custom error class for network-related errors export class NetworkError extends DError { data?: T; statusCode?: number; constructor( message: string, code: string = "NETWORK_ERROR", data?: T, statusCode?: number, ) { super(message, code, { data, statusCode }); this.name = "NetworkError"; this.data = data; this.statusCode = statusCode; Object.setPrototypeOf(this, new.target.prototype); } } export namespace dnetwork { /** * Helper function to categorize and handle axios errors */ function handleAxiosError(error: AxiosError, url: string): never { let code = "NETWORK_ERROR"; let message = "Unknown network error"; if (error.code === "ECONNABORTED") { code = "TIMEOUT_ERROR"; message = `Request timeout. The server took too long to respond.`; } else if (error.code === "ENOTFOUND" || error.code === "ECONNREFUSED") { code = "SERVER_UNREACHABLE"; message = `Server unreachable. Check your internet connection or DNS settings.`; } else if (error.code === "ENETUNREACH") { code = "NO_INTERNET"; message = `No internet connection. Unable to reach ${url}.`; } else if (error.response && error.response.status >= 500) { code = "SERVER_ERROR_5XX"; message = `Server error (${error.response.status}) : ${error.response.statusText}`; } else if (error.response && error.response.status === 404) { code = "NOT_FOUND"; message = `Resource not found (404) `; } else if (error.response && error.response.status === 403) { code = "FORBIDDEN"; message = `Access forbidden (403) `; } else if (error.message === "Network Error") { code = "NETWORK_UNREACHABLE"; message = `Network is unreachable `; } else { message = error.message || "Unknown error occurred"; } dlog.e(`[Network Error] ${message}`, { url, code, status: error.response?.status, }); throw new NetworkError( message, code, error.response?.data, error.response?.status, ); } /** * Perform a GET request and return the response data. */ export async function get( url: string, config?: AxiosRequestConfig, ): Promise { try { const result = await axios.get(url, config); LogNetwork(url, null, result, null); dassert.verifyOrThrow( result.status >= 200 && result.status < 300, `GET request to URL ${url} failed with status ${result.status}`, "HTTP_STATUS_ERROR", ); return result.data; } catch (error) { LogNetwork(url, null, null, error); if (axios.isAxiosError(error)) { handleAxiosError(error, url); } throw error; } } /** * Perform a GET request and return the response as a JSON object. */ export async function getJson( url: string, config?: AxiosRequestConfig, ): Promise { try { const res = await get(url, config); LogNetwork(url, null, res, null); if (typeof res === "object" && res !== null) { return res; } try { return JSON.parse(res); } catch { throw new NetworkError( "Response is not valid JSON", "JSON_PARSE_ERROR", res, ); } } catch (error) { LogNetwork(url, null, null, error); if (axios.isAxiosError(error)) { handleAxiosError(error, url); } throw error; } } /** * Perform a POST request and return the response data. */ export async function post( url: string, data: any, config?: AxiosRequestConfig, ): Promise { try { const result = await axios.post(url, data, config); LogNetwork(url, data, result, null); dassert.verifyOrThrow( result.status >= 200 && result.status < 300, `POST request to URL ${url} failed with status ${result.status}`, "HTTP_STATUS_ERROR", ); return result.data; } catch (error) { LogNetwork(url, data, null, error); if (axios.isAxiosError(error)) { handleAxiosError(error, url); } throw error; } } /** * Perform a POST request and return the response as a JSON object. */ export async function postJson( url: string, data: TObject, config?: AxiosRequestConfig, ): Promise { try { const res = await post(url, data, config); if (typeof res === "object" && res !== null) { LogNetwork(url, data, res, null); return res; } try { let output = JSON.parse(res); LogNetwork(url, data, output, null); return output; } catch { LogNetwork(url, data, res, "Response is not valid JSON"); throw new NetworkError( "Response is not valid JSON", "JSON_PARSE_ERROR", res, ); } } catch (error) { LogNetwork(url, data, null, error); if (axios.isAxiosError(error)) { handleAxiosError(error, url); } throw error; } } /** * GET request to a SimpleStore endpoint, expects {status: "success", ...} */ export async function getSimpleStore( url: string, config?: AxiosRequestConfig, ): Promise { try { const response = await axios.get(url, config); const jsondata = response.data; LogNetwork(url, null, jsondata, null); if (jsondata.status === "success") { return jsondata; } else { throw new NetworkError( jsondata.msg || "SimpleStore request failed", "SIMPLESTORE_ERROR", jsondata, ); } } catch (error) { LogNetwork(url, null, null, error); if (axios.isAxiosError(error)) { handleAxiosError(error, url); } throw error; } } /** * POST request to a SimpleStore endpoint, expects {status: "success", ...} */ export async function postSimpleStore( url: string, data: TObject, config?: AxiosRequestConfig, ): Promise { try { const response = await axios.post(url, data, config); const jsondata = response.data; LogNetwork(url, data, jsondata, null); if (jsondata.status === "success") { return jsondata; } else { throw new NetworkError( jsondata.msg || "SimpleStore request failed", "SIMPLESTORE_ERROR", jsondata, ); } } catch (error) { LogNetwork(url, data, null, error); if (axios.isAxiosError(error)) { handleAxiosError(error, url); } throw error; } } /** * Perform a GET request with a browser-like user-agent header. */ export async function getAsFakeBrowser( url: string, config?: AxiosRequestConfig, ): Promise { try { const headers = { "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.193 Safari/537.36", ...(config?.headers || {}), }; const resp = await axios.get(url, { ...config, headers }); return resp.data; } catch (error) { if (axios.isAxiosError(error)) { handleAxiosError(error, url); } throw error; } } function LogNetwork(url: any, req: any, resp: any, error: any) { count++; dlog.d(`=================== NETWORK REQUEST: ${count} ====================== URL:${url} REQUEST: ${convert(req)} RESP:${convert(resp)} Error:${convert(error)} ========================================================== `); } } function convert(a: any): string { if (a === null) return "null"; if (a === undefined) return "undefined"; if (typeof a === "string") return a; if ( typeof a === "number" || typeof a === "boolean" || typeof a === "bigint" ) { return a.toString(); } if (typeof a === "function") { return a.name ? `[Function: ${a.name}]` : "[Function]"; } if (a instanceof Date) { return a.toISOString(); } if (Array.isArray(a)) { return `[${a.map(convert).join(", ")}]`; } if (typeof a === "object") { try { return JSON.stringify(a); } catch { return "[Object]"; } } return String(a); }