Repository navigation
Release Notes 1.2.0
This guide covers upgrading from Milo 1.1.x to 1.2.0-RC1 on Java 17. It has two parts: the significant new features in 1.2.0, and the changes that may require code changes in your application. Changes already released in 1.1.x, through 1.1.8, are not repeated here.
The guide describes the v1.2.0-RC1 tag at
69cb250878048b8b39ce7cde036f06c5617f31b2
(2026-10-12). RC1 is a release candidate. The final 1.2.0 release may still change.
Recompile applications and extensions against one consistent Milo version. 1.2.0 includes source and binary compatibility breaks.
The stack adds 12 ECC policies, covering NIST P-256/P-384, Brainpool P256r1/P384r1, and
Curve25519/448 with AES-GCM or ChaCha20-Poly1305, plus RSA_DH_AesGcm and RSA_DH_ChaChaPoly.
Milo implements their SecureChannel paths and username and X.509 Session authentication. Peer
interoperability still depends on compatible certificates, token policies, and crypto providers.
SecurityPolicy.getProfile() describes each policy's algorithm, certificate, and channel
requirements.
Milo clients must use forward OPC TCP for ECC or RSA-DH Sessions at this baseline.
ReverseTcpClientTransport does not supply the channel thumbprint required by enhanced Session
authentication. See Transport extensions.
To enable an ECC policy, provision compatible ECC application identities on both client and server,
configure the server endpoint, and select it on the client. The default server username token
policy explicitly names Basic256Sha256 and is rejected on an ECC channel with
Bad_SecurityPolicyRejected. Use a username token policy with no securityPolicyUri (inherit the
channel policy), or a compatible ECC policy URI.
The server advertises an endpoint only when the runtime provider and certificate support it. Only the AEAD-suffixed ECC policy URIs are supported. Brainpool uses the Bouncy Castle dependency already supplied by Milo. Brainpool certificate-path validation registers that provider globally at lowest precedence if it is absent. The other enhanced paths can use an unregistered BC instance.
Aes256_Sha256_RsaPss now works with the built-in JDK 17+ providers, without registering
BouncyCastleProvider. Milo 1.1.x required that registration for its SHA256withRSA/PSS implementation.
See Security policies, the stack security package, and the client identity, certificate group, and token policy changes before enabling them.
Clients can accept server-initiated OPC TCP connections. OpcUaClient.createReverseConnect(...)
uses either a shared ReverseConnectManager plus selector or a pre-claimed connection.
ReverseConnectAcceptor runs GetEndpoints discovery for each deduplicated candidate before
creating a client.
The manager owns the listener sockets. The application starts and stops the manager and disconnects delivered clients. Closing an acceptor or disconnecting one client does not stop the shared manager. Disconnecting a manager-backed client stops rearming and fails pending channel acquisition. A later explicit connect can register again.
A pre-claimed connection is one-shot. After failure, create a new client from a new claim.
Cancelling or exceptionally completing ReverseConnectRegistration.connectionFuture()
(including orTimeout) removes its selector and closes a claim that can no longer be delivered.
CLAIMED means ownership was reserved, so wait for the future before using the connection.
For delivered-connection ownership and the ReverseConnectRegistration.close() race, see
Select, claim, and discover.
On the server, OpcUaServer.addReverseConnectTarget(...) registers a retrying outbound target and
returns its control handle. Server attempts and custom retry policies run on the server executor.
A rejected attempt startup reports FAILED with Bad_ResourceUnavailable. If attempt startup or
retry evaluation is rejected, the target retries after its registration period without calling
the custom policy. Keep callbacks short, because rejected listener dispatch can run inline on the
submitting thread.
Treat ReverseHello as an unauthenticated routing hint. Normal SecureChannel and Session checks authenticate the peer after the connection is claimed. Reverse Connect requires peer support and does not apply to HTTPS or WebSocket. See Reverse Connect for selection, timeouts, and shutdown.
ECC and RSA-DH Sessions are outside the supported baseline for Milo clients using Reverse Connect.
Of the bundled client transports, only OpcTcpClientTransport defers channel-bound request
construction and supplies the active channel thumbprint. ReverseTcpClientTransport inherits an
empty thumbprint and eager request construction. Use forward OPC TCP for those policies. See
Transport extensions.
OpcUaServer.getNodeInstantiator() builds typed object and variable instances from an
InstantiationRequest. It supports inherited declarations, optional members, placeholders,
explicit NodeIds, staged callbacks, and Method binding. instantiate() performs the operation.
describe(), plan(), and apply() expose its stages.
InstantiationResult.deleteCreated() removes recorded new nodes and references, including
references added to pre-existing parents. It preserves reused nodes and later replacements. It
does not undo bindMethod changes to shared or reused Methods. A failed apply rolls back and
returns no result. Keep application resources and binding ownership separate from node cleanup.
For ordinary events, OpcUaServer.newEvent(typeId) returns a TransientEvent that owns temporary
nodes. Close it after use. getEventInstantiator() provides explicit control over event identity
and generated Java type.
apply() revalidates the model and rejects changed declarations with a MODEL_CHANGED
diagnostic before constructing nodes. Call server.getTypeModelCache().invalidate(typeId) after
application model edits and re-plan. Snapshots copy standard OPC UA values, but application values
without a standard codec must be immutable, including such values nested inside standard structures.
The package is experimental for one minor release. See legacy node factories and Types and instantiation.
The server SDK adds a Condition runtime: Condition, AcknowledgeableCondition, and
AlarmCondition behaviors, discrete and limit alarms, acknowledgement, confirmation, shelving,
and ConditionRefresh. create builds a new instance, and attach activates a complete generated
instance. adopt completes a partial instance in place, preserving identities and stored
configuration unless explicitly overridden. OpcUaServer.getConditionManager() owns behavior
registration and lookup. Limit alarms can evaluate values or accept application-driven state.
Condition branches other than the trunk (Part 9 BranchId) are not supported.
Custom Condition types and Java behaviors use Condition.createInstance, attachInstance,
and adoptInstance. The returned behavior is unregistered. These factories own structure and
Method wiring, while the application supplies custom state semantics. A custom factory must
wrap the supplied node. Failure after creation rolls back the new tree, but failure after adoption
leaves the completed existing tree for correction and retry. See
custom types and ownership.
Register the behavior, connect its event source hierarchy, and drive changes through its API. Stop the process feed before unregistering. Unregistration or replacement under the same ConditionId retires the old behavior and releases handlers and timers. Re-registering that instance does not restore those resources, so construct a fresh behavior for reuse. Retirement does not delete nodes or prevent later direct application calls from trying to emit events. Delete owned nodes afterward.
captureSnapshot() and pre-live restoreSnapshot() support recovery. The application stores
snapshots, preserves ConditionIds, and restores before exposing the behavior. See
Alarms and Conditions, the conditions package,
and the alarm example.
The opt-in org.eclipse.milo.opcua.sdk.server.aliases package implements OPC UA Part 17 for the
alias model already in namespace 0. AliasManager provides recursive wildcard search, category
and alias mutation, reference-type filtering, and LastChange version maintenance.
The application starts AliasManager after server startup and shuts it down before the server.
It owns alias NodeIds, categories, authorization, and the AliasVersionStore. The default store
is in memory, but Part 17 requires LastChange to persist across restarts. Supply a durable store,
restore alias definitions yourself, and retain removed category versions if NodeIds may be reused.
Network mutation methods are disabled and denied by default. Mutations support local targets
only. Search does not inherit Browse visibility filtering, so enforce access through checkFind
and includeResult.
See manager setup and default limits for the 1,000-result, 512-character, and 1,000-operation bounds, and persistence for version ownership. The aliases package and AliasNamesExample provide the API reference and workflow.
Add org.eclipse.milo:milo-sdk-client-gds at the same version as the SDK.
GdsClient.create(connectedClient) resolves the GDS namespace and Directory and registers the
generated model. It throws UaException with Bad_NotFound if that namespace is absent. The
client's blocking and asynchronous methods cover application registration, certificate requests,
certificate groups, and trust lists.
resolveCertificateTypeId(...) matches a desired certificate type to a GDS group's advertised
types. It returns null when only compatible abstract ancestor types are offered, meaning "use the
group default." Pass that null through to the request. Incompatible choices fail with
Bad_NotSupported.
TrustListReader reads a remote trust list. TrustListApplier replaces each list named in
SpecifiedLists in one TrustListManager.update. Local entries absent from a selected list are
dropped.
The application owns connecting, scheduling enrollment, polling FinishRequest, persisting
application and request IDs, and validating and installing issued certificates.
GdsClient.verifyIssuedCertificate checks the issued identity before installation. The client does
not retry these workflows. milo-sdk-client-gds-testing publishes FakeGdsNamespace for tests.
It is not a production GDS. See the GDS client guide and
GdsPullExample.
OpcUaServer.addLifecycleParticipant(Lifecycle) transfers startup and shutdown ownership to
the server. Register before startup. Registration after startup or shutdown begins throws
IllegalStateException. Do not also start or stop a participant independently.
Participants start in registration order after core facilities are ready and before binding endpoints, then stop in reverse order. Failed startup rolls back only participants whose startup completed normally. The participant that throws must clean up its own partial allocation. See Server configuration.
ManagedAddressSpace owns a SamplingManager, with one SamplingGroup per supported interval.
A driver supplies a group to batch device reads while retaining framework scheduling, membership,
initial samples, and access refresh. SamplingManagerConfig controls bucketing, minimum interval,
initial-sample debounce, read-access policy, and the executor that groups run on.
Groups run on the server's executor by default. A sampler whose device reads block can move that
work with SamplingManagerConfig.withExecutor(...), such as a virtual thread per task on Java 21
or later. The executor must run tasks on its own threads. The application owns it and shuts it down
after the manager stops. Timers stay on the server's scheduled executor, and a rejected turn is
logged and skipped until the group's next cycle.
The default policy calls AccessController on sampling threads every cycle for each Session. A
cached policy avoids repeated checks until invalidation. Controllers must now tolerate these
concurrent calls as well as ActivateSession and transfer checks, not only service requests.
Keep controllers and access-attribute filters fast and thread-safe, and never call the controller
recursively from a filter it uses.
The SDK invalidates on Session identity or endpoint changes, reactivation, and transfers, and evicts closed Sessions. Applications invalidate after changes to access attributes, roles, policy inputs, and node removal or replacement. See Sampling and read access for migration and Sampling for configuration and batch examples.
A data MonitoredItem denied with Bad_NotReadable or Bad_UserAccessDenied is now created and
reports the denial through Publish. Framework sampling refreshes its stored decision according
to the selected policy. The SDK also rechecks after Session identity or endpoint changes and
transfer. Custom samplers must implement refresh for application policy changes themselves. A
denied item does not recover merely because a device value changes.
For the Value attribute, DefaultAccessController distinguishes a node lacking CurrentRead or
CurrentWrite from a Session lacking it. Other denials, including protected RolePermissions reads
and UserWriteMask checks, can still return Bad_UserAccessDenied. Install custom controllers with
setAccessControllerFactory(...). See Sampling and read access and
Access control.
On a secured endpoint, a SecureChannel failure after Hello and Acknowledge can trigger rediscovery
and retry with a renewed server certificate. The trigger is not limited to certificate errors.
The replacement must preserve endpoint URL, server ApplicationUri, GatewayServerUri, transport
profile, policy, mode, and user-token policies. Its certificate must also pass the client's
validator. Recovery requires a ChannelStateObservable transport, as implemented by TCP and
reverse TCP.
OpcUaClient.create(String) installs an EndpointResolver, but selects a None endpoint with
anonymous access. Use the four-argument discovery-based create overload with a secured endpoint
selector for rotation recovery. create(OpcUaClientConfig) does not install a resolver. Set one
with OpcUaClientConfigBuilder.setEndpointResolver(...) when assembling configuration yourself.
Session.getOriginalServerCertificate() returns a SessionServerCertificate record retaining
the CreateSession certificate and key material. Retained Sessions can reactivate across rotation
when their security bindings remain usable. Changing the client's leaf certificate requires a
new Session, but changing only its issuer chain does not.
An OpenSecureChannel request naming an unknown receiver certificate thumbprint now fails with
Bad_CertificateInvalid, replacing Bad_SecurityChecksFailed. See
Client recovery.
AbstractMethodInvocationHandler.invokeResult(...) can return a complete Call result, including
Uncertain with outputs or per-argument Bad_InvalidArgument results. Override
getRequiredInputArgumentCount(...) to accept omitted trailing inputs.
UaMethodNode.setInvocationHandler(NodeId, ...) installs a handler for one ObjectId.
MethodBindings validates and owns registrations. ManagedAddressSpace selects these handlers,
after giving ConditionManager handlers precedence, and falls back to the Method's default handler.
A custom AddressSpace that dispatches Calls itself must implement equivalent selection. See
Method handlers and Server Methods.
setOperationLimitOverrides accepts either a map or
Function<OperationLimit, Optional<UInteger>>. Pass null to the function overload to remove
overrides. An absent or zero override leaves the advertised value unchanged. Otherwise, a nonzero
override supplies a missing or zero server limit, or lowers an advertised nonzero limit. It never
raises that server limit. The effective limits are cached per Session activation.
SDK consumers such as type discovery and OpcUaSubscription MonitoredItem batching use these
effective values. Arbitrary bulk Read, Write, and Call inputs are not automatically partitioned.
Applications must split them and preserve per-operation results.
The type-tree fallback to one operation when a limit is unavailable predates 1.2. The legacy binary dictionary reader now uses 64 nodes per Read when the effective MaxNodesPerRead is zero or unavailable. Milo 1.1.x used one when the advertised value was zero. See Recovery and limits.
| Change | Who is affected |
|---|---|
| Client certificate identity | Clients that configure an application certificate or leave their application URI unset |
| Server certificate groups | Servers, custom certificate group or manager implementations, and custom or subclassed certificate validators |
| Custom trust list managers | Custom TrustListManager implementations, subclasses of the built-in managers, and file-store users with hidden trust material |
| Certificate revocation checking | Applications validating CA-issued peers, including default validators with CRLs; users of --add-opens or revocation flags |
| Endpoints and user token policies | Servers with endpoint aliases or ECC username policies; custom username choosers and X.509 identities |
| Security algorithm and policy APIs | Code that uses SecurityAlgorithm or SecurityPolicy directly; custom identity providers and subclasses of built-in providers |
| Event routing | Servers that emit events or manage event items |
| Alias Method nodes | Servers with their own FindAlias handlers |
| Method handlers | Handlers returning argument diagnostics or throwing InvalidArgumentException; callers using an ObjectType as the Method ObjectId; UaMethodNode subclasses overriding getInvocationHandler()
|
| Legacy node factories | Servers using NodeFactory or EventFactory
|
| Server lifecycle | Custom lifecycle components and code that invokes startup or shutdown repeatedly |
| Custom identity validators | Servers with custom IdentityValidator implementations |
| Attribute reads and writes | Servers with Variables that accept arrays but declare no array ValueRank; code that synchronizes on a node to group attribute changes |
| Client API behavior | Clients browsing, using typed-node writers, or inspecting connect failures |
| Transport extensions | Custom transports, direct stack users, server transport configuration |
| Serialization | JSON and XML users; code that encodes, decodes, or persists Matrix values; custom codecs and data-type managers |
| Diagnostics nodes | Clients browsing per-Session diagnostics with default EnabledFlag, caching diagnostic NodeIds, or interpreting disabled status |
| Sampling and read access | Servers using SubscriptionModel, custom samplers or controllers; imports of the old access classes, getAccessController overrides, and checkReadAccess or checkWriteAccess callers; Variables without CurrentRead or CurrentWrite; clients matching access statuses or CreateMonitoredItems failures |
| Deprecations | Users of deprecated APIs |
The OpcUaClientConfig builder no longer has separate setKeyPair, setCertificate, and
setCertificateChain methods. Use setCertificateIdentity(keyPair, chain), passing the leaf
certificate first, followed by any issuers. Keep your existing certificate validator.
Before (Milo 1.1.x; given endpoint, keyPair, chain, validator, and an applicationUri
matching the certificate):
var config =
OpcUaClientConfig.builder()
.setEndpoint(endpoint)
.setApplicationUri(applicationUri)
.setKeyPair(keyPair)
.setCertificate(chain[0])
.setCertificateChain(chain)
.setCertificateValidator(validator)
.build();After (Milo 1.2.0-RC1):
var config =
OpcUaClientConfig.builder()
.setEndpoint(endpoint)
.setApplicationUri(applicationUri)
.setCertificateIdentity(keyPair, chain)
.setCertificateValidator(validator)
.build();For managed trust or multiple certificate types, use setCertificateGroup(group) instead. The
last of the two calls wins. DefaultCertificateGroup.forIdentity(...) combines one identity with
application trust, quarantine, and validation.
OpcUaClientConfig.getKeyPair(), getCertificate(), and getCertificateChain() are removed.
Keep the material your application supplies, or call client.getCertificateIdentity(policy.getProfile()).
It takes a SecurityPolicyProfile, returns Optional<CertificateIdentity>, and can throw UaException.
OpcUaClientConfig.getApplicationUri() now returns Optional<String> holding only an explicitly
configured URI. Milo 1.1.x sent urn:eclipse:milo:client:applicationUriNotConfigured when none was
set. In 1.2 Milo uses the selected certificate's SAN URI, or on None connections the SAN URI shared
by the group's identities. The same placeholder remains the fallback when no SAN URI is available.
Call setApplicationUri(...) if server or GDS registrations depend on the prior value.
client.resolveApplicationUri(identity) returns the effective URI.
CreateSession now sends the configured client certificate chain. Milo 1.1.x sent only its leaf.
On the setCertificateIdentity path, build() throws IllegalArgumentException for an empty
chain, unsupported type, or key mismatch. A secured connection without a compatible identity fails
with Bad_ConfigurationError. Inferred certificate types exclude SHA-1-signed RSA certificates
from Basic256Sha256 and the two AES RSA policies. An explicitly typed group instead selects using
the stored type, so this inference check is not a general certificate-validation substitute.
DefaultApplicationGroup is removed. Use DefaultCertificateGroup in
org.eclipse.milo.opcua.stack.core.security. In 1.1.x, group initialization created missing
certificates, and the application already initialized and closed its trust and store resources.
In 1.2, provisioning is explicit. Call factory.createMissingCertificates(group) before startup.
This instance method returns the certificate-type NodeIds created and can throw Exception.
Existing entries are retained, and unreadable entries are not silently replaced. An existing
deprecated RsaSha256CertificateFactory inherits this method, so it can be retained while
migrating.
Before (Milo 1.1.x; given initialized trust and store, plus factory, quarantine, and
validator):
var group = DefaultApplicationGroup.createAndInitialize(trust, store, factory, validator);
var manager = new DefaultCertificateManager(quarantine, group);After (Milo 1.2.0-RC1):
var group = new DefaultCertificateGroup(trust, store, quarantine, validator);
factory.createMissingCertificates(group);
var manager = new DefaultCertificateManager(group);The four-argument group constructor supports RSA SHA-256 only. Use the overload with a
List<NodeId> of certificate types for others. The one-group manager constructor registers the
group as the DefaultApplicationGroup. For additional groups, use new DefaultCertificateManager()
and addCertificateGroup(groupId, group). As in 1.1.x, close application-owned trust and persistent
store resources after the server stops. KeyStoreCertificateStore is closeable.
MemoryCertificateStore holds no closeable resource.
Custom implementations need these changes:
| 1.1.x API | 1.2.0 replacement |
|---|---|
CertificateGroup.getCertificateGroupId() |
Implement the new abstract CertificateManager.getCertificateGroupId(group), returning Optional<NodeId>
|
CertificateGroup.getCertificateFactory() |
Keep the factory in application code |
| No quarantine or presence methods on the group | Implement getCertificateQuarantine() and hasCertificate(NodeId)
|
CertificateGroup.Entry(groupId, typeId, chain) with public fields |
Record Entry(typeId, chain) with certificateTypeId() and certificateChain()
|
CertificateManager.getCertificateQuarantine() (removed) |
The validating group's quarantine; getRejectedCertificates() aggregates across groups |
DefaultCertificateManager(quarantine, ...) constructors (removed) |
Empty manager plus group registration, or DefaultCertificateManager(group)
|
The stack now calls the four-argument CertificateValidator.validateCertificateChain(..., profile).
Its default invokes legacy validation, then checks certificate and profile compatibility. A
wrapper that forwards only the three-argument method bypasses its delegate's profile-specific
checks. Forward the four-argument overload too when wrapping a validator that supplies those checks.
Subclasses of DefaultClientCertificateValidator or DefaultServerCertificateValidator that
override only the three-argument method still compile, but that override is bypassed by SDK calls.
These built-in classes implement the four-argument overload directly. Move custom checks, such as
certificate pinning, to that overload and pass the same profile when calling super.
Fixed endpoint certificates remain supported. Unlike 1.1.x, the builder permits a secured endpoint
without a fixed certificate. The server selects an identity from the default group, or from an
explicit EndpointCertificateConfig. As before, a secured policy requires Sign or SignAndEncrypt
mode. An unsatisfied identity or provider requirement omits the endpoint with a warning, so verify
bound endpoints. See CertificateGroup, DefaultCertificateGroup,
and Server configuration.
KeyStoreCertificateStore now uses <prefix>ecc-* aliases for ECC identities. Applications that
manage aliases directly must use the new names. The RSA SHA-256 alias is unchanged. contains() no
longer treats a certificate-only entry as an application identity. It requires a cached identity
or a keystore key entry.
SelfSignedCertificateBuilder chooses SHA384withECDSA for EC keys whose curve order exceeds 256
bits, and SHA256withECDSA for smaller curves, unless overridden.
TrustListManager adds getSnapshot(), update(UnaryOperator<TrustListSnapshot>), and
replaceAll(...) so a four-list change can be applied atomically. The built-in managers implement
them. Direct TrustListManager implementations still compile with the interface defaults, which
fall back to sequential getters and setters and are not atomic. Override them if validators or GDS
updates must see a consistent trust list. Update functions can run more than once, so keep them
free of side effects.
Subclasses of FileBasedTrustListManager or MemoryTrustListManager that override individual
getters still compile, but the default certificate validators now read getSnapshot() instead.
The built-in managers implement that method directly, so those getter overrides no longer affect
validation. Move additions or filtering of certificates and CRLs to a getSnapshot() override
built from super.getSnapshot(), and keep the individual getters consistent with it.
File-based atomic publication is not an on-disk transaction. A failed file write or deletion is logged, but the published snapshot still reflects the requested state. A later directory reload reconciles it with disk. Monitor persistence failures before relying on changes across restart.
TrustListSnapshot removes duplicate entries from each of its four lists, preserving
the first occurrence order. Do not use duplicate entries as application metadata.
File-based trust and quarantine stores now skip dot-prefixed files and subdirectories when loading certificates or CRLs. Keep active trust material in regular, non-hidden files. Files with other names continue to be decoded regardless of extension. As in 1.1.x, decode failures are logged and skipped.
Revocation checking now uses the JDK's public PKIX revocation checker. Remove
--add-opens java.base/sun.security.provider.certpath=ALL-UNNAMED from your JVM options. It is
no longer needed. ValidationCheck.REVOCATION is deprecated for removal and has no effect.
The checker uses CRLs only, with no OCSP fallback. JVM-wide ocsp.* properties do not configure it.
| Configuration | 1.1.x without --add-opens
|
1.1.x with --add-opens, and 1.2.0 |
|---|---|---|
| No revocation flags (the default) | Revoked peers and missing CRLs are accepted | Revoked peers are rejected; missing CRLs are tolerated |
REVOCATION only |
Revoked peers and missing CRLs are rejected | Revoked peers are rejected; missing CRLs are tolerated |
REVOCATION_LISTS only |
Revoked peers and missing CRLs are accepted | Revoked peers and missing CRLs are rejected |
Both flags, or ALL_OPTIONAL_CHECKS
|
Revoked peers and missing CRLs are rejected | Revoked peers and missing CRLs are rejected |
These rows describe CA-issued peers with otherwise valid trust paths. An applicable clear CRL
permits validation in each case. Applications that already ran 1.1.x with --add-opens keep the
same behavior. REVOCATION becomes a no-op, and the JVM opening is no longer needed. The table
was measured on Java 17, with the --add-opens column measured in a separate JVM for the
1.1.x comparison. The measurement did not include network CRL retrieval.
Check trust-list CRLs against peers you expect to accept. If an applicable CRL is unavailable
locally, the JDK checker can fetch it from CRL distribution points, delaying validation and the
handshake. 1.1.x without the opening did not fetch these CRLs. Milo 1.2 explicitly installs a
PKIXRevocationChecker, so com.sun.security.enableCRLDP cannot disable its fetching.
On isolated networks, provision current issuer CRLs in the trust list.
OpenJDK 17 defaults to a 15-second connection timeout and a 15-second read timeout per CRL
fetch. The system properties com.sun.security.crl.timeout and
com.sun.security.crl.readtimeout control them. Values are whole seconds unless suffixed with
ms, so 5 and 5000ms both mean five seconds. Zero means an infinite timeout. These are JDK
provider settings, not a total validation deadline. Confirm the syntax and behavior for your
deployed JDK. See the OpenJDK 17 CRL store source.
The public classes OpcUaCertPathValidator, OpcUaCertificateRevocationChecker, and
OpcUaCertificateValidityChecker in org.eclipse.milo.opcua.stack.core.util.validation
were removed. Code that directly imported them must migrate to the supported certificate
validators or the public JDK PKIX APIs.
DefaultServerCertificateValidator now masks Bad_CertificateRevocationUnknown and
Bad_CertificateIssuerRevocationUnknown as Bad_SecurityChecksFailed. Untrusted and revoked
certificates were already masked. Path-building failures retain Bad_SecurityChecksFailed,
as in 1.1.x. The client validator retains the builder exception as the cause, but the server
validator omits it. Update code that matches the newly masked revocation statuses.
Server endpoint selection is now deterministic. Endpoints that share transport, URL path,
security policy, mode, and certificate must have equivalent user token policies, even when their
host names or ports differ. Otherwise, startup fails with Bad_ConfigurationError. Combine token
policies on one endpoint, or distinguish the endpoints by path, policy, or mode. A certificate can
also distinguish secured endpoints, but it is not part of the key for None endpoints. Endpoints
sharing a selection key must also advertise identical certificates, including None endpoints.
Session reactivation decodes user tokens against the replacement channel's selected endpoint policies. Custom identity validators should use the Session's current endpoint rather than cache the endpoint from its first activation.
As in 1.1.x, an unsecured channel without a configured None endpoint can only be used for discovery. Configure a None endpoint if you intend to allow unsecured sessions. The server may leave out an endpoint whose certificate or provider is unavailable, so check GetEndpoints output.
A None endpoint offering RSA-encrypted username tokens still needs a compatible server
certificate. Provision it before advertising that token policy. New in 1.2, if no fixed certificate
or EndpointCertificateConfig is set and the default group has no compatible identity, startup
warns and still advertises the None endpoint without a certificate. An unsatisfied explicit
EndpointCertificateConfig instead omits the endpoint. As in 1.1.x, a Milo client detects an
absent certificate locally and fails with Bad_ConfigurationError before sending the token.
New in 1.2, on secured channels a username token policy with an explicit non-None securityPolicyUri must use the channel's public-key family (RSA or ECC). Explicit None is exempt from the family rule.
As in 1.1.x, an omitted URI inherits the channel policy, explicit None leaves the password unencrypted at the token layer, and an omitted URI on a None endpoint has the same result. The password is exposed on a Sign-only or None channel in those cases. Choose SignAndEncrypt with certificate validation, or supply a chooser that requires a compatible encrypted username policy and fails when none is available. A chooser receives no endpoint argument, so classify inherited policies using the selected endpoint. See Match endpoint and token policies.
The default chooser still selects the first policy offered to it, but 1.2 filters that list to usable policies first. An explicit-None policy can still win over an encrypted one without a warning.
As in 1.1.x, on a None endpoint only UsernameProvider's own validator checks the certificate
used to encrypt the password. Pass a validating CertificateValidator, such as
DefaultClientCertificateValidator. The provider still defaults to an insecure validator.
Setting only the client configuration validator does not cover this token-encryption certificate
on a None endpoint. In 1.2, enhanced username secrets require a secured channel, and an enhanced
certificate-token policy cannot be used over a legacy secured channel.
UsernameProvider filters usable policies before calling its custom chooser, but falls back to
the full list if none is usable. The chooser may run more than once per activation and must be
deterministic. X509IdentityProvider has no custom chooser. It now selects the first compatible
Certificate policy rather than the first Certificate policy. If none fits, it fails with
Bad_IdentityTokenRejected. It also stops sending unsigned X.509 tokens for None-resolved
policies, which servers reject.
As in 1.1.x, Milo has no built-in issued-token client provider. A server application can
override AbstractIdentityValidator.validateIssuedIdentityToken. Its default still rejects the
token with Bad_IdentityTokenInvalid. In 1.2, that path does not apply
UserTokenSecurityPolicyRules. The application must validate the issued-token policy,
protection, signature, and identity. See
User-token compatibility.
If you build JCA Signature or Cipher instances from SecurityAlgorithm.getTransformation(),
use SignatureFactory and CipherFactory in org.eclipse.milo.opcua.stack.core.util instead.
The RsaSha256Pss and RsaOaepSha256 transformation strings changed, and those algorithms now
need the parameters from SecurityAlgorithm.getAlgorithmParameterSpec(), which the factories
apply.
For enhanced policies, getSymmetricSignatureAlgorithm(), getSymmetricEncryptionAlgorithm(),
getKeyDerivationAlgorithm(), and getAsymmetricKeyWrapAlgorithm() return null. ECC policies also
return null from getCertificateSignatureAlgorithm(). Their asymmetric signature and encryption
getters return SecurityAlgorithm.None. RSA-DH also returns None from the asymmetric encryption
getter.
Code iterating policies must account for these values. Prefer getProfile(), the
find...Algorithm() methods, or isLegacy(). Update exhaustive switches for the new
SecurityPolicy constants and the SecurityAlgorithm.HmacSha384 and Sha384 constants.
Custom identity providers for enhanced policies need the new IdentityProviderContext. The
default IdentityProvider.getIdentityToken(IdentityProviderContext) calls the two-argument method.
A subclass of UsernameProvider, X509IdentityProvider, or CompositeProvider that overrides only
the two-argument method is bypassed when the SDK requests its token. Override
getIdentityToken(IdentityProviderContext) instead and pass that context when delegating to super.
Use SelfSignedCertificateBuilder.forEccApplicationCertificate(...) to create ECC certificates.
In 1.1.x, namespace 0 registered its event items with the event notifier, and application namespaces had to register their own. Registered items received events without source-hierarchy scoping, while unregistered application items received nothing. In 1.2, the SDK registers event items across namespaces. Items outside the Server Object receive only events from their own node or its HasNotifier or HasEventSource descendants. Server items still receive all events before filters. Application namespaces can therefore begin receiving traffic previously absent.
Set SourceNode and maintain both reference directions. UaNode.addReference and
NodeManager.addReferences add the inverse automatically, but NodeManager.addReference does
not. ManagedNamespace.registerEventNotifier(...) connects a notifier to Server. Connect its
sources with HasEventSource. Organizes and HasComponent alone do not establish event scope.
onEventItemsCreated, onEventItemsModified, and onEventItemsDeleted are notifications, so
omitting old manual registration no longer suppresses delivery.
Invalid select clauses now produce null fields, while valid selections can still be delivered. In 1.1.x a failed select-clause validation suppressed the entire notification.
Event filter conversions from Float to an integer type now round through double and check the
target range, as Double conversions already did. Milo 1.1.x clamped Float values above
Integer.MAX_VALUE, so a Float of 3e9 cast to Int64 or UInt32 gave 2147483647. It now gives
3000000000, and a cast to Int32 out of range yields null instead of 2147483647.
ConditionRefresh markers and retained replay now target the requested Subscription. In 1.1.x the
markers went to all registered items, including other Sessions. A target with no event items
returns Bad_NothingToDo, and an overlapping refresh returns Bad_RefreshInProgress. See
Events, Alarms and Conditions, and the
scope tests.
The standard FindAlias Method nodes now start with Executable and UserExecutable set to false until
an AliasManager enables them. If you installed your own handler, either set both flags yourself
or switch to AliasManager. Installing a handler does not change the flags.
Call argument diagnostics are returned only for operation-level ReturnDiagnostics bits
(0x20 through 0x200). String indexes must come from
DiagnosticsContext.addString, available through InvocationContext.getCallDiagnostics().
Handlers that invent their own string-table indexes must change.
AbstractMethodInvocationHandler maps malformed CallMethodResults to Bad_InternalError,
including an InvalidArgumentException with the wrong number of per-input results. This check
does not cover every raw MethodInvocationHandler. For those, the service replaces null results
and statuses and discards malformed argument diagnostics, but does not apply all those result
invariants.
AbstractMethodInvocationHandler.invoke is no longer abstract. Overriding neither invoke nor
invokeResult compiles but returns Bad_NotImplemented at runtime.
Through ManagedAddressSpace, a modeled instance Method must be called with an instance ObjectId.
Milo 1.1.x allowed its ObjectType to reach the declaration handler. Milo 1.2 returns
Bad_MethodInvalid, or Bad_NodeIdInvalid for Condition instance Methods. Type-level Methods
without a ModellingRule remain callable. Overriding protected getInvocationHandler(...) without
delegating to super also bypasses this validation, ConditionManager dispatch, and per-ObjectId
handler selection.
ManagedAddressSpace now selects a Method node's handler with
UaMethodNode.getInvocationHandler(NodeId), which returns an ObjectId-specific handler or the
stored default without calling getInvocationHandler(). A UaMethodNode subclass that overrides
only the no-argument getter is bypassed for Calls. Install the handler with
setInvocationHandler(...), or override the NodeId overload and apply the same customization to
the handler returned by super.getInvocationHandler(objectId).
NodeFactory, EventFactory, and InstanceDeclarationHierarchy are deprecated but still work
as before. When you migrate to the node instantiator, preserve persisted
NodeIds.
Before (Milo 1.1.x; given OpcUaServer server, ManagedAddressSpace space, an existing parentId,
instanceId (here a String ID File:Example in your namespace), and browseName):
UaObjectNode node =
(UaObjectNode) space.getNodeFactory().createNode(instanceId, NodeIds.FileType);
node.setBrowseName(browseName);
node.setDisplayName(LocalizedText.english("File:Example"));
space.getNodeManager().addNode(node);
node.addReference(new Reference(instanceId, NodeIds.Organizes, parentId.expanded(), false));After (Milo 1.2.0-RC1; import
org.eclipse.milo.opcua.sdk.server.nodes.instantiation.InstantiationRequest):
var request =
InstantiationRequest.of(UaObjectNode.class, NodeIds.FileType)
.nodeId(instanceId)
.browseName(browseName)
.displayName(LocalizedText.english("File:Example"))
.parent(parentId, NodeIds.Organizes)
.legacyPathStrings()
.target(space.getNodeManager())
.build();
UaObjectNode node = server.getNodeInstantiator().instantiate(request).root();Both versions create a mandatory Size child with the String ID File:Example/0:Size.
legacyPathStrings() preserves this old formula for derived child NodeIds. Without it,
IDs change for non-String roots, reserved prefixes, or String roots or member names containing
\, /, or :. The String root File:Example contains :, so without the legacy strategy its
child becomes File\:Example/0:Size. A numeric root 8 derives i=8/0:Size under the new
default.
This example creates only the FileType model. An application must supply the file operations and attribute behavior before offering a usable file service.
Other differences to check when migrating:
- Optional Methods are omitted unless selected with
includeOptional(...). - The default
ConflictPolicy.FAILrejects NodeId collisions instead of silently replacing nodes. A collision found while planning fails withBad_InvalidArgumentand leaves the existing node unchanged. A collision during commit can reportBad_NodeIdExists. Choose another policy deliberately when reuse is required. - Supertype-declared mandatory members previously skipped by NodeFactory are now created. Mandatory descendants of an excluded optional are no longer created as orphaned nodes.
- A subclass that overrode
NodeFactory.instanceNodeId()must supplynodeIdStrategy(...)to preserve its custom identity formula. - Instances copy modeled permissions, AccessLevelEx, Historizing, and sampling interval from their declarations.
-
onNodecallbacks see staged nodes, and Methods are bound after commit. - Custom
NodeManagerimplementations get only best-effort rollback unless they implement the batch primitives. Defaults forremoveNodeIfSameandremoveReferenceIfSamerequire a single writer, so concurrent managers must make these checks and removals atomic. Cleanup relies ongetReferencesreturning the sameReferenceinstances that were added. Managers that rebuild references from storage must overrideremoveReferenceIfSame.
The instantiation guide maps the old callbacks.
AbstractLifecycle.startup() now leaves the component STOPPED when onStartup() throws. In
1.1.x it stayed RUNNING. A later shutdown() does nothing, so release partially acquired
resources inside onStartup() before throwing. LifecycleManager, including
ManagedNamespaceWithLifecycle and ManagedAddressSpaceWithLifecycle, now shuts down the
children that already started when a later child fails.
OpcUaServer could not restart in 1.1.x either, because repeated startup threw synchronously
from its already-started EventFactory. In 1.2, repeated calls return the original startup future,
even after failure or shutdown, and a failed startup rolls back acquired resources. A completed
startup future therefore does not prove the server is still running. To restart, create a fresh
server and owned resources. For a failed instance, finish shutdown.
Cancelling or manually completing the public startup future does not stop startup or let shutdown
release resources early. Request shutdown through shutdown() and await its completion.
ActivateSession verification now uses the server certificate captured by the Session's
CreateSession, allowing reactivation after rotation. Custom validators reading the current
endpoint or channel certificate should instead obtain session.getOriginalServerCertificate().
This returns a SessionServerCertificate record. Use .certificate() for its X509Certificate
or .createSessionCertificate() for the advertised ByteString. Validators using the SDK's
decryptTokenData(...) already receive the corrected binding.
A Write now checks the value's shape for every ValueRank. Milo 1.1.x checked shape only for
ValueRank 1 or more, so an array or Matrix written to a Scalar Variable returned Good. Server code
that cast the stored value to its scalar type then threw ClassCastException. These writes now
fail with Bad_TypeMismatch. A Variable built without setValueRank(...) is Scalar, so declare
an array ValueRank, such as OneDimension or ScalarOrOneDimension, on Variables that accept arrays.
Any still accepts every shape. A ValueRank below -3 is not defined by Part 3 and now rejects every
value.
Writes through AttributeWriter now accept primitive arrays such as int[] wherever their boxed
form is accepted. Milo 1.1.x rejected them with Bad_TypeMismatch, including client IndexRange
Writes into a stored primitive array. A written Matrix.ofNull() is now treated as an empty
Variant, so AllowNulls decides the result. An IndexRange Read or Write of a stored null Matrix
returns Bad_IndexRangeNoData. In 1.1.x both cases threw NullPointerException, and the client
request timed out without a response.
UaNode and its subclasses no longer take the node's monitor to read an attribute. Each read
still sees the value most recently set for that attribute. Writes still hold the monitor while
AttributeObservers run, but a concurrent reader can now see a new value before the observers
have run. Code that holds a node's monitor while setting several attributes no longer keeps
readers from seeing the intermediate state.
AddressSpace.browse, browseAsync, browseNodes, and browseNodesAsync, and the UaNode
methods that delegate to them, now fail with a UaException carrying the StatusCode of a Bad
Browse or BrowseNext result. Milo 1.1.x returned the references collected so far. Browsing an
unknown or deleted Node returned an empty list, and a Bad BrowseNext such as
Bad_ContinuationPointInvalid returned the earlier pages as if they were the whole result. Code
that treated an empty list as "nothing there" must now handle Bad_NodeIdUnknown. Uncertain
pages now keep their references, and their ContinuationPoints are followed. These requests now go
through OpcUaClient.sendRequestAsync, so ServiceFaultListeners see their faults.
The client type tree builders and LazyClientDataTypeTree also fail on a Bad BrowseNext result
instead of building a tree without the missing subtypes or encodings. OpcUaClient.getDataTypeTree()
does not cache the failure, so the next call builds again. LazyClientDataTypeTree records a
failed resolution and retries it after clearFailedResolutions().
Most generated blocking model-property writers now throw UaException for a non-Good operation
status where their 1.1.x counterparts discarded the returned status. The new GDS model has no
1.1.x counterpart.
At this source baseline, 23 generated methods with multiline signatures still discard status,
including ServerDiagnosticsTypeNode.writeServerDiagnosticsSummary(...). Use the asynchronous
property's returned StatusCode for explicit handling. UaVariableNode.writeValue(...) still
has a different contract. It rejects Bad status but accepts Uncertain and updates its cached
Value. The
generated-writer integration test
shows a writer that enforces status, and Writing describes caller choices.
SecureChannelHandshakeException is a UaException that preserves the underlying status.
Blocking connect() already wrapped its cause in 1.1.x, but the new handshake wrapper can move an
original UaServiceFaultException one level deeper. Code using
getCause() instanceof UaServiceFaultException should traverse the cause chain or use
UaException.extractStatusCode(...).
ChannelStateObservable.TransitionListener.onConnectFailure(Throwable) reports failed transport
connection attempts. TCP transport disconnect() aborts an in-flight connection or handshake.
SDK disconnectAsync() now waits at most five seconds for Session closure before initiating
transport disconnect. This bounds the Session-close phase, not the entire cleanup operation.
OpcUaClientConfig.copy(...) now preserves the configured certificate validator. In 1.1.x a
copied configuration lost it and could fall back to the insecure default validator.
UaNode.readWriteMask() now reads the WriteMask attribute. In 1.1.x it incorrectly read
UserWriteMask. Use readUserWriteMask() when the user-specific value is intended.
OPC TCP connection limits are now checked on every inbound connection. The defaults remain 100 per address and 10000 total. Gateways opening many connections from one address can see rejections earlier than in 1.1.x. Loopback remains exempt. Verify actual capacity after changing the limits. See Deployment.
The limits can now be set without code, through the system properties
milo.stack.rateLimit.enabled, milo.stack.rateLimit.maxAttempts, milo.stack.rateLimit.windowMs,
milo.stack.rateLimit.maxConnections, and milo.stack.rateLimit.maxConnectionsPerAddress.
Assigning the Stack.ConnectionLimits fields before the first bind still works and takes
precedence. Integer values must be positive, and enabled accepts only true or false. Any
other value is logged at WARN and the default is kept. Stack.ConnectionLimits.HELLO_DEADLINE_MS
is deprecated because nothing reads it. Set the Hello deadline with
OpcTcpServerTransportConfigBuilder.setHelloDeadline(...).
ExecutionQueue now runs queued work inline when its executor rejects dispatch. Callers using
custom executors must allow for task work on the submitting thread during rejection.
Channel failure now fails requests written to that channel. Requests waiting for a replacement channel survive only when they have a positive timeout hint, and that deadline includes the wait for a channel. Custom transports must fail channel-acquisition futures on terminal disconnect instead of leaving requests pending.
In subclasses of OpcTcpClientTransport, overrides of only
sendRequestMessage(UaRequestMessageType) or getChannel() no longer see CreateSession or Session
reactivation. The SDK sends those through sendRequestMessage(Callable<UaRequestMessageType>, long),
which acquires its channel directly from the ChannelFsm. Override that overload as well if you
customize sending or channel acquisition. Adapt custom getChannel() selection in that deferred
path, because wrapping the supplier alone does not restore it. Preserve the wait for an
established channel before invoking the supplier and the separate channel-wait and response
timeouts.
AbstractUascClientTransport.sendRequestMessage(UaRequestMessageType) no longer calls the protected
sendRequestMessage(UaRequestMessageType, Channel) overload. Subclasses that intercepted all
requests in that protected method now miss ordinary sends, including initial ActivateSession.
Move ordinary-send interception to the public one-argument method. For OpcTcpClientTransport,
cover the public Callable overload too. When intercepting its request, wrap the supplier without
evaluating it early and delegate through the matching super overload.
Custom transport, service-request, and channel code needs these changes:
| 1.1.x API | 1.2.0 API |
|---|---|
ClientApplicationContext.getKeyPair/getCertificate/getCertificateChain |
Implement Optional<CertificateIdentity> getCertificateIdentity(SecurityPolicyProfile) throws UaException; return empty when there is no identity for the profile, and throw when selection fails |
ServiceRequestContext without endpoint access |
Implement Optional<EndpointDescription> getEndpoint(); empty means discovery-only |
ServiceRequest and UascServiceRequest constructors |
Pass the nullable selected endpoint before the request |
UascResponseHandler.handleChannelError(UaException) |
handleChannelError(Channel, UaException) |
UascResponseHandler.handleChannelInactive() |
handleChannelInactive(Channel) |
Protected pending-request maps and cancelRequestTimeout(long) in AbstractUascClientTransport
|
pendingRequests is private with a new value type; pendingTimeouts and cancelRequestTimeout are removed. Use the base request and response path |
ChunkEncoder.EncodedMessage(chunks, requestId) |
EncodedMessage(chunks, requestId, signature); pass null when there is no signature |
OpcTcpServerTransportConfigBuilder.setExecutor, OpcServerTransportConfig.getExecutor(), UascServerConfig.getExecutor()
|
Removed, as 1.1.x ignored them; use OpcUaServerConfigBuilder.setExecutor
|
UascServiceRequestHandler(UascServerConfig, ServerApplicationContext) |
UascServiceRequestHandler(ServerApplicationContext) |
ServerApplicationContext.selectEndpoint(...) is a new default method. An override must return
empty when matching endpoints are not Session-equivalent, rather than choose an ambiguous match.
A client transport with channel-bound requests must override
OpcClientTransport.sendRequestMessage(Callable, long) to wait for the channel before building
the request, and override getChannelThumbprint() to supply its binding. The default Callable
overload builds immediately and preserves the existing request timeout. It does not wait for a
channel. OpcTcpClientTransport supplies this override and bounds the channel wait by the
supplied positive timeout, failing with Bad_Timeout. CreateSession and reactivation use the
configured request timeout for that wait. The request header then bounds the response wait.
At this baseline, ReverseTcpClientTransport inherits the eager default and does not override
the channel thumbprint accessor. Do not infer channel-bound enhanced Session support from its
basic connection lifecycle. See ClientApplicationContext and
UascResponseHandler.
JSON now follows the OPC UA 1.05 mapping, with the differences described in
Encodings. A JSON-native ExtensionObject places its
fields inline beside UaTypeId. Binary and XML bodies remain opaque under UaEncoding and a
base64 UaBody. XML bodies now use base64 UTF-8. JSON-native bodies already used the DataType ID
in 1.1.x, while Binary and XML envelopes used their encoding ID. In 1.2 all three use the
DataType ID in their JSON envelope.
A 1.1.x DataValue nests its Variant under Value, whereas 1.2 places the type and value beside
the status and timestamps. The old nested DataValue fails with Bad_DecodingError. A bare scalar
Int32 Variant, such as the value 42, can still decode, but compatibility depends on its payload.
An old JSON-native ExtensionObject envelope can parse before its structure-body decode fails.
Legacy Binary and XML envelopes instead fail with Bad_DecodingError while 1.2 reads the
envelope, because UaTypeId names an encoding rather than the DataType. A bare Variant
containing one of those envelopes also fails. For XML, correcting the ID alone is not enough. The
old XML text body must also become base64 UTF-8.
Keep an explicit format version, because trial decoding does not identify it. Milo supplies no automatic stored-data converter. See the two-process conversion workflow, including its model-specific limitations.
Null ByteStrings now encode as JSON null instead of empty strings. COMPACT null DataValue array entries now retain their positions instead of being dropped. VERBOSE null ExtensionObject array entries now use null instead of empty objects. Null String and LocalizedText entries already used null. Conversion alone cannot recover data lost by the old encoding, such as null versus empty ByteString.
Null DiagnosticInfo and ExtensionObject elements now encode as JSON null in Variant arrays and
matrices. DiagnosticInfo now omits its default fields in both modes, including VERBOSE. It omits
the index fields when they are -1, and InnerStatusCode when Good.
Configure both sides for the selected JSON mode.
OpcUaJsonDecoder.setEncoding(OpcUaJsonEncoder.Encoding.VERBOSE) is required for VERBOSE input.
Match the decoder mode to the encoder for structures with optional fields, unions, or enum fields.
Mismatches can raise Bad_DecodingError for unexpected members or incompatible enum tokens.
COMPACT writes numeric enum fields, and VERBOSE writes named enums as Name_Value strings.
VERBOSE also accepts enum names with numeric suffixes. VERBOSE now writes null arrays explicitly.
Dynamic structure codecs now assign optional-field mask bits by declared optional-field position.
Milo 1.1.x incorrectly packed selected fields when an earlier optional field was absent. Union
headers now use SwitchField rather than SwitchValue, and JsonStructCodec optional headers
use EncodingMask rather than their old SwitchField name. These names affect XML and JSON wire
data. DynamicUnionType.UnionValue can hold null. Re-test persisted optional structures and
unions, and exchanges with old Milo peers.
JSON structure fields may be reordered. Buffered structures, ExtensionObject envelopes, and
Variant bodies reject duplicates. Unknown structure members also fail with Bad_DecodingError.
Top-level DataValue and Variant members, and directly streamed built-ins such as LocalizedText,
are not checked for duplicates. For those, the last occurrence wins.
The context's EncodingLimits bound JSON input. MaxMessageSize counts UTF-16 code units,
including whitespace, and defaults to 2 MiB even when configured as zero or less.
MaxRecursionDepth defaults to 128 and bounds buffered containers and nested ExtensionObjects.
Violations report Bad_EncodingLimitsExceeded.
JSON QualifiedNames such as 2:Name now decode to namespace index 2 and name Name. Milo
1.1.x treated the whole string as a namespace 0 name. Numeric indexes above 65535 fail with
Bad_DecodingError.
Custom codecs must use UaEncoder.encodeEncodingMask and encodeSwitchField, and the matching
UaDecoder methods, instead of ordinary UInt32 fields. The decoder methods take declared field
names for VERBOSE inference. Custom managers must implement
DataTypeManager.getDataTypeId(NodeId) for JSON wrapping of Binary and XML ExtensionObjects.
Unresolved identity raises Bad_EncodingError.
The client's LazyClientDataTypeManager inherits reverse lookup of registrations already
present. It does not trigger discovery through getDataTypeId. Wrapping a custom Binary
ExtensionObject in JSON can fail until its codec has been resolved. Resolve getCodec(encodingId)
and check for null, or decode the structure before explicitly encoding JSON. See
the resolution workflow.
Dynamic codecs decode subtype-enabled structure arrays and successfully decoded ExtensionObject
arrays in Variant fields as UaStructuredType[]. Callers casting to a concrete array class must
change. Null ExtensionObjects become Java null. If a Variant-field ExtensionObject decode fails,
the dynamic codec retains the raw value or array instead. Subtype-enabled scalar, array, and
Matrix fields can now encode generated Java UaStructuredType values as well as dynamic
structures, using the registered codecs.
milo-encoding-xml no longer depends on JAXB. Applications that relied on that transitive
dependency must declare their own JAXB dependencies. milo-dtd-core still brings
jaxb-runtime.
For a Matrix inside a Binary Variant, empty values with zero or negative dimensions become empty
one-dimensional arrays. Nonempty values fail with Bad_EncodingError unless they have at least
two dimensions and every dimension is positive.
As in 1.1.x, Binary structure fields preserve nonnegative dimensions, but a zero dimension
suppresses all elements. A nonempty Matrix with dimensions [4, 0] decodes as empty. Also as in
1.1.x, positive-dimension element-count validation in the Binary field encoder relies on a Java
assertion, and a negative dimension can be written but fails field decoding with
Bad_DecodingError. Enforce shape and count before encoding.
JSON preserves empty dimensions. XML normalizes an empty Variant Matrix to an empty array and an empty structure-field Matrix to a null Matrix, losing the original dimensions. See encoding limits and Matrix boundaries.
For compatible element types, server Method inputs and Variable writes now accept an empty one-dimensional array for a declared ValueRank of 2 or more. They restore an empty Matrix of that declared rank, with zero in each dimension. Milo 1.1.x rejected that shape.
Direct Binary Matrix field decoding now returns Matrix.ofNull() rather than Java null. Test
with Matrix.isNull() or isNotNull(). JSON and XML decoders already returned the null Matrix
object in 1.1.x. Binary Matrix field encoders now accept Java null, which previously threw.
Matrix.ofNull() already wrote the null wire form. XML encoding of Matrix.ofNull() no longer
throws. See Data types and
Encodings for the Binary
behavior.
Non-null Matrix field decoding and direct Matrix decoding with fewer than two dimensions now
fail with Bad_DecodingError in Binary, JSON, and XML. A Variant with one dimension can
still decode as a one-dimensional array.
XML Matrix encoding now keeps nullable element positions with the schema's nil or default
representation. Null Boolean or numeric elements and incompatible element classes fail with
Bad_EncodingError. XML array fields must be direct children of their field element. Invalid
lexical values, integer overflow, and date-only DateTimes fail with Bad_DecodingError.
XML decoding now honors xsi:nil on String, ByteString, XmlElement, and array containers.
An empty ByteString element means empty bytes, while xsi:nil means null. Null ByteString and
XmlElement values now encode with xsi:nil. Do not infer an original null-versus-empty
distinction from stored data whose old encoding lost it.
ULong arithmetic now allows valid subtraction across the signed-long boundary and handles
negative Long.MIN_VALUE operands without recursive overflow. Its float and double conversions
avoid double rounding for values above Long.MAX_VALUE. Use toBigInteger() when an exact
unsigned magnitude is required. Floating-point conversion remains approximate.
ULong.valueOf(String) now throws NumberFormatException for every value above 2^64-1. Milo 1.1.x
returned an unrelated number for some of them, such as 11553255926290448384 for
30000000000000000000. The JSON decoder reads UInt64 through this method, so it now fails such input
with Bad_DecodingError, as the XML decoder already did. Exception messages for other invalid
strings changed.
Per-Session diagnostics Objects now follow ServerDiagnostics EnabledFlag, which defaults to
false. A default 1.2 server therefore has no per-Session diagnostics Objects. Milo 1.1.x always
created them, but their values returned Bad_NotReadable while disabled. Enabling now creates
Objects for existing Sessions and catches up each Session's SubscriptionDiagnosticsArray.
Disabling removes the dynamic nodes. Array-element gating already existed in 1.1.x.
Static diagnostics Variables remain and report Bad_OutOfService while disabled, replacing
Bad_NotReadable. SessionDiagnostics and SessionSecurityDiagnostics arrays now reference the
same Variables beneath each Session Object. The old separately allocated namespace 1 array
elements with BrowseName 1:SessionDiagnostics are replaced by the Object's namespace 0-named
Variables.
Rediscover after toggling instead of retaining dynamic NodeIds or wrappers. Where an array
element NodeId has a [n] suffix, n is an allocation counter, not its current array position.
Deleting an earlier element no longer lets a new element reuse a survivor's ID.
The public SessionDiagnosticsVariableArray and SessionSecurityDiagnosticsVariableArray
constructors no longer take a NodeManager. See Diagnostics and
diagnostics ownership and lifecycle.
Data-item creation now succeeds for Bad_NotReadable and Bad_UserAccessDenied read denials,
then publishes the denial as the DataValue status (Part 4 §5.13.2.1). In 1.1.x the default
controller returned only Bad_UserAccessDenied for these access-bit checks, and Bad_NotReadable
could come from a custom controller. Any other Denied status still fails data-item creation.
Every Denied status fails event-item creation. NODE_UNKNOWN fails either kind of item with
Bad_NodeIdUnknown. Clients must inspect Publish status, not only the create result.
Access restoration requires refreshing the stored decision. That happens every cycle under the default policy, after invalidation under cached policy, or through application refresh for custom samplers. The SDK rechecks identity or endpoint changes and transfers. A custom sampler otherwise retains its old decision even after application permissions change.
See the Access control example for cached denial and restoration, and the read-access tests for creation followed by both denial statuses.
For Value Reads, the default controller now checks AccessLevel before UserAccessLevel and returns
Bad_NotReadable if CurrentRead is absent. In 1.1.x it checked only UserAccessLevel, so a Variable
whose AccessLevel denied reading could still return a value when UserAccessLevel allowed it.
Monitored items now publish that node-level denial. For Value Writes, AccessLevel lacking
CurrentWrite now gives Bad_NotWritable before UserAccessLevel is checked. Milo 1.1.x checked the
user bit first and left the node-level rejection to the node writer.
SubscriptionModel remains a working deprecated adapter. Its old forwarding overrides replace
the new defaults, so retaining that wiring does not double-sample. To adopt the inherited manager:
- Remove the SubscriptionModel field.
- Remove its lifecycle registration.
- Remove the four forwarding callbacks (or old no-op callbacks), preserving unrelated callback work.
ManagedNamespaceWithLifecycle, ManagedAddressSpaceWithLifecycle, and
ManagedAddressSpaceFragmentWithLifecycle own manager startup and shutdown. Plain
ManagedNamespace or ManagedAddressSpace subclasses must call getSamplingManager().startup() and
shutdown(). Otherwise, created items remain unsampled.
The inherited manager revises intervals upward to 25 ms buckets by default, on creation and modification. A 0 ms request that reaches the namespace becomes 25 ms, where SubscriptionModel polled at least every 1 ms. This comparison requires MinimumSamplingInterval 0 (or another node that permits 0). Variables default to -1 in both releases. For them, the subscription publishing interval replaces a 0 ms request before namespace revision.
New-item sampling now uses a 100 ms debounce with a 500 ms maximum collection window. Milo 1.1.x scheduled an immediate initial sample.
A custom sampler can override protected reviseSamplingInterval(ReadValueId,double) to report
its own supported intervals. Returning 0 requires routing the item to its own push implementation.
Direct AddressSpaces retaining SubscriptionModel keep its unbucketed intervals. See
Sampling migration and the
interval tests.
AccessController, DefaultAccessController, and AccessResult moved from servicesets.impl
to access, so update imports. getAccessController() and getAccessControlManager() are final.
Replace a server getter override with OpcUaServerConfigBuilder.setAccessControllerFactory(...).
The old override reached all 1.1.x checks. In 1.2 the manager and cache retain their own
controller reference, so overriding a getter would not cover every check.
DefaultAccessController.checkWriteAccess now returns an IdentityHashMap. Code that calls it
directly must look up each result with the same WriteValue instance it passed in. An equal but
different instance finds nothing, and equal WriteValues each get their own entry. Custom
AccessController implementations are unaffected.
Default sampling also calls the controller on sampling threads for each Session every cycle. Keep controllers and access-attribute filters fast, thread-safe, and free of recursive controller calls.
Cached-policy users must invalidate changes to AccessLevel, UserAccessLevel, RolePermissions,
AccessRestrictions, role mapping, and node removal or replacement. SDK Session changes,
reactivation, and transfers invalidate automatically, and Session closure evicts cached entries.
Use invalidateReadAccess(ReadAccessScope) to limit invalidation to nodes, a namespace, a
predicate, or a Session. Direct service checks do not use that cache.
checkReadAccess now returns NODE_UNKNOWN for an unrecognized node instead of ALLOWED. Neither
isAllowed() nor isDenied() is true. Ordinary service dispatch lets the address space provide
its unknown-node result. An exhaustive switch over 1.1.x's already-sealed AccessResult fails to
compile until it handles the new NodeUnknown record.
DataItem.setReadAccessResult has a no-op default, and getReadAccessResult returns ALLOWED by
default. A custom implementation enforces no read-access decision through these defaults, so
override them to participate. The overload setReadAccessResult(result,session,accessEpoch)
returns boolean and checks Session identity and Session.getAccessEpoch() before applying a
result. An implementation with its own update lock must compare under that lock too.
Custom producers can use a DataItemListener (OpcUaServer.addDataItemListener and
removeDataItemListener), AddressSpace.onDataItemsTransferred, and Session identity and
endpoint listeners. Managed cache users also have ReadAccessListener. Refresh even while the
device is quiet.
MonitoredDataItem.setValue now drops values while Disabled. Milo 1.1.x enqueued them. A new
denial after a Session change clears values queued for the old Session through
setReadAccessResultAfterSessionChange(...). Ordinary application invalidation does not purge
already queued values.
The OpcUaServerConfigImpl canonical constructor adds both reverseConnectTargets and
accessControllerFactory. Compatibility overloads remain.
| Deprecated API | Replacement |
|---|---|
NodeFactory, its callback, InstanceDeclarationHierarchy, EventFactory, and their accessors |
Node instantiator; OpcUaServer.newEvent(typeId) for transient events |
RsaSha256CertificateFactory |
AbstractCertificateFactory |
ValidationCheck.REVOCATION (for removal) |
None needed; use REVOCATION_LISTS for strict checking |
Stack.ConnectionLimits.HELLO_DEADLINE_MS |
OpcTcpServerTransportConfigBuilder.setHelloDeadline(...) |
SubscriptionModel |
ManagedAddressSpace's inherited SamplingManager, or an explicitly owned SamplingManager with an AddressSpaceSamplingGroup factory for a direct AddressSpace; see Sampling and read access |
User guide for Milo 1.2.0-RC1 at source 69cb25087, 2026-10-12. Home · Getting started · Examples · Troubleshooting · Release notes
- Home
- Start here
- Client guide
- Server guide
- Feature guides
- Reference
- Operations
- Examples
- Release notes
- Contributing