dev_router.erl - Message Routing Device
Overview
Purpose: Route outbound messages to appropriate network recipients via HTTP
Module: dev_router
Device Name: router@1.0
Load Balancing: Supports multiple distribution strategies
This device routes outbound messages from a node to their appropriate network recipients. Routes are defined in the node's configuration as a precedence-ordered list. The device supports multiple load distribution strategies including Random, By-Base, By-Weight, and Nearest.
Supported Operations
- Route Discovery: Find the appropriate route for a message
- Route Registration: Register routes with remote router nodes
- Route Management: Get/add routes via API
- Request Preprocessing: Re-route requests to remote peers
Dependencies
- HyperBEAM:
hb_ao,hb_http,hb_cache,hb_maps,hb_opts,hb_util,hb_message,hb_path,hb_crypto,hb_singleton,hb_http_server,hb_name - Related:
dev_relay(message relay) - Includes:
include/hb.hrl - Testing:
eunit
Public Functions Overview
%% Device Info
-spec info(Msg) -> InfoMap.
-spec info(Msg1, Msg2, Opts) -> {ok, InfoBody}.
%% Route Operations
-spec routes(M1, M2, Opts) -> {ok, Routes} | {ok, <<"Route added.">>}.
-spec route(Msg, Opts) -> {ok, Node} | {error, no_matches}.
-spec route(Base, Msg, Opts) -> {ok, Node} | {error, no_matches}.
%% Route Matching
-spec match(Base, Req, Opts) -> {ok, Route} | {error, no_matching_route}.
%% Route Registration
-spec register(M1, M2, Opts) -> {ok, <<"Routes registered.">>}.
%% Request Preprocessing
-spec preprocess(Msg1, Msg2, Opts) -> {ok, RelayRequest}.Public Functions
1. info/1, info/3
-spec info(Msg) -> InfoMap
when
Msg :: map(),
InfoMap :: map().
-spec info(Msg1, Msg2, Opts) -> {ok, InfoBody}
when
Msg1 :: map(),
Msg2 :: map(),
Opts :: map(),
InfoBody :: map().Description: Returns device information and API documentation. The device exports: info, routes, route, match, register, preprocess.
-module(dev_router_info_test).
-include_lib("eunit/include/eunit.hrl").
info_exports_test() ->
Info = dev_router:info(#{}),
Exports = maps:get(exports, Info),
?assert(lists:member(<<"routes">>, Exports)),
?assert(lists:member(<<"route">>, Exports)),
?assert(lists:member(<<"match">>, Exports)).
info_api_test() ->
{ok, InfoBody} = dev_router:info(#{}, #{}, #{}),
?assertMatch(#{<<"api">> := _}, InfoBody),
?assertMatch(#{<<"version">> := <<"1.0">>}, InfoBody).2. routes/3
-spec routes(M1, M2, Opts) -> {ok, Routes} | {ok, <<"Route added.">>} | {error, Reason}
when
M1 :: map(),
M2 :: map(),
Opts :: map(),
Routes :: [map()],
Reason :: term().Description: Get all known routes (GET) or add a new route (POST). For POST requests, validates that the sender is an authorized operator before adding the route.
Test Code:-module(dev_router_routes_test).
-include_lib("eunit/include/eunit.hrl").
get_routes_test() ->
Node = hb_http_server:start_node(#{
force_signed => false,
routes => [
#{
<<"template">> => <<"*">>,
<<"node">> => <<"our_node">>,
<<"priority">> => 10
}
]
}),
{ok, Recvd} = hb_http:get(Node, <<"/~router@1.0/routes/1/node">>, #{}),
?assertEqual(<<"our_node">>, Recvd).
add_route_test() ->
Owner = ar_wallet:new(),
Node = hb_http_server:start_node(#{
force_signed => false,
routes => [
#{
<<"template">> => <<"/some/path">>,
<<"node">> => <<"old">>,
<<"priority">> => 10
}
],
operator => hb_util:encode(ar_wallet:to_address(Owner))
}),
{ok, _} = hb_http:post(
Node,
hb_message:commit(
#{
<<"path">> => <<"/~router@1.0/routes">>,
<<"template">> => <<"/some/new/path">>,
<<"node">> => <<"new">>,
<<"priority">> => 15
},
Owner
),
#{}
),
{ok, Recvd} = hb_http:get(Node, <<"/~router@1.0/routes/2/node">>, #{}),
?assertEqual(<<"new">>, Recvd).3. route/2, route/3
-spec route(Msg, Opts) -> {ok, Node} | {error, no_matches}.
-spec route(Base, Msg, Opts) -> {ok, Node} | {ok, Route} | {error, no_matches}
when
Base :: map() | undefined,
Msg :: map(),
Opts :: map(),
Node :: binary(),
Route :: map().Description: Find the appropriate route for a given message. If the route resolves to a single host+path, returns that directly. Otherwise, returns the matching route with a list of nodes. Supports load distribution strategies for multi-node routes.
Load Distribution Strategies:All- Return all nodes (default)Random- Distribute evenly, non-deterministicallyBy-Base- Route based on hashpath (same hashpath → same node)By-Weight- Route based on node weightsNearest- Route to nearest node by wallet distance
-module(dev_router_route_test).
-include_lib("eunit/include/eunit.hrl").
route_explicit_url_test() ->
Routes = [#{<<"template">> => <<"*">>, <<"node">> => <<"fallback">>}],
?assertEqual(
{ok, <<"https://google.com">>},
dev_router:route(#{<<"path">> => <<"https://google.com">>}, #{routes => Routes})
).
route_template_match_test() ->
Routes = [
#{<<"template">> => <<"/api/*">>, <<"node">> => <<"api-server">>},
#{<<"template">> => <<"*">>, <<"node">> => <<"fallback">>}
],
?assertMatch(
{ok, _},
dev_router:route(#{<<"path">> => <<"/api/users">>}, #{routes => Routes})
).
route_with_strategy_test() ->
Routes = [
#{
<<"template">> => <<"/.*">>,
<<"strategy">> => <<"Random">>,
<<"nodes">> => [
#{<<"prefix">> => <<"http://node1">>},
#{<<"prefix">> => <<"http://node2">>}
]
}
],
{ok, Result} = dev_router:route(#{<<"path">> => <<"/test">>}, #{routes => Routes}),
?assert(is_map(Result) orelse is_binary(Result)).4. match/3
-spec match(Base, Req, Opts) -> {ok, Route} | {error, no_matching_route}
when
Base :: map(),
Req :: map(),
Opts :: map(),
Route :: map().Description: Find the first matching template in a list of known routes. Matches based on path templates which can be maps or path regexes.
Test Code:-module(dev_router_match_test).
-include_lib("eunit/include/eunit.hrl").
match_route_test() ->
Routes = [#{<<"template">> => <<"/api/*">>, <<"node">> => <<"api">>}],
Base = #{<<"routes">> => Routes},
Req = #{<<"path">> => <<"/api/test">>},
{ok, Match} = dev_router:match(Base, Req, #{}),
?assertMatch(#{<<"node">> := <<"api">>}, Match).
match_no_route_test() ->
Routes = [#{<<"template">> => <<"/api/*">>, <<"node">> => <<"api">>}],
Base = #{<<"routes">> => Routes},
Req = #{<<"path">> => <<"/other/path">>},
?assertEqual({error, no_matching_route}, dev_router:match(Base, Req, #{})).5. register/3
-spec register(M1, M2, Opts) -> {ok, <<"Routes registered.">>}
when
M1 :: map(),
M2 :: map(),
Opts :: map().Description: Register routes with a remote router node. Reads route registration messages from router_opts.offered and posts them to the specified registration peer.
-module(dev_router_register_test).
-include_lib("eunit/include/eunit.hrl").
register_route_test() ->
Wallet = hb:wallet(),
RemoteRouter = hb_http_server:start_node(#{
operator => hb_util:encode(ar_wallet:to_address(Wallet))
}),
Opts = #{
priv_wallet => Wallet,
router_opts => #{
<<"offered">> => [
#{
<<"registration-peer">> => RemoteRouter,
<<"prefix">> => <<"http://localhost:8080">>,
<<"template">> => <<"/my/service/*">>
}
]
}
},
{ok, Result} = dev_router:register(#{}, #{}, Opts),
?assertEqual(<<"Routes registered.">>, Result).6. preprocess/3
-spec preprocess(Msg1, Msg2, Opts) -> {ok, RelayRequest}
when
Msg1 :: map(),
Msg2 :: map(),
Opts :: map(),
RelayRequest :: map().Description: Preprocess a request to check if it should be relayed to a different node. If a matching route is found, constructs a relay request to relay@1.0/call. If no match, executes locally or returns error based on router_preprocess_default option.
-module(dev_router_preprocess_test).
-include_lib("eunit/include/eunit.hrl").
preprocess_reroute_test() ->
Peer1 = hb_http_server:start_node(#{priv_wallet => W1 = ar_wallet:new()}),
Address1 = hb_util:human_id(ar_wallet:to_address(W1)),
Node = hb_http_server:start_node(Opts = #{
priv_wallet => ar_wallet:new(),
routes => [
#{
<<"template">> => <<"/.*/.*/.*">>,
<<"strategy">> => <<"Nearest">>,
<<"nodes">> => [#{<<"prefix">> => Peer1, <<"wallet">> => Address1}]
}
],
on => #{<<"request">> => #{<<"device">> => <<"relay@1.0">>}}
}),
{ok, Res} = hb_http:get(Node, <<"/~meta@1.0/info/address">>, Opts),
?assertEqual(Address1, Res).Route Configuration
Route Structure
#{
<<"template">> => TemplatePattern, % Path regex or message template
<<"node">> => NodeURL, % Single node URL (optional)
<<"nodes">> => [NodeMaps], % List of node configs (optional)
<<"strategy">> => Strategy, % Load distribution strategy
<<"choose">> => N, % Number of nodes to select
<<"priority">> => Priority % Route precedence (lower = higher)
}Node Configuration
#{
<<"prefix">> => <<"http://node.example.com">>,
<<"wallet">> => <<"encoded-wallet-address">>,
<<"weight">> => 1.0, % For By-Weight strategy
<<"opts">> => #{} % Additional options
}Path Transformation
Routes support path transformation via:
prefix- Prepend to pathsuffix- Append to pathmatch/with- Regex replacement
#{
<<"template">> => <<"/api/*">>,
<<"prefix">> => <<"https://backend.example.com">>,
<<"match">> => <<"^/api">>,
<<"with">> => <<"/v2">>
}Load Distribution Strategies
Random
Distributes requests evenly across all nodes using random selection:
#{
<<"strategy">> => <<"Random">>,
<<"nodes">> => [Node1, Node2, Node3]
}By-Base
Routes based on the message's hashpath, ensuring consistent routing for the same base message:
#{
<<"strategy">> => <<"By-Base">>,
<<"nodes">> => [Node1, Node2, Node3]
}By-Weight
Routes based on node weights (higher weight = more traffic):
#{
<<"strategy">> => <<"By-Weight">>,
<<"nodes">> => [
#{<<"prefix">> => Node1, <<"weight">> => 2.0},
#{<<"prefix">> => Node2, <<"weight">> => 1.0}
]
}Nearest
Routes to the node whose wallet address is closest to the message's hashpath (using 256-bit field distance):
#{
<<"strategy">> => <<"Nearest">>,
<<"nodes">> => [
#{<<"prefix">> => Node1, <<"wallet">> => Address1},
#{<<"prefix">> => Node2, <<"wallet">> => Address2}
]
}Common Patterns
%% Configure node with routes
Node = hb_http_server:start_node(#{
routes => [
#{
<<"template">> => <<"/scheduler/*">>,
<<"node">> => <<"https://scheduler.example.com">>
},
#{
<<"template">> => <<"/.*">>,
<<"strategy">> => <<"Nearest">>,
<<"nodes">> => ClusterNodes
}
]
}).
%% Find route for a message
{ok, Route} = dev_router:route(
#{<<"path">> => <<"/scheduler/slot">>},
#{routes => Routes}
).
%% Register with remote router
hb_ao:resolve(
#{<<"device">> => <<"router@1.0">>},
#{<<"path">> => <<"register">>},
#{router_opts => #{<<"offered">> => MyRoutes}}
).
%% Match route from base message
{ok, Match} = dev_router:match(
BaseMsg,
#{<<"path">> => <<"/api/endpoint">>},
Opts
).Configuration Options
| Option | Default | Description |
|---|---|---|
routes | [] | List of route configurations |
router_opts | #{} | Router-specific options |
router_opts.provider | - | Dynamic route provider message |
router_opts.registrar | - | Route registrar message |
router_opts.offered | - | Routes to register with peers |
router_preprocess_default | <<"local">> | Default action when no route matches |
route_owners | [operator] | Addresses authorized to add routes |
operator | - | Node operator address |
Request Preprocessing Flow
1. Extract request from Msg2
2. Load template routes from configuration
3. Convert message to HTTP request format
4. Match against routes
5. If no match:
- If router_preprocess_default = "local": execute locally
- If router_preprocess_default = "error": return 404
6. If match found:
- Check commit-request option
- Prepare relay request to relay@1.0/call
- Include user message with commitment
7. Return relay request bodyError Handling
Common Errors
No Matching Route:{error, no_matches}{ok, #{
<<"body">> => [#{
<<"status">> => 404,
<<"message">> => <<"No matching template found in the given routes.">>
}]
}}{error, not_authorized}throw({routes, routes_provider_failed, Error})References
- Relay Device -
dev_relay.erl - HTTP Client -
hb_http.erl - Message Format -
hb_message.erl - Path Utilities -
hb_path.erl - Template Matching -
hb_util:template_matches/3
Notes
- Precedence Order: Routes are matched in order; first match wins
- Explicit URLs: HTTP/HTTPS URLs bypass routing table entirely
- Dynamic Routes: Routes can be loaded dynamically via
provider - Registration: Routes can be registered with remote routers
- Field Distance: Nearest strategy uses 256-bit wraparound distance
- Weight Normalization: Weights are automatically normalized
- Choose N: Select multiple nodes from cluster
- Path Transformation: Supports prefix, suffix, and regex replacement
- Authorization: Route addition requires operator signature
- Caching: Routes are loaded fresh for each request
- Hashpath Routing: By-Base ensures cache locality
- Statistical Distribution: Random/By-Weight provide even distribution