Network - Full Reference
Exhaustive reference for BaseNetworkRequest and its fetchers, the TCP/TLS client-server hierarchy, NetworkUdpClient, the UrlPolicy/UrlIngest safety guard, and every option type. For a readable introduction and the most common tasks, start with the Network overview.
Find what you need
| You're looking for | Go to |
|---|---|
| The HTTP client base class and URL helpers | BaseNetworkRequest |
| Axios-backed HTTP client | AxiosFetcher / AxiosNetworkRequest |
Native-fetch-backed HTTP client | NodeFetcher / NodeFetchNetworkRequest |
Why the QUERY method must reach the wire uppercase | HTTP.Methods |
| Secret redaction in request logs | Request Logging and Redaction |
| Plain TCP or TLS server, client tracking, authentication | BaseNetworkTcpServer |
| Plain TCP or TLS client, auto-reconnect behavior | BaseNetworkTcpClient |
| UDP client and multicast | NetworkUdpClient |
| Fetching a url a user supplied, without opening an SSRF hole | URL Safety |
| Every option and type in one place | Types Reference |
Files:
packages/helpers/src/modules/network/http-request/base-network-request.helper.ts-BaseNetworkRequestpackages/helpers/src/modules/network/http-request/fetcher/base-fetcher.ts-IFetchable,IRequestOptions,AbstractNetworkFetchableHelperpackages/helpers/src/modules/network/http-request/fetcher/node-fetcher.ts-NodeFetcher,NodeFetchNetworkRequestpackages/helpers/src/modules/network/http-request/fetcher/axios-fetcher.ts-AxiosFetcher,AxiosNetworkRequest(@venizia/ignis-helpers/axiossub-path)packages/helpers/src/modules/network/http-request/common/types.ts-TFetcherVariant,TFetcherResponse,TFetcherWorkerpackages/helpers/src/modules/network/tcp-socket/base-tcp-server.helper.ts-BaseNetworkTcpServer,ITcpSocketClient,ITcpSocketServerOptionspackages/helpers/src/modules/network/tcp-socket/base-tcp-client.helper.ts-BaseNetworkTcpClient,INetworkTcpClientPropspackages/helpers/src/modules/network/tcp-socket/network-tcp-server.helper.ts-NetworkTcpServerpackages/helpers/src/modules/network/tcp-socket/network-tcp-client.helper.ts-NetworkTcpClientpackages/helpers/src/modules/network/tcp-socket/network-tls-tcp-server.helper.ts-NetworkTlsTcpServerpackages/helpers/src/modules/network/tcp-socket/network-tls-tcp-client.helper.ts-NetworkTlsTcpClientpackages/helpers/src/modules/network/udp-socket/network-udp-client.helper.ts-NetworkUdpClient,INetworkUdpClientPropspackages/helpers/src/modules/network/utilities/url-safety/policy.ts-UrlPolicypackages/helpers/src/modules/network/utilities/url-safety/ingest.ts-UrlIngestpackages/helpers/src/modules/network/utilities/url-safety/common/-IUrlSafetyPolicy,UrlSafetyDefaults,UrlSchemes,UrlSafetyErrors,isUrlRefusedErrorpackages/helpers/src/common/redact.ts-redactSecrets,redactUrlCredentialspackages/helpers/src/common/constants/http.ts-HTTP.Methods,THttpMethod
Architecture
BaseHelper
├── BaseNetworkRequest<T extends TFetcherVariant>
│ ├── AxiosNetworkRequest (T = 'axios')
│ └── NodeFetchNetworkRequest (T = 'node-fetch')
│
├── BaseNetworkTcpServer<ServerOpts, ServerType, ClientType>
│ ├── NetworkTcpServer (net.createServer, net.Socket)
│ └── NetworkTlsTcpServer (tls.createServer, tls.TLSSocket)
│
├── BaseNetworkTcpClient<ClientOpts, ClientType>
│ ├── NetworkTcpClient (net.connect, net.Socket)
│ └── NetworkTlsTcpClient (tls.connect, tls.TLSSocket)
│
└── NetworkUdpClient
AbstractNetworkFetchableHelper<V, RQ, RS> (implements IFetchable, NOT a BaseHelper)
├── AxiosFetcher (V = 'axios')
└── NodeFetcher (V = 'node-fetch')Every class that extends BaseHelper inherits scoped logging via this.logger. AbstractNetworkFetchableHelper and its two fetchers do not extend BaseHelper. They accept an optional logger parameter per call instead - see Request Logging and Redaction.
HTTP Request API
BaseNetworkRequest
class BaseNetworkRequest<T extends TFetcherVariant> extends BaseHelperBase class for HTTP request helpers. Holds a base URL and delegates to an IFetchable fetcher.
Constructor
constructor(opts: {
name: string;
baseUrl?: string;
fetcher: IFetchable<T, IRequestOptions, TFetcherResponse<T>>;
})| Parameter | Type | Description |
|---|---|---|
name | string | Helper name, used as both scope and identifier for logging |
baseUrl | string | Base URL prepended to request paths. Defaults to '' |
fetcher | IFetchable | The underlying HTTP fetcher implementation |
Methods
getRequestUrl(opts)
Builds a full URL by combining a base URL with path segments. Throws if no base URL is available.
getRequestUrl(opts: {
baseUrl?: string;
paths: Array<string>;
}): string| Parameter | Type | Description |
|---|---|---|
baseUrl | string | Overrides the instance's base URL. Falls back to this.baseUrl. An explicit empty string does not fall back - it throws |
paths | string[] | Path segments to append. Each is prefixed with / if missing |
Throws: ApplicationError (via getError, statusCode: 500) with message '[getRequestUrl] Invalid configuration for third party request base url!' when both opts.baseUrl and this.baseUrl resolve to empty.
Example:
client.getRequestUrl({ paths: ['v1', 'users', '123'] });
// => 'https://api.example.com/v1/users/123'
client.getRequestUrl({ baseUrl: 'https://other.api.com', paths: ['health'] });
// => 'https://other.api.com/health'getRequestPath(opts)
Joins path segments, ensuring each starts with /. An empty paths array joins to ''.
getRequestPath(opts: { paths: Array<string> }): stringExample:
client.getRequestPath({ paths: ['v1', 'users'] });
// => '/v1/users'getNetworkService()
Returns the underlying IFetchable fetcher instance.
getNetworkService(): IFetchable<T, IRequestOptions, TFetcherResponse<T>>getWorker()
Returns the raw HTTP client from the fetcher (AxiosInstance for Axios, typeof fetch for Node Fetch). Delegates to fetcher.getWorker().
getWorker(): TFetcherWorker<T>IFetchable Interface
interface IFetchable<
V extends TFetcherVariant,
RQ extends IRequestOptions,
RS extends TFetcherResponse<V>,
> {
send(opts: RQ, logger?: any): Promise<RS>;
get(opts: RQ, logger?: any): Promise<RS>;
post(opts: RQ, logger?: any): Promise<RS>;
put(opts: RQ, logger?: any): Promise<RS>;
patch(opts: RQ, logger?: any): Promise<RS>;
delete(opts: RQ, logger?: any): Promise<RS>;
query(opts: RQ, logger?: any): Promise<RS>;
getWorker(): TFetcherWorker<V>;
}All HTTP method shortcuts (get, post, put, patch, delete, query) delegate to send() with the method field set accordingly. query sends the HTTP QUERY method (RFC 9110/10008). It behaves like GET but carries a body - useful for search payloads too large to fit a query string.
IRequestOptions
interface IRequestOptions {
url: string;
method?: THttpMethod;
params?: AnyObject;
timeout?: number;
[extra: symbol | string]: any;
}HTTP.Methods
The const-class every fetcher dispatches on:
HTTP.Methods = {
GET: 'get', POST: 'post', PUT: 'put', PATCH: 'patch',
DELETE: 'delete', HEAD: 'head', OPTIONS: 'options', QUERY: 'query',
} as const;
type THttpMethod = ValueOf<typeof HTTP.Methods> | Uppercase<ValueOf<typeof HTTP.Methods>>;IMPORTANT
Every
HTTP.Methodstoken is lowercase.@hono/zod-openapiroute definitions accept no other case.Both fetchers accept either case on input,
'post'or'POST'. Each callsmethod.toUpperCase()immediately before dispatching to its transport.This is not cosmetic. Node's undici is the
fetchimplementation on Node - not Bun - and it auto-normalizes only some methods:Auto-normalized by undici Sent through verbatim DELETE,GET,HEAD,OPTIONS,POST,PUTPATCH,QUERYA lowercase
patchorqueryreaches the server unchanged, and most servers reject it.Bun and Axios hide the bug. Bun's
fetchand Axios both uppercase every method themselves - Axios vianode:http. The bug surfaces only once the app runs on Node with undici.
AbstractNetworkFetchableHelper
abstract class AbstractNetworkFetchableHelper<
V extends TFetcherVariant,
RQ extends IRequestOptions,
RS extends TFetcherResponse<V>,
> implements IFetchable<V, RQ, RS>Abstract base for fetcher implementations. Provides convenience HTTP method wrappers and protocol detection. Does not extend BaseHelper - it has no this.logger.
Constructor
constructor(opts: { name: string; variant: V })Methods
abstract send(opts, logger?)
Subclasses must implement the actual request dispatch.
abstract send(opts: RQ, logger?: any): Promise<RS>;get(opts, logger?) / post(opts, logger?) / put(opts, logger?) / patch(opts, logger?) / delete(opts, logger?) / query(opts, logger?)
get(opts: RQ, logger?: any): Promise<RS>
post(opts: RQ, logger?: any): Promise<RS>
put(opts: RQ, logger?: any): Promise<RS>
patch(opts: RQ, logger?: any): Promise<RS>
delete(opts: RQ, logger?: any): Promise<RS>
query(opts: RQ, logger?: any): Promise<RS>Each calls send() with method set to the matching HTTP.Methods token (get → HTTP.Methods.GET, ... query → HTTP.Methods.QUERY). Any other field on opts (body, headers, params, ...) passes through untouched.
getProtocol(url)
Returns HTTP.Protocols.HTTP ('http') if url starts with 'http:', otherwise HTTP.Protocols.HTTPS ('https').
getProtocol(url: string): 'http' | 'https'getWorker()
Returns the underlying HTTP client instance (set by the concrete fetcher's constructor).
getWorker(): TFetcherWorker<V>AxiosFetcher
class AxiosFetcher extends AbstractNetworkFetchableHelper<
'axios',
IAxiosRequestOptions,
AxiosResponse['data']
>Axios-based fetcher implementation. Creates an axios instance with the provided default configuration.
Constructor
constructor(opts: {
name: string;
defaultConfigs: AxiosRequestConfig;
logger?: any;
})opts.logger, if provided, logs 'Creating new network request worker instance! Name: %s' once at construction time. This is unrelated to the per-call logger argument on send().
IAxiosRequestOptions
interface IAxiosRequestOptions extends AxiosRequestConfig, IRequestOptions {
url: string;
method?: THttpMethod;
params?: AnyObject;
body?: AnyObject; // Mapped to Axios `data`
headers?: AnyObject;
}NOTE
bodymaps to Axios'sdatafield internally.- Query
paramsare serialized usingnode:querystringvia Axios'sparamsSerializer. - HTTPS gets an
https.Agentautomatically. Certificate verification is ON, and connections are kept alive. PassrejectUnauthorized: false- on the fetcher, for the whole instance, or on a single request - only when you genuinely mean to accept an unverified certificate. A caller-suppliedhttpsAgentalways wins, so a custom CA, pinning or mTLS agent is honoured as given.
Methods
send(opts, logger?)
override send<T = any>(opts: IAxiosRequestOptions, logger?: any): Promise<AxiosResponse<T>>Dispatches the request via the internal axios instance.
methoddefaults toHTTP.Methods.GETand is uppercased before dispatch.- HTTPS URLs automatically get an
https.Agentconfigured. - If
loggeris passed, logs'URL: %s | Props: %s'atinfolevel with the assembled request config run throughredactSecrets().
AxiosNetworkRequest
class AxiosNetworkRequest extends BaseNetworkRequest<'axios'>Pre-configured HTTP client using Axios. Import only from the sub-path - import { AxiosNetworkRequest } from '@venizia/ignis-helpers/axios'.
Constructor
constructor(opts: IAxiosNetworkRequestOptions)interface IAxiosNetworkRequestOptions {
name: string;
networkOptions: Omit<AxiosRequestConfig, 'baseURL'> & {
baseUrl?: string;
};
}Default configuration applied:
| Setting | Default |
|---|---|
headers['content-type'] | 'application/json; charset=utf-8' |
withCredentials | true |
validateStatus | (status) => status < 500 |
timeout | 60000 (1 minute) |
User-provided values in networkOptions override all defaults. networkOptions.baseUrl maps to Axios's baseURL.
NodeFetcher
class NodeFetcher extends AbstractNetworkFetchableHelper<
'node-fetch',
INodeFetchRequestOptions,
Awaited<ReturnType<typeof fetch>>
>Native fetch-based fetcher implementation.
Constructor
constructor(opts: {
name: string;
defaultConfigs: RequestInit;
logger?: any;
})INodeFetchRequestOptions
interface INodeFetchRequestOptions extends RequestInit, IRequestOptions {
url: string;
method?: THttpMethod;
params?: AnyObject;
}Methods
send(opts, logger?)
override async send(opts: INodeFetchRequestOptions, logger?: any): Promise<Response>Dispatches the request using the native fetch API. Behavior:
methoddefaults toHTTP.Methods.GETand is uppercased before dispatch.- Query
paramsare serialized withnode:querystringand appended tourl. The separator adapts:?when the URL carries no query string yet,&when it already does - never a double?. - If
timeoutis provided, an internalAbortControlleraborts the request after that many milliseconds.- The internal timeout signal is composed, never substituted, with a caller-supplied
signal. When both are present, IGNIS buildsAbortSignal.any([signal, timeoutController.signal]). That way a caller aborting its own signal still cancels the request, even while a timeout is also armed. - The timer is cleared as soon as the request settles.
- The internal timeout signal is composed, never substituted, with a caller-supplied
- If
loggeris passed, logs'URL: %s | Props: %s | Timeout: %s'atinfolevel with the request config run throughredactSecrets().
NodeFetchNetworkRequest
class NodeFetchNetworkRequest extends BaseNetworkRequest<'node-fetch'>Pre-configured HTTP client using native fetch. Exported from the root barrel - no extra dependency.
Constructor
constructor(opts: INodeFetchNetworkRequestOptions)interface INodeFetchNetworkRequestOptions {
name: string;
networkOptions: RequestInit & {
baseUrl?: string;
};
}Default configuration applied:
| Setting | Default |
|---|---|
headers['content-type'] | 'application/json; charset=utf-8' |
If headers is a Headers instance, it is converted to a plain object via Object.fromEntries(headers.entries()) before merging with the default.
`timeout` is per-call, not per-instance
networkOptions is RequestInit, which has no timeout field. Passing one there has no effect on request cancellation. NodeFetcher.send() only reads timeout from the arguments of each individual send()/get()/... call. Pass it every time you need an abort:
await this.getNetworkService().send({ url: '/slow-endpoint', method: 'get', timeout: 5000 });Request Logging and Redaction
Neither fetcher logs anything by default. send() and every shortcut accept an optional logger as the second argument, guarded internally with logger?.for(...). Pass a logger to get an info-level line per request - typically this.logger, whether the caller is a BaseHelper subclass or extends BaseNetworkRequest directly:
await this.getNetworkService().post({ url, body }, this.logger);Whenever a logger is passed, IGNIS runs the assembled request config through redactSecrets() before the log line is written. That covers url, method, headers, body/data, and any other option. Redaction matches by key name, case-insensitively, at any depth, against SECRET_KEY_PATTERN:
| Key group | Matched spellings |
|---|---|
| Options-object (camelCase) | pass, password, passphrase, secret, token, apiKey, accessKey, secretKey, privateKey, key, cert, ca, pfx, credentials, authorization, auth, jwtSecret, applicationSecret, connectionString |
| Options-object (snake_case) | api_key, access_key, secret_key, private_key |
| HTTP header spellings | cookie, set-cookie, proxy-authorization, www-authenticate, and any (x-)?(api|auth|access|secret|session|csrf|xsrf)-(key|token|secret|id) pattern (e.g. x-api-key, x-auth-token, x-csrf-token) |
Source: redact.ts SECRET_KEY_PATTERN.
A matched value is replaced with '[REDACTED]'. Buffer/typed-array values under a non-matching key are summarized as '[Binary N bytes]' rather than serialized. There is nothing to configure beyond passing the logger - you never call redactSecrets() yourself at the call site.
TCP Socket API
BaseNetworkTcpServer
class BaseNetworkTcpServer<
SocketServerOptions extends ServerOpts = ServerOpts,
SocketServerType extends SocketServer = SocketServer,
SocketClientType extends SocketClient = SocketClient,
> extends BaseHelperAbstract TCP server with client tracking, authentication flow, and event delegation.
Constructor
constructor(opts: ITcpSocketServerOptions<SocketServerOptions, SocketServerType, SocketClientType>)Throws: ApplicationError when authenticateOptions.required is true and duration is missing, 0, or negative. Message: 'TCP Server | Invalid authenticate duration | Required duration for authenticateOptions'.
The constructor calls configure(), which creates the server via createServerFn and starts listening.
Protected Properties
| Property | Type | Description |
|---|---|---|
serverOptions | Partial<SocketServerOptions> | Server creation options |
listenOptions | Partial<ListenOptions> | Listen configuration |
authenticateOptions | { required: boolean; duration?: number } | Auth settings |
clients | Record<string, ITcpSocketClient<SocketClientType>> | Connected client registry |
server | SocketServerType | The underlying server instance |
extraEvents | Record<string, (opts) => ValueOrPromise<void>> | Additional per-client socket events to register |
- Hooks never crash the process.
onClientData,onClientConnected,onClientClose,onClientError,onServerReady,onServerError, and eachextraEventsentry all run through an internalinvokeHook()wrapper. - Why it exists. A hook throwing synchronously inside a raw
net/tlsevent listener would otherwise be an uncaught exception. That crashes the process.invokeHook()catches the throw and logs it instead.
Methods
configure()
Creates the server using createServerFn, registers a server-level error listener (routed to onServerError, never left to crash the process), and starts listening. Called automatically by the constructor.
configure(): voidonNewConnection(opts)
Handles a new client connection:
- Assigns a unique ID via
getUID(). - Registers
data/error/close/extraEventslisteners on the socket. - Tracks the client in the
clientsregistry. - Invokes
onClientConnected. - Starts the authentication kick-timer when
authenticateOptions.requiredistrue.
onNewConnection(opts: { socket: SocketClientType }): voidgetClients() / getClient(opts) / getServer()
getClients(): Record<string, ITcpSocketClient<SocketClientType>>
getClient(opts: { id: string }): ITcpSocketClient<SocketClientType> | undefined
getServer(): SocketServerTypedoAuthenticate(opts)
Transitions a client's authentication state.
doAuthenticate(opts: {
id: string;
state: 'unauthorized' | 'authenticating' | 'authenticated';
}): void| New state | Effect |
|---|---|
'authenticated' | Sets storage.authenticatedAt to the current Date; clears the pending kick-timer |
'unauthorized' / 'authenticating' | Sets storage.authenticatedAt to null |
emit(opts)
Writes data to a specific client's socket. Never throws - each failure case logs and returns instead:
| Condition | Log level |
|---|---|
Client not found for clientId | error |
| Socket not writable | error |
Payload empty (!payload?.length) | info |
emit(opts: { clientId: string; payload: Buffer | string }): voidshutdown()
Tears the server down completely and resolves even while clients are still attached.
async shutdown(): Promise<void>Order of operations:
- Clears every client's pending authenticate kick-timer.
- Destroys every client socket.
- Empties the
clientsregistry. - Calls
server.close()and awaits its callback.
- Why this order.
server.close()alone never resolves while a socket is still attached. A caller reaching throughgetServer().close()on a busy server would hang forever. - Idempotent. A second call is a no-op that resolves cleanly.
- Safe on a server that never finished
listen(). Logs the resultingERR_SERVER_NOT_RUNNINGrather than throwing. - After
shutdown(), new connection attempts are refused.
ITcpSocketClient
interface ITcpSocketClient<SocketClientType> {
id: string;
socket: SocketClientType;
state: 'unauthorized' | 'authenticating' | 'authenticated';
subscriptions: Set<string>;
storage: {
connectedAt: Date;
authenticatedAt: Date | null;
authenticateTimeout?: ReturnType<typeof setTimeout> | null;
[additionField: symbol | string]: any;
};
}NetworkTcpServer
class NetworkTcpServer extends BaseNetworkTcpServer<ServerOpts, Server, Socket>Plain TCP server using net.createServer.
Constructor
constructor(opts: Omit<ITcpSocketServerOptions, 'createServerFn'>)The createServerFn is pre-set to net.createServer. scope is hardcoded to 'NetworkTcpServer', overriding any opts.scope.
Static Methods
newInstance(opts)
static newInstance(
opts: Omit<ITcpSocketServerOptions, 'createServerFn'>
): NetworkTcpServerNetworkTlsTcpServer
class NetworkTlsTcpServer extends BaseNetworkTcpServer<TlsOptions, tls.Server, TLSSocket>TLS-encrypted TCP server using tls.createServer.
Constructor
constructor(opts: Omit<ITcpSocketServerOptions<TlsOptions, tls.Server, TLSSocket>, 'createServerFn'>)The createServerFn is pre-set to tls.createServer. scope is hardcoded to 'NetworkTlsTcpServer'. Pass TLS certificates and keys in serverOptions (type TlsOptions from node:tls). Every method, including shutdown(), is identical to NetworkTcpServer.
Static Methods
newInstance(opts)
static newInstance(
opts: Omit<ITcpSocketServerOptions<TlsOptions, tls.Server, TLSSocket>, 'createServerFn'>
): NetworkTlsTcpServerBaseNetworkTcpClient
class BaseNetworkTcpClient<
SocketClientOptions extends PlainConnectionOptions | TlsConnectionOptions,
SocketClientType extends PlainSocketClient | TlsSocketClient,
> extends BaseHelperAbstract TCP client with auto-reconnect, encoding support, and lifecycle hooks.
Constructor
constructor(opts: INetworkTcpClientProps<SocketClientOptions, SocketClientType>)Does not call connect() automatically. Construction only stores options - call connect({ resetReconnectCounter }) explicitly.
Protected Properties
| Property | Type | Description |
|---|---|---|
client | SocketClientType | null | The underlying socket, or null/undefined when disconnected |
options | SocketClientOptions | Connection options |
reconnect | boolean | Whether auto-reconnect is enabled (default: false) |
retry | { maxReconnect: number; currentReconnect: number } | Reconnect state. maxReconnect defaults to 5 |
encoding | BufferEncoding | undefined | Socket encoding |
getLoggableOptions()
protected getLoggableOptions(): unknownReturns redactSecrets(this.options). A TLS client's options is its private key material - key/cert/passphrase. Every internal log call uses this method instead of logging this.options directly, or the key would be written to every log file and aggregator downstream.
Methods
connect(opts)
Establishes the connection:
- No-op with a log line if already connected (
isConnected()) or ifoptionsis empty. - Otherwise, in order:
- Destroys any stale
clientfirst. - Creates the socket via
createClientFn. - Registers
data/close/errorlisteners. - Applies
encoding, if set.
- Destroys any stale
connect(opts: { resetReconnectCounter: boolean }): void| Parameter | Type | Description |
|---|---|---|
resetReconnectCounter | boolean | If true, resets retry.currentReconnect to 0 before connecting |
disconnect()
Destroys the socket, clears the reconnect timeout, and sets client to null. No-op with a log line if client is not set.
disconnect(): voidforceReconnect()
Calls disconnect() then connect({ resetReconnectCounter: true }).
forceReconnect(): voidisConnected()
Returns a truthy value if client exists and its readyState is not 'closed'.
isConnected(): SocketClientType | null | undefinedemit(opts)
Writes data to the server. Logs and returns without throwing if client is not initialized or the payload is empty.
emit(opts: { payload: Buffer | string }): voidgetClient()
getClient(): SocketClientType | null | undefinedhandleConnected() / handleData(_opts) / handleClosed() / handleError(error)
Default handlers used when the matching onConnected/onData/onClosed/onError option is omitted.
handleConnected(): void // Logs, resets retry.currentReconnect to 0
handleData(_opts: { identifier: string; message: string | Buffer }): void // No-op
handleClosed(): void // Logs the closure
handleError(error: any): void // Logs; schedules a reconnect if eligibleReconnect gate on handleError:
| Condition | Result |
|---|---|
reconnect is false | No reconnect - the first guard returns immediately |
retry.currentReconnect >= retry.maxReconnect | No reconnect - the first guard returns immediately |
maxReconnect === -1 | No reconnect, ever. currentReconnect starts at 0, and 0 >= -1 is already true, so the first guard returns before any attempt is made |
reconnect is true and currentReconnect < maxReconnect (with maxReconnect >= 0) | Reconnect scheduled |
IMPORTANT
maxReconnect: -1does not mean unlimited reconnects - it disables reconnection entirely. That is the opposite of the "-1= unlimited" convention used elsewhere in the framework - the Redis retry helper, for example.- A second guard further down the method is dead code.
if (maxReconnect > -1 && currentReconnect >= maxReconnect)can never be true. By the time control reaches it, the first guard has already ruled outcurrentReconnect >= maxReconnect. - Source:
base-tcp-client.helper.ts.
The reconnect delay is a fixed 5000 ms. There is no backoff growth on the TCP/TLS client, unlike the Redis helper's exponential strategy.
NetworkTcpClient
class NetworkTcpClient extends BaseNetworkTcpClient<TcpSocketConnectOpts, Socket>Plain TCP client using net.connect.
Constructor
constructor(
opts: Omit<INetworkTcpClientProps<TcpSocketConnectOpts, Socket>, 'createClientFn'>
)The createClientFn is pre-set to net.connect. scope is hardcoded to 'NetworkTcpClient'.
Static Methods
newInstance(opts)
static newInstance(
opts: Omit<INetworkTcpClientProps<TcpSocketConnectOpts, Socket>, 'createClientFn'>
): NetworkTcpClientNetworkTlsTcpClient
class NetworkTlsTcpClient extends BaseNetworkTcpClient<ConnectionOptions, TLSSocket>TLS-encrypted TCP client using tls.connect.
Constructor
constructor(
opts: Omit<INetworkTcpClientProps<ConnectionOptions, TLSSocket>, 'createClientFn'>
)The createClientFn is pre-set to tls.connect. scope is hardcoded to 'NetworkTlsTcpClient'. Pass TLS certificates and keys in options (type ConnectionOptions from node:tls). Every method is identical to NetworkTcpClient.
Static Methods
newInstance(opts)
static newInstance(
opts: Omit<INetworkTcpClientProps<ConnectionOptions, TLSSocket>, 'createClientFn'>
): NetworkTlsTcpClientUDP Socket API
NetworkUdpClient
class NetworkUdpClient extends BaseHelperUDP datagram client with multicast support, using node:dgram internally (type: 'udp4'). No client/server split - one class handles both send and receive. scope is always hardcoded to 'NetworkUdpClient' (no scope option exists on INetworkUdpClientProps).
Constructor
constructor(opts: INetworkUdpClientProps)Construction only stores options; call connect() explicitly to bind the socket.
Static Methods
newInstance(opts)
static newInstance(opts: INetworkUdpClientProps): NetworkUdpClientMethods
connect()
Creates a dgram.Socket and binds it, in order:
- Creates the socket (
type: 'udp4',reuseAddrfrom options). - Registers
close/error/listening/messagelisteners. - Binds to
port/host.
Each listener routes through an internal invokeHook() wrapper - the same synchronous-throw guard as the TCP server. onBind fires after binding completes; it's the place to join multicast groups via socket.addMembership(group, iface).
connect(): voidNo-op with a log line in two cases: client is already set, or port fails Number.isInteger(port) && port >= 0. Port 0 itself is valid - it means "let the OS assign a free port" - and is accepted.
disconnect()
Closes the socket and sets client to null. No-op with a log line if client is not set.
disconnect(): voidisConnected()
Returns the underlying socket if bound, or null/undefined otherwise.
isConnected(): dgram.Socket | null | undefinedgetClient()
getClient(): dgram.Socket | null | undefinedhandleConnected() / handleData(opts) / handleClosed() / handleError(opts)
Default handlers used when the matching onConnected/onData/onClosed/onError option is omitted:
| Handler | Log level | Logged context |
|---|---|---|
handleConnected | info | host, port, multicastAddress |
handleClosed | info | host, port, multicastAddress |
handleData | info | host, port only - no multicastAddress |
handleError | error | host, port only - no multicastAddress |
handleConnected(): void
handleData(opts: { identifier: string; message: string | Buffer; remoteInfo: dgram.RemoteInfo }): void
handleClosed(): void
handleError(opts: { identifier: string; error: Error }): voidTypes Reference
TFetcherVariant / TFetcherResponse / TFetcherWorker
type TFetcherVariant = 'node-fetch' | 'axios';
type TFetcherResponse<T extends TFetcherVariant> =
T extends 'node-fetch' ? Response : AxiosResponse;
type TFetcherWorker<T extends TFetcherVariant> =
T extends 'axios' ? AxiosInstance : typeof fetch;THttpMethod
type THttpMethod = ValueOf<typeof HTTP.Methods> | Uppercase<ValueOf<typeof HTTP.Methods>>;
// 'get' | 'post' | 'put' | 'patch' | 'delete' | 'head' | 'options' | 'query'
// | 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS' | 'QUERY'ITcpSocketServerOptions
interface ITcpSocketServerOptions<
SocketServerOptions extends ServerOpts = ServerOpts,
SocketServerType extends SocketServer = SocketServer,
SocketClientType extends SocketClient = SocketClient,
> {
scope?: string;
identifier: string;
serverOptions: Partial<SocketServerOptions>;
listenOptions: Partial<ListenOptions>;
authenticateOptions: { required: boolean; duration?: number };
extraEvents?: Record<
string,
(opts: { id: string; socket: SocketClientType; args: any }) => ValueOrPromise<void>
>;
createServerFn: (
options: Partial<SocketServerOptions>,
connectionListener: (socket: SocketClientType) => void,
) => SocketServerType;
onServerReady?: (opts: { server: SocketServerType }) => void;
onClientConnected?: (opts: { id: string; socket: SocketClientType }) => void;
onClientData?: (opts: { id: string; socket: SocketClientType; data: Buffer | string }) => void;
onClientClose?: (opts: { id: string; socket: SocketClientType }) => void;
onClientError?: (opts: { id: string; socket: SocketClientType; error: Error }) => void;
onServerError?: (opts: { server: SocketServerType; error: Error }) => void;
}onServerError fires when the underlying net/tls server emits 'error' - for example EADDRINUSE from a port already in use. Without this listener, the event is unhandled and takes the whole process down with it.
INetworkTcpClientProps
interface INetworkTcpClientProps<
SocketClientOptions extends PlainConnectionOptions | TlsConnectionOptions,
SocketClientType extends PlainSocketClient | TlsSocketClient,
> {
identifier: string;
scope?: string;
options: SocketClientOptions;
reconnect?: boolean;
maxRetry?: number;
encoding?: BufferEncoding;
createClientFn: (
options: SocketClientOptions,
connectionListener?: () => void,
) => SocketClientType;
onConnected?: (opts: { client: SocketClientType }) => ValueOrPromise<void>;
onData?: (opts: { identifier: string; message: string | Buffer }) => ValueOrPromise<void>;
onClosed?: (opts: { client: SocketClientType }) => void;
onError?: (error: any) => void;
}INetworkUdpClientProps
interface INetworkUdpClientProps {
identifier: string;
host?: string;
port: number;
reuseAddr?: boolean;
multicastAddress?: {
groups?: Array<string>;
interface?: string;
};
onConnected?: (opts: { identifier: string; host?: string; port: number }) => void;
onData?: (opts: {
identifier: string;
message: string | Buffer;
remoteInfo: dgram.RemoteInfo;
}) => void;
onClosed?: (opts: { identifier: string; host?: string; port: number }) => void;
onError?: (opts: { identifier: string; host?: string; port: number; error: Error }) => void;
onBind?: (opts: {
identifier: string;
socket: dgram.Socket;
host?: string;
port: number;
reuseAddr?: boolean;
multicastAddress?: { groups?: Array<string>; interface?: string };
}) => ValueOrPromise<void>;
}URL Safety
The guard for a url your application did not choose - a webhook target, an avatar link, an import source. Without it a request can be steered at 169.254.169.254 and the cloud credentials behind it.
It ships as part of the network module, so the root barrel carries it:
import { UrlPolicy, UrlIngest, UrlSchemes, UrlSafetyDefaults, isUrlRefusedError } from '@venizia/ignis-helpers';
import type { IUrlSafetyPolicy } from '@venizia/ignis-helpers';Two halves, split on purpose
| Class | Needs | Decides |
|---|---|---|
UrlPolicy | nothing but string and number work | The shape of a url: scheme, host allow-list, and a literal IP address |
UrlIngest | node:dns, and fetch | What a hostname resolves to, and the request itself |
UrlPolicy is re-exported from the browser-pure @venizia/ignis-helpers/core subpath. It never imports ingest.ts, and make purity fails the build if that changes. UrlIngest stays out of /core.
The split has a consequence you must know: a hostname that resolves to a private address passes UrlPolicy on purpose. Only UrlIngest catches that. Call UrlPolicy alone only where DNS does not exist.
IUrlSafetyPolicy
Every field is per call. An application that must reach plain http on one route says so at that call site, rather than weakening a constant for everyone.
| Option | Type | Default | Meaning |
|---|---|---|---|
allowedSchemes | string[] | ['https:'] | The schemes a guarded fetch will speak. file: and gopher: are the classic pivots, and neither is reachable |
allowPrivateAddress | boolean | false | Turning it on in a path that takes user input re-opens the hole this guard exists to close |
allowedHosts | string[] | - | When set, nothing outside the list passes, whatever it resolves to |
maxRedirects | number | 3 | Hops allowed. Each one is re-checked |
timeout | IDuration | 10 seconds | A slow host holds a worker slot for exactly this long |
maxBytes | number | 10485760 (10 MB) | The read stops and cancels the moment the body crosses it |
timeout takes an IDuration, not a number of milliseconds. UrlSafetyDefaults holds the raw values: ALLOWED_SCHEMES, ALLOW_PRIVATE_ADDRESS, MAX_REDIRECTS, TIMEOUT_MILLISECONDS, MAX_BYTES. UrlSchemes holds HTTP ('http:') and HTTPS ('https:'), with the trailing colon that URL.protocol returns.
UrlPolicy
| Method | Signature | Does |
|---|---|---|
assertSafeUrl | (opts: { url: string; policy?: IUrlSafetyPolicy }): URL | Parses and checks the url. Returns the parsed URL, or throws |
isNonPublicAddress | (opts: { address: string }): boolean | true for any address an untrusted url must never reach |
readHostAddress | (opts: { hostname: string }): string | Strips the brackets off an IPv6 literal - [::1] becomes ::1 |
assertSafeUrl refuses, in this order: a malformed url, a scheme outside allowedSchemes, a host outside allowedHosts, and a literal address that is not public.
isNonPublicAddress fails closed. An address it cannot parse counts as non-public, because a guard that fails open on a string it does not understand is not a guard. It covers loopback, the private v4 ranges, carrier-grade NAT, link-local (169.254.0.0/16, the metadata address), benchmarking, multicast, and the v6 equivalents. It also unwraps both spellings of an IPv4-mapped v6 address, because new URL rewrites ::ffff:169.254.169.254 into the hex form and a dotted-quad check alone would read green.
UrlIngest
| Method | Signature | Does |
|---|---|---|
assertPublicHost | (opts: { hostname: string; policy? }): Promise<void> | Resolves the host and rejects if any address is non-public |
assertReachableUrl | (opts: { url: string; policy? }): Promise<URL> | Both halves, in the order they must run: shape, then network |
fetchGuarded | (opts: { url: string; policy?; headers? }): Promise<Response> | A GET an untrusted url cannot steer inside your network |
readCappedBody | (opts: { response: Response; policy? }): Promise<Buffer> | Reads a body, cancelling the moment it crosses maxBytes |
capStream | (opts: { source: ReadableStream<Uint8Array>; policy? }): ReadableStream<Uint8Array> | Enforces maxBytes on a stream you pipe onward instead of buffering |
assertPublicHost checks every resolved address, not the first. A host can publish one public A record beside an internal one.
fetchGuarded follows redirects by hand, with redirect: 'manual', so every hop goes back through assertReachableUrl. Letting fetch follow them would land on an internal host with no second look. It returns the body unread, so you choose between readCappedBody and capStream. Nothing is buffered for you.
readCappedBody checks content-length first because it is free, then counts anyway, because the header may be absent or may lie.
const response = await UrlIngest.fetchGuarded({
url: userSuppliedUrl,
policy: { maxBytes: 2 * 1024 * 1024, timeout: { value: 5, unit: 'second' } },
});
const body = await UrlIngest.readCappedBody({ response, policy: { maxBytes: 2 * 1024 * 1024 } });WARNING
One window stays open. The address can change between the DNS check and the connect - DNS rebinding. Closing it means connecting to the resolved address and setting Host by hand, which no fetch API exposes. Treat fetchGuarded as a strong barrier, not a proof.
Telling a refusal from a retryable failure
Every refusal carries the same code, core.url_safety.url_refused, from the UrlSafetyErrors.URL_REFUSED definition. isUrlRefusedError({ error }) tests it.
A refused url is refused forever. A bad scheme, a non-public address, a host outside the allow-list, a malformed url, too many hops, an oversized body - retrying any of those repeats the same rejection and turns your retry loop into a repeat scanner. Drop the work instead.
A timeout, a DNS failure, or a 5xx answers false. Those are worth another attempt, and fetchGuarded raises them as a plain error with no refusal code.
import { isUrlRefusedError, RetryHelper, UrlIngest } from '@venizia/ignis-helpers';
await RetryHelper.executeWithRetry({
operation: 'fetchAvatar',
execution: () => UrlIngest.fetchGuarded({ url: userSuppliedUrl }),
shouldRetry: context => !isUrlRefusedError({ error: context.error }),
});See also
- Network overview - introduction and the most common tasks