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-browserengine, 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.
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:
- Bootstraps the MTE WASM runtime and validates your license.
- Authenticates with
GET /api/mte-relay?frameVersions=2, receiving a client token and the relay's discovery document. - Checks that the relay speaks frame version 2 and that its MTE profile and
Kyber strength match, and fails with
MteRelaySetupErrorif not. - Pairs with
POST /api/mte-pair. - 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
| Requirement | Notes |
|---|---|
| Angular | >=17.0.0 <23.0.0 |
| MTE Relay Server | v5, frame version 2 |
| Engine | mte-relay-browser 5.0.0 or later |
| MTE WASM library | The 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 access | The @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 forng buildto succeed. It is safe: the reference becomes an inert shim that browser code never calls.- Do not apply
externalDependenciesto a configuration used byng serve; the dev server handles the Node builtin itself and breaks if the module is marked external. allowedCommonJsDependenciesonly silences the CommonJS optimization warning and is optional.
Provider Ordering
Both provideHttpClient() and provideMteRelay() bind Angular's HttpBackend
token, and the last binding wins.
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:
| Option | Type and default | Description |
|---|---|---|
companyName | string, required | MTE license company name. |
licenseKey | string, required | MTE license key. |
relayOrigins | (string | RegExp)[], required | Origins 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. |
minPairs | number, engine default 4 | Warm baseline the pair pool refills toward. |
initialPairs | number, engine default 1 | Pairs created when a session starts. |
maxPairs | number, engine default 8 | Hard ceiling on the pair pool. The relay's advertised maxPairsPerClient also caps it. |
sequenceWindow | number | MTE decoder sequence window. The engine uses the relay's advertised value and warns once when this differs. |
timeWindow | number | MTE decoder time window. The engine uses the relay's advertised value. |
httpTimeoutMs | number, engine default 30000 | Relay transport timeout. |
keepAliveIntervalMs | number | Keepalive 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. |
unencryptedHeaders | string[], [] | 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. |
credentials | RequestCredentials, engine default 'include' | Fetch credentials mode for every request the engine makes to the relay. |
warnings | boolean, engine default true | The engine's once per session console warnings. |
ssr | 'passthrough' | 'block', 'passthrough' | Behavior during server side rendering. See Server Side Rendering. |
fallbackTransport | 'xhr' | 'fetch', framework default | Transport for non relay traffic. When omitted, it mirrors the running Angular major's own default backend: XHR through Angular 21, fetch from Angular 22. |
autoRetry | boolean | object, true | Automatic single retry of idempotent requests after the engine recovered the session. See Automatic Retry. |
eagerInit | boolean, false | Warm up the WASM runtime at application startup instead of on the first relayed request. |
streamDiagnostics | engine option | Frame level diagnostics, a debugging aid. Called outside the Angular zone. |
onPoolEvent | (event) => void | Pool 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();
}
}
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:
| Failure | status | .error contains |
|---|---|---|
| Relay generated error (470 to 490, 500, 502, 503) | the relay code | MteRelayError, 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 page | that status | MteRelayTransportError, with status and requestId |
| Response frames that fail to decode, or no answer at all | 0 | MteRelayTransportError, or the browser's network error |
| Setup failure: no frame version 2, or an MTE profile or Kyber strength mismatch | 0 | MteRelaySetupError |
| Pair pool exhausted | 0 | MteRelayCapacityError |
| Body over the relay's advertised limit, refused before upload | 413 | MteRelayRequestTooLargeError, with size and limit |
| Upstream non 2xx | the upstream status | the 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'sFetchBackend. The relay answers them with404unless the path is one of itspass_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 0HttpErrorResponseduring 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
onPoolEventandstreamDiagnosticscallbacks fire outside the zone; wrap state updates inNgZone.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. MteEventSourcedoes not reconnect on its own or replayLast-Event-ID.- MTE Relay v5 servers with frame version 2 only.
Troubleshooting
| Symptom | Likely cause and fix |
|---|---|
Startup error naming provideMteRelay() and HttpBackend | Provider 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 request | The relay does not speak frame version 2, or its MTE profile or Kyber strength differs. The message names both sides. |
Relay error 478 | The relay refuses the engine's frame version. Install mte-relay-browser 5.0.0 or later. |
Every request fails with 473 or 479 | The mte WASM build does not match the relay's library build. Install the build matched to your relay. |
MteRelayCapacityError | Pair 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 tab | The origin did not match relayOrigins (check scheme, host, and port), or the request set MTE_BYPASS. |