Zum Inhalt springen
  • Dunkel
  • Hell
  • System

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.

Core C# Code Execute C# Code

Doku:

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 ein Client-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.

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 readonly als Klassenfeld. Der Client wird einmal erzeugt und über alle Aufrufe wiederverwendet. Niemals ein new HttpClient() pro Aufruf, mehr dazu unten bei den Fallen.
  • .GetAwaiter().GetResult() statt await. Das ist das Muster, das auch das offizielle HTTP-Beispiel auf docs.streamer.bot für den synchronen Execute()-Kontext nutzt.

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.

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.

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.

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.

  • await direkt in Execute(): Kompiliert nicht, weil Execute() synchron ist. Immer .GetAwaiter().GetResult() nutzen, niemals async/await in der Action-Methode.
  • new HttpClient() pro Aufruf: Jede Instanz hält ihren eigenen Socket offen, der nach Dispose noch im TIME_WAIT hängt. Bei häufigen Aufrufen erschöpft das die verfügbaren Ports (Socket-Exhaustion). Genau deshalb einen static readonly HttpClient als 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 mit CPH.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 in try/catch packen und im Fehlerfall sauber return false.
  • Statuscode ignorieren: GetAsync wirft bei einem 404 oder 500 keine Exception. Ohne response.IsSuccessStatusCode-Prüfung parst du eine Fehlerseite als wäre sie gültiges JSON und bekommst eine NullReferenceException beim Feldzugriff.
  • Rate-Limits: Bind !fact nicht 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.