Making HTTP Requests with HyperBEAM
A beginner's guide to HTTP communication in HyperBEAM
What You'll Learn
By the end of this tutorial, you'll understand:
- HTTP Core — Making GET and POST requests to remote nodes
- Connection Pooling — Efficiently managing persistent connections
- Remote Resolution — Executing messages on remote HyperBEAM nodes
- Gateway Client — Fetching data from Arweave via GraphQL
- Service Discovery — Finding nodes in the AO network
- How these pieces form the HTTP communication layer
No prior HTTP internals knowledge required. Basic Erlang helps, but we'll explain as we go.
The Big Picture
HyperBEAM nodes communicate over HTTP. Whether you're fetching data from Arweave, sending messages to other nodes, or uploading bundles, HTTP is the transport layer. The HTTP client stack handles connection pooling, codec negotiation, parallel requests, and response parsing.
Here's the mental model:
Your Code → hb_http → hb_http_client → Gun/HTTPC → Remote Node
↓ ↓ ↓ ↓
Request Codec Pool/Retry NetworkThink of it like a mail delivery service:
- hb_http = The post office (accepts your package, handles paperwork)
- hb_http_client = The delivery trucks (maintains routes, handles logistics)
- hb_client = Your local mailman (knows common destinations)
- hb_gateway_client = International shipping (Arweave network access)
- hb_router = Address lookup (finds where services live)
Let's build each piece.
Part 1: Simple HTTP Requests
📖 Reference: hb_http
hb_http is your primary interface for HTTP communication. It converts HyperBEAM messages to HTTP requests and parses responses back into messages.
Making a GET Request
%% Simple GET to a node
Node = <<"http://localhost:8421">>,
{ok, Response} = hb_http:get(Node, #{}).
%% GET with a path
{ok, Response} = hb_http:get(Node, <<"/status">>, #{}).
%% GET with a message containing path
Message = #{<<"path">> => <<"/data/123">>},
{ok, Response} = hb_http:get(Node, Message, #{}).The response is a map containing:
<<"status">>— HTTP status code (200, 404, etc.)<<"body">>or<<"data">>— Response content- Headers as key-value pairs
Making a POST Request
%% POST to a path
Node = <<"http://localhost:8421">>,
{ok, Response} = hb_http:post(Node, <<"/submit">>, #{}).
%% POST with a message body
Message = #{
<<"key">> => <<"value">>,
<<"data">> => <<"payload">>
},
{ok, Response} = hb_http:post(Node, <<"/api/create">>, Message, #{}).Generic Request
For full control, use hb_http:request/5:
%% Generic request with method, peer, path, message, options
{ok, Response} = hb_http:request(
<<"GET">>, % Method
<<"http://localhost:8421">>, % Peer
<<"/api/data">>, % Path
#{}, % Message body
#{} % Options
).
%% Response status is categorized
case hb_http:request(<<"GET">>, Node, Path, #{}, #{}) of
{ok, Res} -> handle_success(Res); % 2xx-3xx
{created, Res} -> handle_created(Res); % 201
{error, Res} -> handle_client_error(Res); % 4xx
{failure, Res} -> handle_server_error(Res) % 5xx
end.Quick Reference: hb_http Functions
| Function | What it does |
|---|---|
hb_http:get(Node, Opts) | GET request to node root |
hb_http:get(Node, Path, Opts) | GET request to specific path |
hb_http:post(Node, Path, Opts) | POST request to path |
hb_http:post(Node, Path, Msg, Opts) | POST request with body |
hb_http:request(Method, Peer, Path, Msg, Opts) | Generic request |
Part 2: Connection Pooling
📖 Reference: hb_http_client
Making HTTP connections is expensive. hb_http_client maintains a pool of persistent connections to remote peers, reusing them across requests.
How It Works
The client is a gen_server that:
- Maintains a map of
Peer → Connection PID - Creates new connections on demand
- Reuses existing connections when available
- Automatically reconnects on failure
%% Under the hood, hb_http uses hb_http_client:req/2
Args = #{
peer => <<"http://example.com">>,
path => <<"/api/data">>,
method => <<"GET">>,
headers => #{},
body => <<>>
},
{ok, Status, Headers, Body} = hb_http_client:req(Args, #{}).Client Selection
HyperBEAM supports two HTTP clients:
%% Use Gun (default) - connection pooling, HTTP/2 support
{ok, _, _, _} = hb_http_client:req(Args, #{http_client => gun}).
%% Use HTTPC (fallback) - simpler, built-in OTP client
{ok, _, _, _} = hb_http_client:req(Args, #{http_client => httpc}).Gun (default):
- Connection pooling
- HTTP/2 support
- Streaming responses
- Automatic reconnection
HTTPC (fallback):
- Stateless
- Simpler error handling
- Built into Erlang/OTP
- Good for debugging
Request Arguments
Args = #{
peer => <<"http://example.com:8080">>, % Full URL with scheme
path => <<"/api/v1/data">>, % Request path
method => <<"POST">>, % HTTP method
headers => #{
<<"content-type">> => <<"application/json">>,
<<"authorization">> => <<"Bearer token">>
},
body => <<"{\"key\":\"value\"}">>,
limit => 1048576, % Max response size (1MB)
is_peer_request => true % Mark as peer communication
}.Quick Reference: hb_http_client Functions
| Function | What it does |
|---|---|
hb_http_client:start_link(Opts) | Start the client gen_server |
hb_http_client:req(Args, Opts) | Execute HTTP request |
Part 3: Supervision
📖 Reference: hb_http_client_sup
The HTTP client runs under an OTP supervisor for reliability.
Supervision Tree
hb_http_client_sup (one_for_one)
└── hb_http_client (gen_server, permanent)Starting the Supervisor
%% Start HTTP client supervisor with options
Opts = [#{
http_client => gun,
port => 8734,
prometheus => true
}],
{ok, Pid} = hb_http_client_sup:start_link(Opts).
%% Check if client is running
Children = supervisor:which_children(hb_http_client_sup).
%% [{hb_http_client, <0.123.0>, worker, [hb_http_client]}]Restart Policy
The supervisor uses:
- Strategy:
one_for_one— restart only failed child - Max Restarts: 5 in 10 seconds
- Shutdown Timeout: 10s (debug) or 30s (production)
Part 4: Remote Node Communication
📖 Reference: hb_client
hb_client provides higher-level functions for common operations: remote resolution, route management, and Arweave uploads.
Resolving Messages on Remote Nodes
%% Resolve a message pair on a remote HyperBEAM node
Node = <<"http://localhost:8421">>,
Msg1 = #{
<<"device">> => <<"Router@1.0">>,
<<"key">> => <<"value">>
},
Msg2 = #{
<<"path">> => <<"/routes">>,
<<"method">> => <<"GET">>
},
{ok, Result} = hb_client:resolve(Node, Msg1, Msg2, #{}).Managing Routes
%% Get routes from a remote node
{ok, Routes} = hb_client:routes(<<"http://localhost:8421">>, #{}).
%% Add a new route
Route = #{
<<"template">> => <<"/my-endpoint">>,
<<"node">> => <<"http://handler.ao.computer">>
},
{ok, _} = hb_client:add_route(<<"http://localhost:8421">>, Route, #{}).Uploading to Arweave
%% Get wallet for signing
Wallet = hb:wallet(),
%% Create and commit a message
Msg = #{
<<"data">> => <<"Hello, Arweave!">>,
<<"Content-Type">> => <<"text/plain">>
},
Committed = hb_message:commit(Msg, Wallet, <<"ans104@1.0">>),
%% Upload to ANS-104 bundler
{ok, Result} = hb_client:upload(Committed, #{}, <<"ans104@1.0">>).Getting Arweave Block Info
%% Fetch current block information
{Timestamp, Height, Hash} = hb_client:arweave_timestamp().
%% Timestamp = Unix timestamp
%% Height = Block height
%% Hash = 43-byte block hashQuick Reference: hb_client Functions
| Function | What it does |
|---|---|
hb_client:resolve(Node, Msg1, Msg2, Opts) | Execute message pair on remote node |
hb_client:routes(Node, Opts) | Get routes from node |
hb_client:add_route(Node, Route, Opts) | Add route to node |
hb_client:arweave_timestamp() | Get current Arweave block info |
hb_client:upload(Msg, Opts) | Upload to all commitment devices |
hb_client:upload(Msg, Opts, Device) | Upload to specific device |
Part 5: Arweave Gateway Client
📖 Reference: hb_gateway_client
hb_gateway_client provides access to Arweave data via the GraphQL API. It fetches transaction metadata and raw data, converting them to HyperBEAM messages.
Reading Data Items
%% Read a complete data item by ID
ID = <<"uJBApOt4ma3pTfY6Z4xmknz5vAasup4KcGX7FJ0Of8w">>,
{ok, Message} = hb_gateway_client:read(ID, #{}).
%% Message contains all metadata and data
Data = maps:get(<<"data">>, Message),
Tags = maps:get(<<"tags">>, Message).GraphQL Queries
%% Simple query
Query = <<"query { transactions(first: 10) { edges { node { id } } } }">>,
{ok, Response} = hb_gateway_client:query(Query, #{}).
%% Query with variables
Query = <<"query($ids: [ID!]!) {
transactions(ids: $ids) {
edges { node { id tags { name value } } }
}
}">>,
Variables = #{<<"ids">> => [ID1, ID2, ID3]},
{ok, Response} = hb_gateway_client:query(Query, Variables, #{}).Fetching Raw Data
%% Get raw binary data for a transaction
{ok, Binary} = hb_gateway_client:data(ID, #{}).Quick Reference: hb_gateway_client Functions
| Function | What it does |
|---|---|
hb_gateway_client:read(ID, Opts) | Get complete data item |
hb_gateway_client:data(ID, Opts) | Get raw binary data |
hb_gateway_client:query(Query, Opts) | Execute GraphQL query |
hb_gateway_client:query(Query, Vars, Opts) | Query with variables |
hb_gateway_client:scheduler_location(Addr, Opts) | Find scheduler |
hb_gateway_client:item_spec() | Get GraphQL fragment for items |
Part 6: Service Discovery
📖 Reference: hb_router
hb_router locates services in the AO network using URL-based routing.
Finding Services
%% Find a scheduler node
{ok, URL} = hb_router:find(<<"scheduler">>, ProcessID).
%% Returns: {ok, <<"https://scheduler.ao.computer">>}
%% Find specific compute unit by address
{ok, CU} = hb_router:find(<<"compute-unit">>, TaskID, <<"primary">>).
%% Find any service with wildcard
{ok, MU} = hb_router:find(<<"messenger">>, MsgID, '_').Configuration
Services are configured in the nodes map:
%% Configuration structure
#{
nodes => #{
<<"scheduler">> => #{
'_' => <<"https://scheduler.ao.computer">>
},
<<"compute-unit">> => #{
<<"primary">> => <<"https://cu1.ao.computer">>,
<<"backup">> => <<"https://cu2.ao.computer">>
}
}
}Service Types
| Service Type | Description | Typical Address |
|---|---|---|
<<"scheduler">> | Scheduler Units (SU) | https://scheduler.ao.computer |
<<"compute-unit">> | Compute Units (CU) | https://cu.ao.computer |
<<"messenger">> | Messenger Units (MU) | https://mu.ao.computer |
<<"gateway">> | Arweave Gateway | https://arweave.net |
<<"bundler">> | ANS-104 Bundler | https://up.arweave.net |
Part 7: Complete Test
Save this as src/test/test_hb6.erl and run with rebar3 eunit --module=test_hb6:
-module(test_hb6).
-include_lib("eunit/include/eunit.hrl").
-include("include/hb.hrl").
%% Run with: rebar3 eunit --module=test_hb6
%% ============================================================================
%% Test Helpers
%% ============================================================================
%% Helper to start a mock HTTP server
start_mock_server(ResponseBody, StatusCode) ->
{ok, ListenSock} = gen_tcp:listen(0, [binary, {active, false}, {reuseaddr, true}]),
{ok, Port} = inet:port(ListenSock),
spawn(fun() -> mock_server_loop(ListenSock, ResponseBody, StatusCode) end),
{Port, ListenSock}.
mock_server_loop(ListenSock, ResponseBody, StatusCode) ->
case gen_tcp:accept(ListenSock, 5000) of
{ok, Sock} ->
{ok, _Request} = gen_tcp:recv(Sock, 0, 5000),
ContentLength = integer_to_binary(byte_size(ResponseBody)),
Response = <<"HTTP/1.1 ", (integer_to_binary(StatusCode))/binary, " OK\r\n",
"Content-Type: application/json\r\n",
"Content-Length: ", ContentLength/binary, "\r\n\r\n",
ResponseBody/binary>>,
gen_tcp:send(Sock, Response),
gen_tcp:close(Sock),
mock_server_loop(ListenSock, ResponseBody, StatusCode);
{error, timeout} ->
gen_tcp:close(ListenSock);
{error, _} ->
gen_tcp:close(ListenSock)
end.
%% ============================================================================
%% hb_http Tests
%% ============================================================================
hb_http_start_test() ->
?assertEqual(ok, hb_http:start()),
?debugFmt("hb_http:start() OK", []).
hb_http_get_test() ->
{Port, Sock} = start_mock_server(<<"{\"status\":\"ok\"}">>, 200),
URL = iolist_to_binary([<<"http://127.0.0.1:">>, integer_to_binary(Port)]),
{ok, Res} = hb_http:get(URL, <<"/test">>, #{http_client => httpc}),
?assert(is_map(Res)),
?assertEqual(200, maps:get(<<"status">>, Res)),
?debugFmt("GET request returned status 200", []),
gen_tcp:close(Sock).
hb_http_post_test() ->
{Port, Sock} = start_mock_server(<<"{\"created\":true}">>, 200),
URL = iolist_to_binary([<<"http://127.0.0.1:">>, integer_to_binary(Port)]),
Message = #{<<"key">> => <<"value">>},
{ok, Res} = hb_http:post(URL, <<"/submit">>, Message, #{http_client => httpc}),
?assert(is_map(Res)),
?assertEqual(200, maps:get(<<"status">>, Res)),
?debugFmt("POST request with message body OK", []),
gen_tcp:close(Sock).
hb_http_request_status_codes_test() ->
?debugFmt("=== Testing HTTP Status Code Handling ===", []),
%% Test 200 OK
{Port1, Sock1} = start_mock_server(<<"{\"ok\":true}">>, 200),
URL1 = iolist_to_binary([<<"http://127.0.0.1:">>, integer_to_binary(Port1)]),
{ok, _} = hb_http:request(<<"GET">>, URL1, <<"/">>, #{}, #{http_client => httpc}),
?debugFmt("200 OK → {ok, _}", []),
gen_tcp:close(Sock1),
%% Test 404 Error
{Port2, Sock2} = start_mock_server(<<"{\"error\":\"not found\"}">>, 404),
URL2 = iolist_to_binary([<<"http://127.0.0.1:">>, integer_to_binary(Port2)]),
{error, Res2} = hb_http:request(<<"GET">>, URL2, <<"/missing">>, #{}, #{http_client => httpc}),
?assertEqual(404, maps:get(<<"status">>, Res2)),
?debugFmt("404 Not Found → {error, _}", []),
gen_tcp:close(Sock2),
%% Test 500 Failure
{Port3, Sock3} = start_mock_server(<<"{\"error\":\"internal\"}">>, 500),
URL3 = iolist_to_binary([<<"http://127.0.0.1:">>, integer_to_binary(Port3)]),
{failure, Res3} = hb_http:request(<<"GET">>, URL3, <<"/fail">>, #{}, #{http_client => httpc}),
?assertEqual(500, maps:get(<<"status">>, Res3)),
?debugFmt("500 Internal Error → {failure, _}", []),
gen_tcp:close(Sock3).
%% ============================================================================
%% hb_http_client Tests
%% ============================================================================
hb_http_client_req_test() ->
{ok, ListenSock} = gen_tcp:listen(0, [binary, {active, false}, {reuseaddr, true}]),
{ok, Port} = inet:port(ListenSock),
spawn(fun() ->
{ok, Sock} = gen_tcp:accept(ListenSock),
{ok, _Request} = gen_tcp:recv(Sock, 0, 5000),
Response = <<"HTTP/1.1 200 OK\r\nContent-Length: 13\r\n\r\n{\"test\":true}">>,
gen_tcp:send(Sock, Response),
gen_tcp:close(Sock),
gen_tcp:close(ListenSock)
end),
Args = #{
peer => iolist_to_binary([<<"http://127.0.0.1:">>, integer_to_binary(Port)]),
path => <<"/api/test">>,
method => <<"GET">>,
headers => #{},
body => <<>>
},
Result = hb_http_client:req(Args, #{http_client => httpc}),
?assertMatch({ok, 200, _, _}, Result),
?debugFmt("hb_http_client:req/2 returned {ok, 200, _, _}", []).
hb_http_client_connection_refused_test() ->
Args = #{
peer => <<"http://127.0.0.1:59999">>,
path => <<"/">>,
method => <<"GET">>,
headers => #{},
body => <<>>
},
Result = hb_http_client:req(Args, #{http_client => httpc}),
?assertMatch({error, _}, Result),
?debugFmt("Connection refused returns {error, _}", []).
%% ============================================================================
%% hb_http_client_sup Tests
%% ============================================================================
hb_http_client_sup_init_test() ->
Opts = [#{}],
{ok, {SupSpec, ChildSpecs}} = hb_http_client_sup:init(Opts),
?assertMatch({one_for_one, 5, 10}, SupSpec),
?debugFmt("Supervisor spec: one_for_one, 5 restarts in 10 seconds", []),
?assertEqual(1, length(ChildSpecs)),
[ChildSpec] = ChildSpecs,
?assertMatch({hb_http_client, {hb_http_client, start_link, _}, permanent, _, worker, [hb_http_client]}, ChildSpec),
?debugFmt("Child spec: hb_http_client, permanent worker", []).
%% ============================================================================
%% hb_client Tests
%% ============================================================================
hb_client_resolve_test() ->
{Port, Sock} = start_mock_server(<<"{\"routes\":[]}">>, 200),
Node = iolist_to_binary([<<"http://127.0.0.1:">>, integer_to_binary(Port)]),
Msg1 = #{<<"device">> => <<"Router@1.0">>},
Msg2 = #{<<"path">> => <<"routes">>, <<"method">> => <<"GET">>},
{ok, Result} = hb_client:resolve(Node, Msg1, Msg2, #{http_client => httpc}),
?assert(is_map(Result)),
?assertEqual(200, maps:get(<<"status">>, Result)),
?debugFmt("hb_client:resolve/4 executed remote message pair", []),
gen_tcp:close(Sock).
hb_client_routes_test() ->
{Port, Sock} = start_mock_server(<<"{\"routes\":[\"/api\",\"/data\"]}">>, 200),
Node = iolist_to_binary([<<"http://127.0.0.1:">>, integer_to_binary(Port)]),
{ok, Routes} = hb_client:routes(Node, #{http_client => httpc}),
?assert(is_map(Routes)),
?debugFmt("hb_client:routes/2 fetched route list", []),
gen_tcp:close(Sock).
hb_client_add_route_test() ->
{Port, Sock} = start_mock_server(<<"{\"success\":true}">>, 200),
Node = iolist_to_binary([<<"http://127.0.0.1:">>, integer_to_binary(Port)]),
Route = #{
<<"template">> => <<"/test-path">>,
<<"node">> => Node
},
{ok, Result} = hb_client:add_route(Node, Route, #{http_client => httpc}),
?assert(is_map(Result)),
?debugFmt("hb_client:add_route/3 added route", []),
gen_tcp:close(Sock).
hb_client_arweave_timestamp_test() ->
{Timestamp, Height, Hash} = hb_client:arweave_timestamp(),
?assert(is_integer(Timestamp)),
?assert(is_integer(Height)),
?assert(is_binary(Hash)),
?debugFmt("Arweave timestamp: ~p, height: ~p", [Timestamp, Height]).
hb_client_upload_test() ->
%% Create a signed ANS-104 item
Serialized = ar_bundles:serialize(
ar_bundles:sign_item(#tx{
data = <<"TEST DATA">>,
tags = [{<<"Content-Type">>, <<"text/plain">>}]
}, hb:wallet())
),
Result = hb_client:upload(Serialized, #{}, <<"ans104@1.0">>),
?assertMatch({ok, _}, Result),
?debugFmt("hb_client:upload/3 uploaded ANS-104 item", []).
%% ============================================================================
%% hb_gateway_client Tests
%% ============================================================================
hb_gateway_client_item_spec_test() ->
Spec = hb_gateway_client:item_spec(),
?assert(is_binary(Spec)),
?assertNotEqual(nomatch, binary:match(Spec, <<"id">>)),
?assertNotEqual(nomatch, binary:match(Spec, <<"anchor">>)),
?assertNotEqual(nomatch, binary:match(Spec, <<"signature">>)),
?assertNotEqual(nomatch, binary:match(Spec, <<"tags">>)),
?debugFmt("item_spec contains required GraphQL fields", []).
hb_gateway_client_query_test_() ->
{timeout, 30, fun() ->
_Node = hb_http_server:start_node(#{}),
Query = <<"query { transactions(first: 1) { edges { node { id } } } }">>,
Result = hb_gateway_client:query(Query, #{}),
?assert(is_tuple(Result)),
?debugFmt("GraphQL query executed", [])
end}.
hb_gateway_client_read_test_() ->
{timeout, 30, fun() ->
_Node = hb_http_server:start_node(#{}),
ID = <<"BOogk_XAI3bvNWnxNxwxmvOfglZt17o4MOVAdPNZ_ew">>,
case hb_gateway_client:read(ID, #{}) of
{ok, Msg} ->
?assert(is_map(Msg)),
?debugFmt("Read data item from gateway", []);
{error, Reason} ->
?debugFmt("Gateway unavailable: ~p", [Reason])
end
end}.
%% ============================================================================
%% Complete Workflow Test
%% ============================================================================
complete_workflow_test() ->
?debugFmt("=== Complete HTTP Workflow Test ===", []),
%% 1. Start HTTP infrastructure
ok = hb_http:start(),
?debugFmt("1. HTTP infrastructure started", []),
%% 2. Create mock server simulating a HyperBEAM node
{Port, Sock} = start_mock_server(<<"{\"device\":\"test@1.0\",\"result\":\"success\"}">>, 200),
Node = iolist_to_binary([<<"http://127.0.0.1:">>, integer_to_binary(Port)]),
?debugFmt("2. Mock node started at ~s", [Node]),
%% 3. Make GET request
{ok, GetResp} = hb_http:get(Node, <<"/status">>, #{http_client => httpc}),
?assertEqual(200, maps:get(<<"status">>, GetResp)),
?debugFmt("3. GET /status returned 200", []),
%% 4. Make POST request with message
Message = #{
<<"action">> => <<"test">>,
<<"data">> => <<"hello">>
},
{ok, PostResp} = hb_http:post(Node, <<"/api/submit">>, Message, #{http_client => httpc}),
?assertEqual(200, maps:get(<<"status">>, PostResp)),
?debugFmt("4. POST /api/submit returned 200", []),
%% 5. Test hb_client resolve
Msg1 = #{<<"device">> => <<"Router@1.0">>},
Msg2 = #{<<"path">> => <<"/routes">>, <<"method">> => <<"GET">>},
{ok, ResolveResp} = hb_client:resolve(Node, Msg1, Msg2, #{http_client => httpc}),
?assert(is_map(ResolveResp)),
?debugFmt("5. Remote resolve completed", []),
%% 6. Get Arweave timestamp
{Timestamp, Height, Hash} = hb_client:arweave_timestamp(),
?assert(is_integer(Timestamp)),
?assert(is_integer(Height)),
?assert(is_binary(Hash)),
?debugFmt("6. Arweave timestamp: ~p at height ~p", [Timestamp, Height]),
%% 7. Test gateway client item spec
Spec = hb_gateway_client:item_spec(),
?assert(is_binary(Spec)),
?debugFmt("7. Gateway item_spec retrieved (~p bytes)", [byte_size(Spec)]),
%% 8. Upload to Arweave
Serialized = ar_bundles:serialize(
ar_bundles:sign_item(#tx{
data = <<"Workflow test data">>,
tags = [{<<"App">>, <<"test_hb6">>}]
}, hb:wallet())
),
{ok, UploadResult} = hb_client:upload(Serialized, #{}, <<"ans104@1.0">>),
?debugFmt("8. Uploaded to Arweave: ~p", [UploadResult]),
gen_tcp:close(Sock),
?debugFmt("=== All workflow tests passed! ===", []).Run the Tests
rebar3 eunit --module=test_hb6Common Patterns
Pattern 1: Simple GET → Process Response
Node = <<"http://localhost:8421">>,
case hb_http:get(Node, <<"/api/data">>, #{}) of
{ok, #{<<"status">> := 200} = Resp} ->
process_data(maps:get(<<"body">>, Resp));
{ok, #{<<"status">> := Status}} ->
io:format("Unexpected status: ~p~n", [Status]);
{error, Reason} ->
io:format("Request failed: ~p~n", [Reason])
end.Pattern 2: POST with Signed Message
Wallet = hb:wallet(),
Message = #{
<<"action">> => <<"create">>,
<<"data">> => Payload
},
SignedMsg = hb_message:commit(Message, #{priv_wallet => Wallet}),
{ok, Response} = hb_http:post(Node, <<"/api/submit">>, SignedMsg, #{}).Pattern 3: Upload and Verify
%% Create, sign, and upload
Wallet = hb:wallet(),
Item = ar_bundles:sign_item(#tx{
data = Data,
tags = [{<<"Content-Type">>, <<"application/json">>}]
}, Wallet),
Serialized = ar_bundles:serialize(Item),
{ok, Result} = hb_client:upload(Serialized, #{}, <<"ans104@1.0">>).Pattern 4: Fallback Service Discovery
find_service(Type, ID) ->
case hb_router:find(Type, ID) of
{ok, URL} -> {ok, URL};
{error, _} ->
%% Try wildcard default
hb_router:find(Type, ID, '_')
end.What's Next?
You now understand the HTTP client stack:
| Layer | Module | Key Functions |
|---|---|---|
| High-Level | hb_client | resolve, routes, upload |
| Gateway | hb_gateway_client | read, query, data |
| Core HTTP | hb_http | get, post, request |
| Connection Pool | hb_http_client | req |
| Supervision | hb_http_client_sup | OTP supervisor |
| Discovery | hb_router | find |
Going Further
- Codec Devices — Learn how messages are serialized (
dev_codec_*modules) - Message System — Understand HyperBEAM messages (hb_message)
- Build Devices — Create custom HTTP-based devices (Book)
Quick Reference Card
📖 Reference: hb_http | hb_http_client | hb_client | hb_gateway_client | hb_router
%% === SIMPLE REQUESTS ===
{ok, Resp} = hb_http:get(Node, Path, Opts).
{ok, Resp} = hb_http:post(Node, Path, Message, Opts).
{Status, Resp} = hb_http:request(Method, Peer, Path, Msg, Opts).
%% === LOW-LEVEL CLIENT ===
Args = #{peer => URL, path => Path, method => Method, headers => #{}, body => <<>>}.
{ok, Status, Headers, Body} = hb_http_client:req(Args, Opts).
%% === REMOTE RESOLUTION ===
{ok, Result} = hb_client:resolve(Node, Msg1, Msg2, Opts).
{ok, Routes} = hb_client:routes(Node, Opts).
{ok, _} = hb_client:add_route(Node, Route, Opts).
%% === ARWEAVE UPLOAD ===
Committed = hb_message:commit(Msg, Wallet, <<"ans104@1.0">>).
{ok, Result} = hb_client:upload(Committed, Opts, <<"ans104@1.0">>).
{Timestamp, Height, Hash} = hb_client:arweave_timestamp().
%% === GATEWAY CLIENT ===
{ok, Message} = hb_gateway_client:read(ID, Opts).
{ok, Binary} = hb_gateway_client:data(ID, Opts).
{ok, Response} = hb_gateway_client:query(Query, Variables, Opts).
%% === SERVICE DISCOVERY ===
{ok, URL} = hb_router:find(ServiceType, ID).
{ok, URL} = hb_router:find(ServiceType, ID, Address).Resources
HyperBEAM Documentation- hb_http Reference
- hb_http_client Reference
- hb_client Reference
- hb_gateway_client Reference
- hb_router Reference
- Full Reference
- Arweave GraphQL — Gateway query API
- ANS-104 — Bundled Data v2.0
- Gun HTTP Client — Erlang HTTP/2 client
- HTTPC — Built-in OTP HTTP client