Files
5899354441 Clarify documentation, migration guidance, and SDK selection (#4823)
# Description

Remove stock phrasing and correct misleading or inconsistent technical
documentation. The initial two passes covered 367 Markdown files,
followed by an interactive review of 169 editorial flags. The U5
interpretation originally deferred in that review has since received the
upstream Mantis answer recorded by #4843; remaining server/spec
follow-ups stay explicit in the CTT log.

- Correct package responsibilities, migration steps, API names,
ownership, protocol descriptions and links. Keep bundled migration
guides synchronized. Preserve historical conclusions as historical
records with separate re-check notes.
- Provide at least two rendered TOC levels throughout `docs/`. Convert
appropriate architecture figures to Mermaid without changing code
examples, transcripts or historical quotations.
- Address the October 19 feedback with concrete setup/migration
instructions, corrected certificate paths and PubSub names, shorter
repeated caveats, consistent XML comments, and a full review of the
Concepts guide. Retain #4839's API corrections.
- Integrate #4841's runtime/performance guidance and review
incoming/recent prose. Preserve dated measurements rather than claiming
new benchmark runs; clarify allocation ratios, startup-only processor
settings, JIT warm-up and the benchmark commit's scope. Missing raw
measurement artifacts and repeat ranges remain tracked by #4842.
- Merge master through `e8c5d800a` in `32c6394b7`. Resolve the CTT log
conflict using the updated Mantis states and OPC answers, retaining
historical framing and re-checks for C28/C32/C43.
- Address the latest naming/index feedback in `adb5bf693`: rename the
guides to `docs/WotCon.md` and `docs/WotNodeSetConversion.md`; move the
provider and DataType-declaration contracts into the conversion guide
and remove `docs/WoT.md`. Rename `samples/DI` to `samples/Di` and
`samples/Isa95` to `samples/ISA95`. Add content-index READMEs to Di,
FluentApi, ISA95, Lds and PubSub; update solution entries, project
references, Docker/CI paths, test fixture paths and documentation links
with exact casing.

**WoT guide scope.** This branch retains the detailed conversion guide
that #4758 had replaced with an anchor stub. Conversion limits, verified
document sets, preservation rules and provider ownership now live
together in `WotNodeSetConversion.md`; binding/runtime guidance remains
in `WotBindings.md` and `WotCon.md`. The design-rationale/test-ledger
prose was shortened, the oversized argument-table cell moved into its
section, and existing anchors preserved.

**SDK policy is a separate, explicit user-directed change.**
`global.json` sets a stable minimum of `10.0.400`, `rollForward:
latestMajor`, and `allowPrerelease: false`. Newer major SDKs are
permitted; CI's explicit SDK pins are unchanged. README, GettingStarted,
DeveloperGuide and Recycling instructions reflect the possible analyzer
differences. The image-contract regression fix accepts compatible stable
SDK images instead of requiring exact equality with the floor.

The latest four rename/index threads are resolved after publication.
Three older threads remain open: universal obsolete-removal timing,
unconditional nonce-strength wording, and a blanket Vision NativeAOT
claim. Existing implementation/evidence constraints are explained in
their inline replies. The two-level TOCs remain as requested.

## Validation

- `git diff --check` and staged diff checks: passed. The untouched
upstream AutoID NodeSet retains its provenance bytes, including two
existing license-header trailing spaces.
- `.\.azurepipelines\validate-migration-plugin.ps1`: passed; 15 bundled
migration documents and manifests synchronized.
- Documentation/path validation: **101** documents retain two rendered
TOC levels (**2,228** entries), with idempotent formatting. **52** new
relative links and their anchors passed exact-case checks. No tracked
references to the old guide/sample paths remain. Both relocated
provider-contract sections are preserved in the destination.
- `dotnet sln UA.slnx list`: passed; shows the renamed Di/ISA95 projects
and the existing FluentApi/Lds/PubSub projects. Updated solution and
pump ProjectReference paths also passed exact-case existence checks.
- Latest focused commands, run from the repository root:

```powershell
# Passed: 61 channel send-order and reconnect tests.
dotnet test tests\Opc.Ua.Core.Tests\Opc.Ua.Core.Tests.csproj -p:CustomTestTarget=net10.0 --filter 'FullyQualifiedName~UaSCBinaryChannelSendOrderTests|FullyQualifiedName~TcpReconnectKeyAgreementTests' --verbosity minimal

# Passed: 5 pump datasheet tests; builds the renamed pump sample.
dotnet test tests\Opc.Ua.Di.Tests\Opc.Ua.Di.Tests.csproj -p:CustomTestTarget=net10.0 --filter 'FullyQualifiedName~PumpDatasheetConformanceTests' --verbosity minimal

# Passed: 4 repository NodeSet path/round-trip tests, including the moved pump models.
dotnet test tests\Opc.Ua.Types.Tests\Opc.Ua.Types.Tests.csproj -p:CustomTestTarget=net10.0 --filter 'FullyQualifiedName~RepositorySourceSurvivesSerializedLinkedPartitionsAsync' --verbosity minimal

# Passed: build, zero warnings/errors.
dotnet build samples\ISA95\MinimalIsa95Server\MinimalIsa95Server.csproj -p:CustomTestTarget=net10.0 -f net10.0 --verbosity minimal

# Passed: dependent sample-test project build, zero warnings/errors.
dotnet build tests\Opc.Ua.WotCon.Samples.Tests\Opc.Ua.WotCon.Samples.Tests.csproj -p:CustomTestTarget=net10.0 -f net10.0 --verbosity minimal
```

Earlier focused integration checks passed 2,620 tests across AutoID,
event queues/scheduling, source generation, MCP/package profiles and
WoT. Both reviewed Mermaid diagrams rendered locally without network
access. These checks are not a full solution run.

The full solution has **not** been rerun for this review. Earlier
`dotnet test UA.slnx -p:CustomTestTarget=net10.0 --verbosity quiet` did
not pass: three AOT projects rejected VSTest and 20 Vision tests failed
during graphics initialization with `OutOfMemoryException`. Remaining
long-running tests were stopped at the user's request. A focused Vision
rerun passed 558 tests and reproduced those 20 failures. The earlier
`dotnet test UA.slnx -p:CustomTestTarget=net48 --verbosity quiet` was
blocked by the former SDK pin and was not retried after the policy
change. The user approved proceeding with those limitations documented;
current remote CI is not claimed green.

## Related Issues

- Follow-up #4842 tracks the requested 2.0.2+ benchmark/tuning and
diagnostic-tool documentation refresh. #4841 supplies documentation and
earlier results, not all newly requested measurements.
- #4758 explains the conversion guide's earlier consolidation; the
current provider contracts are now consolidated into
`WotNodeSetConversion.md`.
- #4839 supplies the reviewed API corrections; #4843 supplies the
updated CTT issue record.
- The original prose cleanup and SDK policy were user-requested without
a source issue. No new runtime architecture is introduced by these
documentation edits.

## Checklist

_Put an `x` in the boxes that apply. You can complete these step by step
after opening the PR._

- [ ] 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.
- [ ] 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.
- [ ] 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.

---------

Co-authored-by: Roman Ettlinger <romanett98@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 10:10:55 +02:00

440 lines
19 KiB
Markdown

# OPC UA REST Binding (Part 6 §G.3 "OpenAPI Mapping")
The OPC UA REST binding exposes the OPC UA service set as ASP.NET
Core **Minimal-API endpoints**, implementing the **OpenAPI Mapping**
defined in [OPC UA Part 6 §G.3](https://reference.opcfoundation.org/specs/OPC-10000-6/g-3)
(v1.05.07).
The binding ships as part of
`OPCFoundation.NetStandard.Opc.Ua.Bindings.Https` (net8+ only;
mounted on the same Kestrel host as the binary and
`application/opcua+uajson` sub-profiles).
## Contents
- [Quick start](#quick-start)
- [Routes (full coverage)](#routes-full-coverage)
- [Service sets and the OpenAPI document](#service-sets-and-the-openapi-document)
- [Encoding negotiation](#encoding-negotiation)
- [Discovery](#discovery)
- [Wire format](#wire-format)
- [Authentication](#authentication)
- [JWT claim projection (built-in)](#jwt-claim-projection-built-in)
- [Hosting modes](#hosting-modes)
- [Long-poll `/publish`](#long-poll-publish)
- [Client integration](#client-integration)
- [Client errors](#client-errors)
- [Compressed responses](#compressed-responses)
- **Server side**: ASP.NET Core Minimal-API endpoints (one `MapPost`
per spec service) — **NativeAOT-compatible**; no MVC reflection, no
`[UnconditionalSuppressMessage]` attributes.
- **Client side**: symmetric `IWebApiClient` in
`OPCFoundation.NetStandard.Opc.Ua.Client` under
`src/Opc.Ua.Client/WebApi/`.
## Quick start
```csharp
using Microsoft.Extensions.DependencyInjection;
using Opc.Ua;
using Opc.Ua.Bindings;
services
.AddOpcUa()
.AddHttpsTransport() // existing binary + opcua+uajson
.AddWebApiTransport() // adds REST routes alongside them
.AddWebApiAnonymousAuth(); // or AddWebApiBearerAuth(...) etc.
```
The REST surface mounts inside the existing `HttpsTransportListener`
Kestrel pipeline, so a single port serves binary,
`application/opcua+uajson`, **and** REST traffic without configuring a
second listener.
A spec-shaped client request:
```bash
curl -X POST https://server:4843/read \
-H 'Content-Type: application/json' \
--data '{
"RequestHeader": { "TimeoutHint": 10000 },
"TimestampsToReturn": 2,
"NodesToRead": [
{ "NodeId": "i=2258", "AttributeId": 13 }
]
}'
```
returns the matching `ReadResponse` body. To send a Verbose JSON request body,
use `Content-Type: application/json; encoding=verbose`. To request a Verbose
JSON response, use `Accept: application/json; encoding=verbose`.
## Routes (full coverage)
The binding implements every service in the spec's
[`opc.ua.openapi.allservices.json`](https://github.com/OPCFoundation/UA-Nodeset/blob/latest/OpenApi/opc.ua.openapi.allservices.json)
document — 28 services across the Discovery, Session, View, Attribute,
Method, MonitoredItem, and Subscription service sets. NodeManagement
(Part 4 §5.8) and Query (§5.10) are **not in scope** — the spec
document deliberately omits them.
| Service set (Part 4) | Routes |
| --- | --- |
| Discovery (§5.5) | `/findservers`, `/getendpoints` |
| Session (§5.7) | `/createsession`, `/activatesession`, `/closesession`, `/cancel` |
| View (§5.9) | `/browse`, `/browsenext`, `/translate`, `/registernodes`, `/unregisternodes` |
| Attribute (§5.11) | `/read`, `/write`, `/historyread`, `/historyupdate` |
| Method (§5.12) | `/call` |
| MonitoredItem (§5.13) | `/createmonitoreditems`, `/modifymonitoreditems`, `/setmonitoringmode`, `/settriggering`, `/deletemonitoreditems` |
| Subscription (§5.14) | `/createsubscription`, `/modifysubscription`, `/setpublishingmode`, `/publish`, `/republish`, `/transfersubscriptions`, `/deletesubscriptions` |
All routes are `POST` with a JSON body holding the matching
`<Service>Request`; the response body is the matching
`<Service>Response`. The route table is the source of truth — see
`WebApiServiceRoutes` in `src/Opc.Ua.Core/Stack/WebApi/`.
## Service sets and the OpenAPI document
`WebApiTransportOptions.ServiceSet` selects the services the binding maps:
| `WebApiServiceSet` | Routes |
| --- | --- |
| `AllServices` (default) | all 28 |
| `Sessionless` | `/read`, `/write`, `/historyread`, `/historyupdate`, `/call`, `/browse`, `/browsenext`, `/translate` (the services a client can call without a session, OPC 10000-4 §6.3) |
`Sessionless` leaves discovery and session management to the binary and
`opcua+uajson` endpoints of the same listener.
The binding can describe the routes it maps as an OpenAPI 3.0 document. The
route is **off by default**; set `OpenApiDocumentPath` to serve the document
of the selected service set with `GET`. The route belongs to the REST
routes and requires the same authentication and authorization as they do
(only `/findservers` and `/getendpoints` are anonymous).
```csharp
services.AddOpcUa()
.AddHttpsTransport()
.AddWebApiTransport(opt =>
{
opt.ServiceSet = WebApiServiceSet.Sessionless;
opt.OpenApiDocumentPath = "/openapi.json"; // default null: not served
opt.OpenApiIncludeSchemas = true; // default false
});
```
The document is **generated from the code**, not copied from the
publication: `WebApiOpenApiGenerator` (package `Opc.Ua.Core.Schema`) builds
it from `WebApiServiceRoutes` and the structure definitions of the request
and response types, so it describes what the binding actually accepts and
sends. Without `OpenApiIncludeSchemas` each body is a JSON object and the
document is about 12 KB; with it the document has one component schema per
request, response and contained structure and enumeration (about 46 KB for
all services). An OpenAPI client generator needs these schemas to emit typed
models. A conformance test compares the generated document with the ones
the OPC Foundation publishes in
[`UA-Nodeset/OpenApi`](https://github.com/OPCFoundation/UA-Nodeset/tree/latest/OpenApi);
the one difference in the contract is the status field of a DataValue
(`Status` per Part 6 Table 42, `StatusCode` in the publication,
[UA-Nodeset#146](https://github.com/OPCFoundation/UA-Nodeset/issues/146)).
Hosts that compose their own pipeline call
`endpoints.MapWebApiEndpoints(options)`; the document route joins the
returned group, so `group.RequireAuthorization()` covers it as well. The
[OPC UA over OpenAPI](OpenApi.md) guide covers generating clients, the JSON
generated clients send, their known issues and how the document is kept
correct.
## Encoding negotiation
OPC UA Part 6 §5.4 defines two JSON flavours: **Compact** (default,
mandatory) and **Verbose**. The REST binding selects between them via
the `encoding` media-type parameter:
| Request `Content-Type` | Server response `Content-Type` |
| --- | --- |
| `application/json` | `application/json; encoding=compact` |
| `application/json; encoding=compact` | `application/json; encoding=compact` |
| `application/json; encoding=verbose` | `application/json; encoding=verbose` |
| `application/json; encoding=verbose` + `Accept: application/json; encoding=compact` | `application/json; encoding=compact` |
The `Accept` header takes precedence for the response encoding; the
`Content-Type` header governs how the inbound body is decoded. Unknown
`encoding` parameter values fall back to **Compact**.
## Discovery
When the binding is registered, the `HttpsServiceHost` emits a
discovery-only twin per `SecurityMode.None` HTTPS-binary endpoint with:
- `TransportProfileUri = http://opcfoundation.org/UA-Profile/Transport/https-uajson-openapi` (OPC Foundation [profile/2338](https://profiles.opcfoundation.org/profile/2338); surfaced via `Profiles.HttpsOpenApiTransport`)
- `SecurityMode = None` (TLS provides transport security)
- `SecurityPolicyUri = None`
- `ServerCertificate` and `UserIdentityTokens` copied from the
companion HTTPS-binary description so clients can pick a compatible
identity at activate time.
Clients discover the binding through `GetEndpoints` and resolve it by
the `TransportProfileUri` — the synthetic registry-key scheme
`opc.https+webapi` (`Utils.UriSchemeOpcHttpsWebApi`) maps the profile
to the `WebApiTransportChannelFactory` while the wire-level URL stays
`https://...`. The fluent shortcut
`ManagedSessionBuilder.UseWebApiEndpoint(url)` constructs the
endpoint description with all of these fields pre-populated.
> The companion WebSocket sub-profile
> `Profiles.WssOpenApiTransport` (OPC Foundation [profile/2339](https://profiles.opcfoundation.org/profile/2339))
> is implemented as a peer to the HTTPS surface:
> `WebApiWssTransportChannel` on the client side,
> `HttpsTransportListener.AcceptWebSocketOpenApiAsync` on the server
> side. Fluent shortcut:
> `ManagedSessionBuilder.UseWssOpenApiEndpoint(url, encoding)`. The bearer
> token is encoded in the selected sub-protocol name
> (`opcua+openapi+<accesstoken>`) because browser WebSocket APIs forbid
> custom HTTP request headers.
> **Security considerations for the bearer-prefix sub-protocol:**
> the WebSocket spec requires the server to echo the selected
> sub-protocol back to the client in the 101 handshake. The bearer
> token therefore appears in plain text in:
> 1. The 101 response on the wire (rejected by the client when the
> URL scheme is not `wss://`; rejected by the server when the
> request is not `IsHttps`).
> 2. The server's Kestrel access log (`Sec-WebSocket-Protocol` header).
> 3. Any HTTP proxy / WAF / load-balancer log on the path.
> Operators **must** redact the `Sec-WebSocket-Protocol` header from
> logs and **should** use short-lived tokens (≤ 60 s TTL) so a
> captured token expires before it can be replayed. The server also
> requires `AddWebApiBearerAuth(...)` to be registered: without a
> bearer validator the listener fail-closed rejects every
> bearer-prefix upgrade rather than echo the token back.
The persistent WSS client multiplexes requests with connection-local wire
handles while preserving caller-visible handles. See [Transports](Transports.md)
for cancellation, late-response routing and connection-lifetime behavior.
## Wire format
The body is the bare `<Service>Request` / `<Service>Response` object —
**no `{UaTypeId, UaBody}` envelope** at the HTTPS layer. Body
serialization uses the stack's existing `JsonEncoder` /
`JsonDecoder` driven by `WebApiBodyCodec`. The encoder shape matches
the spec's component schemas property-for-property; see
`WebApiEncoderConformanceTests` in
`tests/Opc.Ua.Core.Tests/Stack/WebApi/`.
## Authentication
Four auth modes are pluggable via DI:
```csharp
services.AddOpcUa()
.AddHttpsTransport()
.AddWebApiTransport()
.AddWebApiAnonymousAuth() // baseline
.AddWebApiBearerAuth(opt => { opt.Authority = "..."; }) // JWT
.AddWebApiBasicAuth(async (user, password) => { /* ... */ }) // RFC 7617
.AddWebApiMutualTlsAuth(opt => { opt.AllowedCertificateTypes = ... });
```
- **Anonymous** — no `Authorization` header; identity =
`AnonymousIdentityToken`.
- **Bearer JWT** — standard
`Microsoft.AspNetCore.Authentication.JwtBearer` middleware; the JWT
flows into the OPC UA dispatcher through
`ISessionlessIdentityProvider`.
- **HTTP Basic** — in-package
`BasicAuthenticationHandler`. Rejects requests over plain HTTP by
default (`Options.RequireHttps = true`).
- **Mutual TLS** —
`Microsoft.AspNetCore.Authentication.Certificate` against the
client cert presented to Kestrel. Kestrel itself must be configured
to request client certificates (the existing
`HttpsSettings.HttpsMutualTls` flag is the supported path).
For sessionless services (Read/Write/Browse/…) the resolved
`IUserIdentity` is attached to the
`WebApiInvocationContext.Identity` and flows into the
dispatcher's role-based-access pipeline through
`ISessionlessIdentityProvider`. For session-based services the
client is responsible for calling `CreateSession` + `ActivateSession`
with credentials in the request body — the binding does not
double-authenticate.
The `WebApiHttpsStartupContributor` automatically inserts
`app.UseAuthentication()` between `UseRouting()` and `UseEndpoints()`
whenever at least one auth scheme is registered (detected via
`IOptions<AuthenticationOptions>.Schemes`), so the
`HttpContext.User` is populated before the dispatcher invokes the
sessionless identity provider. Bindings registered without any
`AddWebApi*Auth()` call skip the authentication middleware entirely
to preserve the historical anonymous request flow.
The default `ISessionlessIdentityProvider` is intentionally
conservative; register a custom implementation to map richer
claim sets (e.g. JWT role claims → OPC UA roles).
### JWT claim projection (built-in)
For services configured with `Microsoft.AspNetCore.Authentication.JwtBearer`,
the binding ships a built-in identity provider that projects the
authenticated JWT principal onto an OPC UA `IUserIdentity`:
```csharp
services.AddOpcUa()
.AddHttpsTransport()
.AddWebApiTransport()
.AddWebApiBearerAuth(opt => { opt.Authority = "..."; })
.UseJwtClaimIdentityProvider(opt =>
{
opt.SubjectClaim = "sub"; // default
opt.ScopeClaim = "scope"; // default — space- or comma-separated
opt.RolesClaim = "roles"; // default — also picks up ClaimTypes.Role
opt.ReturnAnonymousForUnauthenticated = false; // default
opt.TransformIdentity = (identity, ctx) => identity; // optional hook
});
```
The provider builds a `UserIdentity` carrying the raw bearer token as
an `IssuedIdentityToken` (`Profiles.JwtUserToken`) so server-side
authenticators can re-validate the JWT downstream, and exposes a
projection of the subject / scopes / roles via the static helpers
`JwtClaimSessionlessIdentityProvider.GetSubject(IUserIdentity)`,
`GetScopes(IUserIdentity)`, and `GetRoles(IUserIdentity)` for
role-based-access lookups. Scopes accept either the single
space-separated OAuth 2.0 form or repeated `scope` claims; roles
accept comma/semicolon/space-separated values plus
`ClaimTypes.Role` mappings.
## Hosting modes
```csharp
services.AddWebApiTransport(opt =>
{
opt.HostingMode = WebApiHostingMode.SharedWithHttpsListener; // default
opt.DefaultEncoding = WebApiEncoding.Compact; // spec default
});
```
- **`SharedWithHttpsListener`** (default) — Minimal-API endpoints
mount into the existing `HttpsTransportListener` Kestrel pipeline
via the internal `IHttpsListenerStartupContributor` hook. Single
port for binary / `opcua+uajson` / REST.
## Long-poll `/publish`
`PublishRequest`/`PublishResponse` flow through the same dispatcher
the binary path uses. The server side awaits the next notification
(or the request's `RequestHeader.TimeoutHint`) without blocking a
thread. Kestrel timeouts must exceed the largest expected
`TimeoutHint`:
```csharp
webHostBuilder.UseKestrel(o =>
{
o.Limits.KeepAliveTimeout = TimeSpan.FromMinutes(5);
o.Limits.RequestBodyTimeout = TimeSpan.FromMinutes(5);
});
```
On the client, set `WebApiClientOptions.RequestTimeout =
Timeout.InfiniteTimeSpan` for the publish endpoint so the server-side
`TimeoutHint` governs cancellation.
## Client integration
The OPC Foundation publishes reference WebApi clients for several
ecosystems:
- TypeScript: <https://github.com/OPCFoundation/opcua-webapi-typescript>
- Python: <https://github.com/OPCFoundation/opcua-webapi-python>
- .NET: <https://github.com/OPCFoundation/opcua-webapi-dotnet>
On the .NET stack this binding's REST surface is wired into
`ManagedSession` via the fluent
`ManagedSessionBuilder.UseWebApiEndpoint(url, encoding)` shortcut, so
applications open and operate a WebApi session through the same
`ISession` / subscription / monitored-item API that the binary
transport uses:
```csharp
using Opc.Ua.Client;
await using ManagedSession session = await new ManagedSessionBuilder(telemetry)
.UseWebApiEndpoint("https://server:4843/")
.WithSessionName("Aot-Sample-Client")
.WithUserIdentity(new UserIdentity())
.StartAsync(ct)
.ConfigureAwait(false);
ReadResponse response = await session.ReadAsync(new ReadRequest
{
RequestHeader = new RequestHeader { TimeoutHint = 10000 },
NodesToRead = new ArrayOf<ReadValueId>(/* … */)
}, ct).ConfigureAwait(false);
```
The companion `UseWssOpenApiEndpoint(url)` shortcut binds the same
session model to the WebSocket `opcua+openapi` sub-protocol.
## Client errors
Errors of a processed request come back in the OPC UA response
(`ResponseHeader.ServiceResult`, HTTP 200). An HTTP error status alone
does not tell whether the service ran. The WebApi server of this stack
sends 400 (undecodable body), 401 and 403 (authentication and
authorization), 404 and 405 (no matching route), 413 (request body over
the limit) and 429 (rate limiting) before it invokes the service. For
any other status the outcome is unknown: the server answers 500 when it
cannot encode the response of a service that already ran, and a proxy
or gateway can answer 502, 503 or 504 after the server executed the
request. A status from a proxy or another server implementation tells
only what that component reports. Retry a service that changes state,
such as Write or Call, only when repeating it is harmless.
`WebApiClient` (and therefore `ManagedSession` over
`UseWebApiEndpoint`) reports the HTTP error as a
`ServiceResultException`:
| HTTP | StatusCode |
| --- | --- |
| 400 | `Bad_DecodingError` (the server could not decode the body) |
| 401 | `Bad_IdentityTokenInvalid` when the client sent no credentials, `Bad_UserAccessDenied` when it did |
| 403 | `Bad_UserAccessDenied` |
| 404, 405, 501 | `Bad_ServiceUnsupported` (the route is not mapped) |
| 408, 504 | `Bad_Timeout` |
| 413 | `Bad_RequestTooLarge` |
| 415 | `Bad_DataEncodingUnsupported` |
| 429, 503 | `Bad_ServerTooBusy`, with a `Retry-After` header as `RetryAfterMs=<n>` in `AdditionalInfo` |
| 500 | `Bad_InternalError` |
| any other | `Bad_CommunicationError` |
The message names the HTTP status, the reason phrase and the route, and
the inner exception is an `HttpRequestException` (on .NET 5 and later
with its `StatusCode` set). An elapsed `RequestTimeout` or
`HttpClient.Timeout` is `Bad_RequestTimeout`. A request that fails below
HTTP (no connection, failed TLS handshake) gets the StatusCode the HTTPS
transport channel reports for the same failure, for example
`Bad_NotConnected` for a refused connection. Cancelling the caller's
token still throws `OperationCanceledException`.
## Compressed responses
OPC 10000-6 §7.4.5 lets JSON messages be compressed with gzip (IETF RFC
1952), announced by `Content-Encoding: gzip`. On the client,
`WebApiClientOptions.AcceptCompressedResponses = true` sends
`Accept-Encoding: gzip` (on a shared `HttpClient` per request, without
changing its default headers):
```csharp
using WebApiClient client = WebApiClient.Create(
new Uri("https://server:4843/"),
new WebApiClientOptions { AcceptCompressedResponses = true });
```
A gzip response is inflated whether it was asked for or not. The
`MaxMessageSize` limit applies to the inflated body, a corrupt gzip body
is `Bad_DecodingError`, and a content coding other than gzip or identity
is rejected with `Bad_DecodingError`. The HTTPS binary channel reads its
responses through the same code.