Add Brotli-first API compression for sidecar and nginx

This commit is contained in:
Elie Habib
2026-02-20 08:41:22 +04:00
parent bf2c0b1598
commit 2b7b35efd8
4 changed files with 151 additions and 8 deletions

View File

@@ -860,7 +860,7 @@ All three variants run on three platforms that work together:
│ ┌───────────────────────────────────┐
│ │ Tauri Desktop (Rust + Node) │
│ │ OS keychain · Token-auth sidecar │
│ │ 60+ local API handlers · gzip │
│ │ 60+ local API handlers · br/gzip │
│ │ Cloud fallback · Traffic logging │
│ └───────────────────────────────────┘
@@ -883,7 +883,7 @@ All three variants run on three platforms that work together:
The Vercel edge functions connect to Railway via `WS_RELAY_URL` (server-side, HTTPS) while browser clients connect via `VITE_WS_RELAY_URL` (client-side, WSS). This separation keeps the relay URL configurable per deployment without leaking server-side configuration to the browser.
All Railway relay responses are gzip-compressed (zlib `gzipSync`) when the client accepts it and the payload exceeds 1KB, reducing egress by ~80% for JSON and XML responses.
All Railway relay responses are gzip-compressed (zlib `gzipSync`) when the client accepts it and the payload exceeds 1KB, reducing egress by ~80% for JSON and XML responses. The desktop local sidecar now prefers Brotli (`br`) and falls back to gzip for payloads larger than 1KB, setting `Content-Encoding` and `Vary: Accept-Encoding` automatically.
---
@@ -969,7 +969,7 @@ Static assets use content-hash filenames with 1-year immutable cache headers. Th
### Railway Relay Compression
All relay server responses pass through `gzipSync` when the client accepts gzip and the payload exceeds 1KB. This applies to OpenSky aircraft JSON, RSS XML feeds, UCDP event data, AIS snapshots, and health checks — reducing wire size by approximately 80%.
All relay server responses pass through `gzipSync` when the client accepts gzip and the payload exceeds 1KB. Sidecar API responses prefer Brotli and use gzip fallback with proper `Content-Encoding`/`Vary` headers for the same threshold. This applies to OpenSky aircraft JSON, RSS XML feeds, UCDP event data, AIS snapshots, and health checks — reducing wire size by approximately 5080%.
### Frontend Polling Intervals

View File

@@ -0,0 +1,34 @@
# Nginx API proxy compression baseline for WorldMonitor.
# Requires ngx_brotli (or Nginx Plus Brotli module) to be installed.
# Prefer Brotli for HTTPS clients and keep gzip as fallback.
brotli on;
brotli_comp_level 5;
brotli_min_length 1024;
brotli_types application/json application/javascript text/css text/plain application/xml text/xml;
gzip on;
gzip_comp_level 5;
gzip_min_length 1024;
gzip_vary on;
gzip_proxied any;
gzip_types application/json application/javascript text/css text/plain application/xml text/xml;
server {
listen 443 ssl;
server_name api.worldmonitor.local;
location /api/ {
proxy_pass http://127.0.0.1:8787;
proxy_http_version 1.1;
# Preserve upstream compression behavior and pass through client preferences.
proxy_set_header Accept-Encoding $http_accept_encoding;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# If upstream sends pre-compressed content, do not decompress.
gunzip off;
}
}

View File

@@ -3,10 +3,13 @@ import http, { createServer } from 'node:http';
import https from 'node:https';
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
import { readdir } from 'node:fs/promises';
import { gzipSync } from 'node:zlib';
import { promisify } from 'node:util';
import { brotliCompress, gzipSync } from 'node:zlib';
import path from 'node:path';
import { pathToFileURL } from 'node:url';
const brotliCompressAsync = promisify(brotliCompress);
// Monkey-patch globalThis.fetch to force IPv4 for HTTPS requests.
// Node.js built-in fetch (undici) tries IPv6 first via Happy Eyeballs.
// Government APIs (EIA, NASA FIRMS, FRED) publish AAAA records but their
@@ -96,6 +99,28 @@ function json(data, status = 200, extraHeaders = {}) {
});
}
function canCompress(headers, body) {
return body.length > 1024 && !headers['content-encoding'];
}
async function maybeCompressResponseBody(body, headers, acceptEncoding = '') {
if (!canCompress(headers, body)) return body;
const varyValue = headers['vary'];
headers['vary'] = varyValue ? `${varyValue}, Accept-Encoding` : 'Accept-Encoding';
if (acceptEncoding.includes('br')) {
headers['content-encoding'] = 'br';
return brotliCompressAsync(body);
}
if (acceptEncoding.includes('gzip')) {
headers['content-encoding'] = 'gzip';
return gzipSync(body);
}
return body;
}
function isBracketSegment(segment) {
return segment.startsWith('[') && segment.endsWith(']');
}
@@ -898,10 +923,10 @@ export async function createLocalApiServer(options = {}) {
}
const acceptEncoding = req.headers['accept-encoding'] || '';
if (acceptEncoding.includes('gzip') && body.length > 1024) {
body = gzipSync(body);
headers['content-encoding'] = 'gzip';
headers['vary'] = 'Accept-Encoding';
body = await maybeCompressResponseBody(body, headers, acceptEncoding);
if (headers['content-encoding']) {
delete headers['content-length'];
}
res.writeHead(response.status, headers);

View File

@@ -1,6 +1,7 @@
import { strict as assert } from 'node:assert';
import { mkdtemp, mkdir, rm, writeFile } from 'node:fs/promises';
import { createServer } from 'node:http';
import { brotliDecompressSync, gunzipSync } from 'node:zlib';
import os from 'node:os';
import path from 'node:path';
import test from 'node:test';
@@ -656,3 +657,86 @@ test('auth-required behavior unchanged — rejects unauthenticated requests when
await localApi.cleanup();
}
});
test('prefers Brotli compression for payloads larger than 1KB when supported by the client', async () => {
const remote = await setupRemoteServer();
const localApi = await setupApiDir({
'compression-check.js': `
export default async function handler() {
const payload = { value: 'x'.repeat(3000) };
return new Response(JSON.stringify(payload), {
status: 200,
headers: { 'content-type': 'application/json' }
});
}
`,
});
const app = await createLocalApiServer({
port: 0,
apiDir: localApi.apiDir,
remoteBase: remote.remoteBase,
logger: { log() {}, warn() {}, error() {} },
});
const { port } = await app.start();
try {
const response = await fetch(`http://127.0.0.1:${port}/api/compression-check`, {
headers: { 'Accept-Encoding': 'gzip, br' },
});
assert.equal(response.status, 200);
assert.equal(response.headers.get('content-encoding'), 'br');
const compressed = Buffer.from(await response.arrayBuffer());
const decompressed = brotliDecompressSync(compressed).toString('utf8');
const body = JSON.parse(decompressed);
assert.equal(body.value.length, 3000);
assert.equal(remote.hits.length, 0);
} finally {
await app.close();
await localApi.cleanup();
await remote.close();
}
});
test('uses gzip compression when Brotli is unavailable but gzip is accepted', async () => {
const remote = await setupRemoteServer();
const localApi = await setupApiDir({
'compression-check.js': `
export default async function handler() {
const payload = { value: 'x'.repeat(3000) };
return new Response(JSON.stringify(payload), {
status: 200,
headers: { 'content-type': 'application/json' }
});
}
`,
});
const app = await createLocalApiServer({
port: 0,
apiDir: localApi.apiDir,
remoteBase: remote.remoteBase,
logger: { log() {}, warn() {}, error() {} },
});
const { port } = await app.start();
try {
const response = await fetch(`http://127.0.0.1:${port}/api/compression-check`, {
headers: { 'Accept-Encoding': 'gzip' },
});
assert.equal(response.status, 200);
assert.equal(response.headers.get('content-encoding'), 'gzip');
const compressed = Buffer.from(await response.arrayBuffer());
const decompressed = gunzipSync(compressed).toString('utf8');
const body = JSON.parse(decompressed);
assert.equal(body.value.length, 3000);
assert.equal(remote.hits.length, 0);
} finally {
await app.close();
await localApi.cleanup();
await remote.close();
}
});