Files
romanett 052576115d Fix ManagedSession.CreateAsync returning an unconnected session when the initial connect fails (#4350)
# Description

When the **initial** connect of a `ManagedSession` failed, `CreateAsync`
did not throw. It returned a session with no inner `Session` behind it,
so every subsequent member threw `BadNotConnected` and the real error —
`BadCertificateUntrusted` in the reported case — was swallowed.

**Mechanism.** A failed initial connect moves the state machine
`Connecting` → `Reconnecting`, which calls
`ManagedSession.HandleReconnectAsync`. That method skipped its entire
body when `m_session` was `null` — exactly the state after a failed
initial connect — and fell through to `return ServiceResult.Good`. The
state machine read that as a successful reconnect, transitioned to
`Connected`, released the `CreateAsync` waiter, and handed back a
session that was never connected. The logs show it plainly: a
"Reconnected" line immediately after "Reconnecting" with no service call
in between.

The terminal path was broken as well: once the reconnect policy **and**
the failover were exhausted the machine settled in `Disconnected` and
nothing ever set the connected event, so `WaitForConnectedAsync` — and
with it `CreateAsync` — waited forever rather than surfacing the
failure.

**Fix.**

- `ManagedSession.HandleReconnectAsync` reads the inner session once
under the service lock and, when there is none, runs a full connect
(`HandleConnectAsync`) instead of reporting success. The reconnect
policy now genuinely retries the initial connect.
- `ConnectionStateMachine` tracks the error of the last failed
connect/reconnect attempt and sets a new *settled* event when the
connect cycle ends — either by connecting or by giving up
(`Disconnected` after a failed failover, or `Closing`/`Closed`).
`WaitForConnectedAsync` waits on that and throws a
`ServiceResultException` carrying the last attempt's error when the
connection was abandoned. A failover failure only supplies that error if
no attempt recorded one, so the `BadNotSupported` returned by a failover
with no redundant server configured never masks the actual cause.

`CreateAsync` can no longer hand back a session that reports connected
with `m_session == null`.

## Behavior change worth reviewing

A permanently failing initial connect no longer returns instantly:
`CreateAsync` retries under the configured policy and throws only once
`MaxRetries` / `MaxTotalReconnectTime` and failover are exhausted
(default budget 5 minutes). Callers wanting fail-fast cap the policy or
pass a cancellation token — documented in `docs/Sessions.md`.

No status code is treated as non-retryable. Classifying e.g.
`BadCertificateUntrusted` as fatal would break the very scenario in the
issue, where the connect can start succeeding once an operator trusts
the certificate.

## Tests

-
`ConnectionStateMachineTests.WaitForConnectedAsyncThrowsOriginalErrorWhenConnectGivesUp`
— the original error survives the failover's `BadNotSupported`.
-
`ConnectionStateMachineTests.WaitForConnectedAsyncThrowsWhenClosedWhileConnecting`
— a close while connecting releases the waiter instead of hanging.
-
`ManagedSessionTests.CreateAsyncThrowsInitialConnectErrorInsteadOfReturningUnconnectedSession`
— asserts the original status propagates out of `CreateAsync` and that
the connect was attempted more than once.
-
`WssOpenApiIntegrationTests.ManagedSessionOverWssOpenApiBearerRejectedWhenNoBearerAuthRegistered`
relied on the old behavior (it asserted `Connected == false` on the
session returned by a failed connect). Rewritten to bound the retry
policy and assert that `ConnectAsync` throws; the fail-closed HTTP 401
property it guards is unchanged.

Suites run locally on net10.0, all green: `Opc.Ua.Client.Tests` (2155),
`Opc.Ua.Sessions.Tests` (775), `Opc.Ua.Gds.Tests` (1096),
`Opc.Ua.Client.ComplexTypes.Tests` (2773),
`Opc.Ua.Redundancy.Client.Tests` (122). No new build or analyzer
warnings.

## Related Issues

- Fixes #4347

## Checklist

- [x] I have signed the
[CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf)
and read the
[CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md)
doc.
- [x] I have added tests that prove my fix is effective or that my
feature works and increased code coverage.
- [x] I have added all necessary documentation.
- [x] 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.
- [ ] I fixed **all** failing and flaky tests in the CI pipelines and
**all** CodeQL warnings.
- [ ] I have addressed **all** PR feedback received.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 12:02:02 +02:00
..