Guides

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.

plugin.cpp
#include "source2mod/ISource2ModHTTP.h"

A GET request

plugin.cpp
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
    }
});

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
MacroCall
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_DELETEsame 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

plugin.cpp
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);

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

FieldMeaning
m_bSuccessThe request completed. A 404 that arrived is a success with m_nStatusCode == 404
m_nStatusCodeHTTP status, 0 when the request never completed
m_sBodyRaw body
m_pJsonThe 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 CHandle into the lambda (the data cell or a DataPack in SourcePawn) and re-resolve the player, never a raw CCSPlayerController*.
  • 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_bSuccess and m_nStatusCode (Success() and StatusCode()).
  • 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

On this page