[Web runtime] Add SSE subscribe + CORS to HttpTransport#1537
Open
minggangw wants to merge 8 commits into
Open
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 can
subscribeto 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:HttpSseConnection: a long-livedConnectionthat streams one subscription astext/event-stream._ensureStream()lazily and idempotently writes the200SSE headers and starts an unref'd keep-alivesetInterval; 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.subscribeis served only whensse: true, viaGET /capability/subscribe/<name>(otherwise404/405);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 preflight.OPTIONSpreflight returns204for capability routes when CORS is on.keepAliveMsconfigurable (default15000,0disables).cli-config.js(+72) — new flags--http-sse,--http-sse-keep-alive <ms>, and repeatable--http-cors <origin>; maps tohttp.sse/http.sseKeepAliveMs/http.cors. DEFAULTS, validation, merge, and HELP all updated.index.d.ts(+3) —HttpTransportOptionsgainssse?: boolean; sseKeepAliveMs?: number; cors?: boolean | string | string[];.CLI (
bin/rclnodejs-web.js)sse/sseKeepAliveMs/corsthrough toHttpTransport; banner distinguishes "call/publish + subscribe (SSE)" vs "call/publish only".Demos (
demo/web/javascript,demo/web/typescript)runtime.mjs—HttpTransportruns withsse: true, cors: true; exposes/add_two_ints, publishes/subscribes/web_demo_chatter, subscribes/topic; banner gains an "HTTP SSE" line.web.json—httpblock now setsport: 9001, sse: true, cors: "*".index.html— new SSE panel (EventSource withready/message/errorhandlers, open/close controls) plus a curl example; identifiers renamed for clarity (tickSub→chatterSub,tickLog→subLog).README.md(both JS and TS) — simplified and documents the SSE/CORS flow.Tests (
test/test-web-cli.js)--http-sse,--http-sse-keep-alive,--http-cors(single, accumulation, precedence) and config validation.Fix: #1510