Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: port the Http Provider to v3 #5580

Merged
merged 17 commits into from
Aug 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
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
45 changes: 45 additions & 0 deletions v-next/hardhat-errors/src/descriptors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ export const ERROR_CATEGORIES: {
},
ARGUMENTS: { min: 500, max: 599, websiteTitle: "Arguments related errors" },
BUILTIN_TASKS: { min: 600, max: 699, websiteTitle: "Built-in tasks errors" },
NETWORK: { min: 700, max: 799, websiteTitle: "Network errors" },
};

export const ERRORS = {
Expand Down Expand Up @@ -473,4 +474,48 @@ Please double check your script's path.`,
Please check Hardhat's output for more details.`,
},
},
NETWORK: {
INVALID_URL: {
number: 700,
messageTemplate: "Invalid URL {value} for network or forking.",
websiteTitle: "Invalid URL for network or forking",
websiteDescription: `You are trying to connect to a network with an invalid network or forking URL.

Please check that you are sending a valid URL string for the network or forking \`URL\` parameter.`,
},
INVALID_REQUEST_PARAMS: {
number: 701,
messageTemplate:
"Invalid request arguments: only array parameters are supported.",
websiteTitle: "Invalid method parameters",
websiteDescription:
"The JSON-RPC request parameters are invalid. You are trying to make an EIP-1193 request with object parameters, but only array parameters are supported. Ensure that the 'params' parameter is correctly specified as an array in your JSON-RPC request.",
},
INVALID_JSON_RESPONSE: {
number: 702,
messageTemplate: "Invalid JSON-RPC response received: {response}",
websiteTitle: "Invalid JSON-RPC response",
websiteDescription: `One of your JSON-RPC requests received an invalid response.

Please make sure your node is running, and check your internet connection and networks config.`,
},
CONNECTION_REFUSED: {
number: 703,
messageTemplate: `Cannot connect to the network {network}.
Please make sure your node is running, and check your internet connection and networks config`,
websiteTitle: "Cannot connect to the network",
websiteDescription: `Cannot connect to the network.

Please make sure your node is running, and check your internet connection and networks config.`,
},
NETWORK_TIMEOUT: {
number: 704,
messageTemplate: `Network connection timed out.
Please check your internet connection and networks config`,
websiteTitle: "Network timeout",
websiteDescription: `One of your JSON-RPC requests timed out.

Please make sure your node is running, and check your internet connection and networks config.`,
},
},
} as const;
4 changes: 2 additions & 2 deletions v-next/hardhat-errors/src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ export class HardhatError<
other: unknown,
descriptor?: ErrorDescriptor,
): other is HardhatError<ErrorDescriptor> {
if (!isObject(other) || other === null) {
if (!isObject(other)) {
return false;
}

Expand Down Expand Up @@ -200,7 +200,7 @@ export class HardhatPluginError extends CustomError {
public static isHardhatPluginError(
other: unknown,
): other is HardhatPluginError {
if (!isObject(other) || other === null) {
if (!isObject(other)) {
return false;
}

Expand Down
62 changes: 62 additions & 0 deletions v-next/hardhat-utils/src/errors/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type UndiciT from "undici";

import { CustomError } from "../error.js";
import { sanitizeUrl } from "../internal/request.js";
import { isObject } from "../lang.js";

export class RequestError extends CustomError {
constructor(url: string, type: UndiciT.Dispatcher.HttpMethod, cause?: Error) {
Expand All @@ -20,3 +21,64 @@ export class DispatcherError extends CustomError {
super(`Failed to create dispatcher: ${message}`, cause);
}
}

export class RequestTimeoutError extends CustomError {
constructor(url: string, cause?: Error) {
super(`Request to ${sanitizeUrl(url)} timed out`, cause);
}
}

export class ConnectionRefusedError extends CustomError {
constructor(url: string, cause?: Error) {
super(`Connection to ${sanitizeUrl(url)} was refused`, cause);
}
}

export class ResponseStatusCodeError extends CustomError {
public readonly statusCode: number;
public readonly headers:
| string[]
| Record<string, string | string[] | undefined>
| null;
public readonly body: null | Record<string, any> | string;

constructor(url: string, cause: Error) {
super(`Received an unexpected status code from ${sanitizeUrl(url)}`, cause);
this.statusCode =
"statusCode" in cause && typeof cause.statusCode === "number"
? cause.statusCode
: -1;
this.headers = this.#extractHeaders(cause);
this.body = "body" in cause && isObject(cause.body) ? cause.body : null;
}

#extractHeaders(
cause: Error,
): string[] | Record<string, string | string[] | undefined> | null {
if ("headers" in cause) {
const headers = cause.headers;
if (Array.isArray(headers)) {
return headers;
} else if (this.#isValidHeaders(headers)) {
return headers;
}
}
return null;
}

#isValidHeaders(
headers: unknown,
): headers is Record<string, string | string[] | undefined> {
if (!isObject(headers)) {
return false;
}

return Object.values(headers).every(
(header) =>
typeof header === "string" ||
(Array.isArray(header) &&
header.every((item: unknown) => typeof item === "string")) ||
header === undefined,
);
}
}
30 changes: 29 additions & 1 deletion v-next/hardhat-utils/src/internal/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,15 @@ import path from "node:path";
import url from "node:url";

import { mkdir } from "../fs.js";
import { isObject } from "../lang.js";
import {
ConnectionRefusedError,
DEFAULT_MAX_REDIRECTS,
DEFAULT_TIMEOUT_IN_MILLISECONDS,
DEFAULT_USER_AGENT,
getDispatcher,
RequestTimeoutError,
ResponseStatusCodeError,
} from "../request.js";

export async function generateTempFilePath(filePath: string): Promise<string> {
Expand All @@ -34,7 +38,7 @@ export async function getBaseRequestOptions(
signal?: EventEmitter | AbortSignal | undefined;
dispatcher: UndiciT.Dispatcher;
headers: Record<string, string>;
throwOnError: boolean;
throwOnError: true;
}> {
const { Dispatcher } = await import("undici");
const dispatcher =
Expand Down Expand Up @@ -135,3 +139,27 @@ export function sanitizeUrl(requestUrl: string): string {
const parsedUrl = new URL(requestUrl);
return url.format(parsedUrl, { auth: false, search: false, fragment: false });
}

export function handleError(e: Error, requestUrl: string): void {
let causeCode: unknown;
if (isObject(e.cause)) {
causeCode = e.cause.code;
}
const errorCode = "code" in e ? e.code : causeCode;

if (errorCode === "ECONNREFUSED") {
throw new ConnectionRefusedError(requestUrl, e);
}

if (
errorCode === "UND_ERR_CONNECT_TIMEOUT" ||
errorCode === "UND_ERR_HEADERS_TIMEOUT" ||
errorCode === "UND_ERR_BODY_TIMEOUT"
) {
throw new RequestTimeoutError(requestUrl, e);
}

if (errorCode === "UND_ERR_RESPONSE_STATUS_CODE") {
throw new ResponseStatusCodeError(requestUrl, e);
}
}
10 changes: 10 additions & 0 deletions v-next/hardhat-utils/src/lang.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,13 @@ export function isObject(
): value is Record<string | symbol, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}

/**
* Pauses the execution for the specified number of seconds.
*
* @param seconds The number of seconds to pause the execution.
* @returns A promise that resolves after the specified number of seconds.
*/
export async function sleep(seconds: number): Promise<void> {
await new Promise((resolve) => setTimeout(resolve, seconds * 1000));
}
66 changes: 60 additions & 6 deletions v-next/hardhat-utils/src/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,18 @@ import {
getBasicDispatcher,
getPoolDispatcher,
getProxyDispatcher,
handleError,
} from "./internal/request.js";

export const DEFAULT_TIMEOUT_IN_MILLISECONDS = 30_000;
export const DEFAULT_MAX_REDIRECTS = 10;
export const DEFAULT_POOL_MAX_CONNECTIONS = 128;
export const DEFAULT_USER_AGENT = "Hardhat";

export type Dispatcher = UndiciT.Dispatcher;
export type TestDispatcher = UndiciT.MockAgent;
export type Interceptable = UndiciT.Interceptable;

/**
* Options to configure the dispatcher.
*
Expand Down Expand Up @@ -64,7 +69,9 @@ export interface RequestOptions {
* @param requestOptions The options to configure the request. See {@link RequestOptions}.
* @param dispatcherOrDispatcherOptions Either a dispatcher or dispatcher options. See {@link DispatcherOptions}.
* @returns The response data object. See {@link https://undici.nodejs.org/#/docs/api/Dispatcher?id=parameter-responsedata}.
* @throws RequestError If the request fails.
* @throws ConnectionRefusedError If the connection is refused by the server.
* @throws RequestTimeoutError If the request times out.
* @throws RequestError If the request fails for any other reason.
*/
export async function getRequest(
url: string,
Expand All @@ -85,6 +92,9 @@ export async function getRequest(
});
} catch (e) {
ensureError(e);

handleError(e, url);

throw new RequestError(url, "GET", e);
}
}
Expand All @@ -97,7 +107,9 @@ export async function getRequest(
* @param requestOptions The options to configure the request. See {@link RequestOptions}.
* @param dispatcherOrDispatcherOptions Either a dispatcher or dispatcher options. See {@link DispatcherOptions}.
* @returns The response data object. See {@link https://undici.nodejs.org/#/docs/api/Dispatcher?id=parameter-responsedata}.
* @throws RequestError If the request fails.
* @throws ConnectionRefusedError If the connection is refused by the server.
* @throws RequestTimeoutError If the request times out.
* @throws RequestError If the request fails for any other reason.
*/
export async function postJsonRequest(
url: string,
Expand All @@ -124,6 +136,9 @@ export async function postJsonRequest(
});
} catch (e) {
ensureError(e);

handleError(e, url);

throw new RequestError(url, "POST", e);
}
}
Expand All @@ -136,7 +151,9 @@ export async function postJsonRequest(
* @param requestOptions The options to configure the request. See {@link RequestOptions}.
* @param dispatcherOrDispatcherOptions Either a dispatcher or dispatcher options. See {@link DispatcherOptions}.
* @returns The response data object. See {@link https://undici.nodejs.org/#/docs/api/Dispatcher?id=parameter-responsedata}.
* @throws RequestError If the request fails.
* @throws ConnectionRefusedError If the connection is refused by the server.
* @throws RequestTimeoutError If the request times out.
* @throws RequestError If the request fails for any other reason.
*/
export async function postFormRequest(
url: string,
Expand Down Expand Up @@ -164,6 +181,9 @@ export async function postFormRequest(
});
} catch (e) {
ensureError(e);

handleError(e, url);

throw new RequestError(url, "POST", e);
}
}
Expand All @@ -175,7 +195,9 @@ export async function postFormRequest(
* @param destination The absolute path to save the file to.
* @param requestOptions The options to configure the request. See {@link RequestOptions}.
* @param dispatcherOrDispatcherOptions Either a dispatcher or dispatcher options. See {@link DispatcherOptions}.
* @throws DownloadFailedError If the download fails.
* @throws ConnectionRefusedError If the connection is refused by the server.
* @throws RequestTimeoutError If the request times out.
* @throws DownloadFailedError If the download fails for any other reason.
*/
export async function download(
url: string,
Expand Down Expand Up @@ -204,6 +226,9 @@ export async function download(
await move(tempFilePath, destination);
} catch (e) {
ensureError(e);

handleError(e, url);

throw new DownloadError(url, e);
}
}
Expand All @@ -228,7 +253,7 @@ export async function getDispatcher(
maxConnections,
isTestDispatcher,
}: DispatcherOptions = {},
): Promise<UndiciT.Dispatcher> {
): Promise<Dispatcher> {
try {
if (pool !== undefined && proxy !== undefined) {
throw new Error(
Expand All @@ -255,6 +280,17 @@ export async function getDispatcher(
}
}

export async function getTestDispatcher(
options: {
timeout?: number;
} = {},
): Promise<TestDispatcher> {
const { MockAgent } = await import("undici");

const baseOptions = getBaseDispatcherOptions(options.timeout, true);
return new MockAgent(baseOptions);
}

/**
* Determines whether a proxy should be used for a given url.
*
Expand All @@ -280,8 +316,26 @@ export function shouldUseProxy(url: string): boolean {
return true;
}

/**
* Determines whether an absolute url is valid.
*
* @param url The url to check.
* @returns `true` if the url is valid, `false` otherwise.
*/
export function isValidUrl(url: string): boolean {
try {
new URL(url);
return true;
} catch {
return false;
}
}

export {
ConnectionRefusedError,
DispatcherError,
DownloadError,
RequestError,
DispatcherError,
RequestTimeoutError,
ResponseStatusCodeError,
} from "./errors/request.js";
Loading