Repository navigation
fix(binding-opcua): own the octet-stream codec for opc.tcp forms (#1400) - #1574
Conversation
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## master #1574 +/- ##
==========================================
+ Coverage 78.76% 79.44% +0.68%
==========================================
Files 91 91
Lines 16149 16533 +384
Branches 1576 1674 +98
==========================================
+ Hits 12720 13135 +415
+ Misses 3404 3367 -37
- Partials 25 31 +6 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
|
||
| | Content-Type | body encoding | | ||
| | -------------------------------------------------------- | ------------------------------ | | ||
| | `application/octet-stream`, `application/opcua+uabinary` | OPC UA Binary (§5.2) | |
There was a problem hiding this comment.
I think, a hint should be provided that this binding only supports OPC UA Binary, right?
There was a problem hiding this comment.
In fact that whole point of this PR and the shift I want to operate, we cannot support OPC UA Binary in an universal way.
This could work for datavalue containing basic types as scalar arry or matrix, but will fall short for datavalue containing extension object. In OPC UA Binary, extension object can only be decoded if the consumer knows to get the data type definitons of the extensions object, this information is not stored in the datavalue bin blob, but requires additional complex round trip ( read, browse, etc) with the OPC UA server
This is the reason I want to suggest that hte default OPCUA encoding in WOT to be the OPCUA+JSON representation, that is self-contained, and provide different flavors from short representation (number=> number, extension object=> json object) to reversible representation (Full dataValue with types nodes and extra opcua upper structure).
=> shifting default => binary to OPCUA/JSON (that is normative) is something I want to propose to the WOT OPCUA group is the next release of the spec.
https://reference.opcfoundation.org/specs/OPC-10000-6/annex-g
and this PR will serve as the proof of concept and demonstrator for why it has to be this way.
|
I did run the same example/process as in #1400 once again with this PR. I am still getting the same error CMD shellTo reproduce the test
JS Code// client.js
const { Servient } = require("@node-wot/core");
const { OPCUAClientFactory } = require( "@node-wot/binding-opcua");
// create Servient and add binding
const servient = new Servient();
servient.addClientFactory(new OPCUAClientFactory());
const thingDescription = {
"@context": "https://www.w3.org/2019/wot/td/v1",
"@type": ["Thing"],
securityDefinitions: { nosec_sc: { scheme: "nosec" } },
security: "nosec_sc",
properties: {
pumpSpeed: {
description: "the pump speed",
type: "number",
forms: [
{
href: "opc.tcp://opcuademo.sterfive.com:26543?id=ns=1;s=PumpSpeed",
contentType: "application/octet-stream",
op: ["readproperty", "observeproperty"],
},
],
},
},
};
servient
.start()
.then(async (WoT) => {
try {
const thing = await WoT.consume(thingDescription);
// read property
const read1 = await thing.readProperty("pumpSpeed");
console.log("pumpSpeed value is: ", await read1.value());
} catch (err) {
console.error("Script error:", err);
}
})
.catch((err) => {
console.error("Start error:", err);
}); |
That stack is master's code, not this branch. Yours goes The install is why: Quick check — 1 means master, 0 means this PR: To get the branch: That drops the two tarballs in the repo root. Then from your test project: I ran your client.js that way. PumpSpeed is a Double, so octet-stream is refused — |
|
Sorry, you are right; it really picked the wrong code. I checked out some examples, and it seems to work (I want to do some more tests, so please be patient with me), but I also have some questions.
|
|
Note: The TD is about describing what is on the wire. Only the octet-stream is what should work. The JSON format is an internal format of the node-opcua library. It does not represent what is on the wire. Does that make sense to you? For example, in the case of OPCUA SOAP, the contentType should match XML. But here we are interested in OPCUA Binary encoding. |
|
The octet-stream / opcua+octet-stream split is the one proposed on #1400 (comment): application/opcua+octet-stream: the value in OPC UA Binary (OPC 10000-6 §5.2). It carries its own type id, so a Double works. value() decodes it back, which is why you get the same object. What's on the wire. On opc.tcp, the wire is always OPC UA Binary, and it carries a whole ReadResponse (header, status codes, DataValue), not the bare value. No contentType can describe that. The form's contentType describes the representation the binding hands to the consumer. The JSON is not node-opcua's. application/opcua+json is the OPC UA JSON encoding (OPC 10000-6 §5.4). In your sample, Type: 11 is the built-in type id of Double (1.04 names; 1.05 calls it UaType / Value). One point is still open: OPC 10000-6 Table 80 maps application/octet-stream to OPC UA Binary, and this PR gives it to ByteString instead. The reasoning is in decisions.md, D4.. |
|
The Having said that. I think it should be forbidden to put "application/json" in the context of OPC UA, since it is never the wire format. @sebastiankb @egekorkan please chime in |
|
I agree with @danielpeintner The OPC UA Binding 1.0 essentially targets OPC UA Client/Server over opc.tcp, where the encoding on the wire is always UA Binary. In a TD, the form's contentType tells the consumer what is actually exchanged over the protocol. It does not describe a representation that a library produces internally after decoding. application/opcua+json never appears on the wire in Client/Server, so it should not be used for opc.tcp forms for now. It may become relevant when we consider describing HTTP-based OPC UA servers or PubSub over MQTT. From my point of view, the only open question is application/octet-stream vs. application/opcua+uabinary vs. application/opcua+octet-stream:
To keep things simple, I would suggest that the implementation keeps the existing contentType variations for now for backward compatibility. However, I would ask @erossignon to change the behavior of application/octet-stream in this PR so that the payload is treated as UA Binary and value() returns the decoded value, i.e., the consumer gets the same result as with application/json. This will ensure that Thing Descriptions following OPC 10101, which uses application/octet-stream, work as expected. |
|
Agreed: on The full plan: octet-stream-ua-binary-plan.md. What the request leaves open. On
On I am starting the implementation on this PR now, as a short series of commits that can be reverted as one range if you prefer another choice. node-opcua stays on 2.184; a later commit moves to the release that contains node-opcua/node-opcua#1835, which aligns node-opcua's OPC UA JSON on Part 6 (namespace URIs, matrices, structures, unions). Caveats:
|
|
Question A3 (writing a structure, or passing one as an action argument) now has its own issue, #1583, so it can be settled apart from this review. Until then this PR refuses a structure write with a message that links to the issue. The binding is now on node-opcua 2.187.10: OPC UA JSON 1.05 names namespaces by URI ( |
|
I did some basic testing... TD with contentType
TD with contentType
As said elsewhere, I think MY only valid option would be I am open to opinions! |
|
My points:
|
|
Sorry for the contradictory directions in this discussion. The root cause is that the WoT OPC UA Binding 1.0 specifies the generic application/octet-stream. This causes problems in WoT implementations, which assume that the contentType alone defines how the payload is decoded. After discussing this with @danielpeintner, we agree not to change this philosophy in node-wot. So instead of To keep things clean, I suggest that this PR:
Returning the complete DataValue container (e.g. with SourceTimestamp) should be out of scope for now and can be addressed in the next node-wot update. Again, apologies for the confusion. |
I hope, it is also ok to use |
|
Thanks for the clarification. Before the details, what it means for this PR: it is mostly an unwinding. Of the four contentTypes the PR implements, three are removed and the fourth is renamed (table below). Postponing the DataValue would cut further: today a TD can get the status and timestamps of a value ( Here is how I would implement your request, with one point where I disagree.
Where I disagree is postponing the DataValue. The payload already is the OPC UA DataValue, and a parameter of the same media type can select what
With the value alone:
The content type alone still decides the decoding: the parameter is part of it, and core already passes parameters to the codec. No core change, and no per-scheme codec (#1572). What this decision changes in the PR
One question: a form without contentType is |
…eb#1400 Reads one Double over a real OPC UA connection once per combination of contentType and schema-type placement, and prints the result table. Assertions pin the current master behaviour so the test goes red when eclipse-thingweb#1400 is fixed: application/octet-stream fails in every form with the same OctetstreamCodec error Daniel reported when he opened the issue. Run against PR eclipse-thingweb#1449 the matrix differs - only the row declaring type on BOTH the affordance and the form passes, which is why his report against that PR was correct.
Twelve OPC UA shapes - scalars, arrays, LocalizedText, Variant, ExtensionObject - crossed with six contentType spellings. Self-contained: registers its own namespace and variables on the fixture server, so the same file drops into any checkout. Also records the JS type value() returns and the raw wire bytes, which is how the float32 narrowing under application/octet-stream was found.
OPC UA payloads no longer go through the global ContentSerdes registry, where application/octet-stream belongs to Modbus and silently narrows a 64-bit Double to a big-endian float32. opcua-content-negotiation.ts is now the single place a form's contentType becomes a representation decision: bare value, Variant, DataValue, OPC UA Binary, or raw bytes for a genuine ByteString. Anything else is refused with a message naming the form, the actual OPC UA dataType, and a usable alternative, instead of failing inside a codec that knows nothing about OPC UA. The default stays application/json, which W3C WoT TD 1.1 5.3.4.2 fixes normatively and a binding may not redefine. 153 passing, no regressions.
ByteString is the one shape for which application/octet-stream is legitimate, and it was untested - the only legal use of that media type had no coverage. Int64 now carries 2^53+1 as an explicit [high, low] pair with arrayType Scalar. A plain JS number literal silently rounds to 2^53 before reaching the server, so the previous input proved nothing; and node-opcua throws without an explicit arrayType, which is what hung the earlier run inside before().
Write now resolves contentType the same way read does, so the two agree on what a flavour means, and adds a write round-trip test per flavour. The test found two real bugs: - OpcuaJSONCodec.valueToBytes encoded with "ascii" in three places, so any non-ASCII string was corrupted on write - "hello woerld" came back with a replacement character. Pre-existing on master, unrelated to this work, now utf-8. - Int64 needs an explicit arrayType: coerceInt64 yields a [high, low] pair that node-opcua cannot distinguish from a two-element array. It also pinned down the limit of what a binding can fix alone. ConsumedThing.writeProperty calls ContentSerdes.valueToContent before the binding is reached, so on write core has already chosen a codec by media type. application/octet-stream writes therefore still land in the Modbus-oriented codec and cannot be intercepted. That is recorded as a failing-by-design expectation rather than papered over, and needs the core change discussed in Dec 2025. 174 passing.
Adds the remaining numeric scalars, the non-numeric built-ins (DateTime, Guid, NodeId, QualifiedName, StatusCode, XmlElement), enumerations, the null value, more array shapes and 2-dimensional matrices. Matrices are the reason for the arrayDimensions field: OPC UA sends a flat array plus a separate Dimensions member, which no value-shaped contentType can carry.
A ByteString read as application/octet-stream came back as a garbled string, and such a write never reached the binding at all: core used the generic OctetstreamCodec, which packs Modbus registers. With core eclipse-thingweb#1572 the binding registers its own codec for the opc.tcp scheme, so both directions stay OPC UA aware and every other binding is untouched. The value is base64, as the JSON flavours already report for a ByteString, and arrayBuffer() still returns the raw bytes. An array of ByteStrings is refused: a raw octet stream cannot frame several.
Explains what the binding does and why: the two converters a value crosses, what each contentType produces, the measured behaviour per OPC UA data type, the essence of OPC UA JSON, and the decisions with their costs. Diagrams are Mermaid inline, with PlantUML sources beside them. Written for reviewers and for whoever maintains this next: the choices that differ from OPC 10101 or from Part 6 Table 80 are stated as such, with the reason and what it costs.
A 1.05 StatusCode is {"Code":...}, plus "Symbol" in Verbose (Part 6
5.4.2.12). The 1.05 schema allowed only an integer, so any 1.05
DataValue with a status other than Good was refused with "Invalid JSON
dataValue". node-opcua 2.186 and earlier wrote that field as
StatusCode, never UaStatus, so StatusCode replaces UaStatus.
With node-opcua 2.187.10 the 1.05 encodings name a namespace by its URI (nsu=) when given the table, so a payload no longer depends on the server's namespace indexes. Every encoder and decoder of the binding now gets the table of its session. The codecs that decode in core have no session: opcua+json and octet-stream reads now carry ;uaconn= to the session's context, which holds the table and the structure definitions. On write, opcua+json passes the consumer's JSON through instead of decoding it with no table, which turned an nsu= NodeId into a String NodeId of namespace 0.
The plan's appendix lists what the move to 2.187.10 did in the binding: the namespace table, the schemas, the 1.05 status object and the stricter Call check. The JSON pages show the new matrix shapes and a structure's fields beside UaTypeId instead of UaBody.
The plan's question A3 and the error a structure write gets now point at eclipse-thingweb/node-wot issue 1583, which carries the proposal past this PR: structures given as OPC UA JSON 1.05, encoded by the binding with its session.
The media type of OPC 10000-6 Table 80, planned for WoT OPC UA Binding 1.1, as asked on PR 1574. The payload stays the UA Binary DataValue; a type parameter picks what value() returns: Value (the default), Variant or DataValue, as OPC UA JSON, so the timestamps, status, type and Union choice the value alone loses stay reachable. application/json, opcua+json, opcua+octet-stream, octet-stream and an omitted contentType are refused, each naming its replacement. The codec needs no URI scheme; OpcuaJSONCodec, OpcuaBinaryCodec and the JSON schemas are removed. BREAKING CHANGE: an opc.tcp form needs application/opcua+uabinary.
bindings:opcua:1 and :2 still referred to opcua-demo1.js and opcua-demo2.js, renamed to demo-opcua1.js and demo-opcua2.js by the kebab-case rename; both scripts failed with "Cannot find module". Adds bindings:opcua:secure for the security demo, which had no script.
brewCoffee and fillTank passed the CoffeeMachineA object as the OPC UA objectId, but MakeCoffee and FillTank are components of its 2:MethodSet, not of the machine. The server rejected the pairing with BadMethodInvalid, after resolving the method node itself just fine. Point opcua:nodeId at 2:MethodSet in both action forms.
The temperature property copied the unit of pumpSpeed, and demo-opcua2 printed it. The demo server reports values around 27, i.e. degrees.
Describes what each of the four demos shows, how to run it (including the test server the secure demo needs), and the two TD idioms readers trip on: browse paths, and the method owner in an action form. Output samples come from real runs; images are hand-made SVGs. Written in ASD-STE100 Simplified Technical English.
The 15 opc.tcp forms of the OPC UA examples: 14 had no contentType and one said application/json, which the binding now refuses.
D8 replaces D2, D3, D5, D6 and D7, which stay for the record. The content negotiation and JSON pages describe the three views; the octet-stream plan is marked superseded.
The views of application/opcua+uabinary each use one OPC UA JSON edition, so the edition and mode fields of ContentFormat, and the 1.04 Reversible and 1.05 Verbose encoders behind them, were unreachable. Value stays 1.04 NonReversible: 1.05 has no bare value and flattens a matrix. Variant and DataValue are 1.05 Compact. Writes still accept either edition. D8 and the encoding page say so.
3cfb484 to
fb9a781
Compare
Review of PR 1574. A write is converted to the target's DataType or refused, never written approximately: integers out of range or with a fraction, texts that are not numbers or dates, nested arrays in the Value view, and Variant/DataValue JSON that decodes to no value were written as wrapped, truncated, NaN or Null values. DateTime texts now write back, and an abstract DataType takes the type of the value. A refused contentType now fails before Read, Call or CreateMonitoredItems, and a payload is read as JSON first: a JSON number looked like UA Binary. The connection context keeps its structure definitions across calls.
|
I black-box tested the changes:
@sebastiankb mentions in #1574 (comment) he would like to use I am fine having the |
exactly, only
I like the idea. If only |
No problem at all; I didn't hope we could use |
|
@danielpeintner I think you tested old code again. This time it is not your mistake. What happened GitHub did not update the pull request ref after my push of Oct 23. You can see it with: Your error message comes from the old code. It says "Supported: application/json, application/opcua+json…". The PR head says "Supported: application/opcua+uabinary, with an optional ;type=Value|Variant|DataValue." What the PR head does
How to test it
The script clones the branch (not the pull ref), checks out Line 2 of the output tells you which code is installed: Note: the build uses make-black-box.js// make-black-box.js: black-box test of PR 1574 from a fresh clone, Windows or Linux.
//
// node make-black-box.js [commit] (default commit: fb9a7818)
//
// In the current folder it creates:
// node-wot/ the PR branch at <commit>, built and packed
// opcua-test/ a project that installs only the two tarballs, with black-box.js
// evidence-<commit>.txt
const { execSync } = require("child_process");
const crypto = require("crypto");
const fs = require("fs");
const path = require("path");
const REPO = "https://github.com/node-opcua/node-wot.git";
const BRANCH = "wip/octet-stream-e2e";
const COMMIT = process.argv[2] || "fb9a7818";
const root = process.cwd();
const src = path.join(root, "node-wot");
const test = path.join(root, "opcua-test");
const log = [];
function sh(cmd, cwd) {
// relative paths only: the evidence is meant to be posted
const shown = cmd.split(root + path.sep).join("");
console.log(`> ${shown}`);
log.push(`> ${shown}`);
// npm ci output is long; only the commands go to the evidence
return execSync(cmd, { cwd, stdio: ["ignore", "pipe", "inherit"], encoding: "utf8" });
}
function sha256(file) {
return crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex");
}
// 1. the PR code, from the branch and not from refs/pull/1574/head (stale since Oct 23)
if (fs.existsSync(src)) {
throw new Error(`${src} exists, start from an empty folder`);
}
sh(`git clone -q -b ${BRANCH} ${REPO} node-wot`, root);
sh(`git -c advice.detachedHead=false checkout -q ${COMMIT}`, src);
const sha = sh("git rev-parse HEAD", src).trim();
// --ignore-scripts: node-mbus (M-Bus binding) needs Visual Studio on Windows; OPC UA does not
sh("npm ci --ignore-scripts --no-audit --no-fund", src);
sh("npx tsc -b packages/binding-opcua", src);
const packed = sh("npm pack --workspace @node-wot/core --workspace @node-wot/binding-opcua", src)
.trim()
.split(/\r?\n/)
.map((name) => path.join(src, name));
// 2. a consumer project that knows nothing but the two tarballs
fs.mkdirSync(test);
fs.writeFileSync(path.join(test, "package.json"), JSON.stringify({ name: "opcua-test", private: true }, null, 2));
sh(`npm install --no-audit --no-fund ${packed.map((p) => `"${p}"`).join(" ")}`, test);
fs.copyFileSync(path.join(__dirname, "black-box.js"), path.join(test, "black-box.js"));
// 3. run it
const output = sh("node black-box.js", test);
console.log(output);
const evidence = [
`PR 1574 black-box test, ${new Date().toISOString()}`,
`repository: ${REPO} branch: ${BRANCH}`,
`commit: ${sha}`,
`node ${process.version}, npm ${execSync("npm -v", { encoding: "utf8" }).trim()}, ${process.platform}`,
"",
"tarballs (sha256):",
...packed.map((p) => ` ${sha256(p)} ${path.basename(p)}`),
"",
"commands:",
...log.map((l) => ` ${l}`),
"",
"output of node black-box.js:",
output.trimEnd(),
"",
].join("\n");
const file = path.join(root, `evidence-${sha.slice(0, 8)}.txt`);
fs.writeFileSync(file, evidence);
console.log(`evidence written to ${file}`);black-box.js// black-box.js: reads PumpSpeed once per contentType, with the client of PR 1574 (2026-10-02)
const fs = require("fs");
const path = require("path");
const { Servient } = require("@node-wot/core");
const { OPCUAClientFactory } = require("@node-wot/binding-opcua");
const HREF = "opc.tcp://opcuademo.sterfive.com:26543?id=ns=1;s=PumpSpeed";
const CASES = [
["application/opcua+uabinary", "number"],
["application/opcua+uabinary;type=Value", "number"],
["application/opcua+uabinary;type=Variant", "object"],
["application/opcua+uabinary;type=DataValue", "object"],
["application/octet-stream", "number"],
["application/opcua+octet-stream", "object"],
["application/json", "number"],
["application/opcua+json", "object"],
[undefined, "number"],
];
function installedCode() {
const dist = path.dirname(require.resolve("@node-wot/binding-opcua"));
const file = path.join(dist, "opcua-content-negotiation.js");
const text = fs.existsSync(file) ? fs.readFileSync(file, "utf8") : "";
return text.includes("application/opcua+uabinary")
? "PR head (Oct 23 or later): application/opcua+uabinary is known"
: "OLD CODE (before Oct 23): application/opcua+uabinary is unknown, results below are not the PR";
}
function thingDescription(contentType, type) {
const form = { href: HREF, op: ["readproperty", "observeproperty"] };
if (contentType !== undefined) form.contentType = contentType;
return {
"@context": "https://www.w3.org/2019/wot/td/v1",
"@type": ["Thing"],
securityDefinitions: { nosec_sc: { scheme: "nosec" } },
security: "nosec_sc",
properties: { pumpSpeed: { description: "the pump speed", type, forms: [form] } },
};
}
async function main() {
console.log(`### ${new Date().toISOString()} node ${process.version}`);
console.log(`### installed @node-wot/binding-opcua: ${installedCode()}`);
const servient = new Servient();
servient.addClientFactory(new OPCUAClientFactory());
const WoT = await servient.start();
for (const [contentType, type] of CASES) {
console.log(`\n----- contentType: ${contentType ?? "(none)"} (TD type: ${type})`);
try {
const thing = await WoT.consume(thingDescription(contentType, type));
const output = await thing.readProperty("pumpSpeed");
console.log("pumpSpeed value is: ", await output.value());
} catch (err) {
console.log("Script error:", err.message);
}
}
await servient.shutdown();
process.exit(0);
}
main().catch((err) => {
console.error("Start error:", err);
process.exit(1);
});
Our result evidence-fb9a7818.txt (Windows, node 22) |
VERY strange and also very annoying since a simple local PR checkout points to a previous commit. Something like this did not happen to me before. Does anyone have an idea what caused it? Anyhow, blackbox testing looks fine now. Please give me some time to look at your code. |
danielpeintner
left a comment
There was a problem hiding this comment.
@sebastiankb @relu91 @egekorkan please comment if you have another opinion
Review of PR 1574. The old contentTypes are refused with the generic "unsupported contentType" error instead of naming a replacement: application/opcua+uabinary does not replace JSON, they were never right. doc/ is removed: it recorded decisions and plans that no longer hold. What still holds moves to the README contentType section: the three views, writes, actions, what the Value view loses, structure writes (issue 1583). Comments pointing at doc/ or at plan questions now say the rule itself. The removed pages stay readable at the tag archive/pr-1574-design-notes of node-opcua/node-wot.
|
Two new commits:
Black-box test of eaad6a6, same scripts as before ( evidence-eaad6a6f.txtAbout the stale ref: I don't know the cause. GitHub resynced |
danielpeintner
left a comment
There was a problem hiding this comment.
Looks good to me now.
I have one minor comment which we might want to address...
Review of PR 1574. The code accepts the explicit form, and only the README left it out.
|
Checked offline with @sebastiankb. He is fine too. |
Fixes #1400.
Symptom
Adding
"contentType": "application/octet-stream"to an OPC UA form breaks the read:Cause
binding-opcuanever owned its media types, so core's genericOctetstreamCodecwas handed OPC UA content and tried to treat a node-opcuaDataTypeas a string. Scheme-scoped codec registration only became available with #1572 (merged today).Fix
The binding registers its own codecs for
opc.tcpforms.application/octet-streamnow means the raw bytes of a ByteString; any other target type is refused with an error naming the form and the actual OPC UA type, rather than mangling it. The default staysapplication/json(W3C WoT TD 1.1 §5.3.4.2) — OPC 10101 §6.5.3 is conditional and does not set a default.Tests
npm run test --workspace @node-wot/binding-opcua— 316 passing, 1 pending (pre-existingxit).test/octet-stream-e2e-test.tstest/octet-stream-shapes-test.tstest/contenttype-write-test.tsopcua+json, octet-stream, 1.04/1.05The octet-stream refusal is asserted strictly: a base64 payload written to a
Doublemust name both the ByteString requirement and the actual type. An earlier version of that test accepted either that message or the codec'smust be a Buffer, and only ever hit the second, so the refusal went unexercised.To see it fail without the fix: put the issue's TD (a
DoublewithcontentType: application/octet-stream) against master and the read throws theTypeErrorabove. On this branch the same form is refused with a message naming the form and the type.Please do not squash
The 12 commits are ordered to be read in sequence — negotiation POC, then the write path, then codec ownership, then the 1.05 encoding — and one of them is a breaking change (
refactor(binding-opcua)!: stop exposing node-opcua internal shapes). Squashing buries that marker in a body nobody reads and loses the bisect points. A merge commit or a rebase merge both keep them.Caveats
!commit has the details.application/opcua+jsonstill fails core's schema check; that is out of scope here and belongs with InteractionOutput - no schema/schema.type and the expectations of the value() function #1243 (no schematype) and Optional Disabling of the schema validation #1265 (optional validation).application/octet-streamas the default. That reading is withdrawn: the default stays JSON.