SOAP and WSDL
SOAP works in both directions: a flow can accept a SOAP request and answer with a SOAP response, and a flow can call a SOAP service. Envelope handling is automatic — you work with the body content.
Both SOAP 1.1 and 1.2 are supported. The version is the namespace the Envelope element is in,
whatever prefix binds it: <soap:Envelope>, <env:Envelope> and <Envelope xmlns="…"> are all
recognised.
Accepting SOAP
Nothing in the flow declares this. A request is treated as SOAP when it arrives on the ordinary
/run route and both of these hold:
Content-Typecontainstext/xmlorapplication/soap+xml, in any letter case, and- a
SOAPActionheader is present, or the first 512 bytes of the body contain anEnvelopeelement.
The 512-byte window matters for one case only: an envelope preceded by a long preamble — comments,
processing instructions, generous whitespace — is not recognised unless the caller also sends
SOAPAction.
The value of SOAPAction is not checked and does not choose the operation: a request with the
WSDL’s action, another value, an unquoted value, "" or no header at all runs the same flow.
The envelope is unwrapped and the flow receives the body content as an XML value — not the envelope, not the header block.
The body is the envelope’s own Body child, and only that one: an optional Header first, then
exactly one Body, then nothing, as SOAP 1.1 §4 and SOAP 1.2 Part 1 §5 prescribe. An element named
Body inside the Header is header content and never reaches the flow. Any other shape (a second
Body, a Body nested inside another element, an element after the Body, a DOCTYPE anywhere
in the document, an element declaring more than 256 namespace bindings) is a malformed envelope
and is refused before the flow runs; see Faults. Elements are matched by namespace and
local name, so any prefix works, the default namespace included.
The version comes from the envelope, not from the content type and not from the text of the body:
an Envelope in http://schemas.xmlsoap.org/soap/envelope/ is SOAP 1.1, one in
http://www.w3.org/2003/05/soap-envelope is SOAP 1.2, and the request runs and is answered in that
version even when its Content-Type names the other. Header and Body count only in the
envelope’s own namespace. An Envelope in any other namespace, or in none, is refused before the
flow runs with a VersionMismatch fault; see Faults.
Two variables are set:
| Variable | Contents |
|---|---|
ctx.soap_action |
The SOAPAction header value, if any |
ctx.soap_version |
1.1 or 1.2 |
$ curl -X POST http://localhost:9090/flows/acme/order-intake/run \ -H "Authorization: Bearer $NEXUS_KEY" \ -H 'Content-Type: text/xml' \ -H 'SOAPAction: SubmitOrder' \ -d '<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"> <soap:Body> <SubmitOrder><orderId>A-1</orderId></SubmitOrder> </soap:Body> </soap:Envelope>'The message is the <SubmitOrder> element, so $.orderId reads A-1: field access on an XML value
returns the attribute of that name if there is one, and otherwise the first child element with that
local name. See JSON and XML for the whole rule, including what comes back as text
and what stays a node.
One-way submission on /enqueue
The same envelope can be sent to the flow’s /enqueue route, detected by the same rule. The
envelope is unwrapped when it arrives and the content of its Body is queued; the flow receives the
same $ as on /run. The Header and SOAPAction are not queued, so for a queued message
ctx.soap_action and ctx.soap_version are absent.
The answer is 202 with the plain queue receipt,
not an envelope. A malformed envelope, or one that is not UTF-8, is answered with a JSON 400 and
nothing is queued, never a SOAP Fault. A sender that retries only a 5xx does not send it again.
Answering SOAP
If the request was SOAP, the response is wrapped for you. Produce an XML value and it becomes the body of a correctly versioned envelope, with the matching content type:
| Version | Response content type |
|---|---|
| 1.1 | text/xml; charset=utf-8 |
| 1.2 | application/soap+xml; charset=utf-8 |
A flow whose last step produces something other than XML still gets a valid envelope: the value is
serialised as text inside the body, the content type is still correct, and the response carries
X-Nexus-Soap-Warning: non-xml-output so the mismatch is visible rather than silent. Treat that
header as a bug report about your flow.
A complete SOAP-to-SOAP flow
---flowmarkdown_version: "0.1"flow: order-intaketenant: acmesoap_operation: SubmitOrdersoap_namespace: urn:acme:orders---
Accepts a SOAP order submission and answers with an acknowledgement.
## Step: check```validate$["$tag"] == "SubmitOrder" | "unexpected operation" | syntactic```
## Step: acknowledge```ntd<SubmitOrderResponse xmlns="urn:acme:orders"> <accepted>true</accepted> <receivedAt>{{ date_format(now()) }}</receivedAt> <correlationId>{{ ctx.correlation_id }}</correlationId></SubmitOrderResponse>```Calling a SOAP service
Add soap_version to an http_egress step. Its presence is what turns the call into a SOAP call.
| Key | Meaning |
|---|---|
soap_version |
1.1 or 1.2. Required to enable SOAP. Any other value is refused at publication |
soap_action |
Value for the SOAPAction header |
soap_operation |
Fallback for SOAPAction when soap_action is absent. It does not appear in the envelope |
## Step: build-body```ntd<GetOrderStatus xmlns="urn:partner:orders"> <orderId>{{ $.orderId }}</orderId></GetOrderStatus>```
## Step: call-partnereffects: [http_egress]endpoint: https://partner.example.com/OrderServicemethod: POSTsoap_version: "1.1"soap_action: urn:partner:orders/GetOrderStatussoap_operation: GetOrderStatusread_timeout_ms: 10000The message is wrapped in an envelope, the content type is set for the version, and the response
envelope is unwrapped so the next step sees the body content. The response is held to the same
shape as a request (see Accepting SOAP). One that is not a well-formed envelope,
a misplaced or second Body included, or whose Envelope is in a namespace that is neither SOAP
1.1 nor SOAP 1.2, is not unwrapped: the next step receives the whole response
as text, and the platform logs a warning.
Do not set content_type yourself on a SOAP step — it is derived from soap_version.
A SOAP call is an http_egress step, so it shares the circuit breaker of its destination with
every other HTTP call to the same scheme, host and port — see
Circuit breaker.
How the body is built depends on its value. An XML value is serialised as XML. A string whose first
non-blank character is < is taken for markup and sent as written. Any other string is escaped and
wrapped in <value>…</value>. A value of any other kind is refused, with an error naming the step.
The version vocabulary is closed
soap_version accepts 1.1 and 1.2, and nothing else. A value outside that vocabulary is refused
when the flow is published — by nexus deploy and by nexus validate alike — on ordinary steps, on
## Fault: steps, and inside a library expanded through call_flow or foreach. The refusal names
every wrong step in one verdict, not just the first one:
step 'call-partner': `soap_version: 1.0` is not a SOAP version — write one of "1.1", "1.2"Before this refusal existed, a misspelled version left a warning in the log and turned SOAP mode
off: the step sent bare XML with no envelope, no SOAPAction and the ordinary content type, and
the difference showed up only at the far end. An artifact published before the refusal still behaves
that way, with a warning that names what the request became and asks for the flow to be republished.
Republish such a flow and the typo becomes a refusal you can see.
Faults
When a SOAP request fails after the access gate, the response is a SOAP fault of the request’s version, with the correct envelope and content type. No configuration is needed. The few answers that stay plain HTTP are listed below.
The fault string is a fixed phrase per error, or the flow’s own message, never the error’s text
unless the flow copies it. It is the same phrase the JSON door sends in error, and the gRPC
door sends it as its message for every row but the last, which gRPC answers
flow execution failed:
| What failed | Fault string |
|---|---|
A validate rule |
the rule’s message as rendered; validate rule failed when it renders empty |
| The message lacks the dedup key | this message does not carry the field this flow deduplicates on |
| The message lacks the correlation key | this message does not carry the field this flow correlates on |
| The state kept for a reply is too large | the state this flow keeps for the reply is over the platform's ceiling for it |
| A reply with no open correlation | no correlation is open for this key; nothing here asked for this reply |
| A reply whose correlation is closed | the correlation for this key is already closed; this reply was already processed |
| A correlation already open for the key | a correlation is already open for this key; the earlier exchange is still awaiting its reply |
| The flow’s maximum duration | flow exceeded its maximum duration |
| Flow state unavailable | flow state unavailable: this flow cannot be delivered right now |
| The delivery lost its claim on its dedup key | flow execution stopped before it finished; some of its steps may have run |
The ## Fault: section itself failed |
fault handler failed |
| Anything else: a failed call, a route with no match, an expression error | flow execution failed |
| The platform could not load or run the flow | internal error |
On both SOAP versions, the two refusals of deduplication
that answer 409 on JSON are faults too, and their fault string is the sentence the JSON door sends in error:
another delivery with this key is in flight or
the recorded state of this key is not one this build understands for a delivery held back, and
this delivery already ran (recorded status <status>), but its response was larger than `dedup_max_response_bytes` and was not kept — it cannot be replayed
for a replay that cannot be served. Both are a soap:Server (1.1) or soap:Receiver (1.2) fault on
500, with the headers they carry on JSON (below).
The error itself goes to the operator. It carries, for example, the step, the downstream address
of a failed connection, HTTP <status> for an HTTP answer that is not 2xx (whose body is in
ctx.HTTP_RESPONSE_BODY), the code of a failed gRPC call, and up to 512 characters of an OAuth2
token endpoint’s error body; an error of the flow-state store names the flow and carries the
store’s own message, and a correlation refusal carries its own text. It is in the server log and in the request’s L1
record, under the correlation id the caller sent in X-Correlation-ID or X-Request-ID, or the
one the platform generated when it sent none. The same id is returned to the caller in the
X-Correlation-ID HTTP response header of the Fault and of a successful answer alike, never inside the
envelope: a SOAP client reads it from the HTTP headers, and quotes it to support.
When a ## Fault: section itself fails, the record holds both errors: the section’s own first, the
original after original error:.
A failed flow that no ## Fault: section handles leaves one flow-error line in the server log, with the tenant and the flow, at the
level of its error: WARN for a validate refusal, for a correlation refusal or conflict, and for
every other error, whose line reads execution error: …; ERROR for the maximum duration and for
a failed ## Fault: section. Only the execution error line carries status, the number sent. A
control character in the error text is written as its escape (\n, \r, \u{1}), so the line
stays one line. An http_egress call whose request or answer is over the message limit or whose
answer is not 2xx also writes a WARN line of its own, naming the step, and so does a
grpc_egress call whose request or streamed answer is over its limit, that cannot connect, or that
is answered with an error status. Not every failed grpc_egress call writes one: none is written
for a call refused because every circuit is open, its contract is unknown or its request does not
match the contract, or for one whose answer cannot be converted for the flow. The platform’s own
refusals, and its failures outside the flow, keep their own line, none with status; for example
ERROR for unavailable state on a flow with deduplication, WARN for a message that lacks the
dedup key or for a delivery held back, WARN for an access refusal, a rate limit or a signature
refusal at the gate, and a line without the correlation id for internal error.
The fault code and the HTTP status of that fault are decided together, by who made the mistake.
When the caller’s message is wrong or lacks something the flow needs — a validate rule that
refuses it, a dedup key that comes out empty, a correlation key that cannot be formed — the fault
names the sender. Every other failure names the server.
| Inbound envelope | The caller’s mistake | Any other failure |
|---|---|---|
| SOAP 1.1 | soap:Client on 500 |
soap:Server on 500 |
| SOAP 1.2 | soap:Sender on 400 |
soap:Receiver on 500 |
The 1.1 HTTP binding (§6.2) requires a fault to be carried on 500. SOAP 1.2 Part 2 (§7.5.2,
Table 20) names 400 for a Sender fault and 500 for every other. A client generated from the WSDL
can therefore tell a request that will never be accepted (Client, Sender) from one worth
retrying (Server, Receiver) by the code. The JSON door keeps its own statuses (422, 502, 409
and so on); a SOAP caller never sees them. A flow that sets ctx.soap_fault_code keeps its code,
and the status follows the code written: soap:Sender is 400 on SOAP 1.2, every other code is 500.
A response_status: the flow declares is honoured as before.
A request refused before the flow runs is the caller’s fault, and says so. Two refusals happen
before any flow is looked at: an envelope the platform cannot unwrap (a Body that is not the
envelope’s own single child is one), and a Body nested deeper than
100 elements (see Limits and constraints). Both answer a SOAP fault
whose code names the sender, with the status its version prescribes:
| Inbound envelope | HTTP status | Fault code |
|---|---|---|
| SOAP 1.1 | 500 | soap:Client |
| SOAP 1.2 | 400 | soap:Sender |
Envelope in a namespace that is neither SOAP 1.1 nor SOAP 1.2 |
500 on both versions | soap:VersionMismatch |
The fault takes the version of the envelope’s namespace. A refused envelope that declares neither
version is answered in the content type’s version: SOAP 1.2 for application/soap+xml, SOAP 1.1
otherwise. That is also the version of the VersionMismatch fault, on 500 in both versions; its
fault string is Invalid SOAP envelope: envelope namespace is neither SOAP 1.1 nor SOAP 1.2, and
it does not repeat the namespace that was sent. On SOAP 1.2 the fault carries an Upgrade header
block listing the supported envelopes, SOAP 1.2 first, then SOAP 1.1.
The fault string starts with Invalid SOAP envelope: or Invalid SOAP body:, followed by the
reason; for the depth refusal the reason is the limit alone. A Body that is not XML at all is not
refused: it reaches the flow as text.
What stays plain HTTP
These answers to a SOAP request are not faults:
- the access gate’s refusals, 401, 403, 404, 413, 429 and 503, decided before the envelope is
read; with a signing secret configured, the
401for a missing or wrongX-Nexus-Signature; - the 404 for a flow or tenant the API key cannot see, the same bytes as the gate’s 404 for an unknown tenant;
- the 503 when the execution budget is full;
- a dedup replay of a delivery whose answer was not kept (
dedup_memoize_response: false) and whose first answer succeeded: the recorded status with an empty body andX-Nexus-Idempotent-Replay: true.
A dedup delivery that is refused is a fault, and keeps its headers. A delivery held back because
another with the same key is in flight carries Retry-After and no replay header. A replay that
cannot be served (its answer was over dedup_max_response_bytes, or the recorded state of its key is
not one this build understands) carries X-Nexus-Idempotent-Replay: unavailable and
X-Nexus-First-Correlation-Id, and no Retry-After. Both are soap:Server or soap:Receiver on 500.
The replay of a delivery that failed, when its answer was not kept, is a fault too: the phrase is
flow execution failed, the code names the sender when the recorded status is 400 or 422 and the
server otherwise, and the replay headers stay. It does not repeat the text of the first answer.
Choosing the fault code and string
A flow can choose the fault code and string by writing two variables before the failure. Each one wins over the default on its own: a flow that sets only the code gets its code and the phrase above.
| Variable | Effect |
|---|---|
ctx.soap_fault_code |
Fault code, if it is a code of the request’s SOAP version (below); otherwise the default code |
ctx.soap_fault_string |
Fault string, when it holds a string, sent as written, except that a character XML 1.0 does not allow (U+0000 to U+001F other than tab, line feed and carriage return, U+FFFE, U+FFFF) is sent as U+FFFD. It holds even when a ## Fault: section sets it and then fails |
The fault code is one of the request’s version, with the soap: prefix the fault declares:
- SOAP 1.1:
soap:Client,soap:Server,soap:VersionMismatch,soap:MustUnderstand. A dot refinement is accepted:soap:Client.Authentication. - SOAP 1.2:
soap:Sender,soap:Receiver,soap:VersionMismatch,soap:MustUnderstand,soap:DataEncodingUnknown, with no refinement. - The match is exact and case-sensitive. Spaces, tabs and line breaks around the code are removed.
- Anything else is replaced by the default code of the error (above):
soap:Clientorsoap:Serveron 1.1,soap:Senderorsoap:Receiveron 1.2: no prefix, another prefix, another spelling, the other version’s code, a value that is not a string. The server log gets aWARNthat names the flow and the correlation id, not the value. - To pick a code that suits both versions, branch on
ctx.soap_version. - The HTTP status does not depend on the code.
$.fault_error is internal. In a ## Fault: section it holds the error as the operator sees
it. It carries, for example, the step, the downstream address of a failed connection,
HTTP <status> for an HTTP answer that is not 2xx (whose body is in ctx.HTTP_RESPONSE_BODY), the
code of a failed gRPC call, and up to 512 characters of an OAuth2 token endpoint’s error body; an
error of the flow-state store names the flow and carries the store’s own message, and a correlation
refusal carries its own text. Copying it into
ctx.soap_fault_string, or into any response body, sends all of that to the caller. To give the
caller something to quote, send a fixed phrase and $.fault_correlation_id.
A ## Fault: section works as it does elsewhere, and its response_status: is honoured for SOAP
responses too — so you can answer a validation failure with 400 and a fault envelope. What a SOAP
branch does not pick up is the response-header set: it composes its own reply, so
response_header_<name>: does not reach it. See Handling failures.
Serving a WSDL
GET /flows/{tenant}/{name}/wsdlThe document is generated from the flow’s front matter:
| Front-matter key | Meaning |
|---|---|
soap_operation |
Operation name |
soap_namespace |
Target namespace of the generated WSDL |
Both keys or neither. A flow that declares only one of the two keys, an empty value, an
operation that is not an XML name without a colon (NCName) or a namespace that is not an absolute
URI is refused at publication: nexus validate and nexus deploy exit with an error that names the
key. There is no default for either one. That is deliberate — an operation name guessed from the
flow name, or a namespace invented by the platform, would appear in a contract handed to somebody
else’s code generator.
The route needs no credentials, but it does need a visible tenant. A tenant that was never registered, or that has been disabled, gets the same 404 here as everywhere else, so a WSDL request cannot be used to find out which organisations exist on an installation.
A flow with routes: has a WSDL only with serve_by_name: true. Without the key, its WSDL
route answers the same 404 as for a flow that is not published. See
Served by name.
$ curl http://localhost:9090/flows/acme/order-intake/wsdlWhat the generated document contains: one service, one port, one operation, a SOAP 1.1
document/literal binding, and an address pointing at the flow’s /run endpoint.
The operation’s soapAction is the WS-Addressing default action of its input,
{namespace}{d}{op}PortType{d}{op}Request, where {namespace} is soap_namespace and {op} is
soap_operation. The delimiter {d} is : for a urn: namespace and / for any other; a
namespace that already ends with / gets no second one. A non-ASCII letter of the operation is
percent-encoded from its UTF-8 bytes, so Notificăre is written Notific%C4%83re. For the
operation SubmitOrder:
soap_namespace |
soapAction |
|---|---|
urn:acme:orders |
urn:acme:orders:SubmitOrderPortType:SubmitOrderRequest |
http://acme.example/orders |
http://acme.example/orders/SubmitOrderPortType/SubmitOrderRequest |
A client sends that value, in double quotes, in the SOAPAction header.
What it does not contain: a schema for your messages. The message types are declared as
xsd:anyType, because the platform does not know the shape of your payloads — it moves them. The
WSDL is enough for a loosely typed client to discover the endpoint and the operation; a client that
generates strongly typed stubs from a schema will not get useful types out of it. If the consumer
needs a typed contract, hand them a hand-written schema alongside.
The binding is always declared as SOAP 1.1, even for a flow that accepts 1.2 requests.