English | Π ΡΡΡΠΊΠΈΠΉ
A lightning-fast, highly concurrent, non-blocking HTTP client for Arma 3.
Warning
This is a pre-release version! Use in production at your own risk and only for non-critical tasks.
If you find any bugs, please open an issue or submit a pull request if needed.
Phoenix HTTP is a modern HTTP client for Arma 3 dedicated servers, built as a native extension with C# Native AOT. It lets your missions and frameworks talk to REST APIs and web services without ever stalling the game loop.
It comes in two parts that work together:
- a native extension (
PhoenixHttp_x64.dll) that performs the actual HTTP work asynchronously on a background thread pool; - a thin SQF wrapper (
PHTTP_fnc_Request) that builds requests, sends them throughcallExtension, and delivers responses back to your code through a callback.
Because the extension never blocks the engine and the response is parsed into a HashMap you can read with simple dotted keys, working with web APIs from SQF stops being painful.
- β‘ Flattened JSON responses β Arma 3 has no native JSON support, so Phoenix HTTP parses the
response and returns a HashMap you read with dotted keys. A server payload
{"user":{"id":123}}is reachable as_response get "body.user.id". No SQF JSON parser required. - π Arrays stay iterable β objects are flattened into dotted keys, but arrays are kept as real
SQF arrays so you can
forEachthem. Each object inside an array is itself a usable HashMap. - ποΈ Safe null handling β JSON
nullvalues are omitted from the result, so a missing field is simply absent andgetOrDefaultalways works. Your scripts never crash on unexpected nulls. - β³ Fully asynchronous & non-blocking β requests run on a background thread pool and complete via an engine callback. The server never freezes or drops TPS while waiting for an API.
- π§΅ Bounded concurrency β a configurable queue limits how many requests run at once, so a burst of calls can never exhaust the server's resources.
- π§© Macro environments β
{{NAME}}placeholders are replaced fromconfig.json, so API keys and base URLs live in configuration, not hardcoded in mission files. - π¦ Automatic chunking β
callExtensionhas a strict reply-size limit; Phoenix HTTP splits large responses into chunks and reassembles them on the SQF side automatically. - π Zero runtime dependencies β compiled with Native AOT, so no .NET runtime is needed on the server. Drop in the mod and go.
- π File logging β all network activity, chunking and errors are written to per-run log files for easy debugging.
| Requirement | Notes |
|---|---|
| Windows dedicated server | The extension is built for win-x64 only (PhoenixHttp_x64.dll). Linux servers are not supported. |
| Arma 3 server β₯ 2.02 | Required for the array form of callExtension. |
| .NET runtime | Not required β the extension is self-contained (Native AOT). |
- Download the latest build from the Releases page.
- Extract the
@PhoenixHTTPfolder into your Arma 3 server directory. - Add
-serverMod=@PhoenixHTTPto your server startup parameters.
The @PhoenixHTTP folder looks like this:
@PhoenixHTTP/
βββ addons/
β βββ phoenixhttp_main.pbo # SQF wrapper (PHTTP_fnc_Request, callback handler)
βββ PhoenixHttp_x64.dll # native extension
βββ config.json # your configuration (copied from config.example.json)
βββ logs/ # created at runtime, one log file per server start
config.json and logs/ live next to the DLL.
All keys are optional; defaults are used when the file is missing or a key is absent. Numeric values
are clamped to a minimum of 1, so an invalid value can never break the extension.
{
"maxConcurrentRequests": 8,
"requestTimeoutSeconds": 30,
"chunkSize": 8192,
"debug": false,
"environments": {
"API_BASE": "https://api.yoursite.com/v1",
"API_KEY": "secret_token_123"
}
}| Key | Type | Default | Description |
|---|---|---|---|
maxConcurrentRequests |
Integer | 8 |
Maximum number of requests executed at the same time. Extra requests wait in a queue. |
requestTimeoutSeconds |
Integer | 30 |
Per-request timeout. A request that exceeds it fails with a Timeout error. |
chunkSize |
Integer | 8192 |
Maximum size, in bytes, of each response chunk handed back to SQF. |
debug |
Boolean | false |
When true, verbose DEBUG lines are written to the log. |
environments |
Object | {} |
Named values substituted into requests via the {{NAME}} macro syntax. |
The configuration can be reloaded at runtime without restarting the server β see
Advanced: raw callExtension protocol below.
The wrapper exposes a single function, PHTTP_fnc_Request. It takes one HashMap describing the
request and returns the transaction id (a string) immediately. The response is delivered later to
your callback.
private _id = [_request] call PHTTP_fnc_Request;| Key | Type | Default | Description |
|---|---|---|---|
method |
String | "GET" |
HTTP method. Any method is allowed (GET, POST, PUT, PATCH, DELETE, HEAD, β¦); it is upper-cased automatically. |
url |
String | "" |
Target URL. Supports {{macros}}. |
parser |
String | "json" |
"json" parses and flattens the response body; any other value (e.g. "raw") returns the body as a plain string. |
headers |
HashMap | createHashMap |
Request headers as name β value. Values support {{macros}}. |
query |
HashMap | createHashMap |
Query parameters as name β value. They are URL-encoded and appended to the URL. Values support {{macros}}. |
body |
String or HashMap | "" |
Request payload. A HashMap is serialized to JSON and sent with Content-Type: application/json. A String is sent verbatim β set Content-Type yourself via headers. Supports {{macros}}. |
callback |
Code | {} |
Runs when the response is ready, receiving the response HashMap as _this select 0. Omit it for fire-and-forget, where the response is discarded and never fetched. |
The callback receives one HashMap. Its keys are flattened, so you read nested values with dotted
keys rather than chained get calls.
| Key | Type | Description |
|---|---|---|
success |
Boolean | true if a response was received from the server (any HTTP status, including 404/500). false only on a transport failure (network error, timeout, bad request). |
status_code |
Number | On success, the HTTP status code (200, 404, β¦). On failure, a negative error code (see below). |
headers.<name> |
String | Response headers, flattened with lower-cased names β e.g. _response get "headers.content-type". This is not a nested HashMap. |
body / body.<path> |
Any | The response payload (see Response body: how flattening works below). Absent when the request failed entirely. |
| Code | Meaning |
|---|---|
-1 |
Network unreachable (DNS failure, connection refused, host down). |
-2 |
Request timed out (requestTimeoutSeconds exceeded). |
-4 |
Response serialization/parsing failed (e.g. invalid JSON when parser is "json"). |
-100 |
Unknown failure (e.g. an invalid URL or method). Check the log for details. |
The response body is normalized for SQF using one consistent rule:
- Objects are flattened into dotted keys.
{"user": {"name": "Bob"}}becomesbody.user.name, read with a singleget. - Arrays are kept as real SQF arrays so they stay iterable with
forEach. Each object inside an array is itself a flattened HashMap, accessed by index then key. nullvalues are dropped β the key simply does not appear.
// Server returns: { "users": [ {"name": "Bob"}, {"name": "Alice"} ], "total": 2 }
_response get "body.total"; // 2
private _users = _response get "body.users"; // a real SQF array
{
diag_log (_x get "name"); // each element is a flattened HashMap
} forEach _users;When parser is not "json", body is returned as a plain string instead.
private _request = createHashMapFromArray [
["method", "GET"],
["url", "{{API_BASE}}/users/1"],
["query", createHashMapFromArray [
["include_stats", "true"]
]],
["callback", {
params ["_response"];
if !(_response get "success") exitWith {
diag_log "[MyMod] Request failed (network/timeout).";
};
if ((_response get "status_code") != 200) exitWith {
diag_log format ["[MyMod] API error: %1", _response get "status_code"];
};
// Nested fields via dotted keys; getOrDefault is safe because nulls are omitted.
private _name = _response getOrDefault ["body.user.name", "Unknown"];
diag_log format ["[MyMod] User: %1", _name];
}]
];
[_request] call PHTTP_fnc_Request;private _request = createHashMapFromArray [
["method", "POST"],
["url", "{{API_BASE}}/users"],
["headers", createHashMapFromArray [
["Authorization", "Bearer {{API_KEY}}"]
// Content-Type is set automatically because 'body' is a HashMap.
]],
["body", createHashMapFromArray [
["name", "John Doe"],
["role", "admin"],
["tags", ["pvp", "staff"]] // nested arrays and maps are serialized too
]],
["callback", {
params ["_response"];
diag_log format ["[MyMod] Created -> %1", _response get "status_code"];
}]
];
[_request] call PHTTP_fnc_Request;private _request = createHashMapFromArray [
["method", "POST"],
["url", "{{API_BASE}}/webhook"],
["parser", "raw"], // get the response as a plain string
["headers", createHashMapFromArray [
["content-type", "text/xml"] // set it yourself for a String body
]],
["body", "<event>player_joined</event>"],
["callback", {
params ["_response"];
diag_log (_response getOrDefault ["body", ""]);
}]
];
[_request] call PHTTP_fnc_Request;Omit callback and the response is never fetched β useful for telemetry or webhooks where you don't
care about the result.
[createHashMapFromArray [
["method", "POST"],
["url", "{{API_BASE}}/heartbeat"]
]] call PHTTP_fnc_Request;Any {{NAME}} token is replaced with the matching value from environments in config.json. Macros
are expanded in the URL, header values, query values and body. An unknown macro is
left untouched (braces included), so a typo is visible rather than silently blanked.
"environments": { "API_BASE": "https://api.example.com", "API_KEY": "abc123" }["url", "{{API_BASE}}/status"] // -> https://api.example.com/status
["headers", createHashMapFromArray [["Authorization", "Bearer {{API_KEY}}"]]]PHTTP_fnc_Request is built on top of these verbs. You can call them directly if you need to. Every
reply is a string in the form status:data, where status is success or error β split on the
first colon only, because data may itself contain colons.
| Command | Arguments | Returns | Description |
|---|---|---|---|
request:create |
[method, url] |
success:<id> |
Creates a transaction and returns its id. |
request:header |
[id, key, value] |
success: |
Adds a request header. |
request:query |
[id, key, value] |
success: |
Adds a query parameter. |
request:body |
[id, chunk] |
success: |
Appends a chunk to the request body. |
request:send |
[id, parser, needsResponse] |
success:<id> |
Dispatches the request. parser is "json"/"raw"; needsResponse is "true"/"false". |
request:get |
[id, chunkIndex] |
success:<chunk> |
Returns one chunk of the ready response. |
request:delete |
[id] |
success: |
Removes a transaction. Call it after reading the response. |
request:clear |
[] |
success: |
Removes all transactions. |
config:reload |
[] |
success: |
Re-reads config.json at runtime (concurrency, timeout, macros, debug). |
When needsResponse is "true", the extension raises the ExtensionCallback mission event handler
with ["PhoenixHttp", "response", "[""<id>"", <chunkCount>]"] once the response is ready. You then
pull the chunks with request:get, concatenate them, and call compile the result into the response
HashMap (this is exactly what the bundled wrapper does).
private _reply = ("PhoenixHttp" callExtension ["request:create", ["GET", "https://api.ipify.org?format=json"]]) select 0;
private _sep = _reply find ":";
private _status = _reply select [0, _sep];
private _id = _reply select [_sep + 1]; // robust: split on the first colon only
if (_status isEqualTo "success") then {
"PhoenixHttp" callExtension ["request:send", [_id, "json", "true"]];
};To reload configuration live:
"PhoenixHttp" callExtension ["config:reload", []];You need the .NET 10 SDK and HEMTT.
# Build the addon (PBO)
hemtt build
# Build the native extension (Windows, Native AOT)
dotnet publish extension/PhoenixHTTP/PhoenixHTTP.csproj -c Release
# Run the unit tests for the serialization/chunking logic
dotnet test extension/PhoenixHTTP.Test/PhoenixHTTP.Test.csprojThe extension's AOT publish requires the MSVC toolchain on Windows. CI builds the addon on Linux and
the extension on Windows, then assembles @PhoenixHTTP.