From 3108abc13b5154f947e7d4e16629626e1d7f375c Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Mon, 13 Jul 2026 09:52:07 -0400 Subject: [PATCH] =?UTF-8?q?fix(r2-06):=20GREEN=20adapter=20guard=20?= =?UTF-8?q?=E2=80=94=20TryCreate=20+=20named=20InvalidOperationException?= =?UTF-8?q?=20on=20bad=20ServerHistorian:Endpoint=20(06/S-11)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...erverhistorian-failfast-plan.md.tasks.json | 47 ++++++++++++++----- .../HistorianGatewayClientAdapter.cs | 19 +++++++- 2 files changed, 52 insertions(+), 14 deletions(-) diff --git a/archreview/plans/R2-06-serverhistorian-failfast-plan.md.tasks.json b/archreview/plans/R2-06-serverhistorian-failfast-plan.md.tasks.json index eb478c82..ea61746a 100644 --- a/archreview/plans/R2-06-serverhistorian-failfast-plan.md.tasks.json +++ b/archreview/plans/R2-06-serverhistorian-failfast-plan.md.tasks.json @@ -4,43 +4,53 @@ "tasks": [ { "id": "T1", - "subject": "RED: misconfig repro test — HistorianGatewayClientAdapter.Create with empty/malformed Endpoint must throw a named InvalidOperationException (currently UriFormatException; test MUST fail on current code)", + "subject": "RED: misconfig repro test \u2014 HistorianGatewayClientAdapter.Create with empty/malformed Endpoint must throw a named InvalidOperationException (currently UriFormatException; test MUST fail on current code)", "status": "completed", "blockedBy": [] }, { "id": "T2", - "subject": "GREEN: adapter guard — Uri.TryCreate + InvalidOperationException naming ServerHistorian:Endpoint in HistorianGatewayClientAdapter.Create (defense in depth)", - "status": "pending", - "blockedBy": ["T1"] + "subject": "GREEN: adapter guard \u2014 Uri.TryCreate + InvalidOperationException naming ServerHistorian:Endpoint in HistorianGatewayClientAdapter.Create (defense in depth)", + "status": "completed", + "blockedBy": [ + "T1" + ] }, { "id": "T3", "subject": "RED: ServerHistorianOptionsValidator unit tests (7 cases incl. the AlarmHistorian-enabled/ServerHistorian-disabled corner naming both sections)", "status": "pending", - "blockedBy": ["T1"] + "blockedBy": [ + "T1" + ] }, { "id": "T4", "subject": "GREEN: implement ServerHistorianOptionsValidator (OptionsValidatorBase, ctor IConfiguration, consumed = Enabled || AlarmHistorian:Enabled, fail on empty/non-absolute/non-http(s) Endpoint)", "status": "pending", - "blockedBy": ["T3"] + "blockedBy": [ + "T3" + ] }, { "id": "T5", "subject": "Wiring: AddValidatedOptions in Program.cs hasDriver block + OptionsValidationException wiring test + residual alarm-only-mode startup warning", "status": "pending", - "blockedBy": ["T4"] + "blockedBy": [ + "T4" + ] }, { "id": "T6", "subject": "Warning parity: malformed-endpoint warning in ServerHistorianOptions.Validate() + Runtime.Tests case; commit 1 (S-11) after T1-T6 green", "status": "pending", - "blockedBy": ["T4"] + "blockedBy": [ + "T4" + ] }, { "id": "T7", - "subject": "U-7 pin: GatewayTagProvisionerTests Boolean_definition_carries_explicit_Int1_presence (asserts HistorianDataType.Int1 AND HasDataType — 0.2.0 proto3-optional presence witness; pin, no RED phase)", + "subject": "U-7 pin: GatewayTagProvisionerTests Boolean_definition_carries_explicit_Int1_presence (asserts HistorianDataType.Int1 AND HasDataType \u2014 0.2.0 proto3-optional presence witness; pin, no RED phase)", "status": "pending", "blockedBy": [] }, @@ -48,13 +58,17 @@ "id": "T8", "subject": "U-7 live leg: HISTGW_BOOL_SANDBOX_TAG in GatewayLiveFixture + EnsureTags_boolean_sandbox_provisions_as_Int1 + EnsureTags_type_change_outcome_is_observable retype probe (skip-clean offline; verify all-skip run)", "status": "pending", - "blockedBy": ["T7"] + "blockedBy": [ + "T7" + ] }, { "id": "T9", "subject": "U-7 docs: migration/assessment note (EnsureTags is upsert; pre-bump Float Booleans; three retype hypotheses; failed=N tally observability) in docs/Historian.md + CLAUDE.md HISTGW_BOOL_SANDBOX_TAG recipe update; commit 2 (U-7) after T7-T9", "status": "pending", - "blockedBy": ["T8"] + "blockedBy": [ + "T8" + ] }, { "id": "T10", @@ -66,13 +80,20 @@ "id": "T11", "subject": "Bookkeeping: update archreview/plans/STATUS.md + 06-gateway-integrations.md prior-finding rows for S-11/U-7/C-7 when the work lands", "status": "pending", - "blockedBy": ["T6", "T9", "T10"] + "blockedBy": [ + "T6", + "T9", + "T10" + ] }, { "id": "T12", "subject": "OPERATOR GATE: run Category=LiveIntegration on the VPN against a 0.2.0 gateway (all 6 tests, incl. Boolean EnsureTags + retype probe); record the observed retype policy back into docs/Historian.md + STATUS.md; also discharges 06/U-2's full documented run", "status": "pending", - "blockedBy": ["T9", "T11"] + "blockedBy": [ + "T9", + "T11" + ] } ] } diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway/HistorianGatewayClientAdapter.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway/HistorianGatewayClientAdapter.cs index 251b0fc6..5f429f30 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway/HistorianGatewayClientAdapter.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway/HistorianGatewayClientAdapter.cs @@ -35,14 +35,31 @@ public sealed class HistorianGatewayClientAdapter : IHistorianGatewayClient, IDi /// The bound ServerHistorian configuration (endpoint, key, TLS posture). /// Logger factory threaded into the package client's channel diagnostics. /// A ready-to-use adapter whose underlying channel has not yet dialed the gateway. + /// + /// is empty or not an absolute http(s) URI. + /// Defense in depth (archreview 06/S-11): the Host's ServerHistorianOptionsValidator fails + /// these configs at startup, but any caller that bypasses ValidateOnStart (tests, the live + /// fixture, future non-Host wiring) still gets a named, config-key-carrying error here instead of + /// the raw the bare new Uri(...) ctor throws. + /// public static HistorianGatewayClientAdapter Create(ServerHistorianOptions options, ILoggerFactory loggerFactory) { ArgumentNullException.ThrowIfNull(options); ArgumentNullException.ThrowIfNull(loggerFactory); + // Fail fast with a named, actionable error rather than letting `new Uri("")` throw a raw + // UriFormatException deep in the factory (archreview 06/S-11). The Endpoint is not a secret, + // so it is safe to echo; the ApiKey is never surfaced. + if (!Uri.TryCreate(options.Endpoint, UriKind.Absolute, out var endpointUri) + || (endpointUri.Scheme != Uri.UriSchemeHttp && endpointUri.Scheme != Uri.UriSchemeHttps)) + { + throw new InvalidOperationException( + $"ServerHistorian:Endpoint must be an absolute http(s) URI (e.g. https://host:5222); got '{options.Endpoint}'."); + } + var clientOptions = new HistorianGatewayClientOptions { - Endpoint = new Uri(options.Endpoint), + Endpoint = endpointUri, ApiKey = options.ApiKey, UseTls = options.UseTls, CaCertificatePath = options.CaCertificatePath,