Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions packages/sdk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,13 +74,13 @@
"ws": "^8.18.0"
},
"devDependencies": {
"@types/ws": "^8.5.13",
"@vechain/sdk-solo-setup": "3.0.0-beta.1",
"bignumber.js": "^9.1.2",
"jest-fetch-mock": "^3.0.3",
"loglevel": "^1.9.2",
"thor-devkit": "^2.0.9",
"tsd": "^0.31.2",
"@types/ws": "^8.5.13",
"loglevel": "^1.9.2",
"jest-fetch-mock": "^3.0.3",
"whatwg-fetch": "^3.6.20",
"@vechain/sdk-solo-setup": "3.0.0-beta.1"
"whatwg-fetch": "^3.6.20"
}
}
119 changes: 119 additions & 0 deletions packages/sdk/src/common/http/FetchHttpClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { type HttpQuery, type HttpPath, type HttpClient } from '@common/http';
import { type HttpOptions } from './HttpOptions';
import { CookieStore } from './CookieStore';
import { log, type LogItemWithVerbosity } from '@common/logging';
import { HttpException } from './HttpException';
import { HttpNetworkException } from './HttpNetworkException';

const FQP = 'packages/sdk/src/common/http/FetchHttpClient.ts';

Expand Down Expand Up @@ -156,6 +158,8 @@ class FetchHttpClient implements HttpClient {
* @param httpPath - The path of the request.
* @param httpQuery - The query parameters of the request.
* @returns The response from the request.
* @throws HttpException for non-200 responses
* @throws HttpNetworkException for network issues
*/
async get(
httpPath: HttpPath = { path: '' },
Expand All @@ -178,10 +182,65 @@ class FetchHttpClient implements HttpClient {
this.options.onRequest?.(request) ?? request
);
await this.logResponse(request, response);

// Check for non-200 responses and raise HttpException
if (!response.ok) {
const responseBody = await response.text();
throw new HttpException(
`${FQP}get(httpPath: HttpPath, httpQuery: HttpQuery): Promise<Response>`,
`HTTP request failed with status ${response.status}`,
response.status,
response.statusText,
responseBody,
response.url,
{
method: 'GET',
path: httpPath.path,
query: httpQuery.query
}
);
}

return (
this.options.onResponse?.(this.processResponse(response)) ??
response
);
} catch (error) {
// Handle network errors and timeouts
if (error instanceof HttpException) {
throw error; // Re-throw HTTP exceptions
}

// Handle abort signal (timeout)
if (error instanceof Error && error.name === 'AbortError') {
throw new HttpNetworkException(
`${FQP}get(httpPath: HttpPath, httpQuery: HttpQuery): Promise<Response>`,
'Request timed out',
'timeout',
pathUrl.toString(),
{
method: 'GET',
path: httpPath.path,
query: httpQuery.query,
timeout: this.options.timeout
},
error
);
}

// Handle other network errors
throw new HttpNetworkException(
`${FQP}get(httpPath: HttpPath, httpQuery: HttpQuery): Promise<Response>`,
error instanceof Error ? error.message : 'Network request failed',
'connection',
pathUrl.toString(),
{
method: 'GET',
path: httpPath.path,
query: httpQuery.query
},
error instanceof Error ? error : undefined
);
} finally {
abortSignal?.cleanup(); // cleanup the abort signal
}
Expand All @@ -193,6 +252,8 @@ class FetchHttpClient implements HttpClient {
* @param httpQuery - The query parameters of the request.
* @param body - The body of the request.
* @returns The response from the request.
* @throws HttpException for non-200 responses
* @throws HttpNetworkException for network issues
*/
async post(
httpPath: HttpPath = { path: '' },
Expand Down Expand Up @@ -220,10 +281,68 @@ class FetchHttpClient implements HttpClient {
this.options.onRequest?.(request) ?? request
);
await this.logResponse(request, response);

// Check for non-200 responses and raise HttpException
if (!response.ok) {
const responseBody = await response.text();
throw new HttpException(
`${FQP}post(httpPath: HttpPath, httpQuery: HttpQuery, body?: unknown): Promise<Response>`,
`HTTP request failed with status ${response.status}`,
response.status,
response.statusText,
responseBody,
response.url,
{
method: 'POST',
path: httpPath.path,
query: httpQuery.query,
body: body
}
);
}

return (
this.options.onResponse?.(this.processResponse(response)) ??
response
);
} catch (error) {
// Handle network errors and timeouts
if (error instanceof HttpException) {
throw error; // Re-throw HTTP exceptions
}

// Handle abort signal (timeout)
if (error instanceof Error && error.name === 'AbortError') {
throw new HttpNetworkException(
`${FQP}post(httpPath: HttpPath, httpQuery: HttpQuery, body?: unknown): Promise<Response>`,
'Request timed out',
'timeout',
pathUrl.toString(),
{
method: 'POST',
path: httpPath.path,
query: httpQuery.query,
body: body,
timeout: this.options.timeout
},
error
);
}

// Handle other network errors
throw new HttpNetworkException(
`${FQP}post(httpPath: HttpPath, httpQuery: HttpQuery, body?: unknown): Promise<Response>`,
error instanceof Error ? error.message : 'Network request failed',
'connection',
pathUrl.toString(),
{
method: 'POST',
path: httpPath.path,
query: httpQuery.query,
body: body
},
error instanceof Error ? error : undefined
);
} finally {
abortSignal?.cleanup(); // cleanup the abort signal
}
Expand Down
63 changes: 63 additions & 0 deletions packages/sdk/src/common/http/HttpException.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { VeChainSDKError } from '@common/errors';

/**
* Represents an HTTP error for non-200 responses.
*
* @remarks
* This error class extends the `VeChainSDKError` and denotes HTTP errors encountered
* when the server returns a non-200 status code (e.g., 400, 404, 500, etc.).
* It includes additional information about the HTTP status code and response details.
*/
class HttpException extends VeChainSDKError {
/**
* The [HTTP Status Code](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status)
* returned by the server.
*/
readonly status: number;

/**
* The HTTP status text returned by the server.
*/
readonly statusText: string;

/**
* The response body as text.
*/
readonly responseBody: string;

/**
* The URL that was requested.
*/
readonly url: string;

/**
* Constructs a new instance of the class.
*
* @param {string} fqn - The fully qualified name associated with the instance.
* @param {string} message - The message describing the instance context or error.
* @param {number} status - The HTTP status code.
* @param {string} statusText - The HTTP status text.
* @param {string} responseBody - The response body as text.
* @param {string} url - The URL that was requested.
* @param {Record<string, unknown>} [args] - Optional additional arguments related to the instance.
* @param {Error} [cause] - Optional underlying error that caused the issue.
*/
constructor(
fqn: string,
message: string,
status: number,
statusText: string,
responseBody: string,
url: string,
args?: Record<string, unknown>,
cause?: Error
) {
super(fqn, message, args, cause);
this.status = status;
this.statusText = statusText;
this.responseBody = responseBody;
this.url = url;
}
}

export { HttpException };
46 changes: 46 additions & 0 deletions packages/sdk/src/common/http/HttpNetworkException.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { VeChainSDKError } from '@common/errors';

/**
* Represents a network error for connection issues, timeouts, or no response.
*
* @remarks
* This error class extends the `VeChainSDKError` and denotes network errors encountered
* when there are connection issues, timeouts, or when no response is received from the server.
* It includes additional information about the network error type and request details.
*/
class HttpNetworkException extends VeChainSDKError {
/**
* The type of network error that occurred.
*/
readonly networkErrorType: 'timeout' | 'connection' | 'no_response' | 'abort';

/**
* The URL that was requested.
*/
readonly url: string;

/**
* Constructs a new instance of the class.
*
* @param {string} fqn - The fully qualified name associated with the instance.
* @param {string} message - The message describing the instance context or error.
* @param {string} networkErrorType - The type of network error that occurred.
* @param {string} url - The URL that was requested.
* @param {Record<string, unknown>} [args] - Optional additional arguments related to the instance.
* @param {Error} [cause] - Optional underlying error that caused the issue.
*/
constructor(
fqn: string,
message: string,
networkErrorType: 'timeout' | 'connection' | 'no_response' | 'abort',
url: string,
args?: Record<string, unknown>,
cause?: Error
) {
super(fqn, message, args, cause);
this.networkErrorType = networkErrorType;
this.url = url;
}
}

export { HttpNetworkException };
2 changes: 2 additions & 0 deletions packages/sdk/src/common/http/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,5 @@ export type { HttpClientFactory } from './HttpClientFactory';
export { DefaultHttpClientFactory } from './HttpClientFactory';
export type { HttpPath } from './HttpPath';
export type { HttpQuery } from './HttpQuery';
export { HttpException } from './HttpException';
export { HttpNetworkException } from './HttpNetworkException';
7 changes: 6 additions & 1 deletion packages/sdk/src/thor/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
export * from './certificate';
export * from './signer';
export * from './thor-client';
export * from './thorest';
export * from './thor-client/ThorClient';
export * from './thor-client/accounts';
export * from './thor-client/gas';
export * from './thor-client/model/accounts';
export * from './thor-client/model/nodes';
export * from './utils';
export * from './ws';
47 changes: 30 additions & 17 deletions packages/sdk/src/thor/thorest/accounts/methods/InspectClauses.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { type HttpClient, type HttpPath, type HttpQuery } from '@common/http';
import { handleHttpError } from '@thor/thorest/utils';
import { ExecuteCodesResponse, ExecuteCodesRequest } from '@thor/thorest';
import { ThorError, type ThorRequest, type ThorResponse } from '@thor/thorest';
import { Revision } from '@common/vcdm';
Expand Down Expand Up @@ -59,13 +60,30 @@ class InspectClauses
httpClient: HttpClient
): Promise<ThorResponse<InspectClauses, ExecuteCodesResponse>> {
const fqp = `${FQP}askTo(httpClient: HttpClient): Promise<ThorResponse<InspectClauses, ExecuteCodesResponse>>`;
const response = await httpClient.post(
InspectClauses.PATH,
this.query,
this.request.toJSON()
);
if (response.ok) {
const json = (await response.json()) as ExecuteCodesResponseJSON;
try {
const response = await httpClient.post(
InspectClauses.PATH,
this.query,
this.request.toJSON()
);
let json: ExecuteCodesResponseJSON;
try {
json = (await response.json()) as ExecuteCodesResponseJSON;
} catch (parseErr) {
const body = await response.text().catch(() => undefined);
throw new ThorError(
fqp,
parseErr instanceof Error ? parseErr.message : 'Bad response.',
{
url: response.url,
status: response.status,
statusText: response.statusText,
body
},
parseErr instanceof Error ? parseErr : undefined,
response.status
);
}
try {
return {
request: this,
Expand All @@ -74,25 +92,20 @@ class InspectClauses
} catch (error) {
throw new ThorError(
fqp,
'Bad response.',
error instanceof Error ? error.message : 'Bad response.',
{
url: response.url,
status: response.status,
statusText: response.statusText,
body: json
},
error instanceof Error ? error : undefined,
response.status
);
}
} catch (error) {
throw handleHttpError(fqp, error);
}
throw new ThorError(
fqp,
'Bad response.',
{
url: response.url
},
undefined,
response.status
);
}

/**
Expand Down
Loading
Loading