Skip to main content
Version: 5.x

MTE Relay Client for Angular

Current SDK release: 5.0.0.

MTE Relay Client for Angular​

The MTE Relay client library for Angular, mte-relay-angular, plugs into Angular's HttpClient as its transport. Instead of asking you to replace every HTTP call, it replaces the transport layer underneath HttpClient, so your existing services, interceptors, and error handling keep working while requests to your MTE Relay server are encrypted end to end.

  • Supports Angular 17 through 22 (peer range >=17.0.0 <23.0.0), standalone and NgModule bootstrap, zone.js and zoneless apps.
  • Runs on the mte-relay-browser engine, which handles the MTE session for you: WASM and license bootstrap, authentication, Kyber pairing, the pair pool, background refills, recovery from relay errors, and keepalive.
Server and engine versions

This client works only with MTE Relay v5 servers, which speak frame version 2. Use the mte-relay-browser engine 5.0.0, which speaks frame version 2. Engines that speak frame version 1 cannot connect to a v5 relay. v4 relays are not supported; see Upgrading from v4.

Quick Start​

1. Install the packages. Add the Eclypses registry for the MTE WASM library to your project's .npmrc, then install:

@eclypses:registry=https://npm.eclypses.com
npm install mte-relay-angular@5.0.0
npm install mte@npm:@eclypses/<your-matched-mte-build>@^4.2.1

2. Add one provider to your bootstrap, after provideHttpClient():

// main.ts
import { provideHttpClient } from '@angular/common/http';
import { bootstrapApplication } from '@angular/platform-browser';
import { provideMteRelay } from 'mte-relay-angular';

bootstrapApplication(AppComponent, {
providers: [
provideHttpClient(),
provideMteRelay({
companyName: 'your-company',
licenseKey: 'your-license-key',
relayOrigins: ['https://relay.example.com'],
}),
],
});

3. Use HttpClient exactly as you already do.

@Injectable({ providedIn: 'root' })
export class UserService {
private http = inject(HttpClient);

getUsers() {
// Encrypted by the relay transport, with no call site changes.
return this.http.get<User[]>('https://relay.example.com/users');
}
}

Requests to a relay origin go out as binary frames (POST / with Content-Type: application/octet-stream), and requests to every other origin behave exactly as before.


Detailed Documentation​

How It Works​

Angular sends every HttpClient request through an interceptor chain that ends in an HttpBackend. provideMteRelay() replaces that backend:

HttpClient
│
▼
your interceptors (unchanged, they see plaintext)
│
▼
MteRelayBackend ──── origin NOT in relayOrigins ───► normal transport
│ (XHR or fetch)
origin in relayOrigins
│
▼
mte-relay-browser engine (frame version 2)
│
▼
POST / (application/octet-stream) to your relay server

Because interceptors run above the backend, your authentication headers, logging, and error mapping interceptors keep working on plaintext requests and decoded responses.

On the first request to a relay origin the engine:

  1. Bootstraps the MTE WASM runtime and validates your license.
  2. Authenticates with GET /api/mte-relay?frameVersions=2, receiving a client token and the relay's discovery document.
  3. Checks that the relay speaks frame version 2 and that its MTE profile and Kyber strength match, and fails with MteRelaySetupError if not.
  4. Pairs with POST /api/mte-pair.
  5. Encrypts each request's method, path, query, headers, and body into frames, and decodes the response back into an Angular HttpResponse. For the nine standard methods the frame also carries a one byte method code in the clear, which the relay checks against the encrypted copy.

After that it sends keepalive calls, refills the pair pool, and recovers from relay errors with the action the relay's error registry names for each code.

Requirements​

RequirementNotes
Angular>=17.0.0 <23.0.0
MTE Relay Serverv5, frame version 2
Enginemte-relay-browser 5.0.0 or later
MTE WASM libraryThe mte client build matched to your relay server's MTE library family. A settings mismatch fails the first request with MteRelaySetupError; a build mismatch fails every request, typically with 473 decode_failed or 479 replay_rejected.
Registry accessThe @eclypses npm scope must resolve to https://npm.eclypses.com

Build Configuration​

The MTE WASM library contains a Node only require("crypto") behind a runtime guard that never runs in browsers, but the Angular production builder still tries to resolve it. Add this to your application's builder options in angular.json:

{
"options": {
"allowedCommonJsDependencies": ["mte"]
},
"configurations": {
"production": {
"externalDependencies": ["crypto"]
}
}
}
  • externalDependencies: ["crypto"] is required for ng build to succeed. It is safe: the reference becomes an inert shim that browser code never calls.
  • Do not apply externalDependencies to a configuration used by ng serve; the dev server handles the Node builtin itself and breaks if the module is marked external.
  • allowedCommonJsDependencies only silences the CommonJS optimization warning and is optional.

Provider Ordering​

Both provideHttpClient() and provideMteRelay() bind Angular's HttpBackend token, and the last binding wins.

warning

provideMteRelay() must be listed after provideHttpClient(). The library checks this at startup and fails with a descriptive error if the order is wrong, so a misconfigured application refuses to start rather than silently sending plaintext.

Once installed, the library owns the transport for all traffic. withFetch() on provideHttpClient() has no effect; use the fallbackTransport option to choose the transport for non relay traffic.

Configuration Reference​

provideMteRelay(config) accepts:

OptionType and defaultDescription
companyNamestring, requiredMTE license company name.
licenseKeystring, requiredMTE license key.
relayOrigins(string | RegExp)[], requiredOrigins running an MTE Relay v5 server. Strings are normalized to their origin. RegExps are tested against the resolved origin. There is deliberately no "relay everything" mode.
defaultEncodeType'MTE' | 'MKE', 'MKE'Encode type for relayed requests.
minPairsnumber, engine default 4Warm baseline the pair pool refills toward.
initialPairsnumber, engine default 1Pairs created when a session starts.
maxPairsnumber, engine default 8Hard ceiling on the pair pool. The relay's advertised maxPairsPerClient also caps it.
sequenceWindownumberMTE decoder sequence window. The engine uses the relay's advertised value and warns once when this differs.
timeWindownumberMTE decoder time window. The engine uses the relay's advertised value.
httpTimeoutMsnumber, engine default 30000Relay transport timeout.
keepAliveIntervalMsnumberKeepalive interval, 60000 to 600000. Without it the engine uses one third of the relay's advertised session timeout, within the same range, which is 600000 for a relay that advertises the default or no session timeout.
unencryptedHeadersstring[], []Header names sent as ordinary HTTP headers instead of inside the encrypted frame. The relay forwards them to the origin only when its forward_plain_headers setting lists them.
credentialsRequestCredentials, engine default 'include'Fetch credentials mode for every request the engine makes to the relay.
warningsboolean, engine default trueThe engine's once per session console warnings.
ssr'passthrough' | 'block', 'passthrough'Behavior during server side rendering. See Server Side Rendering.
fallbackTransport'xhr' | 'fetch', framework defaultTransport for non relay traffic. When omitted, it mirrors the running Angular major's own default backend: XHR through Angular 21, fetch from Angular 22.
autoRetryboolean | object, trueAutomatic single retry of idempotent requests after the engine recovered the session. See Automatic Retry.
eagerInitboolean, falseWarm up the WASM runtime at application startup instead of on the first relayed request.
streamDiagnosticsengine optionFrame level diagnostics, a debugging aid. Called outside the Angular zone.
onPoolEvent(event) => voidPool lifecycle callback. Called outside the Angular zone.

Per-Request Options​

Per request behavior uses HttpContext tokens:

import { HttpContext } from '@angular/common/http';
import {
MTE_BYPASS,
MTE_ENCODE_TYPE,
MTE_RETRY_UNSAFE,
MTE_UNENCRYPTED_HEADERS,
} from 'mte-relay-angular';

// Use standard MTE encoding (instead of the MKE default) for one request:
this.http.post(url, body, {
context: new HttpContext().set(MTE_ENCODE_TYPE, 'MTE'),
});

// Send one header in the clear for one request:
this.http.get(url, {
headers: { 'x-request-id': id },
context: new HttpContext().set(MTE_UNENCRYPTED_HEADERS, ['x-request-id']),
});

// Send one request to a relay origin WITHOUT the relay transport:
this.http.get(url, {
context: new HttpContext().set(MTE_BYPASS, true),
});

// Allow automatic retry for one non idempotent request:
this.http.post(url, body, {
context: new HttpContext().set(MTE_RETRY_UNSAFE, true),
});

Automatic Retry​

When a relay error's registered action replaces the pair or repairs the session (470 to 474, 475 invalid_client_id and invalid_token, 477 eof_before_end, 503 state_store_write_failed), when the engine's own retry of 486 pair_busy fails too, when one pair gets 479 replay_rejected twice in a row, or when an answer is not a frame, the engine recovers first and then surfaces the error with pairReplaced or repaired set. By default this library then retries the request once, for idempotent methods only (GET, HEAD, OPTIONS), on the recovered session.

The engine itself retries once, for any method, only for 486 pair_busy and 503 state_store_unavailable, where nothing ran upstream. Back off actions are never retried.

Mutating methods (POST, PATCH, ...) are not retried automatically. 474 encode_failed happens after the origin ran the request, and 503 state_store_write_failed can, so only your application can judge whether replay is safe.

provideMteRelay({
// ...
// Extend the idempotent list if your APIs treat PUT and DELETE as idempotent:
autoRetry: { idempotentMethods: ['GET', 'HEAD', 'OPTIONS', 'PUT', 'DELETE'] },
// Or disable it:
// autoRetry: false,
});

Use the MTE_RETRY_UNSAFE context token to opt in a single mutating request.

Streaming and Server-Sent Events​

Native EventSource cannot connect through an MTE Relay, because relay traffic is binary POST frames. Two supported paths replace it.

1. HttpClient streaming events. Decoded relay streams, including SSE, surface like stock Angular streaming responses:

this.http
.request('GET', url, {
observe: 'events',
responseType: 'text',
reportProgress: true,
})
.subscribe((event) => {
if (event.type === HttpEventType.DownloadProgress) {
console.log((event as HttpDownloadProgressEvent).partialText);
}
});

2. MteEventSource, an Observable replacement for EventSource with parsed events:

import { MteEventSource } from 'mte-relay-angular';

@Component({ /* ... */ })
export class TickerComponent {
private sse = inject(MteEventSource);

ngOnInit() {
this.subscription = this.sse
.connect('https://relay.example.com/sse/ticker')
.subscribe((event) => {
console.log(event.type, event.data, event.id);
});
}

ngOnDestroy() {
// Unsubscribing closes the stream and aborts the relay request.
this.subscription.unsubscribe();
}
}
Streaming limits

Each active stream reserves one encoder and decoder pair for its whole lifetime. Many concurrent streams can exhaust the pool (maxPairs, default 8) and fail with MteRelayCapacityError, so budget maxPairs accordingly. MteEventSource does not reconnect on its own and does not replay Last-Event-ID; resubscribe to reconnect.

Error Handling​

Every failure surfaces as a standard HttpErrorResponse, so existing error interceptors keep working. Relay specific failures carry a typed error in .error:

Failurestatus.error contains
Relay generated error (470 to 490, 500, 502, 503)the relay codeMteRelayError, with code, reason, action, repaired, pairReplaced, requestId
An answer that is not a frame, under an error status, such as an intermediary's 401, 404, or 502 pagethat statusMteRelayTransportError, with status and requestId
Response frames that fail to decode, or no answer at all0MteRelayTransportError, or the browser's network error
Setup failure: no frame version 2, or an MTE profile or Kyber strength mismatch0MteRelaySetupError
Pair pool exhausted0MteRelayCapacityError
Body over the relay's advertised limit, refused before upload413MteRelayRequestTooLargeError, with size and limit
Upstream non 2xxthe upstream statusthe decoded upstream body, as with stock HttpClient
import { MteRelayCapacityError, MteRelayError, RelayCode } from 'mte-relay-angular';

catchError((err: HttpErrorResponse) => {
if (err.error instanceof MteRelayCapacityError) {
// Pair pool exhausted: back off and retry later.
}
if (err.error instanceof MteRelayError) {
// err.status is the relay code, err.error.reason the registry reason,
// and err.error.action the recovery the registry names for it.
if (err.error.code === RelayCode.UPSTREAM) {
// 502: upstream_response_failed means the origin ran the request.
// upstream_connect_failed means it was unreachable or did not answer
// in time; after a timeout it may still have run the request.
}
}
return throwError(() => err);
});

MteRelayHttpError and MteRelayProtocolError remain exported as the engine's older names for MteRelayError and MteRelayTransportError. requestId is the relay's X-MTE-Relay-Request-Id, which matches correlationId in relay logs. Errors and recovery lists every code.

Server-Side Rendering (SSR)​

The MTE engine runs only in the browser. During server rendering the backend never touches the engine and follows the ssr option instead:

  • 'passthrough' (default): during SSR, relay origin requests go out as plaintext to the same URL through Angular's FetchBackend. The relay answers them with 404 unless the path is one of its pass_through_routes, so only those routes return data during SSR. Include the plaintext hop in your threat model, or use 'block'.
  • 'block': relay origin requests fail fast with a status 0 HttpErrorResponse during SSR. The request runs encrypted when your app issues it again in the browser; the library does not repeat it for you.

Zones and Change Detection​

  • All engine work, including WASM, pairing, keepalive timers, and stream pumps, runs outside the Angular zone, so it never blocks application stability and never triggers extra change detection.
  • Response events are delivered back in the zone you subscribed from, so change detection works in zone.js apps, and zoneless applications work through observable emissions.
  • The onPoolEvent and streamDiagnostics callbacks fire outside the zone; wrap state updates in NgZone.run() if they must trigger change detection.

NgModule-Based Applications​

Standalone bootstrap is not required. For NgModule apps, import the module after your HTTP setup:

@NgModule({
imports: [
BrowserModule,
HttpClientModule,
MteRelayModule.forRoot({
companyName: 'your-company',
licenseKey: 'your-license-key',
relayOrigins: ['https://relay.example.com'],
}),
],
})
export class AppModule {}

Testing Your Application​

Use provideMteRelayTesting() from the mte-relay-angular/testing entry point. It is a pure bypass: relay routing is disabled, HttpBackend is not replaced, and Angular's standard HTTP testing works untouched.

import { provideHttpClient } from '@angular/common/http';
import { provideHttpClientTesting } from '@angular/common/http/testing';
import { provideMteRelayTesting } from 'mte-relay-angular/testing';

TestBed.configureTestingModule({
providers: [
provideHttpClient(),
provideHttpClientTesting(),
provideMteRelayTesting(),
],
});

To test your relay error handling, build realistic errors with the included factories:

import { createMteRelayHttpErrorResponse } from 'mte-relay-angular/testing';

httpSpy.get.and.returnValue(
throwError(() => createMteRelayHttpErrorResponse(470, { pairReplaced: true })),
);

createMteRelayHttpError(code, { reason }) builds an MteRelayError whose action follows from the code and reason exactly as in the engine; the factories refuse codes outside the relay error registry. createMteRelayProtocolError, createMteRelaySetupError, createMteRelayCapacityError, and createMteRelayRequestTooLargeError build the other engine errors.

Known Limitations​

  • No upload progress events for relayed requests. The engine encodes the entire request body before sending it, so no progress exists during the encode phase. Non relay traffic on the 'xhr' passthrough still reports upload progress.
  • MteEventSource does not reconnect on its own or replay Last-Event-ID.
  • MTE Relay v5 servers with frame version 2 only.

Troubleshooting​

SymptomLikely cause and fix
Startup error naming provideMteRelay() and HttpBackendProvider ordering. Move provideMteRelay() after provideHttpClient().
ng build fails with Could not resolve "crypto"Add externalDependencies: ["crypto"] to your production build configuration. See Build Configuration.
MteRelaySetupError on the first requestThe relay does not speak frame version 2, or its MTE profile or Kyber strength differs. The message names both sides.
Relay error 478The relay refuses the engine's frame version. Install mte-relay-browser 5.0.0 or later.
Every request fails with 473 or 479The mte WASM build does not match the relay's library build. Install the build matched to your relay.
MteRelayCapacityErrorPair pool exhausted, usually by many concurrent streams. Raise maxPairs or reduce concurrent streams.
Requests to the relay origin appear as plain JSON in the network tabThe origin did not match relayOrigins (check scheme, host, and port), or the request set MTE_BYPASS.