php-opcua/opcua-client-ext-transport-https — v4.4.0 skill
An optional transport extension of php-opcua/opcua-client implementing the OPC UA HTTPS mappings (Part 6 §7.4). Each UA service message is exchanged as a single HTTPS POST; TLS is the secure channel, so the OPC UA OpenSecureChannel handshake is skipped entirely. The core opcua-client only adds two contract methods on ClientTransportInterface; everything else (transport, encoding strategies, HTTP client, events, exceptions) lives here under PhpOpcua\Client\ExtTransportHttps\*.
When to use this skill
Activate when any of these apply:
- An endpoint URL starts with
opc.https:// (or a plain https:// OPC UA endpoint)
- The task is to run OPC UA over 443 / through a corporate proxy / firewall where
opc.tcp:// is blocked
- The HTTPS binary (
application/octet-stream) or JSON (application/opcua+uajson) mapping is mentioned (Part 6 §7.4.4 / §7.4.5)
- A
HttpsTransport, HttpsEncodingStrategy, BinaryHttpsEncoding, JsonHttpsEncoding, CurlHttpClient, or ServiceCodecInterface appears in code
- Someone is wiring a non-TCP transport into
ClientBuilder::setTransport()
Do NOT activate for: ordinary opc.tcp:// connections (core opcua-client), Reverse Connect (opcua-client-ext-reverse-connect), or generic HTTP/REST work unrelated to OPC UA.
The 60-second mental model
ClientBuilder->setTransport($httpsTransport)->connect('opc.https://…')
│
▼ core Client::connect() pipeline (HEL → ACK → [OPN skipped] → CreateSession → …)
HttpsTransport (implements ClientTransportInterface)
│ send(HEL) → encoding->fakeAcknowledge() → buffers a synthetic ACK (no network)
│ send(MSG) → encoding->encodeRequest() → HTTP POST body
│ │
│ ▼
│ HttpClientInterface::post() ── one POST per UA message
│ (CurlHttpClient, TLS here)
│ │
│ receive() ← encoding->decodeResponse() ← HTTP response body
▼
HttpsEncodingStrategy: BinaryHttpsEncoding (§7.4.4) | JsonHttpsEncoding (§7.4.5)
Five things to know:
- One POST per UA message. The transport is stateless at the OPC UA layer:
connect() is a no-op, every MSG/CLO frame becomes an HTTPS POST, the response body is re-framed for the core decoder.
- TLS is the secure channel.
HttpsTransport::isSecureChannelExternal() returns true, so the core skips OpenSecureChannel. You connect with OPC UA SecurityPolicy::None / SecurityMode::None — security lives in the TLS layer of CurlHttpClient, not in OPC UA message security.
- HEL/ACK is synthesised locally. Part 6 §7.4 does not carry the UA-TCP handshake on the wire. The encoding strategy's
fakeAcknowledge() builds the ACK from the client's HEL with no network traffic, so the unchanged core pipeline still runs HEL → ACK → CreateSession.
- The encoding strategy is pluggable.
BinaryHttpsEncoding is production-ready for any service. JsonHttpsEncoding is a reference that currently ships only the GetEndpoints codec — see the limits below.
- TLS config goes on the HTTP client, not OPC UA. Certificate verification, CA bundle, and mutual-TLS client cert/key are
CurlHttpClient constructor options.
Quick start (binary, the production path)
use PhpOpcua\Client\ClientBuilder;
use PhpOpcua\Client\ExtTransportHttps\HttpsTransport;
use PhpOpcua\Client\ExtTransportHttps\Encoding\BinaryHttpsEncoding;
use PhpOpcua\Client\ExtTransportHttps\Http\CurlHttpClient;
use PhpOpcua\Client\Security\SecurityMode;
use PhpOpcua\Client\Security\SecurityPolicy;
$transport = new HttpsTransport(
httpClient: new CurlHttpClient(verifyTls: true, caBundle: '/etc/ssl/certs/ca-bundle.crt'),
encoding: new BinaryHttpsEncoding(),
endpointUrl: 'opc.https://server.example:443/UA/',
timeoutSeconds: 10.0,
);
$client = (new ClientBuilder())
->setSecurityPolicy(SecurityPolicy::None) // TLS is the secure channel
->setSecurityMode(SecurityMode::None)
->setTransport($transport)
->connect('opc.https://server.example:443/UA/');
try {
$state = $client->read('i=2259')->getValue(); // Server.ServerStatus.State
} finally {
$client->disconnect();
}
When to load deeper references
| If the task involves... |
Read |
| How the transport works: POST-per-message, the HEL/ACK synthesis, the 24-byte frame strip, external secure channel |
references/ARCHITECTURE.md |
| Choosing/configuring an encoding: binary vs JSON, content types, the codec registry, JSON's GetEndpoints-only limit |
references/ENCODINGS.md |
The HTTP layer: HttpClientInterface, CurlHttpClient options, connection reuse, writing a custom client |
references/HTTP-CLIENT.md |
TLS trust, certificate verification, mutual TLS, why OPC UA security is None |
references/SECURITY.md |
Unit testing with InMemoryHttpClient, the JSON fixtures, the dotnet fixture generator, the integration E2E |
references/TESTING.md |
| Debugging errors / unexpected behaviour |
references/PITFALLS.md |
| A complete working example (binary connect, proxy, custom client, mTLS, JSON GetEndpoints) |
assets/recipes.md |
Public API surface (must-know)
All classes are in PhpOpcua\Client\ExtTransportHttps.
| Class / interface |
Role |
HttpsTransport |
implements ClientTransportInterface. Ctor: (HttpClientInterface $httpClient, HttpsEncodingStrategy $encoding, string $endpointUrl, float $timeoutSeconds = 30.0, ?LoggerInterface $logger = null, ?EventDispatcherInterface $dispatcher = null). Accepts opc.https:// or https:// (normalises opc.https:// → https://). isSecureChannelExternal() → true. |
Encoding\HttpsEncodingStrategy |
Interface: contentType(), acceptHeader(), encodeRequest(string $uaTcpFrame), decodeResponse(string $httpBody), fakeAcknowledge(string $helFrame). |
Encoding\BinaryHttpsEncoding |
§7.4.4. CONTENT_TYPE = 'application/octet-stream'. Ctor: (int $negotiatedMaxMessageSize = 16*1024*1024, int $negotiatedMaxChunkCount = 0). Production-ready for any service. |
Encoding\JsonHttpsEncoding |
§7.4.5. CONTENT_TYPE = 'application/opcua+uajson'. Ctor: (int $negotiatedMaxMessageSize = 16MB, int $negotiatedMaxChunkCount = 0, JsonEncoder $encoder = new, JsonDecoder $decoder = new). register(ServiceCodecInterface $codec). Ships GetEndpointsCodec by default; other services raise UnsupportedEncodingException. |
Encoding\Json\Service\ServiceCodecInterface |
binaryRequestTypeId(), jsonRequestTypeId(), binaryResponseTypeId(), jsonResponseTypeId(), encodeRequestBody(BinaryDecoder): array, decodeResponseBody(array): string. |
Encoding\Json\Service\GetEndpointsCodec |
The one shipped codec. Binary req TypeId 428, JSON req 426, binary resp 431, JSON resp 429. |
Encoding\Json\JsonEncoder / JsonDecoder |
Reversible-mode codecs for NodeId, Variant, DataValue, StatusCode, DateTime. |
Http\HttpClientInterface |
post(HttpRequest $request, float $timeoutSeconds): HttpResponse, close(). |
Http\CurlHttpClient |
Default impl (ext-curl). Ctor: (bool $verifyTls = true, ?string $caBundle = null, ?string $clientCertPath = null, ?string $clientKeyPath = null, ?string $clientKeyPassword = null, array $extraCurlOptions = []). Reuses one cURL handle (keep-alive + TLS resumption). |
Http\HttpRequest |
final readonly: url, body, contentType, acceptHeader, array $extraHeaders = []. |
Http\HttpResponse |
final readonly: statusCode, body, array $headers = []. isSuccessful() = 2xx. |
Events (PSR-14) — dispatched only when a dispatcher is supplied to HttpsTransport
| Event |
Payload |
Event\HttpsRequestSent |
string $url, string $contentType, int $bodyLength |
Event\HttpsResponseReceived |
string $url, int $statusCode, int $bodyLength |
Event\HttpsRequestFailed |
string $url, int $statusCode, Throwable $cause |
Exceptions — all extend Exception\HttpsTransportException (which extends RuntimeException)
| Exception |
Meaning |
HttpsTransportException |
Base; also raised on an invalid endpoint URL and on out-of-order receive(). |
HttpsRequestException |
Network / TLS / connect-layer failure (no HTTP response available). |
HttpsStatusException |
Non-2xx HTTP response. Ctor (string $message, int $statusCode, string $responseBody = ''); exposes public readonly int $statusCode, public readonly string $responseBody. |
EncodingException |
Encode/decode failure (bad frame, empty body, malformed HEL/ACK). |
UnsupportedEncodingException |
JSON: no codec registered for a service, or a non-ns=0-numeric TypeId. |
Idiomatic patterns AI agents should follow
- Connect with OPC UA security
None over HTTPS. TLS is the secure channel; set SecurityPolicy::None + SecurityMode::None and put trust/encryption config on CurlHttpClient. Mixing in an OPC UA security policy is wrong here.
- Use
BinaryHttpsEncoding unless you specifically need JSON. Binary is production-ready and works against real servers; JSON currently covers only GetEndpoints and no mainstream server implements §7.4.5 end-to-end.
- Put TLS settings on the HTTP client.
verifyTls, caBundle, and mutual-TLS clientCertPath/clientKeyPath/clientKeyPassword are CurlHttpClient ctor args — not ClientBuilder options.
verifyTls: false is test-only. Never disable certificate verification against a real server.
- Reuse one
CurlHttpClient/transport per client. The cURL handle is kept alive across POSTs for keep-alive and TLS resumption; don't reconstruct per request.
- Pass
opc.https:// (or https://) consistently to both the transport and connect(). The transport normalises opc.https:// → https:// internally for cURL.
- Observability via PSR-14 / PSR-3 — pass a dispatcher and/or logger to
HttpsTransport; events carry only URL + sizes/status (no bodies). Dispatcher exceptions are swallowed and logged, never propagated.
- To add a JSON service, implement
ServiceCodecInterface and register() it — don't expect arbitrary services to work over JSON out of the box.
Common pitfalls (read before generating code)
- Setting an OPC UA
SecurityPolicy/SecurityMode other than None over HTTPS — the secure channel is external (TLS); the core skips OPN.
- Expecting JSON to handle any service — only
GetEndpoints ships; everything else raises UnsupportedEncodingException. And no common production server speaks §7.4.5 anyway.
- Disabling
verifyTls against a real endpoint.
- Putting CA/cert config on
ClientBuilder instead of CurlHttpClient.
- Treating a non-2xx response as a transport failure — that's a
HttpsStatusException (you get the status + body), distinct from HttpsRequestException (no response at all).
Full catalog in references/PITFALLS.md.
Related packages in the php-opcua ecosystem
opcua-client — the core client this transport plugs into via ClientBuilder::setTransport(). Load its skill for read/write/browse/subscribe once connected.
opcua-client-ext-reverse-connect — sibling extension (server-initiated opc.tcp:// via ReverseHello) on the same ClientTransportInterface family.
uanetstandard-test-suite — Docker test servers; the opcua-https-binary service (port 4852, v1.5.0+) backs the integration E2E.
1---2name: opcua-client-ext-transport-https3description: Connect a PHP OPC UA client over opc.https:// (OPC UA Part 6 §7.4) using php-opcua/opcua-client-ext-transport-https v4.4.0 — a ClientTransportInterface that exchanges each UA message as one HTTPS POST, with TLS acting as the secure channel (no OpenSecureChannel). Ships a Binary encoding (§7.4.4, production-ready) and a JSON encoding (§7.4.5, GetEndpoints-only reference). Use this skill whenever a task involves opc.https://, opc.wss://-style HTTPS OPC UA transport, HTTPS binary/JSON mappings, corporate-proxy/firewall-friendly OPC UA over 443, or wiring a custom transport into opcua-client.4license: MIT5---67# php-opcua/opcua-client-ext-transport-https — v4.4.0 skill89An optional transport extension of [`php-opcua/opcua-client`](https://github.com/php-opcua/opcua-client) implementing the OPC UA **HTTPS mappings (Part 6 §7.4)**. Each UA service message is exchanged as a single HTTPS `POST`; **TLS is the secure channel**, so the OPC UA `OpenSecureChannel` handshake is skipped entirely. The core `opcua-client` only adds two contract methods on `ClientTransportInterface`; everything else (transport, encoding strategies, HTTP client, events, exceptions) lives here under `PhpOpcua\Client\ExtTransportHttps\*`.1011## When to use this skill1213Activate when any of these apply:1415- An endpoint URL starts with **`opc.https://`** (or a plain `https://` OPC UA endpoint)16- The task is to run OPC UA **over 443 / through a corporate proxy / firewall** where `opc.tcp://` is blocked17- The HTTPS **binary** (`application/octet-stream`) or **JSON** (`application/opcua+uajson`) mapping is mentioned (Part 6 §7.4.4 / §7.4.5)18- A `HttpsTransport`, `HttpsEncodingStrategy`, `BinaryHttpsEncoding`, `JsonHttpsEncoding`, `CurlHttpClient`, or `ServiceCodecInterface` appears in code19- Someone is wiring a non-TCP transport into `ClientBuilder::setTransport()`2021Do NOT activate for: ordinary `opc.tcp://` connections (core `opcua-client`), Reverse Connect (`opcua-client-ext-reverse-connect`), or generic HTTP/REST work unrelated to OPC UA.2223## The 60-second mental model2425```26ClientBuilder->setTransport($httpsTransport)->connect('opc.https://…')27 │28 ▼ core Client::connect() pipeline (HEL → ACK → [OPN skipped] → CreateSession → …)29HttpsTransport (implements ClientTransportInterface)30 │ send(HEL) → encoding->fakeAcknowledge() → buffers a synthetic ACK (no network)31 │ send(MSG) → encoding->encodeRequest() → HTTP POST body32 │ │33 │ ▼34 │ HttpClientInterface::post() ── one POST per UA message35 │ (CurlHttpClient, TLS here)36 │ │37 │ receive() ← encoding->decodeResponse() ← HTTP response body38 ▼39 HttpsEncodingStrategy: BinaryHttpsEncoding (§7.4.4) | JsonHttpsEncoding (§7.4.5)40```4142Five things to know:43441. **One POST per UA message.** The transport is stateless at the OPC UA layer: `connect()` is a no-op, every `MSG`/`CLO` frame becomes an HTTPS POST, the response body is re-framed for the core decoder.452. **TLS is the secure channel.** `HttpsTransport::isSecureChannelExternal()` returns `true`, so the core skips `OpenSecureChannel`. You connect with OPC UA **`SecurityPolicy::None` / `SecurityMode::None`** — security lives in the TLS layer of `CurlHttpClient`, not in OPC UA message security.463. **HEL/ACK is synthesised locally.** Part 6 §7.4 does not carry the UA-TCP handshake on the wire. The encoding strategy's `fakeAcknowledge()` builds the ACK from the client's HEL with **no network traffic**, so the unchanged core pipeline still runs HEL → ACK → CreateSession.474. **The encoding strategy is pluggable.** `BinaryHttpsEncoding` is production-ready for any service. `JsonHttpsEncoding` is a reference that currently ships **only the `GetEndpoints` codec** — see the limits below.485. **TLS config goes on the HTTP client, not OPC UA.** Certificate verification, CA bundle, and mutual-TLS client cert/key are `CurlHttpClient` constructor options.4950## Quick start (binary, the production path)5152```php53use PhpOpcua\Client\ClientBuilder;54use PhpOpcua\Client\ExtTransportHttps\HttpsTransport;55use PhpOpcua\Client\ExtTransportHttps\Encoding\BinaryHttpsEncoding;56use PhpOpcua\Client\ExtTransportHttps\Http\CurlHttpClient;57use PhpOpcua\Client\Security\SecurityMode;58use PhpOpcua\Client\Security\SecurityPolicy;5960$transport = new HttpsTransport(61 httpClient: new CurlHttpClient(verifyTls: true, caBundle: '/etc/ssl/certs/ca-bundle.crt'),62 encoding: new BinaryHttpsEncoding(),63 endpointUrl: 'opc.https://server.example:443/UA/',64 timeoutSeconds: 10.0,65);6667$client = (new ClientBuilder())68 ->setSecurityPolicy(SecurityPolicy::None) // TLS is the secure channel69 ->setSecurityMode(SecurityMode::None)70 ->setTransport($transport)71 ->connect('opc.https://server.example:443/UA/');7273try {74 $state = $client->read('i=2259')->getValue(); // Server.ServerStatus.State75} finally {76 $client->disconnect();77}78```7980## When to load deeper references8182| If the task involves... | Read |83| --- | --- |84| How the transport works: POST-per-message, the HEL/ACK synthesis, the 24-byte frame strip, external secure channel | [`references/ARCHITECTURE.md`](references/ARCHITECTURE.md) |85| Choosing/configuring an encoding: binary vs JSON, content types, the codec registry, JSON's GetEndpoints-only limit | [`references/ENCODINGS.md`](references/ENCODINGS.md) |86| The HTTP layer: `HttpClientInterface`, `CurlHttpClient` options, connection reuse, writing a custom client | [`references/HTTP-CLIENT.md`](references/HTTP-CLIENT.md) |87| TLS trust, certificate verification, mutual TLS, why OPC UA security is `None` | [`references/SECURITY.md`](references/SECURITY.md) |88| Unit testing with `InMemoryHttpClient`, the JSON fixtures, the dotnet fixture generator, the integration E2E | [`references/TESTING.md`](references/TESTING.md) |89| Debugging errors / unexpected behaviour | [`references/PITFALLS.md`](references/PITFALLS.md) |90| A complete working example (binary connect, proxy, custom client, mTLS, JSON GetEndpoints) | [`assets/recipes.md`](assets/recipes.md) |9192## Public API surface (must-know)9394All classes are in `PhpOpcua\Client\ExtTransportHttps`.9596| Class / interface | Role |97| --- | --- |98| `HttpsTransport` | `implements ClientTransportInterface`. Ctor: `(HttpClientInterface $httpClient, HttpsEncodingStrategy $encoding, string $endpointUrl, float $timeoutSeconds = 30.0, ?LoggerInterface $logger = null, ?EventDispatcherInterface $dispatcher = null)`. Accepts `opc.https://` or `https://` (normalises `opc.https://` → `https://`). `isSecureChannelExternal()` → `true`. |99| `Encoding\HttpsEncodingStrategy` | Interface: `contentType()`, `acceptHeader()`, `encodeRequest(string $uaTcpFrame)`, `decodeResponse(string $httpBody)`, `fakeAcknowledge(string $helFrame)`. |100| `Encoding\BinaryHttpsEncoding` | §7.4.4. `CONTENT_TYPE = 'application/octet-stream'`. Ctor: `(int $negotiatedMaxMessageSize = 16*1024*1024, int $negotiatedMaxChunkCount = 0)`. Production-ready for any service. |101| `Encoding\JsonHttpsEncoding` | §7.4.5. `CONTENT_TYPE = 'application/opcua+uajson'`. Ctor: `(int $negotiatedMaxMessageSize = 16MB, int $negotiatedMaxChunkCount = 0, JsonEncoder $encoder = new, JsonDecoder $decoder = new)`. `register(ServiceCodecInterface $codec)`. Ships `GetEndpointsCodec` by default; other services raise `UnsupportedEncodingException`. |102| `Encoding\Json\Service\ServiceCodecInterface` | `binaryRequestTypeId()`, `jsonRequestTypeId()`, `binaryResponseTypeId()`, `jsonResponseTypeId()`, `encodeRequestBody(BinaryDecoder): array`, `decodeResponseBody(array): string`. |103| `Encoding\Json\Service\GetEndpointsCodec` | The one shipped codec. Binary req TypeId `428`, JSON req `426`, binary resp `431`, JSON resp `429`. |104| `Encoding\Json\JsonEncoder` / `JsonDecoder` | Reversible-mode codecs for `NodeId`, `Variant`, `DataValue`, `StatusCode`, `DateTime`. |105| `Http\HttpClientInterface` | `post(HttpRequest $request, float $timeoutSeconds): HttpResponse`, `close()`. |106| `Http\CurlHttpClient` | Default impl (ext-curl). Ctor: `(bool $verifyTls = true, ?string $caBundle = null, ?string $clientCertPath = null, ?string $clientKeyPath = null, ?string $clientKeyPassword = null, array $extraCurlOptions = [])`. Reuses one cURL handle (keep-alive + TLS resumption). |107| `Http\HttpRequest` | `final readonly`: `url`, `body`, `contentType`, `acceptHeader`, `array $extraHeaders = []`. |108| `Http\HttpResponse` | `final readonly`: `statusCode`, `body`, `array $headers = []`. `isSuccessful()` = 2xx. |109110### Events (PSR-14) — dispatched only when a dispatcher is supplied to `HttpsTransport`111112| Event | Payload |113| --- | --- |114| `Event\HttpsRequestSent` | `string $url`, `string $contentType`, `int $bodyLength` |115| `Event\HttpsResponseReceived` | `string $url`, `int $statusCode`, `int $bodyLength` |116| `Event\HttpsRequestFailed` | `string $url`, `int $statusCode`, `Throwable $cause` |117118### Exceptions — all extend `Exception\HttpsTransportException` (which extends `RuntimeException`)119120| Exception | Meaning |121| --- | --- |122| `HttpsTransportException` | Base; also raised on an invalid endpoint URL and on out-of-order `receive()`. |123| `HttpsRequestException` | Network / TLS / connect-layer failure (no HTTP response available). |124| `HttpsStatusException` | Non-2xx HTTP response. Ctor `(string $message, int $statusCode, string $responseBody = '')`; exposes `public readonly int $statusCode`, `public readonly string $responseBody`. |125| `EncodingException` | Encode/decode failure (bad frame, empty body, malformed HEL/ACK). |126| `UnsupportedEncodingException` | JSON: no codec registered for a service, or a non-`ns=0`-numeric TypeId. |127128## Idiomatic patterns AI agents should follow1291301. **Connect with OPC UA security `None` over HTTPS.** TLS is the secure channel; set `SecurityPolicy::None` + `SecurityMode::None` and put trust/encryption config on `CurlHttpClient`. Mixing in an OPC UA security policy is wrong here.1312. **Use `BinaryHttpsEncoding` unless you specifically need JSON.** Binary is production-ready and works against real servers; JSON currently covers only `GetEndpoints` and no mainstream server implements §7.4.5 end-to-end.1323. **Put TLS settings on the HTTP client.** `verifyTls`, `caBundle`, and mutual-TLS `clientCertPath`/`clientKeyPath`/`clientKeyPassword` are `CurlHttpClient` ctor args — not `ClientBuilder` options.1334. **`verifyTls: false` is test-only.** Never disable certificate verification against a real server.1345. **Reuse one `CurlHttpClient`/transport per client.** The cURL handle is kept alive across POSTs for keep-alive and TLS resumption; don't reconstruct per request.1356. **Pass `opc.https://` (or `https://`) consistently** to both the transport and `connect()`. The transport normalises `opc.https://` → `https://` internally for cURL.1367. **Observability via PSR-14 / PSR-3** — pass a dispatcher and/or logger to `HttpsTransport`; events carry only URL + sizes/status (no bodies). Dispatcher exceptions are swallowed and logged, never propagated.1378. **To add a JSON service, implement `ServiceCodecInterface` and `register()` it** — don't expect arbitrary services to work over JSON out of the box.138139## Common pitfalls (read before generating code)140141- Setting an OPC UA `SecurityPolicy`/`SecurityMode` other than `None` over HTTPS — the secure channel is external (TLS); the core skips OPN.142- Expecting JSON to handle any service — only `GetEndpoints` ships; everything else raises `UnsupportedEncodingException`. And no common production server speaks §7.4.5 anyway.143- Disabling `verifyTls` against a real endpoint.144- Putting CA/cert config on `ClientBuilder` instead of `CurlHttpClient`.145- Treating a non-2xx response as a transport failure — that's a `HttpsStatusException` (you get the status + body), distinct from `HttpsRequestException` (no response at all).146147Full catalog in [`references/PITFALLS.md`](references/PITFALLS.md).148149## Related packages in the php-opcua ecosystem150151- **`opcua-client`** — the core client this transport plugs into via `ClientBuilder::setTransport()`. Load its skill for read/write/browse/subscribe once connected.152- **`opcua-client-ext-reverse-connect`** — sibling extension (server-initiated `opc.tcp://` via ReverseHello) on the same `ClientTransportInterface` family.153- **`uanetstandard-test-suite`** — Docker test servers; the `opcua-https-binary` service (port 4852, v1.5.0+) backs the integration E2E.