Skip to content

Client Browsing

Kevin Herron edited this page Oct 12, 2026 · 10 revisions

Milo 1.2.0-RC1, Java 17. Source baseline 69cb25087804.

Reading, writing, and calling methods all start from a NodeId, and browsing is how a client finds NodeIds it does not know in advance. With the connected client from Connecting, this page starts at the Objects folder, finds Thermostat, and lists its components. It then translates a browse path straight to Setpoint.

Browsing follows references

The address space is a graph, not a file tree. Nodes are joined by typed references such as Organizes, HasComponent, and HasProperty, and a browse asks one node for its references, filtered by direction and reference type. The answer is a list of ReferenceDescription entries, each describing the node at the other end of one reference. Hierarchical references make the address space look like a tree, but a node can be reached along several paths, so recursive traversal needs a visited set.

A BrowseName is a QualifiedName, a namespace index plus a name. The server assigns namespace indexes, so they can differ between servers and change after a reconfiguration. To recognize Thermostat by name, you first look up the index the server gave to the tutorial namespace URI, urn:eclipse:milo:wiki.

Find Thermostat and its components

The client reads the server's namespace table each time it opens a Session, and client.readNamespaceTable() refreshes that copy on demand. tutorialNamespaceIndex looks up the tutorial URI in that table and fails when the server does not have it, instead of falling back to namespace 0.

static UShort tutorialNamespaceIndex(OpcUaClient client) {
  NamespaceTable namespaceTable = client.getNamespaceTable();
  UShort namespaceIndex = namespaceTable.getIndex("urn:eclipse:milo:wiki");
  if (namespaceIndex == null) {
    throw new IllegalStateException("Server does not expose the tutorial namespace");
  }
  return namespaceIndex;
}

With the index you can build Thermostat's BrowseName and look for it among the Objects folder's references. client.getAddressSpace().browse(nodeId) follows forward hierarchical references, including subtypes, and collects every continuation page into one list.

static Optional<NodeId> findThermostat(OpcUaClient client, UShort namespaceIndex)
    throws UaException {
  var thermostatName = new QualifiedName(namespaceIndex, "Thermostat");
  List<ReferenceDescription> references = client.getAddressSpace().browse(NodeIds.ObjectsFolder);

  for (ReferenceDescription reference : references) {
    if (thermostatName.equals(reference.getBrowseName())) {
      ExpandedNodeId targetId = reference.getNodeId();
      return targetId.toNodeId(client.getNamespaceTable());
    }
  }

  return Optional.empty();
}

The method compares BrowseNames because a BrowseName is namespace-qualified and stable, while DisplayName is localized presentation text, not a durable key. The target arrives as an ExpandedNodeId, which may be remote or use a namespace URI, so the code resolves it with the client's namespace table instead of discarding those qualifiers. toNodeId returns an empty result when the target is on another server or its namespace URI is unknown.

Browsing Thermostat itself lists its components:

static void printComponents(OpcUaClient client, NodeId thermostatId) throws UaException {
  List<ReferenceDescription> references = client.getAddressSpace().browse(thermostatId);

  for (ReferenceDescription reference : references) {
    String name = reference.getBrowseName().name();
    NodeClass nodeClass = reference.getNodeClass();
    System.out.println(name + ": " + nodeClass);
  }
}

Against the tutorial server, it prints one line per HasComponent reference, in the order the server returns them:

Temperature: Variable
Setpoint: Variable
AdjustSetpoint: Method

Depending on the result mask, a reference supplies direction, reference type, BrowseName, DisplayName, NodeClass, target ExpandedNodeId, and type definition. The default browse requests all of them.

Translate a browse path to Setpoint

When you know where a node sits in the model, you can skip browsing level by level. A BrowsePath names the steps from a starting NodeId as a RelativePath. Each RelativePathElement gives a reference type, the direction (forward or inverse), whether subtypes count, and the namespace-qualified BrowseName of the target. The server follows the steps and returns the nodes it reaches. This method resolves Setpoint from the Objects folder by way of Thermostat:

static NodeId resolveSetpoint(OpcUaClient client, UShort namespaceIndex) throws UaException {
  var thermostatName = new QualifiedName(namespaceIndex, "Thermostat");
  var setpointName = new QualifiedName(namespaceIndex, "Setpoint");

  RelativePathElement[] elements = {
    new RelativePathElement(NodeIds.Organizes, false, true, thermostatName),
    new RelativePathElement(NodeIds.HasComponent, false, true, setpointName)
  };
  var browsePath = new BrowsePath(NodeIds.ObjectsFolder, new RelativePath(elements));

  TranslateBrowsePathsToNodeIdsResponse response =
      client.translateBrowsePaths(List.of(browsePath));
  BrowsePathResult result = requireNonNull(response.getResults())[0];
  if (!result.getStatusCode().isGood()) {
    throw new UaException(result.getStatusCode());
  }

  BrowsePathTarget target = requireNonNull(result.getTargets())[0];
  if (!target.getRemainingPathIndex().equals(UInteger.MAX)) {
    throw new UaException(StatusCodes.Bad_NoMatch, "Setpoint path resolved only partly");
  }

  ExpandedNodeId setpointId = target.getTargetId();
  return setpointId.toNodeIdOrThrow(client.getNamespaceTable());
}

The two booleans in each element are isInverse and includeSubtypes, so each step here follows its reference forward and also accepts subtypes. The path is built from references and BrowseNames, not from a slash-separated NodeId string, so its QualifiedNames need the server's current namespace index.

Inspect every BrowsePathResult and its targets, because a path can resolve to multiple targets or only partly. remainingPathIndex indicates an unresolved suffix, and the maximum UInt32 sentinel means the whole path resolved, so the method checks it before treating the target as the final node. client.translateBrowsePaths(...) takes several paths in one request, and translateBrowsePathsAsync(...) is its asynchronous counterpart.

Narrow a browse with options

AddressSpace.BrowseOptions selects direction, reference type, subtype inclusion, node-class mask, result mask, and maximum references per node. This browse returns only the Variables that are components of Thermostat, so AdjustSetpoint drops out:

static List<ReferenceDescription> browseVariables(OpcUaClient client, NodeId thermostatId)
    throws UaException {
  AddressSpace.BrowseOptions options =
      AddressSpace.BrowseOptions.builder()
          .setReferenceType(NodeIds.HasComponent)
          .setIncludeSubtypes(true)
          .setNodeClassMask(EnumSet.of(NodeClass.Variable))
          .build();

  return client.getAddressSpace().browse(thermostatId, options);
}

setMaxReferencesPerNode(...) limits the size of each response page. The AddressSpace helpers still collect every page, so the setting changes the number of round trips, not the references you get back.

client.getAddressSpace().getNode(nodeId).browse() does the same browse from a node wrapper and also collects every page. browseNodes() goes further and builds client-side wrappers for the targets, which can require additional service reads. It logs and omits references whose wrapper creation fails, so use reference descriptions when names and NodeIds are sufficient, and see Client nodes when you want the wrappers.

Own continuation points at the service level

The AddressSpace helpers trade detail for convenience. When a Browse or BrowseNext operation returns a Bad status, they release any continuation point it still carries and fail with a UaException holding that status. Uncertain pages, such as Uncertain_NotAllNodesAvailable, keep their references, and paging continues. Use client.browse(view, maxReferencesPerNode, descriptions) and browseNext(...) when you need per-node status within a batch, Uncertain results, or page-by-page control.

Validate the service response and each BrowseResult.statusCode, then read its references and its opaque continuationPoint. A successful result with no references can still carry a nonempty continuation point, so do not ignore it. Pass outstanding points to client.browseNext(false, points) and repeat until each point is empty. In a batch, track each starting node separately. A response can replace a point, so keep the latest one instead of reusing the original indefinitely.

Continuation points from explicit service calls are yours to manage. They consume server resources and belong to a Session, so do not persist them or transfer them to another client. If traversal stops early, call browseNext(true, points) in cleanup and inspect the release service result. BrowseNext release responses do not provide ordinary per-node browse data. Limit traversal depth, total references, and concurrent points. AddressSpace node wrappers, by contrast, are client cache entries and hold no server resources.

When browsing fails

An unknown starting node, inaccessible node, or unsupported view can fail one operation while another succeeds. How you see that depends on the API you used.

Situation What you see What to do
A Browse or BrowseNext operation fails in an AddressSpace helper Blocking helpers throw UaException with the operation's status, such as Bad_NodeIdUnknown for an unknown starting node, and async helpers complete exceptionally. References from earlier pages are not returned. Handle the status. An empty list now means the node has no matching references.
Bad_ContinuationPointInvalid The server no longer recognizes a continuation point. Restart the affected browse if appropriate and deduplicate already processed results.
Bad_NoMatch from translation A path step's BrowseName matched no node. Check the namespace index and names in each QualifiedName.
Partial translation A target with a remainingPathIndex below the maximum UInt32 value. Treat the target as an intermediate node, not the final one.
Service or connection failure Blocking helpers throw UaException, and async helpers complete exceptionally. Handle it like any other failed request, as described in Recovery and limits.

High-level service calls set ReturnDiagnostics=0. To request DiagnosticInfos, build a request with a RequestHeader diagnostics mask and use sendRequest(...) or sendRequestAsync(...). A server may still omit diagnostics.

Examples and reference

Next steps

You can find Thermostat, Temperature, and Setpoint and hold their NodeIds. To read their current values and interpret what comes back, continue with Reading.

Related: Client SDK · Client nodes · OPC UA concepts

Clone this wiki locally