[Web runtime] Add SSE subscribe + CORS to HttpTransport#1537
Open
minggangw wants to merge 10 commits into
Open
[Web runtime] Add SSE subscribe + CORS to HttpTransport#1537minggangw wants to merge 10 commits into
minggangw wants to merge 10 commits into
Conversation
There was a problem hiding this comment.
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
HttpSseConnectionand aGET /capability/subscribe/<name>SSE route toHttpTransport(opt-in viasse/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 matchingweb.jsonkeys). - 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, { |
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', | ||
| }); | ||
| } |
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', | ||
| }); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds Server-Sent Events (SSE) subscription support and CORS to the HTTP transport, so browsers and other HTTP clients can
subscribeto ROS topics over plain HTTP (no WebSocket required) and call the runtime cross-origin.Core (
lib/runtime/)transports/http.js(+334 / −26) — the bulk of the change:HttpSseConnection: a long-livedConnectionthat streams one subscription astext/event-stream. The200SSE 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; on success it emits areadyevent ({capability, subId:"sse"}) followed bymessageevents, and starts an unref'd keep-alivesetInterval. Includes_writeEvent,_writeError, and a once-only close path.subscribeis served only whensse: true, viaGET /capability/subscribe/<name>(non-GET →405withallow: GET; unexposed /sseoff → JSON404);callandpublishcontinue to work as before._applyCors/_normaliseCors:corsacceptstrue/'*'(any origin), a single origin string, or an allow-list array. Allow-list mode echoes a matchingOriginand setsVary: Origin.Access-Control-Allow-Headersreflects the browser'sAccess-Control-Request-Headerson preflight (falling back tocontent-type), soAuthorizationand custom headers pass.OPTIONSpreflight returns204for capability routes when CORS is on. CORS headers are applied first, so they appear on JSON replies, 204s, and SSE streams alike.sseKeepAliveMsconfigurable (default15000,0disables).cli-config.js(+67 / −5) — new flags--http-sse,--http-sse-keep-alive <ms>, and repeatable--http-cors <origin>; maps tohttp.sse/http.sseKeepAliveMs/http.cors. DEFAULTS, validation, merge, HELP, and examples all updated.index.d.ts(+3) —HttpTransportOptionsgainssse?: boolean; sseKeepAliveMs?: number; cors?: boolean | string | string[];.CLI (
bin/rclnodejs-web.js)sse/sseKeepAliveMs/corsthrough toHttpTransport, and re-runsvalidateConfigon the merged result so flag-only values (e.g.--http-sse-keep-alive foo→ NaN) are rejected before reaching the transport; banner distinguishes "call/publish + subscribe (SSE)" vs "call/publish only".Demos (
demo/web/javascript,demo/web/typescript)runtime.mjs(+20 / −13) —HttpTransportruns withsse: true, cors: true; exposes/add_two_ints, publishes/subscribes/web_demo_chatter, and subscribes/topic(drops the standalone/web_demo_tickpublisher so all panels share one topic); banner gains an "HTTP SSE" line.web.json(+6 / −4) —httpblock now setsport: 9001, sse: true, cors: "*"; subscribe exposes/web_demo_chatter+/topic.index.html(+237 / −18) — new nativeEventSource(SSE) panel andfetch()panel withready/message/errorhandlers and open/close controls, plus curl examples.javascript/README.md(+82 / −19),typescript/README.md(+15 / −15) — document the SSE/CORS flow; the TS demo notes its HTTP transport stays call/publish-only and points to the JS demo for SSE.Docs
web/README.md(+25 / −1) — §3 curl recipes gain an SSEcurl -Nexample and a browserEventSourcesnippet.README.md(+4 / −4),scripts/npmjs-readme.md(+1 / −1) —rclnodejs/webbullet notessubscribeover SSE and a CORS-enabled browserfetch()/EventSource.Tests
test/test-web-http.js(+278) — SSE + CORS HTTP integration suite: deferred-header behaviour,ready/messageevents, keep-alive, 404/405 routing, preflight, origin echo / allow-list.test/test-web-cli.js(+105) — new cases for--http-sse,--http-sse-keep-alive(including bin-flow NaN rejection), and--http-cors(single, accumulation, precedence) plus config validation.Fix: #1510