@vechain/sdk-network
Version:
This module serves as the standard interface connecting decentralized applications (dApps) and users to the VeChainThor blockchain
249 lines (222 loc) • 9.13 kB
text/typescript
import { HttpMethod } from './HttpMethod';
import { InvalidHTTPParams, InvalidHTTPRequest } from '@vechain/sdk-errors';
import { type HttpClient } from './HttpClient';
import { type HttpParams } from './HttpParams';
import { logRequest, logResponse, logError } from './trace-logger';
/**
* This class implements the HttpClient interface using the Fetch API.
*
* The SimpleHttpClient allows making {@link HttpMethod} requests with timeout
* and base URL configuration.
*/
class SimpleHttpClient implements HttpClient {
/**
* Represent the default timeout duration for network requests in milliseconds.
*/
public static readonly DEFAULT_TIMEOUT = 10000;
/**
* Return the root URL for the API endpoints.
*/
public readonly baseURL: string;
public readonly headers: HeadersInit;
/**
* Return the amount of time in milliseconds before a timeout occurs
* when requesting with HTTP methods.
*/
public readonly timeout: number;
/**
* Constructs an instance of SimpleHttpClient with the given base URL,
* timeout period and HTTP headers.
* The HTTP headers are used each time this client send a request to the URL,
* if not overwritten by the {@link HttpParams} of the method sending the request.
*
* @param {string} baseURL - The base URL for HTTP requests.
* @param {HeadersInit} [headers=new Headers()] - The default headers for HTTP requests.
* @param {number} [timeout=SimpleHttpClient.DEFAULT_TIMEOUT] - The timeout duration in milliseconds.
*/
constructor(
baseURL: string,
headers: HeadersInit = new Headers(),
timeout: number = SimpleHttpClient.DEFAULT_TIMEOUT
) {
this.baseURL = baseURL;
this.timeout = timeout;
this.headers = headers;
}
/**
* Sends an HTTP GET request to the specified path with optional query parameters.
*
* @param {string} path - The endpoint path to which the HTTP GET request is sent.
* @param {HttpParams} [params] - Optional parameters for the request,
* including query parameters, headers, body, and response validation.
* {@link HttpParams.headers} override {@link SimpleHttpClient.headers}.
* @return {Promise<unknown>} A promise that resolves with the response of the GET request.
*/
public async get(path: string, params?: HttpParams): Promise<unknown> {
return await this.http(HttpMethod.GET, path, params);
}
/**
* Determines if specified url is valid
* @param {string} url Url to check
* @returns {boolean} if value
*/
private isValidUrl(url: string): boolean {
try {
new URL(url);
return true;
} catch {
return false;
}
}
/**
* Executes an HTTP request with the specified method, path, and optional parameters.
*
* @param {HttpMethod} method - The HTTP method to use for the request (e.g., GET, POST).
* @param {string} path - The URL path for the request. Leading slashes will be automatically removed.
* @param {HttpParams} [params] - Optional parameters for the request,
* including query parameters, headers, body, and response validation.
* {@link HttpParams.headers} override {@link SimpleHttpClient.headers}.
* @return {Promise<unknown>} A promise that resolves to the response of the HTTP request.
* @throws {InvalidHTTPRequest} Throws an error if the HTTP request fails.
*/
public async http(
method: HttpMethod,
path: string,
params?: HttpParams
): Promise<unknown> {
const controller = new AbortController();
const timeoutId = setTimeout(() => {
controller.abort();
}, this.timeout);
let url: URL | undefined;
let requestStartTime = Date.now(); // Initialize with current timestamp
let headerObj: Record<string, string> = {};
try {
// Remove leading slash from path
if (path.startsWith('/')) {
path = path.slice(1);
}
// Add trailing slash from baseURL if not present
let baseURL = this.baseURL;
if (!baseURL.endsWith('/')) {
baseURL += '/';
}
// Check if path is already a fully qualified URL
if (/^https?:\/\//.exec(path)) {
url = new URL(path);
} else {
url = new URL(path, baseURL);
}
if (params?.query && url !== undefined) {
Object.entries(params.query).forEach(([key, value]) => {
(url as URL).searchParams.append(key, String(value));
});
}
if (
params?.query !== undefined &&
params?.query != null &&
url !== undefined
) {
Object.entries(params.query).forEach(([key, value]) => {
(url as URL).searchParams.append(key, String(value));
});
}
const headers = new Headers(this.headers);
if (params?.headers !== undefined && params?.headers != null) {
Object.entries(params.headers).forEach(([key, value]) => {
headers.append(key, String(value));
});
}
// Convert Headers to plain object for logging
headerObj = Object.fromEntries(headers.entries());
// Log the request
requestStartTime = logRequest(
method,
url.toString(),
headerObj,
method !== HttpMethod.GET ? params?.body : undefined
);
// Send request
const response = await fetch(url.toString(), {
method,
headers,
body:
method !== HttpMethod.GET
? JSON.stringify(params?.body)
: undefined,
signal: controller.signal
});
const responseHeaders = Object.fromEntries(
response.headers.entries()
);
if (response.ok) {
if (
params?.validateResponseHeader != null &&
responseHeaders != null
) {
params.validateResponseHeader(responseHeaders);
}
// Parse response body
// Using explicit type annotation to handle the 'any' returned by response.json()
const responseBody: unknown = await response.json();
// Log the successful response
logResponse(
requestStartTime,
url.toString(),
responseHeaders,
responseBody
);
// Return the responseBody as unknown rather than 'any'
return responseBody;
}
throw new Error(`HTTP ${response.status} ${response.statusText}`, {
cause: response
});
} catch (error) {
// Different error handling based on whether it's a params error or request error
if (url) {
// Log the error if url is defined (request was started)
const urlString = url.toString();
logError(requestStartTime, urlString, method, error);
throw new InvalidHTTPRequest(
'HttpClient.http()',
(error as Error).message,
{
method,
url: urlString
},
error
);
} else {
// Parameter error before request was even started
const fallbackUrl = !this.isValidUrl(this.baseURL)
? path
: new URL(path, this.baseURL).toString();
throw new InvalidHTTPParams(
'HttpClient.http()',
(error as Error).message,
{
method,
url: fallbackUrl
},
error
);
}
} finally {
clearTimeout(timeoutId);
}
}
/**
* Makes an HTTP POST request to the specified path with optional parameters.
*
* @param {string} path - The endpoint to which the POST request is made.
* @param {HttpParams} [params] - Optional parameters for the request,
* including query parameters, headers, body, and response validation.
* {@link HttpParams.headers} override {@link SimpleHttpClient.headers}.
* @return {Promise<unknown>} A promise that resolves with the response from the server.
*/
public async post(path: string, params?: HttpParams): Promise<unknown> {
return await this.http(HttpMethod.POST, path, params);
}
}
export { SimpleHttpClient };