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 setztcontentTypeselbst).
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) oderundefined.payload— ein XML-String, ein perxml.create(...)gebauter Knoten odernull(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/bearergehen als HTTP-Header (Authorization).wsssetzt einen WS-SecurityUsernameToken(PasswordText) in den SOAP-Header — das übliche Muster bei SAP-Middleware und anderen klassischen SOAP-Diensten.- SOAP 1.1 setzt die
actionin denSOAPAction-HTTP-Header; SOAP 1.2 trägt sie imContent-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.
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 perxml.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
wssinnvoll 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.