hb_http.erl - HyperBEAM HTTP Request/Reply Core
Overview
Purpose: Core HTTP request/reply functionality for HyperBEAM
Module: hb_http
Protocol: HTTP/HTTPS with message-based communication
Pattern: Message request → HTTP → Message response
This module provides HyperBEAM's primary HTTP functionality, converting messages to HTTP requests and processing responses back into message form. It handles both client requests (GET, POST) and server-side request processing, supporting multiple codec devices and serialization formats.
Core Responsibilities
- Client Requests: GET/POST operations to remote nodes
- Message Conversion: Transform HyperBEAM messages to/from HTTP
- Codec Support: Handle
httpsig@1.0,ans104@1.0, and structured messages - Cookie Management: Parse and normalize Set-Cookie headers
- Response Processing: Convert HTTP responses to appropriate message formats
- Multi-Request: Delegate to
hb_http_multifor route-based requests
Dependencies
- HyperBEAM:
hb_ao,hb_message,hb_util,hb_maps,hb_opts,hb_json,hb_singleton - Codecs:
dev_codec_ans104,dev_codec_structured,dev_codec_cookie,dev_codec_httpsig - HTTP Client:
hb_http_client,hb_http_multi - Arweave:
ar_bundles,ar_wallet - External:
cowboy_req,httpc - Includes:
include/hb.hrl
Public Functions Overview
%% Initialization
-spec start() -> ok.
%% Client Requests
-spec get(Node, Opts) -> {ok, Message} | {error, Reason}.
-spec get(Node, Path, Opts) -> {ok, Message} | {error, Reason}.
-spec get(Node, Message, Opts) -> {ok, Message} | {error, Reason}.
-spec post(Node, Path, Opts) -> {ok, Message} | {error, Reason}.
-spec post(Node, Message, Opts) -> {ok, Message} | {error, Reason}.
-spec post(Node, Path, Message, Opts) -> {ok, Message} | {error, Reason}.
%% Generic Request
-spec request(Message, Opts) -> {ok | created | error | failure, Result}.
-spec request(Method, Peer, Path, Opts) -> {ok | created | error | failure, Result}.
-spec request(Method, Peer, Path, Message, Opts) -> {ok | created | error | failure, Result}.
%% Message Conversion
-spec message_to_request(Message, Opts) -> {ok, Method, Peer, Path, Message, Opts}.
%% Server Processing
-spec reply(Request, StatusCode, Headers, Body) -> cowboy_req:req().
-spec accept_to_codec(Accept, Opts) -> CodecDevice.
%% Utilities
-spec req_to_tabm_singleton(Request, Message, Opts) -> Messages.Public Functions
1. start/0
-spec start() -> ok.Description: Initialize HTTP client by disabling keep-alive connections.
Test Code:-module(hb_http_test).
-include_lib("eunit/include/eunit.hrl").
%% 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.
%% Helper to create mock Cowboy request for req_to_tabm_singleton testing
mock_cowboy_req(Path, Qs, Headers, Method) ->
#{
method => Method,
path => Path,
qs => Qs,
headers => Headers,
host => <<"localhost">>,
port => 8080,
scheme => <<"http">>,
peer => {{127,0,0,1}, 12345},
version => 'HTTP/1.1',
ref => test_ref,
pid => self(),
streamid => 1
}.
start_test() ->
?assertEqual(ok, hb_http:start()).2. get/2, get/3
-spec get(Node, Opts) -> {ok, Message} | {error, Reason}
when
Node :: binary(),
Opts :: map(),
Message :: map(),
Reason :: term().
-spec get(Node, PathOrMessage, Opts) -> {ok, Message} | {error, Reason}
when
Node :: binary(),
PathOrMessage :: binary() | map(),
Opts :: map(),
Message :: map(),
Reason :: term().Description: Perform HTTP GET request to a remote node, returning deserialized message response.
Test Code:get_node_only_test() ->
%% get/2 - just node and opts, defaults to path "/"
{Port, Sock} = start_mock_server(<<"{\"root\":true}">>, 200),
URL = iolist_to_binary([<<"http://127.0.0.1:">>, integer_to_binary(Port)]),
{ok, Res} = hb_http:get(URL, #{http_client => httpc}),
?assert(is_map(Res)),
?assertEqual(200, maps:get(<<"status">>, Res)),
gen_tcp:close(Sock).
get_with_path_test() ->
%% get/3 with binary path
{Port, Sock} = start_mock_server(<<"{\"data\":123}">>, 200),
URL = iolist_to_binary([<<"http://127.0.0.1:">>, integer_to_binary(Port)]),
{ok, Res} = hb_http:get(URL, <<"/api/data">>, #{http_client => httpc}),
?assert(is_map(Res)),
gen_tcp:close(Sock).
get_with_message_test() ->
%% get/3 with message map containing path
{Port, Sock} = start_mock_server(<<"{\"msg\":true}">>, 200),
URL = iolist_to_binary([<<"http://127.0.0.1:">>, integer_to_binary(Port)]),
Message = #{<<"path">> => <<"/query">>, <<"param">> => <<"value">>},
{ok, Res} = hb_http:get(URL, Message, #{http_client => httpc}),
?assert(is_map(Res)),
gen_tcp:close(Sock).3. post/3, post/4
-spec post(Node, PathOrMessage, Opts) -> {ok, Message} | {error, Reason}
when
Node :: binary(),
PathOrMessage :: binary() | map(),
Opts :: map(),
Message :: map(),
Reason :: term().
-spec post(Node, Path, Message, Opts) -> {ok, Message} | {error, Reason}
when
Node :: binary(),
Path :: binary(),
Message :: map(),
Opts :: map(),
Reason :: term().Description: Post message to remote node via HTTP, returning deserialized response.
Test Code:post_path_only_test() ->
%% post/3 with binary path
{Port, Sock} = start_mock_server(<<"{\"posted\":true}">>, 200),
URL = iolist_to_binary([<<"http://127.0.0.1:">>, integer_to_binary(Port)]),
{ok, Res} = hb_http:post(URL, <<"/submit">>, #{http_client => httpc}),
?assert(is_map(Res)),
?assertEqual(200, maps:get(<<"status">>, Res)),
gen_tcp:close(Sock).
post_message_only_test() ->
%% post/3 with message map containing path
{Port, Sock} = start_mock_server(<<"{\"saved\":true}">>, 200),
URL = iolist_to_binary([<<"http://127.0.0.1:">>, integer_to_binary(Port)]),
Message = #{<<"path">> => <<"/api/save">>, <<"data">> => <<"test">>},
{ok, Res} = hb_http:post(URL, Message, #{http_client => httpc}),
?assert(is_map(Res)),
gen_tcp:close(Sock).
post_path_and_message_test() ->
%% post/4 with separate path and message
{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, <<"/api/create">>, Message, #{http_client => httpc}),
?assert(is_map(Res)),
gen_tcp:close(Sock).4. request/2, request/4, request/5
-spec request(Message, Opts) -> {Status, Result}
when
Message :: map(),
Opts :: map(),
Status :: ok | created | error | failure,
Result :: term().
-spec request(Method, Peer, Path, Message, Opts) -> {Status, Result}
when
Method :: binary(),
Peer :: binary() | map(),
Path :: binary(),
Message :: map(),
Opts :: map(),
Status :: ok | created | error | failure,
Result :: term().Description: Generic HTTP request handler supporting GET, POST, PUT, DELETE. Handles routing, multi-requests, codec selection, and response processing.
Special Cases:- Routes: Delegates to
hb_http_multiwhen<<"nodes">>present - URI Options: Merges additional options from route configuration
- AO-Result: Returns single key value when
ao-resultheader present
request_from_message_test() ->
%% request/2 requires dev_router routing infrastructure
%% Test the error handling when no routes are configured
Message = #{<<"method">> => <<"GET">>},
Result = hb_http:request(Message, #{http_client => httpc}),
%% Without routes configured, this returns error
?assertMatch({error, {no_viable_route, _, _}}, Result).
request_no_message_test() ->
%% request/4 - method, peer, path, opts (no message body)
{Port, Sock} = start_mock_server(<<"{\"simple\":true}">>, 200),
URL = iolist_to_binary([<<"http://127.0.0.1:">>, integer_to_binary(Port)]),
{ok, Res} = hb_http:request(<<"GET">>, URL, <<"/status">>, #{http_client => httpc}),
?assert(is_map(Res)),
?assertEqual(200, maps:get(<<"status">>, Res)),
gen_tcp:close(Sock).
request_with_message_test() ->
%% request/5 - full form with message
{Port, Sock} = start_mock_server(<<"{\"method\":\"POST\"}">>, 200),
URL = iolist_to_binary([<<"http://127.0.0.1:">>, integer_to_binary(Port)]),
{ok, Res} = hb_http:request(<<"POST">>, URL, <<"/data">>, #{<<"key">> => <<"val">>}, #{http_client => httpc}),
?assert(is_map(Res)),
gen_tcp:close(Sock).
request_error_status_test() ->
%% 4xx returns {error, Response}
{Port, Sock} = start_mock_server(<<"{\"error\":\"not found\"}">>, 404),
URL = iolist_to_binary([<<"http://127.0.0.1:">>, integer_to_binary(Port)]),
{error, Res} = hb_http:request(<<"GET">>, URL, <<"/missing">>, #{}, #{http_client => httpc}),
?assert(is_map(Res)),
?assertEqual(404, maps:get(<<"status">>, Res)),
gen_tcp:close(Sock).
request_failure_status_test() ->
%% 5xx returns {failure, Response}
{Port, Sock} = start_mock_server(<<"{\"error\":\"internal\"}">>, 500),
URL = iolist_to_binary([<<"http://127.0.0.1:">>, integer_to_binary(Port)]),
{failure, Res} = hb_http:request(<<"GET">>, URL, <<"/fail">>, #{}, #{http_client => httpc}),
?assert(is_map(Res)),
?assertEqual(500, maps:get(<<"status">>, Res)),
gen_tcp:close(Sock).
request_created_status_test() ->
%% 201 returns {created, Response}
{Port, Sock} = start_mock_server(<<"{\"id\":123}">>, 201),
URL = iolist_to_binary([<<"http://127.0.0.1:">>, integer_to_binary(Port)]),
{created, Res} = hb_http:request(<<"POST">>, URL, <<"/create">>, #{}, #{http_client => httpc}),
?assert(is_map(Res)),
?assertEqual(201, maps:get(<<"status">>, Res)),
gen_tcp:close(Sock).
request_put_method_test() ->
%% Test PUT method
{Port, Sock} = start_mock_server(<<"{\"updated\":true}">>, 200),
URL = iolist_to_binary([<<"http://127.0.0.1:">>, integer_to_binary(Port)]),
{ok, Res} = hb_http:request(<<"PUT">>, URL, <<"/resource/123">>, #{<<"data">> => <<"updated">>}, #{http_client => httpc}),
?assert(is_map(Res)),
?assertEqual(200, maps:get(<<"status">>, Res)),
gen_tcp:close(Sock).
request_delete_method_test() ->
%% Test DELETE method
{Port, Sock} = start_mock_server(<<"{\"deleted\":true}">>, 200),
URL = iolist_to_binary([<<"http://127.0.0.1:">>, integer_to_binary(Port)]),
{ok, Res} = hb_http:request(<<"DELETE">>, URL, <<"/resource/123">>, #{}, #{http_client => httpc}),
?assert(is_map(Res)),
?assertEqual(200, maps:get(<<"status">>, Res)),
gen_tcp:close(Sock).
request_with_query_string_test() ->
%% Test path with query string
{Port, Sock} = start_mock_server(<<"{\"results\":[]}">>, 200),
URL = iolist_to_binary([<<"http://127.0.0.1:">>, integer_to_binary(Port)]),
{ok, Res} = hb_http:request(<<"GET">>, URL, <<"/search?q=test&limit=10">>, #{}, #{http_client => httpc}),
?assert(is_map(Res)),
?assertEqual(200, maps:get(<<"status">>, Res)),
gen_tcp:close(Sock).5. message_to_request/2
-spec message_to_request(Message, Opts) -> {ok, Method, Peer, Path, Message, Opts} | {error, Reason}
when
Message :: map(),
Opts :: map(),
Method :: binary(),
Peer :: binary(),
Path :: binary(),
Reason :: term().Description: Convert HyperBEAM message to HTTP request components, extracting method, peer, path, and preparing message for transmission. Uses dev_router:route/2 to resolve routes.
%% message_to_request/2 requires dev_router routing infrastructure.
%% These tests configure routes through opts and verify the conversion.
message_to_request_with_routes_test() ->
%% Configure routes that dev_router will use
{Port, Sock} = start_mock_server(<<"{\"ok\":true}">>, 200),
URL = iolist_to_binary([<<"http://127.0.0.1:">>, integer_to_binary(Port), <<"/api/data">>]),
%% Provide routes configuration for dev_router
Routes = #{
<<"default">> => #{
<<"template">> => URL
}
},
Message = #{<<"method">> => <<"POST">>},
Opts = #{routes => Routes},
%% message_to_request uses dev_router:route internally
%% If routing is not configured, it returns error
Result = hb_http:message_to_request(Message, Opts),
case Result of
{ok, Method, _Peer, _Path, _Msg, _NewOpts} ->
?assertEqual(<<"POST">>, Method);
{error, {no_viable_route, _, _}} ->
%% Expected when dev_router doesn't have routes configured
ok
end,
gen_tcp:close(Sock).
message_to_request_error_handling_test() ->
%% Test error case when no route matches
Message = #{<<"method">> => <<"GET">>},
Result = hb_http:message_to_request(Message, #{}),
?assertMatch({error, {no_viable_route, _, _}}, Result).
message_to_request_put_method_test() ->
%% Test PUT method extraction (error case)
Message = #{<<"method">> => <<"PUT">>, <<"data">> => <<"update">>},
Result = hb_http:message_to_request(Message, #{}),
?assertMatch({error, {no_viable_route, _, _}}, Result).
message_to_request_delete_method_test() ->
%% Test DELETE method extraction (error case)
Message = #{<<"method">> => <<"DELETE">>},
Result = hb_http:message_to_request(Message, #{}),
?assertMatch({error, {no_viable_route, _, _}}, Result).
message_to_request_with_body_test() ->
%% Test message with body (error case)
Message = #{<<"method">> => <<"POST">>, <<"body">> => <<"test data">>},
Result = hb_http:message_to_request(Message, #{}),
?assertMatch({error, {no_viable_route, _, _}}, Result).
message_to_request_with_path_test() ->
%% Test message with path (error case)
Message = #{<<"method">> => <<"GET">>, <<"path">> => <<"/api/resource">>},
Result = hb_http:message_to_request(Message, #{}),
?assertMatch({error, {no_viable_route, _, _}}, Result).
%% Note: Full message_to_request success testing requires dev_router configuration.
%% The function is tested indirectly through request/4 and request/5 which
%% handle routing internally. See request_* tests above.6. reply/4
-spec reply(Req, TABMReq, Message, Opts) -> {ok, cowboy_req:req(), no_state}
when
Req :: cowboy_req:req(),
TABMReq :: map(),
Message :: map(),
Opts :: map().Description: Send HTTP response to client via Cowboy, handling headers, status codes, and body serialization. Extracts status from Message or defaults to 200.
Test Code:%% reply/4 requires Cowboy connection for stream_reply/stream_body.
%% Test via hb_http_server which internally calls reply/4 to send responses.
%% These tests verify reply/4 is called correctly by checking response characteristics.
reply_basic_test() ->
URL = hb_http_server:start_node(),
{ok, Res} = hb_http:get(URL, <<"/~meta@1.0/info">>, #{}),
?assert(is_map(Res)).
reply_cors_headers_test() ->
%% reply/4 adds CORS headers via add_cors_headers/3
URL = hb_http_server:start_node(),
{ok, Res} = hb_http:get(URL, <<"/~meta@1.0/info">>, #{}),
?assertEqual(<<"*">>, hb_ao:get(<<"access-control-allow-origin">>, Res, #{})).
reply_with_message_test() ->
%% reply/4 encodes message and sends via stream_body
URL = hb_http_server:start_node(),
TestMsg = #{<<"path">> => <<"/key1">>, <<"key1">> => <<"Value1">>},
?assertEqual({ok, <<"Value1">>}, hb_http:post(URL, TestMsg, #{})).
reply_status_from_message_test() ->
%% reply/4 extracts status from message (default 200)
URL = hb_http_server:start_node(),
Wallet = hb:wallet(),
TestMsg = #{<<"path">> => <<"/key1">>, <<"key1">> => <<"Value1">>},
{ok, Res} = hb_http:post(URL, hb_message:commit(TestMsg, Wallet), #{}),
?assertEqual(<<"Value1">>, Res).
reply_nested_resolution_test() ->
%% reply/4 handles nested path resolution results
URL = hb_http_server:start_node(),
Wallet = hb:wallet(),
{ok, Res} = hb_http:post(
URL,
hb_message:commit(#{
<<"path">> => <<"/key1/key2/key3">>,
<<"key1">> => #{<<"key2">> => #{<<"key3">> => <<"Value2">>}}
}, Wallet),
#{}
),
?assertEqual(<<"Value2">>, Res).- Auto-detects content type from body
- Converts headers map to list format
- Handles binary and JSON bodies
- Sets appropriate CORS headers
- Handles Set-Cookie headers via
dev_codec_cookie
7. accept_to_codec/2
-spec accept_to_codec(OriginalReq, Opts) -> CodecDevice
when
OriginalReq :: map(),
Opts :: map(),
CodecDevice :: binary().Description: Convert HTTP Accept header from request map to HyperBEAM codec device identifier. Looks for <<"accept">> key in the request map.
accept_to_codec_json_test() ->
Req = #{<<"accept">> => <<"application/json">>},
Codec = hb_http:accept_to_codec(Req, #{}),
?assert(is_binary(Codec)).
accept_to_codec_octet_stream_test() ->
Req = #{<<"accept">> => <<"application/octet-stream">>},
Codec = hb_http:accept_to_codec(Req, #{}),
?assert(is_binary(Codec)).
accept_to_codec_text_plain_test() ->
Req = #{<<"accept">> => <<"text/plain">>},
Codec = hb_http:accept_to_codec(Req, #{}),
?assert(is_binary(Codec)).
accept_to_codec_default_test() ->
%% Empty request uses default codec
Req = #{},
Codec = hb_http:accept_to_codec(Req, #{}),
?assert(is_binary(Codec)).
accept_to_codec_wildcard_test() ->
%% Wildcard accept uses default
Req = #{<<"accept">> => <<"*/*">>},
Codec = hb_http:accept_to_codec(Req, #{}),
?assert(is_binary(Codec)).
accept_to_codec_require_codec_test() ->
%% require-codec takes precedence
Req = #{<<"require-codec">> => <<"application/json">>},
Codec = hb_http:accept_to_codec(Req, #{}),
?assert(is_binary(Codec)).8. req_to_tabm_singleton/3
-spec req_to_tabm_singleton(Req, Body, Opts) -> Message
when
Req :: cowboy_req:req(),
Body :: binary(),
Opts :: map(),
Message :: map().Description: Convert Cowboy request to a normalized HyperBEAM message for singleton execution. Parses the request path, query parameters, headers, and body into a single TABM message.
Test Code:%% req_to_tabm_singleton/3 converts Cowboy request to a normalized TABM message.
%% Uses mock_cowboy_req/4 helper defined in main test module.
req_to_tabm_singleton_get_test() ->
MockReq = mock_cowboy_req(<<"/test/path">>, <<>>, #{}, <<"GET">>),
Body = <<"{}">>,
Result = hb_http:req_to_tabm_singleton(MockReq, Body, #{}),
?assert(is_map(Result)),
?assertEqual(<<"GET">>, maps:get(<<"method">>, Result, undefined)).
req_to_tabm_singleton_post_test() ->
MockReq = mock_cowboy_req(<<"/api/data">>, <<>>, #{}, <<"POST">>),
Body = <<"{}">>,
Result = hb_http:req_to_tabm_singleton(MockReq, Body, #{}),
?assert(is_map(Result)),
?assertEqual(<<"POST">>, maps:get(<<"method">>, Result, undefined)).
req_to_tabm_singleton_with_query_test() ->
MockReq = mock_cowboy_req(<<"/api/data">>, <<"foo=bar&num=123">>, #{}, <<"GET">>),
Body = <<"{}">>,
Result = hb_http:req_to_tabm_singleton(MockReq, Body, #{}),
?assert(is_map(Result)),
?assertEqual(<<"bar">>, maps:get(<<"foo">>, Result, undefined)).
req_to_tabm_singleton_with_headers_test() ->
Headers = #{
<<"x-custom-header">> => <<"custom-value">>,
<<"content-type">> => <<"application/json">>
},
MockReq = mock_cowboy_req(<<"/test">>, <<>>, Headers, <<"GET">>),
Body = <<"{}">>,
Result = hb_http:req_to_tabm_singleton(MockReq, Body, #{}),
?assert(is_map(Result)),
?assertEqual(<<"custom-value">>, maps:get(<<"x-custom-header">>, Result, undefined)).
req_to_tabm_singleton_nested_path_test() ->
MockReq = mock_cowboy_req(<<"/a/b/c">>, <<>>, #{}, <<"GET">>),
Body = <<"{}">>,
Result = hb_http:req_to_tabm_singleton(MockReq, Body, #{}),
?assert(is_map(Result)),
?assertMatch(<<"/a/b/c", _/binary>>, maps:get(<<"path">>, Result, <<>>)).
req_to_tabm_singleton_json_body_test() ->
Headers = #{<<"content-type">> => <<"application/json">>},
MockReq = mock_cowboy_req(<<"/api">>, <<>>, Headers, <<"POST">>),
Body = <<"{\"key1\":\"value1\",\"key2\":42}">>,
Result = hb_http:req_to_tabm_singleton(MockReq, Body, #{}),
?assert(is_map(Result)),
?assertEqual(<<"value1">>, maps:get(<<"key1">>, Result, undefined)).- Extract HTTP method and path from Cowboy request
- Parse query parameters via
hb_singleton:from_path/1 - Extract and normalize headers
- Detect codec from
codec-deviceheader orcontent-type - Decode body based on codec (httpsig, ans104, or structured)
- Handle cookie parsing via
dev_codec_cookie - Add peer information if
ao-peer-portheader present - Return normalized TABM message map
Response Status Mapping
response_status_to_atom(Status) ->
case Status of
201 -> created;
X when X < 400 -> ok;
X when X < 500 -> error;
_ -> failure
end.created- HTTP 201ok- HTTP 2xx-3xx (except 201)error- HTTP 4xx (client errors)failure- HTTP 5xx (server errors)
Codec Device Handling
Outbound Requests
% Detect codec from message or use default
CodecDev = hb_maps:get(<<"codec-device">>, Message, <<"httpsig@1.0">>, Opts),
% Prepare request based on codec
prepare_request(CodecDev, Method, Peer, Path, Message, Opts)Inbound Responses
% Find codec from response headers
CodecDev = hb_maps:get(<<"codec-device">>, Headers, <<"httpsig@1.0">>, Opts),
% Convert response to message
outbound_result_to_message(CodecDev, Status, Headers, Body, Opts)httpsig@1.0- HTTP Signature (default)ans104@1.0- ANS-104 data itemsstructured@1.0- Structured messagesjson@1.0- JSON encoding
Cookie Handling
Set-Cookie Processing
% Extract Set-Cookie headers from response
SetCookieLines = [{<<"set-cookie">>, KeyVal} || {<<"set-cookie">>, KeyVal} <- Headers],
% Convert to cookie message
{ok, MsgWithCookies} = dev_codec_cookie:from(
#{<<"set-cookie">> => SetCookieLines},
#{},
Opts
),
% Merge into response headers
HeaderMap = hb_maps:merge(hb_maps:from_list(Headers), MsgWithCookies, Opts)Cookie Request Handling
% Parse cookie from request
{ok, WithCookie} = case maps:get(<<"cookie">>, RawHeaders, undefined) of
undefined -> {ok, BaseMsg};
Cookie -> dev_codec_cookie:from(BaseMsg#{<<"cookie">> => Cookie}, Req, Opts)
endCommon Patterns
%% Simple GET request
{ok, Response} = hb_http:get(<<"http://node.example.com">>, <<"/path">>, #{}).
%% POST with message
Message = #{<<"key">> => <<"value">>},
{ok, Response} = hb_http:post(Node, <<"/endpoint">>, Message, #{}).
%% Signed POST request
Wallet = hb:wallet(),
SignedMsg = hb_message:commit(Message, #{priv_wallet => Wallet}),
{ok, Response} = hb_http:post(Node, SignedMsg, #{}).
%% Request with specific codec
Opts = #{<<"codec-device">> => <<"ans104@1.0">>},
{ok, Response} = hb_http:post(Node, Message, Opts).
%% Extract single result key
Opts = #{http_only_result => true},
Message = #{<<"path">> => <<"/data">>, <<"ao-result">> => <<"body">>},
{ok, BodyOnly} = hb_http:post(Node, Message, Opts).
%% Handle routes (multi-request)
Config = #{
<<"nodes">> => [
<<"http://node1.example.com">>,
<<"http://node2.example.com">>
]
},
{ok, Response} = hb_http:request(<<"GET">>, Config, <<"/path">>, #{}, #{}).Configuration Options
Opts = #{
% HTTP client selection
http_client => gun | httpc, % Default: gun
% Result extraction
http_only_result => true | false, % Default: true
% Codec selection
<<"codec-device">> => <<"httpsig@1.0">> | <<"ans104@1.0">> | <<"structured@1.0">>,
% Wallet for signing
priv_wallet => {PrivateKey, PublicKey},
% Request timeout
http_request_send_timeout => Milliseconds,
% Response size limit
limit => Bytes | infinity,
% Peer identification
is_peer_request => boolean(),
% HTTP monitoring
http_monitor => MonitorMessage,
http_reference => ReferenceValue,
% Cache control
cache_control => [<<"no-cache">>, <<"no-store">>]
}.Error Handling
% Client errors (4xx)
{error, Reason}
% Server errors (5xx)
{failure, Reason}
% Network errors
{error, client_error}
{error, connection_closed}
{error, timeout}
% Codec errors
{error, unsupported_codec}
{error, invalid_message}Server-Side Processing
Request Normalization
% Convert Cowboy request to HyperBEAM message
Message = hb_http:req_to_tabm_singleton(Req, Body, Opts),
% Process through singleton
Result = hb_ao:resolve(Message, Opts)Response Generation
% Send response back to client
{ok, Req2, no_state} = hb_http:reply(Req, TABMReq, Message, Opts)Performance Notes
- Keep-Alive Disabled: Connections don't persist between requests
- Parallel Requests: Use
hb_http_multifor concurrent node access - Streaming: Supports chunked transfer encoding via gun
- Metrics: Records request duration in Prometheus when enabled
- Connection Pooling: Managed by
hb_http_clientgen_server
References
- HTTP Client -
hb_http_client.erl - Multi-Request -
hb_http_multi.erl - Message System -
hb_message.erl,hb_ao.erl - Codecs -
dev_codec_*.erlmodules - Server -
hb_http_server.erl
Notes
- Default Codec:
httpsig@1.0used unless specified - Message Signing: Optional, controlled by wallet presence
- Cookie Support: Full Set-Cookie and Cookie header processing
- CORS: Automatic CORS headers on responses
- Query Parameters: Extracted and added to message
- Path Resolution: Supports nested path traversal in messages
- AO-Result: Can extract single key from response for efficiency
- Peer Detection: Honors
x-real-ipfor accurate peer address - Body Streaming: Large responses handled efficiently
- Status Normalization: Consistent status atom mapping
- Multi-Gateway: Seamless delegation to multi-request system
- Header Filtering: Default filter for
content-length - Content-Type Detection: Auto-detects from body or headers
- Accept Header: Maps to appropriate codec device
- Monitoring: Optional HTTP request monitoring via AO-Core# hb_http.erl - HyperBEAM HTTP Request/Reply Core