Externe APIs abrufen mit HttpClient
Die No-Code Fetch URL Sub-Action holt dir den Inhalt einer URL als Argument, und für einfache GET-Requests reicht das völlig. Sobald du aber einen POST mit JSON-Body abschicken, einen Authorization-Header setzen oder eine verschachtelte Antwort gezielt auseinandernehmen willst, stößt sie an ihre Grenzen. Dann nimmst du HttpClient aus System.Net.Http direkt im C#-Code. Diese Seite zeigt das eine Muster, das du dafür brauchst, und die Fallen, die fast jeden beim ersten Versuch erwischen.
Doku:
Warum nicht einfach Fetch URL?
Abschnitt betitelt „Warum nicht einfach Fetch URL?“Die Fetch URL Sub-Action ist ein reiner GET ohne Header-Kontrolle. Greif zu HttpClient, sobald einer dieser Punkte zutrifft:
- POST, PUT oder PATCH mit einem Request-Body, etwa um Daten an eine API zu schicken.
- Header setzen, vor allem
Authorization(Bearer-Token, API-Key) oder einClient-Id. - Komplexes JSON parsen: ein einzelnes Feld aus einer tief verschachtelten Antwort ziehen, statt den ganzen Roh-String im Argument zu haben.
- Fehlerbehandlung mit Statuscode-Prüfung und
try/catch, statt blind den Body zu nehmen.
Für ein simples GET ohne Auth bleibt Fetch URL die schnellere Wahl. Erst bei den Punkten oben lohnt der Schritt in den Code.
Das synchrone Bridge-Muster
Abschnitt betitelt „Das synchrone Bridge-Muster“Hier ist der entscheidende Punkt, an dem fast jeder zuerst scheitert. Die Methode Execute() ist synchron, sie gibt direkt einen bool zurück. Die HttpClient-Methoden wie GetStringAsync oder PostAsync sind dagegen asynchron und liefern einen Task. Du kannst in Execute() kein await schreiben, das kompiliert schlicht nicht, weil die Methode nicht async ist.
Die Brücke ist .GetAwaiter().GetResult(). Damit wartest du synchron auf das Ergebnis des Task und bekommst den entpackten Wert zurück.
using System;using System.Net.Http;
public class CPHInline { // EIN statischer Client fuer die ganze Action-Klasse, wiederverwendet private static readonly HttpClient client = new HttpClient { Timeout = TimeSpan.FromSeconds(30) };
public bool Execute() { // GetStringAsync liefert einen Task<string>, GetAwaiter().GetResult() brueckt synchron string json = client.GetStringAsync("https://api.example.com/data").GetAwaiter().GetResult();
CPH.LogInfo($"Antwort: {json}"); return true; }}Zwei Dinge sind hier nicht verhandelbar:
static readonlyals Klassenfeld. Der Client wird einmal erzeugt und über alle Aufrufe wiederverwendet. Niemals einnew HttpClient()pro Aufruf, mehr dazu unten bei den Fallen..GetAwaiter().GetResult()stattawait. Das ist das Muster, das auch das offizielle HTTP-Beispiel auf docs.streamer.bot für den synchronenExecute()-Kontext nutzt.
GET mit voller Kontrolle
Abschnitt betitelt „GET mit voller Kontrolle“GetStringAsync gibt dir nur den Body. Wenn du den Statuscode prüfen oder Header lesen willst, nimm GetAsync, das liefert eine HttpResponseMessage. Den Body holst du dann über response.Content.ReadAsStringAsync(), ebenfalls synchron gebrückt.
using System;using System.Net.Http;
public class CPHInline { private static readonly HttpClient client = new HttpClient { Timeout = TimeSpan.FromSeconds(30) };
public bool Execute() { HttpResponseMessage response = client .GetAsync("https://api.example.com/data") .GetAwaiter().GetResult();
if (!response.IsSuccessStatusCode) { CPH.LogWarn($"API antwortete mit {(int)response.StatusCode}"); return false; }
string body = response.Content.ReadAsStringAsync().GetAwaiter().GetResult(); CPH.LogInfo($"Body: {body}"); return true; }}response.IsSuccessStatusCode ist true bei allen 2xx-Codes. Bei einem 404 oder 500 fliegt keine Exception, du musst den Statuscode selbst prüfen.
POST mit JSON-Body
Abschnitt betitelt „POST mit JSON-Body“Für einen POST baust du den Body als StringContent mit dem Content-Type application/json. Den Body-String erzeugst du am saubersten mit JsonConvert.SerializeObject aus Newtonsoft, dann musst du nichts von Hand zusammenstückeln.
using System;using System.Text;using System.Net.Http;using Newtonsoft.Json;
public class CPHInline { private static readonly HttpClient client = new HttpClient { Timeout = TimeSpan.FromSeconds(30) };
public bool Execute() { // Objekt zu JSON serialisieren, statt den String per Hand zu bauen var requestBody = new { message = "Hallo von Streamer.bot", count = 3 }; string json = JsonConvert.SerializeObject(requestBody);
var payload = new StringContent(json, Encoding.UTF8, "application/json");
HttpResponseMessage response = client .PostAsync("https://api.example.com/submit", payload) .GetAwaiter().GetResult();
string result = response.Content.ReadAsStringAsync().GetAwaiter().GetResult(); CPH.LogInfo($"POST-Antwort: {result}"); return true; }}Der Encoding.UTF8-Konstruktor von StringContent setzt den Content-Type-Header gleich mit auf application/json, du musst ihn nicht extra anhängen.
Authorization-Header und API-Keys
Abschnitt betitelt „Authorization-Header und API-Keys“Viele APIs verlangen einen Authorization-Header, meist als Bearer-Token. Den setzt du über client.DefaultRequestHeaders. Das Token selbst gehört niemals in den Code, sondern in einen Global, den du einmalig setzt und hier ausliest.
using System;using System.Net.Http;using System.Net.Http.Headers;
public class CPHInline { private static readonly HttpClient client = new HttpClient { Timeout = TimeSpan.FromSeconds(30) };
public bool Execute() { // Key aus dem Global lesen, NIE im Code hardcoden string apiKey = CPH.GetGlobalVar<string>("weatherApiKey", true); if (string.IsNullOrEmpty(apiKey)) { CPH.LogError("API-Key fehlt. Setze den Global weatherApiKey."); return false; }
using var request = new HttpRequestMessage(HttpMethod.Get, "https://api.example.com/weather"); request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", apiKey);
HttpResponseMessage response = client.SendAsync(request).GetAwaiter().GetResult(); string body = response.Content.ReadAsStringAsync().GetAwaiter().GetResult(); CPH.LogInfo(body); return true; }}Den Header pro Request über ein HttpRequestMessage zu setzen ist hier bewusst gewählt: weil der HttpClient statisch und geteilt ist, willst du nicht dauerhaft fremde Tokens an DefaultRequestHeaders hängen. Pro Request ist sauberer, wenn sich Werte ändern können.
Wie du an Token und Client-Id für die Twitch-API kommst, ist ein eigenes Thema, das die Seite Twitch Helix Custom-Calls abdeckt.
Antwort mit Newtonsoft parsen
Abschnitt betitelt „Antwort mit Newtonsoft parsen“Die Antwort kommt als String. Um ein einzelnes Feld zu ziehen, parst du mit JObject.Parse aus Newtonsoft.Json.Linq und greifst per Bracket-Notation darauf zu. Newtonsoft ist in jeder C#-Action by default referenziert, du musst nur das using setzen.
using Newtonsoft.Json.Linq;
JObject data = JObject.Parse(body);string feld = data["ergebnis"]["wert"].ToString();Verschachtelte Felder kettest du mit ["parent"]["child"]. Tiefergehende Muster (in eine eigene Klasse mit DeserializeObject<T> deserialisieren, Arrays durchlaufen) stehen auf der Seite JSON mit Newtonsoft.
Vollständiges Beispiel: kostenlose API in den Chat
Abschnitt betitelt „Vollständiges Beispiel: kostenlose API in den Chat“Ein Mini-Command !fact, der von einer offenen API ohne Key einen zufälligen Fakt holt und ein Feld davon in den Chat postet. Lauffähig, mit try/catch und Statuscode-Prüfung.
using System;using System.Net.Http;using Newtonsoft.Json.Linq;
public class CPHInline { private static readonly HttpClient client = new HttpClient { Timeout = TimeSpan.FromSeconds(10) };
public bool Execute() { try { HttpResponseMessage response = client .GetAsync("https://uselessfacts.jsph.pl/api/v2/facts/random?language=de") .GetAwaiter().GetResult();
if (!response.IsSuccessStatusCode) { CPH.LogWarn($"Fakt-API antwortete mit {(int)response.StatusCode}"); CPH.SendMessage("Konnte gerade keinen Fakt laden, versuch es gleich nochmal."); return false; }
string body = response.Content.ReadAsStringAsync().GetAwaiter().GetResult(); JObject data = JObject.Parse(body);
// nur das Feld "text" aus der Antwort holen string fact = data["text"].ToString();
CPH.SendMessage($"💡 {fact}"); return true; } catch (Exception e) { // Netzwerkfehler, Timeout, kaputtes JSON: alles hier abfangen CPH.LogError($"Fakt abrufen fehlgeschlagen: {e.Message}"); CPH.SendMessage("Fakt-Abruf hat nicht geklappt."); return false; } }}Das Muster ist immer gleich: GET brücken, Statuscode prüfen, Body lesen, ein Feld mit JObject.Parse ziehen, in den Chat posten. Der try/catch umschließt alles, weil jeder Netzwerk-Aufruf scheitern kann.
Häufige Fallen
Abschnitt betitelt „Häufige Fallen“awaitdirekt inExecute(): Kompiliert nicht, weilExecute()synchron ist. Immer.GetAwaiter().GetResult()nutzen, niemalsasync/awaitin der Action-Methode.new HttpClient()pro Aufruf: Jede Instanz hält ihren eigenen Socket offen, der nachDisposenoch imTIME_WAIThängt. Bei häufigen Aufrufen erschöpft das die verfügbaren Ports (Socket-Exhaustion). Genau deshalb einenstatic readonly HttpClientals Klassenfeld wiederverwenden, so zeigt es auch das offizielle HTTP-Beispiel auf docs.streamer.bot.- API-Key im Code: Landet beim Export in der geteilten
.sb-Datei. Key in einen persisted Global legen und mitCPH.GetGlobalVar<string>lesen. - Kein
try/catch: Ein Timeout, ein DNS-Fehler oder eine kurze Netzwerkstörung wirft eine Exception, und die ganze Action bricht hart ab. Jeden Netzwerk-Aufruf intry/catchpacken und im Fehlerfall sauberreturn false. - Statuscode ignorieren:
GetAsyncwirft bei einem 404 oder 500 keine Exception. Ohneresponse.IsSuccessStatusCode-Prüfung parst du eine Fehlerseite als wäre sie gültiges JSON und bekommst eineNullReferenceExceptionbeim Feldzugriff. - Rate-Limits: Bind
!factnicht an jede Chat-Nachricht und feuer keine Schleife los. Viele kostenlose APIs sperren dich nach wenigen Requests pro Sekunde. Setz ein Cooldown auf den Command und cache Antworten, wo es geht.