Zum Inhalt

WebService-Client (ws)

Das Objekt ws ruft fremde Web-Services auf — SOAP für klassische Fachsystem-Schnittstellen und REST/JSON für moderne APIs. Es steht in allen Skripten bereit: Formular-Hooks (siehe Lifecycle), Schaltflächen, Skript-Konsole, Jobs und REST-Services.

ws oder http?

ws ist der bequeme Client für Konnektoren und SOAP. Für alles Weitere — Formular- und Multipart-Uploads, Downloads, Cookies, OAuth2, Wiederholungen — gibt es den allgemeinen http-Client. Beide nutzen dieselben Konnektoren und dieselbe Allowlist.

Beide Welten kennen dieselben zwei Grundmuster:

  • sendReceive — synchron: Anfrage raus, auf Antwort warten, Ergebnis verwenden.
  • fireAndForget — asynchron: Anfrage absetzen, nicht auf die Antwort warten (z. B. Benachrichtigungen). Keine Rückgabe.

Endpunkt + Zugangsdaten gehören in einen Konnektor

Für alles Produktive lohnt sich ein benannter Konnektor (System-Administration → Schnittstellen & Verteilung → HTTP-/WebService-Konnektoren): Basis-URL, Auth und Timeout liegen zentral, und kein Passwort steht im Skript. Das Skript referenziert nur den Namen: ws.connect("sap-erp"). Siehe Benannte Konnektoren.

Kurzreferenz

Aufruf Zweck
ws.rest.get(url[, opt]) REST GET, Response-Objekt
ws.rest.post(url, body[, opt]) REST POST (Body als JSON/String)
ws.rest.put(url, body[, opt]) REST PUT
ws.rest.patch(url, body[, opt]) REST PATCH
ws.rest.del(url[, opt]) REST DELETE
ws.rest.request(opt) / ws.rest.sendReceive(opt) REST generisch (alle Optionen)
ws.rest.fireAndForget(opt) REST asynchron, ohne Antwort
ws.soap.sendReceive(endpoint[, action], payload[, opt]) SOAP-Aufruf, Antwort als XmlNode
ws.soap.fireAndForget(endpoint[, action], payload[, opt]) SOAP asynchron, ohne Antwort
ws.sendReceive(...) / ws.fireAndForget(...) Top-Level-Aliase auf ws.soap.*
ws.connect(name[, overrides]) Benannten Konnektor öffnen

REST / JSON — ws.rest

Für jede HTTP-Methode gibt es eine Kurzform. Alle geben ein Response-Objekt zurück (siehe unten):

var r = ws.rest.get("https://api.example.com/v1/status");
var r = ws.rest.post("https://api.example.com/v1/orders", { id: 42 });
var r = ws.rest.put("https://api.example.com/v1/orders/42", { id: 42 });
var r = ws.rest.patch("https://api.example.com/v1/orders/42", { done: true });
var r = ws.rest.del("https://api.example.com/v1/orders/42");

Der body

  • Ein JS-Objekt oder -Array wird automatisch als JSON gesendet (Content-Type: application/json).
  • Ein String geht roh raus (Content-Type: text/plain, außer du setzt contentType selbst).
var url = "http://localhost:8099/api/demo/ws-echo";   // Echo-Endpunkt der App

// Objekt → JSON
ws.rest.post(url, { orderId: 42, items: ["A", "B"] });

// String → roh, mit eigenem Content-Type
ws.rest.post(url, "<xml/>", { contentType: "application/xml" });

Das Response-Objekt

get/post/… liefern immer ein Objekt mit diesen Feldern:

Feld Typ Bedeutung
status int HTTP-Statuscode (z. B. 200, 404)
ok boolean true bei 2xx
contentType String Content-Type der Antwort
url String tatsächlich aufgerufene URL
text String Antwort-Body als Text
headers Objekt Response-Header { name: "wert" }
json() Funktion parst text als JSON in ein navigierbares JS-Objekt

ok prüfen — HTTP-Fehler werfen nicht automatisch

Ein 404 oder 500 ist eine gültige Antwort: r.status ist gesetzt, r.ok ist false — es fliegt keine Exception. Nur echte Kommunikationsfehler (DNS, Timeout, TLS, SSRF-Sperre) werfen. Prüfe also immer r.ok, und fange den Rest mit try/catch. r.json() wirft zusätzlich, wenn der Body kein gültiges JSON ist.

ws.rest.request(options) — die generische Form

Wenn Query-Parameter, Header oder Auth ins Spiel kommen, nimmst du die Options-Variante (request und sendReceive sind identisch):

Option Bedeutung
method "GET", "POST", …
url Ziel-URL
headers { name: "wert" } zusätzliche HTTP-Header
query { name: "wert" } — wird als ?name=wert angehängt (URL-kodiert)
body Objekt/Array (→ JSON) oder String (roh)
contentType Content-Type des Requests überschreiben
auth "none" (Standard), "basic" oder "bearer"
username, password für auth:"basic"
token für auth:"bearer"
timeout Timeout in ms (höchstens 60000, siehe Zeitlimit)

Beispiel 1 — GET mit Query + Bearer-Token, json() auswerten

// onFieldChange: Kundendaten zu einer eingegebenen Nummer laden
try {
  var r = ws.rest.request({
    method:  "GET",
    url:     "https://api.example.com/v1/customers",
    query:   { number: $kundennummer, fields: "name,city" },
    auth:    "bearer",
    token:   "eyJhbGciOi…",           // besser: aus einem Konnektor, s. u.
    timeout: 10000
  });

  if (!r.ok) {
    view.add(new view.Message("Service antwortete mit HTTP " + r.status));
  } else {
    var data = r.json();               // JSON → JS-Objekt
    $kundenname = data.name;
    $kundenort  = data.city;
  }
} catch (e) {
  view.add(new view.Message("Service nicht erreichbar: " + e.message));
}

Beispiel 2 — POST eines Formulardatensatzes als JSON

// onSubmit: den Vorgang an ein Fremdsystem melden
var r = ws.rest.post("https://api.example.com/v1/orders", {
  orderId: $orderId,
  customer: $kundennummer,
  items: [
    { sku: $artikel1, qty: $menge1 },
    { sku: $artikel2, qty: $menge2 }
  ]
});

if (r.ok) {
  $status = r.json().status;           // Antwortfeld in ein Formularfeld
} else {
  throw new Error("Übertragung fehlgeschlagen (HTTP " + r.status + "): " + r.text);
}

Ein Fehler in onSubmit macht das Speichern nicht rückgängig

onSubmit läuft nach dem Speichern: Der Datensatz ist schon in der Datenbank, eine geworfene Exception zeigt nur die Meldung an. Soll ein fehlgeschlagener Aufruf das Speichern verhindern, gehört er in onBeforeSave (siehe Lifecycle).

SOAP — ws.soap

ws.soap.sendReceive baut den kompletten SOAP-Envelope (Header + Body) selbst. Du lieferst nur die Nutzlast (das Body-Element). Die Antwort kommt als XML-Knoten zurück — das erste Element im SOAP-Body der Antwort (oder null bei leerem Body). Wie du darin navigierst, steht unter XML lesen und bauen.

Überladungen:

ws.soap.sendReceive(endpoint, payload)
ws.soap.sendReceive(endpoint, action, payload)
ws.soap.sendReceive(endpoint, action, payload, options)
  • endpoint — die volle URL des Service.
  • action — die SOAPAction (String) oder undefined.
  • payload — ein XML-String, ein per xml.create(...) gebauter Knoten oder null (leerer Body).
  • options — Objekt, alle Felder optional:
Option Werte / Bedeutung
soapVersion "1.1" (Default) oder "1.2"
auth "none", "basic", "bearer", "wss"
username, password für "basic" (HTTP) bzw. "wss" (UsernameToken)
token für "bearer" (HTTP-Header)
headers { name: "wert" } zusätzliche HTTP-Header
timeout Timeout in ms (höchstens 60000, siehe Zeitlimit)

Zum Auth-Verhalten:

  • basic / bearer gehen als HTTP-Header (Authorization).
  • wss setzt einen WS-Security UsernameToken (PasswordText) in den SOAP-Header — das übliche Muster bei SAP-Middleware und anderen klassischen SOAP-Diensten.
  • SOAP 1.1 setzt die action in den SOAPAction-HTTP-Header; SOAP 1.2 trägt sie im Content-Type (action="…") mit.

Fehler und SOAP-Faults werfen

Ein Kommunikationsfehler oder ein <Fault> in der Antwort wirft eine Exception; der Fault-Text steht in e.message. SOAP-Aufrufe daher immer in try/catch kapseln.

Beispiel 3 — sendReceive mit XML-String + SOAPAction

var payload =
  '<GetCity xmlns="http://example.org/geo">' +
  '  <zip>' + $plz + '</zip>' +
  '</GetCity>';

try {
  var resp = ws.soap.sendReceive(
    "https://geo.example.org/soap",     // endpoint
    "http://example.org/geo/GetCity",   // SOAPAction
    payload                             // Body-Element
  );

  // resp ist das erste Element im SOAP-Body der Antwort
  $ort = resp.text;                     // z. B. <GetCityResponse>Hamburg</…>
  log.debug("Antwort-XML: " + xml.stringify(resp));
} catch (e) {
  view.add(new view.Message("SOAP-Fehler: " + e.message));
}

Tiefere Strukturen liest du über resp.children oder gezielt mit resp.find("tag") (direktes Kind) bzw. resp.findDeep("tag") (irgendwo im Teilbaum) — siehe XML lesen und bauen.

Beispiel 4 — Payload per xml.create(...) + WS-Security

Statt XML als String zu stückeln, baust du den Body strukturiert mit xml.create. Werte werden dabei automatisch korrekt maskiert (&, < …) — beim Zusammensetzen von Strings musst du das selbst tun. Der Knoten wird direkt in den Envelope gesetzt:

// Body strukturiert bauen
var req = xml.create("GetMaterial");
req.attr("xmlns", "urn:sap-com:document:sap:rfc:functions");
req.addChild("Material", $materialnr);
req.addChild("Plant", $werk);

var resp = ws.soap.sendReceive(
  "https://sap-po.firma.local/XISOAPAdapter/…",
  "urn:GetMaterial",
  req,                                  // Knoten als Payload
  {
    soapVersion: "1.1",
    auth: "wss",                        // WS-Security UsernameToken
    username: "ws_service",
    password: "geheim"
  }
);

$bezeichnung = resp.children[0].text;

Credentials nicht ins Skript schreiben

username/password gehören in einen Konnektor — im Skript stehen sie sonst im Klartext, und jeder mit Zugriff auf das Formular im Formular-Editor kann sie lesen. Oben nur zur Illustration.

Beispiel 5 — SOAP 1.2

Bei soapVersion:"1.2" wandert die action in den Content-Type — das Skript ändert sich sonst nicht:

var resp = ws.soap.sendReceive(
  "https://svc.example.org/v12",
  "http://example.org/DoWork",          // wird im Content-Type mitgeführt
  "<DoWork xmlns='http://example.org/'><n>1</n></DoWork>",
  { soapVersion: "1.2", auth: "basic", username: "u", password: "p" }
);

Top-Level-Aliase

Die SOAP-Grundoperationen gibt es auch direkt auf ws:

ws.sendReceive(endpoint, action, payload)   // == ws.soap.sendReceive(...)
ws.fireAndForget(endpoint, action, payload) // == ws.soap.fireAndForget(...)

fireAndForget — abschicken ohne Warten

Wenn dich die Antwort nicht interessiert (Benachrichtigung, Log-Ping), sparst du dir das Warten. Es gibt keine Rückgabe, und Fehler bei der Zustellung landen nur im Server-Protokoll — nicht im Skript. Nur eine unzulässige Adresse (z. B. durch den SSRF-Schutz gesperrt) meldet fireAndForget sofort:

// REST: Benachrichtigung asynchron rausschicken
ws.rest.fireAndForget({
  method: "POST",
  url:    "https://hooks.example.com/notify",
  body:   { event: "form.submitted", id: ctx.instanceId }
});

// SOAP: Event feuern, Antwort ignorieren
ws.soap.fireAndForget(
  "https://svc.example.org/events",
  "http://example.org/Notify",
  "<Notify><id>" + ctx.instanceId + "</id></Notify>"
);

fireAndForget schluckt Fehler

Weil niemand auf die Antwort wartet, siehst du Fehler nicht im Formular. Für alles, wo du wissen musst, ob es geklappt hat, nimm sendReceive/get/post und prüfe das Ergebnis.

Benannte Konnektoren

Ein Konnektor bündelt Basis-URL, Auth, Default-Header, Timeout und SOAP-Version unter einem Namen (System-Administration → Schnittstellen & Verteilung → HTTP-/WebService-Konnektoren). Das Skript referenziert nur den Namen — Endpunkt und Zugangsdaten bleiben zentral. Ein deaktivierter Konnektor wirft beim connect einen Fehler.

var c = ws.connect("sap-erp");

Das Verbindungsobjekt bietet die volle REST-/SOAP-API plus Kurzformen, und löst relative Pfade gegen die Basis-URL auf:

Aufruf Bedeutung
c.rest.*, c.soap.* die vollen APIs (wie ws.rest / ws.soap)
c.get(path[, opt]) REST GET auf baseUrl + path
c.post(path, body[, opt]) REST POST
c.put(path, body), c.del(path) REST PUT / DELETE
c.sendReceive(action, payload) SOAP-Aufruf gegen die Konnektor-URL
c.fireAndForget(action, payload) SOAP asynchron
c.close() tut nichts; nur vorhanden, damit übernommene Skripte laufen

Beispiel 6 — Konnektor + relativer OData-Pfad

// Konnektor "sap-erp": baseUrl = https://s4.firma.local, auth = bearer/OAuth
var c = ws.connect("sap-erp");

var r = c.get("/odata/v4/API_BUSINESS_PARTNER/A_BusinessPartner", {
  query: {                              // wird URL-kodiert angehängt
    "$filter": "BusinessPartner eq '" + $partnerId + "'",
    "$select": "BusinessPartnerName"
  }
});

if (r.ok && r.json().value.length > 0) {
  $partnerName = r.json().value[0].BusinessPartnerName;
}

Pro Aufruf lassen sich einzelne Konnektor-Werte über overrides überschreiben (2. Argument von ws.connect) — auth, username, password, token, soapVersion, baseUrl, timeout, headers:

// gleicher Konnektor, aber mit einem Einmal-Token statt der hinterlegten Auth
var c = ws.connect("sap-erp", { auth: "bearer", token: einmalToken });

ws in Button-/Custom-Hooks

ws steht in jedem Hook zur Verfügung, auch hinter einem kc:button. So klinkst du z. B. einen „Jetzt an ERP senden"-Button ein (Details zum Hook-Modell in Lifecycle):

// Custom-Hook "sendeAnErp", an einem kc:button verdrahtet
var c = ws.connect("sap-erp");
var r = c.post("/api/v1/orders", {
  orderId:  $orderId,
  customer: $kundennummer
});

if (r.ok) {
  view.add(new view.Message("An ERP übertragen: " + r.json().status));
} else {
  view.add(new view.Message("Fehlgeschlagen (HTTP " + r.status + ")"));
}

Anbindung an SAP & andere Systeme

Als Faustregel:

  • SOAP für klassische Schnittstellen — SAP-Middleware, viele Bestandsdienste. Auth meist als WS-Security UsernameToken (auth:"wss"). Body strukturiert per xml.create(...).
  • REST/OData (v2/v4, JSON) für S/4HANA-APIs und moderne Dienste. Auth per Bearer/OAuth-Token oder Basic. Antwort mit r.json() auswerten.

Empfehlung für beides: Endpunkt und Zugangsdaten als benannten Konnektor pflegen und im Skript nur referenzieren (ws.connect("sap-erp")). Das hält Zugangsdaten aus den Skripten heraus, erlaubt zentrale Wartung (URL-Wechsel, Passwort-Rotation) und macht die Skripte kürzer.

XML lesen und bauen

SOAP-Antworten, r.xml() beim http-Client und xml.parse(text) liefern XML-Knoten. Mit xml.create(tag[, text]) baust du selbst welche.

Die vollständige Erklärung samt Referenz aller Knoten-Handgriffe steht im Kapitel XML verstehen und verarbeiten — inklusive Namensräumen, Stolpersteinen und einer Umsteigertabelle für alle, die von E4X kommen. Hier nur die Kurzfassung:

Handgriff Bedeutung
node.text / node.attr(name) Textinhalt bzw. Attribut lesen
node.find(tag) / node.findAll(tag) erstes bzw. alle direkten Kinder
node.findDeep(tag) / node.findAllDeep(tag) erstes bzw. alle im ganzen Teilbaum
node.addChild(tag[, text]) Kind anlegen, liefert das neue Element
xml.stringify(node[, true]) als Text; true = eingerückt

Tag-Namen mit Präfix

find/findDeep vergleichen den Tag-Namen so, wie er im XML steht — einschließlich Präfix. Heißt das Element in der Antwort <ns2:Material>, suchst du findDeep("ns2:Material").

var doc = xml.parse("<liste><eintrag id='1'>A</eintrag><eintrag id='2'>B</eintrag></liste>");
doc.findAll("eintrag").forEach(function (e) {
  log.info(e.attr("id") + " = " + e.text);
});

var neu = xml.create("bestellung");
neu.attr("nr", String(ctx.instanceId));
neu.addChild("kunde", $name);
log.info(xml.stringify(neu, true));

Sicherheit

Ausgehende Aufrufe laufen durch denselben SSRF-Schutz wie beim http-Client: öffentliche Ziele sind erlaubt, interne Ziele (localhost, private Netze) nur mit Eintrag in der Allowlist, link-local-Adressen wie die Cloud-Metadaten-Adresse 169.254.169.254 nie. Einzelheiten und wo die Allowlist gepflegt wird: HTTP-Client → Sicherheit.

Timeout und Antwortgröße sind gedeckelt

  • Timeout — kcforms.webservice.max-timeout-millis (Standard 60000 ms); ein größer gesetzter Wert wird gekappt. Beachte außerdem das Zeitlimit des Skripts: In Formular-Hooks bricht das Skript nach 6 Sekunden ab.
  • Antwortgröße — kcforms.webservice.max-response-bytes (Standard 10 MB).

In-App-Demo (Echo-Endpunkte)

Zum Ausprobieren bringt KCForms zwei Echo-Endpunkte mit, die Anfragen zurück- spiegeln:

Endpunkt Art Verhalten
http://localhost:8099/api/demo/ws-echo REST spiegelt Methode, Body, Query und Header als JSON
POST http://localhost:8099/api/demo/ws-soap SOAP antwortet mit tns:echoResponse: tns:message (Textinhalt), tns:service und tns:received (Kopie des gesendeten Elements)

localhost meint hier das Backend selbst — dort läuft das Skript.

Vorher localhost auf die Allowlist setzen

localhost ist ein internes Ziel; ohne Eintrag sperrt der SSRF-Schutz den Aufruf (siehe Sicherheit). Auf Test- und Entwicklungssystemen localhost in die Allowlist eintragen.

REST gegen den Echo-Endpunkt:

var r = ws.rest.post("http://localhost:8099/api/demo/ws-echo", {
  gruss: "hallo",
  nummer: 42
});

var echo = r.json();
log.debug("Zurückgespiegelt: " + JSON.stringify(echo.body));  // { gruss:"hallo", nummer:42 }

SOAP gegen den Echo-Endpunkt:

var resp = ws.soap.sendReceive(
  "http://localhost:8099/api/demo/ws-soap",
  "urn:Echo",
  "<Ping xmlns='urn:demo'><wert>test</wert></Ping>"
);

log.debug("Echo-XML: " + xml.stringify(resp));   // <tns:echoResponse> …
log.debug("Wert: " + resp.findDeep("wert").text);  // "test" — aus der Kopie in tns:received

Verwandte Themen

  • http-Client — Uploads, Downloads, Cookies, OAuth2, Wiederholungen.
  • Lifecycle-Hooks — in welchem Hook du ws sinnvoll aufrufst (und wann ein Fehler das Speichern verhindert).
  • view — Rückmeldungen ans Formular (view.Message) statt harter Abbrüche.
  • fs-Sandbox — Dateien lesen/schreiben, z. B. Antworten archivieren.