Skip to content

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:

  1. HTTP Core — Making GET and POST requests to remote nodes
  2. Connection Pooling — Efficiently managing persistent connections
  3. Remote Resolution — Executing messages on remote HyperBEAM nodes
  4. Gateway Client — Fetching data from Arweave via GraphQL
  5. Service Discovery — Finding nodes in the AO network
  6. 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      Network

Think 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

FunctionWhat 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:

  1. Maintains a map of Peer → Connection PID
  2. Creates new connections on demand
  3. Reuses existing connections when available
  4. 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

FunctionWhat 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 hash

Quick Reference: hb_client Functions

FunctionWhat 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

FunctionWhat 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 TypeDescriptionTypical 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 Gatewayhttps://arweave.net
<<"bundler">>ANS-104 Bundlerhttps://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_hb6

Common 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:

LayerModuleKey Functions
High-Levelhb_clientresolve, routes, upload
Gatewayhb_gateway_clientread, query, data
Core HTTPhb_httpget, post, request
Connection Poolhb_http_clientreq
Supervisionhb_http_client_supOTP supervisor
Discoveryhb_routerfind

Going Further

  1. Codec Devices — Learn how messages are serialized (dev_codec_* modules)
  2. Message System — Understand HyperBEAM messages (hb_message)
  3. 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 Protocol Documentation Libraries