HTTP
Asynchronous HTTP requests through Steam's HTTP client — no curl, queued until Steam is up, callbacks on the main thread.
ISource2ModHTTP sends requests through the HTTP client Steam already gives the
game server, so there is no library to link and nothing to pump. Responses
arrive in a callback on the main thread, with the body already parsed as JSON
when it is JSON.
#include "source2mod/ISource2ModHTTP.h"#include <source2mod/api> // the whole API, http.inc includedA GET request
HTTP_GET("https://api.example.com/status", [](const Source2ModHTTPResponse& r)
{
if (!r.m_bSuccess || r.m_nStatusCode != 200)
return;
if (r.m_pJson)
{
const char* motd = r.m_pJson->GetString("motd", "");
int online = r.m_pJson->GetInt("online");
// copy anything you keep -- see below
}
});public void OnPluginStart()
{
Source2ModHTTP.Get("https://api.example.com/status", OnStatus);
}
public void OnStatus(Source2ModHTTPResponse r, any data)
{
if (!r.Success() || r.StatusCode() != 200)
return;
ISource2ModJSONValue json = r.Json();
if (json != null)
{
char motd[128];
json.GetString("motd", motd, sizeof(motd));
int online = json.GetInt("online");
// copy anything you keep -- see below
}
}The calls
using Source2ModHTTPCallback = Source2ModCallback<void(const Source2ModHTTPResponse& response)>;
virtual void Request(ESource2ModHTTPMethod method,
const char* pszUrl,
const char* pszBody,
Source2ModHTTPCallback callback,
const std::vector<Source2ModHTTPHeader>* pHeaders = nullptr) = 0;
virtual void Get(const char* pszUrl, Source2ModHTTPCallback callback,
const std::vector<Source2ModHTTPHeader>* pHeaders = nullptr) = 0;
virtual void Post(const char* pszUrl, const char* pszBody, Source2ModHTTPCallback callback,
const std::vector<Source2ModHTTPHeader>* pHeaders = nullptr) = 0;
// Put, Patch, Delete: same as Post| Macro | Call |
|---|---|
HTTP_REQUEST(method, url, body, cb, ...) | Request(method, url, body, cb, ...) |
HTTP_GET(url, cb, ...) | Get(url, cb, ...) |
HTTP_POST(url, body, cb, ...) | Post(url, body, cb, ...) |
HTTP_PUT, HTTP_PATCH, HTTP_DELETE | same shape as HTTP_POST |
ESource2ModHTTPMethod is Get, Post, Put, Patch, Delete or Head. The
callback may be empty (nullptr) for fire-and-forget requests.
The callback is a Source2ModCallback: a lambda, a free or static function, or an
object and a method (SOURCE2MOD_MEMBER(this, &Plugin::OnStats)). There is no
plugin id argument — the core reads which plugin the request belongs to off the
callback itself, and cancels it if that plugin unloads.
In SourcePawn the calls are Source2ModHTTP.Request(), Get(), Post(),
Put(), Patch() and Delete(). The callback is a named function
void (Source2ModHTTPResponse response, any data); the optional data cell
you pass rides along to it, and headers are a Source2ModHTTPHeaders handle.
POST with headers
std::vector<Source2ModHTTPHeader> headers = {
{ "Authorization", "Bearer " + g_ApiToken },
};
std::string body = R"({"steamid":")" + std::to_string(steamid) + R"(","kills":12})";
HTTP_POST("https://api.example.com/stats", body.c_str(), [steamid](const Source2ModHTTPResponse& r)
{
if (!r.m_bSuccess)
return; // transport failure: no connection, DNS, timeout ...
if (r.m_nStatusCode < 200 || r.m_nStatusCode > 299)
g_Source2ModAPI->ConPrintf("Stats for %llu refused: HTTP %d\n", steamid, r.m_nStatusCode);
}, &headers);char g_ApiToken[128];
void SendStats(const char[] steamid)
{
char auth[160];
Source2ModApi.Format(auth, sizeof(auth), "Bearer %s", g_ApiToken);
Source2ModHTTPHeaders headers = new Source2ModHTTPHeaders();
headers.Add("Authorization", auth);
char body[128];
Source2ModApi.Format(body, sizeof(body), "{\"steamid\":\"%s\",\"kills\":12}", steamid);
// The callback gets one cell of data: a DataPack carries the SteamID.
DataPack pack = new DataPack();
pack.WriteString(steamid);
Source2ModHTTP.Post("https://api.example.com/stats", body, OnStatsSent, pack, headers);
delete headers; // the request copied them
}
public void OnStatsSent(Source2ModHTTPResponse r, any data)
{
DataPack pack = view_as<DataPack>(data);
char steamid[32];
pack.Reset();
pack.ReadString(steamid, sizeof(steamid));
delete pack;
if (!r.Success())
return; // transport failure: no connection, DNS, timeout ...
if (r.StatusCode() < 200 || r.StatusCode() > 299)
Source2ModApi.Log("Stats for %s refused: HTTP %d", steamid, r.StatusCode());
}The body is sent with the content type application/json. The headers vector
and body only have to live until the call returns; in SourcePawn the request
copies the Source2ModHTTPHeaders, so delete them right after the call.
The response
| Field | Meaning |
|---|---|
m_bSuccess | The request completed. A 404 that arrived is a success with m_nStatusCode == 404 |
m_nStatusCode | HTTP status, 0 when the request never completed |
m_sBody | Raw body |
m_pJson | The body parsed as JSON, or nullptr when empty or not JSON |
In SourcePawn the response is a handle with methods instead: Success(),
StatusCode(), GetBody(buffer, maxlen) / BodyLength() and Json()
(null when not JSON).
m_pJson (Json()) is owned by Source2Mod and valid only during the callback.
Strings from it (AsString(), GetString()) die with it too — copy them
into std::string before you store them (SourcePawn copies them into your
buffer anyway). See JSON.
If your plugin already uses its own JSON library, parse m_sBody with it
instead — FUNPLAY does exactly that with nlohmann::json, so its own JSON types
never cross the plugin boundary.
Availability
Steam hands the HTTP client over only once the Steam API has activated, after
level startup. Requests made before that are queued, not dropped, and sent
when it comes up — so a plugin can send requests from Load() / OnPluginStart() without caring.
IsAvailable()— whether Steam's HTTP client is there yet. Only worth checking to avoid building a body you cannot send yet.PendingCount()— how many requests are waiting for it.
Pitfalls
- Callbacks run on the main thread, so touching game state inside them is
fine — but the world may have changed while the request was in flight. Carry
a SteamID, slot or
CHandleinto the lambda (thedatacell or aDataPackin SourcePawn) and re-resolve the player, never a rawCCSPlayerController*. - Unload cancels. A request still in flight or queued when your plugin unloads is cancelled, callback and all — the callback lives in your library.
- Do not block waiting for a response. There is no synchronous variant; chain the next step from the callback.
- Transport vs. status. Check both
m_bSuccessandm_nStatusCode(Success()andStatusCode()). - The callback can run before the call returns. When Steam refuses to even
create or send the request, the callback is invoked right away with an empty,
unsuccessful response. Do not rely on code after
HTTP_*()running first.
Reference
ISource2ModHTTPAPI reference- JSON — walking
m_pJson