Skip to content

Client: never select an OpenAPI endpoint in SelectEndpoint, and keep a closed Session from sending requests without authenticationToken - #4646

Merged
marcschier merged 3 commits into
OPCFoundation:masterfrom
biancode:fix/select-endpoint-skips-openapi
Oct 4, 2026
Merged

marcschier merged 3 commits into
OPCFoundation:masterfrom
biancode:fix/select-endpoint-skips-openapi

Conversation

@biancode

@biancode biancode commented Oct 3, 2026

Copy link
Copy Markdown
Contributor

Description

Two client fixes, one commit each.

1. CoreClientUtils.SelectEndpoint skips OpenAPI endpoints.

  • An HTTPS listener with the REST binding announces its OpenAPI endpoint with the same URL and message mode as its binary mode-None endpoint. SelectEndpoint matched on the URL scheme and the message mode only, so the first of the two in the GetEndpoints result won, in the main pass and in the HTTPS fallback for useSecurity.
  • The OpenAPI mapping (OPC 10000-6 §G.3) is the REST binding, not the binary transport the caller connects with. With the default channel bindings no channel factory is registered for the OpenAPI profile (ClientChannelManager maps it to opc.https+webapi), so a Session cannot be created on it; with the REST channel registered, the Session silently runs over REST.
  • Following OPC 10000-4 §5.5.4 (select by TransportProfileUri), both passes now skip endpoints with the HTTPS or WSS OpenAPI transport profile. Endpoints without a TransportProfileUri are selected as before.

2. A closed Session does not send requests without authenticationToken.

  • A request with a null authenticationToken is a session-less invocation (OPC 10000-4 §6.3.1).
  • After CloseAsync(closeChannel: false), a request sent through the same Session object went out that way. A server that supports session-less invocation processed it as one instead of rejecting it.
  • Session now records that a session was created (SessionCreated with a non-null cookie; OpenAsync now calls it through the override instead of base.SessionCreated). From then on UpdateRequestHeader fails a request without token locally with Bad_SessionIdInvalid, and the request is never sent. CreateSession and ActivateSession are exempt, so reopening and reactivating keep working. A Session that was never created behaves as before.

Changes

  • src/Opc.Ua.Client/CoreClientUtils.cs: IsOpenApiEndpoint, used in both selection passes; the <returns> documentation says that an OpenAPI endpoint is never returned.
  • src/Opc.Ua.Client/Session/Session.cs: overrides of SessionCreated and UpdateRequestHeader (both virtual in SessionClient / ClientBase), one volatile bool field.

Tests

  • New, part 1 (Opc.Ua.Client.Tests, CoreClientUtilsSelectEndpointTests): SelectEndpointSkipsTheOpenApiEndpointListedFirst, SelectEndpointWithSecurityFallbackSkipsTheOpenApiEndpoint, SelectEndpointSkipsTheWssOpenApiEndpoint, SelectEndpointReturnsNullWhenOnlyOpenApiEndpointsMatch (with and without useSecurity), SelectEndpointKeepsTheFirstBinaryEndpointWithoutTransportProfile.
  • New, part 2 (Opc.Ua.Client.Tests, SessionTests with the channel mock): RequestAfterCloseFailsLocallyWithBadSessionIdInvalidAsync (the Read never reaches the channel), ActivateSessionIsStillSentAfterCloseAsync, RequestAfterANewSessionIsCreatedCarriesItsTokenAsync, RequestBeforeASessionIsCreatedIsNotRefusedAsync.
  • Updated: Opc.Ua.Sessions.Tests ClientTest.ReconnectSessionOnAlternateChannelAsync closes the Session and then the channel. The read that follows now fails with Bad_SessionIdInvalid before it reaches the closed channel, and the test expects that code instead of the codes of the closed channel (on master it gets Bad_NotConnected).
  • Fail without the fix: with CoreClientUtils.cs and Session.cs from master, all five SelectEndpoint tests that involve an OpenAPI endpoint (6 cases) fail, as does RequestAfterCloseFailsLocallyWithBadSessionIdInvalidAsync, and ReconnectSessionOnAlternateChannelAsync fails with Bad_NotConnected. SelectEndpointKeepsTheFirstBinaryEndpointWithoutTransportProfile and the three other Session tests pass there by design: they pin behaviour that must not change.
  • Opc.Ua.Client.Tests 2743/2743 and Opc.Ua.Sessions.Tests 672 passed / 287 skipped / 0 failed on net10.0 and net8.0 (macOS).
  • Opc.Ua.Client builds for net48, net8.0, net9.0 and net10.0 with 0 warnings; dotnet format (whitespace, style, analyzers) reports nothing on the changed lines; changed lines covered 26/26 (100 %) with the two suites above.
  • Fork CI (full matrix incl. macOS) and CodeQL: CI run 37128535726 (154 jobs) and CodeQL run 37128537502. CodeQL is green; CI is green except Opc.Ua.Interop.Tests LegacyClientExtendedCheckAsync("ComplexTypes") / LoadAndDecodeLegacyComplexTypesAsync ("1 of N structures were not decoded"), which came with Add 1.5.378 <-> 2.0 interop tests; return BadAttributeIdInvalid for unset optional security attributes #4630 and fails the same way on other open pull requests; this change does not touch ComplexTypes.

Notes for review

  • Part 2 changes the code a caller sees for a request on a closed Session: Bad_SessionIdInvalid is raised locally instead of whatever the server or the closed channel answers. Reconnect paths are not affected, because they use ActivateSession or a new Session object. If you prefer to keep part 2 out, its commit can be dropped without affecting part 1.
  • The test framework's own endpoint selection (ClientFixture) is left as it is; the upstream test servers do not need the change.

Related Issues

Checklist

  • I have signed the CLA and read the CONTRIBUTING doc.
  • I have added tests that prove my fix is effective or that my feature works and increased code coverage.
  • I have added all necessary documentation.
  • I have verified that my changes do not introduce (new) build or analyzer warnings.
  • I ran all tests locally using the UA.slnx solution against at least .net framework and .net 10, and all passed. (Affected suites on net10.0 and net8.0 locally on macOS; .NET Framework is covered by the fork CI's Windows net48 legs.)
  • I fixed all failing and flaky tests in the CI pipelines and all CodeQL warnings.
  • I have addressed all PR feedback received.

An HTTPS listener with the REST binding announces its OpenAPI endpoint
(OPC 10000-6 §G.3) with the same URL and message mode None as its binary
endpoint. CoreClientUtils.SelectEndpoint matched on the URL scheme and
the message mode only, so the first of the two in the GetEndpoints
result won, both in the main pass and in the HTTPS fallback for
useSecurity. A caller that connects with the binary transport then got
the REST endpoint: with the default channel bindings the Session could
not be created on it, and with the REST channel registered it ran over
REST instead.

Following OPC 10000-4 §5.5.4 (select by TransportProfileUri), both
passes now skip endpoints with the HTTPS or WSS OpenAPI transport
profile. Endpoints without a TransportProfileUri are selected as before.
A request with a null authenticationToken is a session-less invocation
(OPC 10000-4 §6.3.1). After CloseAsync the Session's token is null, so a
request sent through the same object afterwards, with the channel still
open, reached the server as a session-less invocation. A server that
supports those processed it instead of rejecting it.

Session now records that a session was created (SessionCreated with a
non-null cookie, which OpenAsync now calls through the override). From
then on UpdateRequestHeader fails a request without token locally with
Bad_SessionIdInvalid; CreateSession and ActivateSession are exempt, so
reopening the Session and reactivating it keep working. A Session that
was never created behaves as before.

ClientTest closes the session and then the channel; the read that
follows now fails with Bad_SessionIdInvalid before it reaches the closed
channel, so the test expects that code instead of the channel's.
@marcschier marcschier added the ready Ready to merge once CI Passes label Oct 4, 2026
@marcschier
marcschier requested a balanced review from Copilot October 4, 2026 06:54

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟢 Approval recommended

The fixes are narrowly scoped, consistent with the stated requirements, and comprehensively covered by regression tests.

Review effort: Balanced
Findings: None

What changed in this PR

Prevents invalid OpenAPI endpoint selection and blocks unauthenticated requests from previously closed sessions.

Changes:

  • Excludes HTTPS/WSS OpenAPI profiles during endpoint selection.
  • Rejects tokenless post-close requests with BadSessionIdInvalid.
  • Adds focused unit and integration regression coverage.
File Description
src/​Opc.Ua.Client/​CoreClientUtils.cs Filters OpenAPI endpoints in both selection passes.
src/​Opc.Ua.Client/​Session/​Session.cs Tracks prior session creation and rejects invalid tokenless requests.
tests/​Opc.Ua.Client.Tests/​CoreClientUtilsSelectEndpointTests.cs Covers OpenAPI filtering and compatibility behavior.
tests/​Opc.Ua.Client.Tests/​Session/​SessionTests.cs Covers closed, reopened, and not-yet-created session behavior.
tests/​Opc.Ua.Sessions.Tests/​ClientTest.cs Updates the expected post-close error.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@marcschier
marcschier merged commit 2a619cd into OPCFoundation:master Oct 4, 2026
169 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready Ready to merge once CI Passes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

CoreClientUtils.SelectEndpoint can return the OpenAPI (REST) endpoint, and a closed Session sends requests without authenticationToken

3 participants