Skip to content

[Web runtime] Add SSE subscribe + CORS to HttpTransport#1537

Open
minggangw wants to merge 8 commits into
RobotWebTools:developfrom
minggangw:fix-1510-sse
Open

[Web runtime] Add SSE subscribe + CORS to HttpTransport#1537
minggangw wants to merge 8 commits into
RobotWebTools:developfrom
minggangw:fix-1510-sse

Conversation

@minggangw

@minggangw minggangw commented Jun 12, 2026

Copy link
Copy Markdown
Member

Adds Server-Sent Events (SSE) subscription support and CORS to the HTTP transport, so browsers can subscribe to ROS topics over plain HTTP (no WebSocket required) and call the runtime cross-origin.

Core (lib/runtime/)

  • transports/http.js (+360 / −31) — the bulk of the change:
    • New HttpSseConnection: a long-lived Connection that streams one subscription as text/event-stream. _ensureStream() lazily and idempotently writes the 200 SSE headers and starts an unref'd keep-alive setInterval; headers are deferred until the dispatcher acks ({ok:true}) so a rejected subscribe still returns a normal JSON error instead of a half-open stream. Includes _writeEvent, _writeError, and a once-only _emitCloseOnce.
    • subscribe is served only when sse: true, via GET /capability/subscribe/<name> (otherwise 404/405); call and publish continue to work as before.
    • CORS support via _applyCors/_normaliseCors: cors accepts true/'*' (any origin), a single origin string, or an allow-list array. Allow-list mode echoes a matching Origin and sets Vary: Origin. Access-Control-Allow-Headers reflects the browser's Access-Control-Request-Headers on preflight (falling back to content-type), so Authorization and custom headers pass preflight. OPTIONS preflight returns 204 for capability routes when CORS is on.
    • keepAliveMs configurable (default 15000, 0 disables).
  • cli-config.js (+72) — new flags --http-sse, --http-sse-keep-alive <ms>, and repeatable --http-cors <origin>; maps to http.sse / http.sseKeepAliveMs / http.cors. DEFAULTS, validation, merge, and HELP all updated.
  • index.d.ts (+3) — HttpTransportOptions gains sse?: boolean; sseKeepAliveMs?: number; cors?: boolean | string | string[];.

CLI (bin/rclnodejs-web.js)

  • Passes sse / sseKeepAliveMs / cors through to HttpTransport; banner distinguishes "call/publish + subscribe (SSE)" vs "call/publish only".

Demos (demo/web/javascript, demo/web/typescript)

  • runtime.mjsHttpTransport runs with sse: true, cors: true; exposes /add_two_ints, publishes/subscribes /web_demo_chatter, subscribes /topic; banner gains an "HTTP SSE" line.
  • web.jsonhttp block now sets port: 9001, sse: true, cors: "*".
  • index.html — new SSE panel (EventSource with ready/message/error handlers, open/close controls) plus a curl example; identifiers renamed for clarity (tickSubchatterSub, tickLogsubLog).
  • README.md (both JS and TS) — simplified and documents the SSE/CORS flow.

Tests (test/test-web-cli.js)

  • +91 lines / 9 new cases covering --http-sse, --http-sse-keep-alive, --http-cors (single, accumulation, precedence) and config validation.

Fix: #1510

Copilot AI review requested due to automatic review settings June 12, 2026 07:03

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds opt-in HTTP subscribe support for the Web Runtime via Server-Sent Events (SSE) and introduces configurable CORS handling so browser/HTTP-only clients can access the HTTP transport cross-origin.

Changes:

  • Add HttpSseConnection and a GET /capability/subscribe/<name> SSE route to HttpTransport (opt-in via sse / sseKeepAliveMs), plus CORS + OPTIONS preflight support.
  • Extend the CLI/config system with --http-sse, --http-sse-keep-alive <ms>, and repeatable --http-cors <origin> flags (and matching web.json keys).
  • Update the web demos/docs to showcase SSE + native browser EventSource, and add CLI parsing/validation tests.

Reviewed changes

Copilot reviewed 7 out of 8 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
test/test-web-cli.js Adds unit tests for new CLI flags and config validation/merge behavior (SSE + CORS).
lib/runtime/transports/http.js Implements SSE subscribe connection/route and adds CORS handling + OPTIONS preflight to the HTTP transport.
lib/runtime/cli-config.js Adds defaults, argv parsing, config validation, merge rules, and help text for http.sse, http.sseKeepAliveMs, http.cors.
demo/web/typescript/README.md Documents that HTTP subscribe-over-SSE is opt-in and points to the JS demo for an SSE example.
demo/web/javascript/runtime.mjs Enables SSE + CORS for the JS demo runtime and exposes /topic for pairing with the publisher example.
demo/web/javascript/README.md Documents SSE subscribe usage (curl + browser EventSource) and the required CORS setting.
demo/web/javascript/index.html Adds a native EventSource panel demonstrating SSE subscribe from the browser.
bin/rclnodejs-web.js Wires new config fields into HttpTransport construction and updates the startup banner wording.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +721 to +736
function _normaliseCors(value) {
if (value === undefined || value === null || value === false) return false;
if (value === true) return true;
if (typeof value === 'string') return new Set([value]);
if (Array.isArray(value)) {
if (!value.every((v) => typeof v === 'string')) {
throw new TypeError(
'HttpTransport: cors array must contain only origin strings'
);
}
return new Set(value);
}
throw new TypeError(
`HttpTransport: cors must be a boolean, string, or string[], got ${typeof value}`
);
}
Comment on lines +469 to +474
// Preflight: browsers send OPTIONS before a cross-origin POST with a
// JSON content-type. Answer it directly (no auth, no body).
if (req.method === 'OPTIONS' && this.cors) {
res.writeHead(204).end();
return;
}
Comment on lines +389 to +402
* @param {boolean} [options.sse=false]
* Enable `GET /capability/subscribe/<name>` Server-Sent Events
* streaming. Off by default; `subscribe` returns `unsupported_kind`
* unless this is set.
* @param {number} [options.sseKeepAliveMs=15000]
* Heartbeat comment interval for SSE streams, in milliseconds. Set to
* `0` to disable heartbeats.
* @param {boolean|string|string[]} [options.cors=false]
* Cross-Origin Resource Sharing policy. `false` (default) sends no
* CORS headers. `true` allows any origin (`Access-Control-Allow-Origin:
* *`). A string or array of strings allows only those origins (the
* request's `Origin` is echoed back when it matches). Required for a
* browser on a different origin to `fetch()` / `EventSource()` this
* transport.
Comment on lines +539 to +556
if (kind === 'subscribe') {
if (!this.sse) {
return _writeJson(res, 404, {
ok: false,
error:
'subscribe over HTTP is disabled (enable `sse` or use WebSocket)',
code: 'unsupported_kind',
});
}
if (req.method !== 'GET') {
res.setHeader('allow', 'GET');
return _writeJson(res, 405, {
ok: false,
error: `method not allowed: ${req.method} (use GET for SSE subscribe)`,
code: 'method_not_allowed',
});
}
const conn = new HttpSseConnection(req, res, name, {
@coveralls

coveralls commented Jun 12, 2026

Copy link
Copy Markdown

Coverage Status

coverage: 91.178% (-0.04%) from 91.219% — minggangw:fix-1510-sse into RobotWebTools:develop

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 10 changed files in this pull request and generated 2 comments.

Comment on lines +622 to +624
res.setHeader('access-control-allow-methods', 'GET, POST, OPTIONS');
res.setHeader('access-control-allow-headers', 'content-type');
res.setHeader('access-control-max-age', '86400');
Comment on lines +540 to +548
if (kind === 'subscribe') {
if (!this.sse) {
return _writeJson(res, 404, {
ok: false,
error:
'subscribe over HTTP is disabled (enable `sse` or use WebSocket)',
code: 'unsupported_kind',
});
}

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 9 out of 11 changed files in this pull request and generated 2 comments.

Comment thread lib/runtime/cli-config.js
Comment on lines +96 to +98
} else if (a === '--http-sse-keep-alive') {
partial.http.sseKeepAliveMs = Number(eat(a));
} else if (a === '--http-cors') {
Comment on lines +540 to +556
if (kind === 'subscribe') {
if (!this.sse) {
return _writeJson(res, 404, {
ok: false,
error:
'subscribe over HTTP is disabled (enable `sse` or use WebSocket)',
code: 'unsupported_kind',
});
}
if (req.method !== 'GET') {
res.setHeader('allow', 'GET');
return _writeJson(res, 405, {
ok: false,
error: `method not allowed: ${req.method} (use GET for SSE subscribe)`,
code: 'method_not_allowed',
});
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Browser ↔ ROS 2 capability runtime (Web Runtime)

3 participants