fix(h1): ignore type of service errors - #5547

Merged
mcollina merged 2 commits into
mainfrom
fix-h1-type-of-service-crash
Jul 12, 2026
Merged

fix(h1): ignore type of service errors#5547
mcollina merged 2 commits into
mainfrom
fix-h1-type-of-service-crash

Conversation

@mcollina

Copy link
Copy Markdown
Member

This relates to...

Fixes#5544.

Rationale

socket.setTypeOfService() is best-effort QoS marking. It can throw synchronously on some platforms depending on socket state, and the default request path should not touch it when no ToS value was explicitly requested.

Changes

Features

N/A

Bug Fixes

  • Skip setTypeOfService(0) for fresh HTTP/1.1 sockets when typeOfService was not explicitly provided.
  • Track the last ToS value applied to each socket and avoid redundant calls.
  • Reset a reused socket back to ToS 0 after an explicitly prioritized request.
  • Ignore synchronous setTypeOfService() failures so they do not abort requests or crash the process.
  • Add regression coverage for default requests and thrown setTypeOfService() errors.

Breaking Changes and Deprecations

N/A

Status

Tested with:

  • npx borp -p "test/ip-prioritization.js"
  • npm run lint

@codecov-commenter

codecov-commenter commented Jul 11, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 86.20690% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 93.44%. Comparing base (c7c7e7f) to head (794f99f).
⚠️ Report is 6 commits behind head on main.

Files with missing linesPatch %Lines
lib/dispatcher/client-h1.js85.71%4 Missing ⚠️
Additional details and impacted files
@@ Coverage Diff @@## main #5547 +/- ##
==========================================
- Coverage 93.45% 93.44% -0.01% 
==========================================
Files 110 110 Lines 37376 37443 +67 ==========================================
+ Hits 34928 34987 +59 - Misses 2448 2456 +8 

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@mcollina
mcollina merged commit 197a83d into mainJul 12, 2026
36 checks passed
@mcollina
mcollina deleted the fix-h1-type-of-service-crash branch July 12, 2026 13:13
@github-actionsgithub-actionsBot mentioned this pull request Jul 20, 2026
rinchen added a commit to Colorado-Mesh/mesh-client that referenced this pull request Aug 29, 2026
…ocklist tools, Four in a Row, RRC linkify + nick cache, plus MQTT/BLE/undici stability fixes (#912)
* feat(reticulum): in-app Micron page editor for Nomad My Pages
Hosting a Nomad site previously required editing .mu files on disk in an
external editor. My Pages now offers New/Edit/Delete on the hosted-page
list, opening a split-pane editor with the raw Micron source plus a
formatting toolbar on the left and a live preview on the right. Saves
write into the watched folder, so the existing FS watcher re-indexes them.
Authoring is gated on a configured content source rather than on active
serving, because page mutations only need a live stack and resolved roots.
The sidecar previously stringified NomadError straight to the renderer,
so write failures surfaced as untranslatable English prose interpolating
paths and byte counts. page_error_code now maps them to stable snake_case
codes (page_too_large, page_not_found, invalid_page_path, page_io_error,
page_not_utf8, page_write_failed) and logs the original detail, letting
nomadPageErrorHumanize translate what the user sees.
File uploads and the local-files list remain intentionally absent.
* feat(reticulum): add Serial, AX.25 KISS and Local interfaces via a shared catalog
Adding one Reticulum interface type previously took ~20 coordinated edits split
across the Rust sidecar and the renderer with no compiler link between them, so
the two sides could silently disagree about supported types, default modes or
flow-control policy.
Introduce src/shared/reticulumInterfaceCatalog.json as the single source of
truth. The sidecar compiles it in via include_str! and the renderer imports the
same file, making that drift structurally impossible rather than checked after
the fact. Both sides assert every configType is a name the upstream factory
synthesize_interface actually accepts.
On top of that, add SerialInterface, AX25KISSInterface and LocalInterface. A
catalog-driven ReticulumInterfaceFieldSet renders their forms, and add and edit
share one serialization path instead of two hand-maintained branches.
Also fixes a latent bug: ui_type_to_config echoed unknown types verbatim, so an
unmapped type was written to disk and then dropped on the next parse, leaving a
config block the user could see but never edit or delete. Unknown types are now
rejected up front.
The nine existing types keep their bespoke controls and hand-written branches;
the goal is that a new type needs no new branch, not that every old one is
rewritten. check-reticulum-interface-modes.mjs was scraping match arms that no
longer exist and is repointed at the mode list, the ap/gw aliases, and the
invariant that both sides still read the catalog.
* feat(rrc): linkify Nomad page addresses and LXMF hashes in chat
Reticulum addresses pasted into RRC rooms were inert text, so reaching a
shared page or peer meant copying the hash into another tab by hand.
Addresses now render as inline links: `<hash>:/page/foo.mu` switches to the
Nomad tab and loads the page, while a bare or lxmf:// hash opens a Chat DM.
* fix(reticulum): render Micron editor preview like the Nomad browser
The editor mounted parsed Micron into a bare div, so it missed the
nomad-micron-page class that supplies white-space: pre and the bundled
MeshClientNomadMono font. HTML collapsed the interior padding spaces and
the fallback UI font kept borders from joining, which scrambled Unicode
and ASCII box-drawing art that renders correctly in the browser.
Render the preview through NomadMicronPageView itself rather than copying
its class list, which would silently drift. Links get no-op handlers since
the modal has no browsing context, and the pane scrolls on both axes so
wide art stays reachable.
Fit-width wraps long lines and breaks wide art even once the class is
restored, so the editor gains the same toggle as the browser, backed by a
shared nomadPageFitWidth helper reading the one storage key so the two
surfaces always agree.
* feat(reticulum): clear path table, blocklist import/export, auto-resend on announce
Three MeshChatX parity gaps, each independently useful:
Clear path table — a stale route can keep sending to a next hop that no
longer works, with no way to force rediscovery short of restarting the
stack. Uses the upstream TransportQuery::DropPathTable verb (a single
control query, not a per-entry DropPath loop) and also resets the
path-request backoff gate so peers that exhausted their retry budget can
be discovered again. Local caches are dropped even when the query times
out, since the transport-side clear may still have applied.
Blocklist import/export — a blocklist was previously trapped in one
install's SQLite. Import validates strictly rather than reusing the
lenient normalizer, whose documented non-hex fallback would otherwise
persist junk as blocked hashes, and uses its own SQL so imported-vs-
skipped counts are accurate (db:blockContact always reports 1 change).
Auto-resend on announce — an announce means a path may exist again, so
failed sends to that peer are worth one retry. Off by default; guarded by
a per-destination cooldown, an in-flight lock, and a per-announce cap so
an announce burst cannot fan out into a send storm.
Also corrects the parity notes: the claim that no bulk path-table clear
verb existed was wrong, and adds the deferred transport-blackhole
write-up (it keys on identity hashes, not the destination hashes our
blocklist stores, so existing rows cannot be mirrored).
* fix(main): stop setTypeOfService EINVAL from crashing the main process
Link-preview fetches use undici, which set the advisory IP ToS byte on every
outbound HTTP/1.1 socket. macOS rejects that with EINVAL on some network
configurations, and because the call happens inside a socket connect listener
the throw escaped fetchLinkPreview's try/catch and surfaced as an "Unexpected
Error" dialog. Depend on undici ^8.10.0 directly for the upstream guard
(nodejs/undici#5547) — it was previously an undeclared transitive dependency
capped at 7.x — and keep a scoped undici@<7.29.0 floor so jsdom and node-gyp
stay on 7.29.0. A narrow uncaughtException/unhandledRejection filter now
tolerates the failure even if a future socket option regresses.
Add check:pinned-majors, run from pnpm run update, so an override that falls
behind a newer npm major surfaces instead of silently withholding fixes like
this one; caps that are correct carry a documented reason.
* fix(reticulum): style clear path table as caution, not danger
The action was neutral slate with a red confirm button, which read as
destructive. Clearing the path table only forces route rediscovery — RNS
repopulates the entries on its own — so red overstated the risk on an
action users should reach for when messages to a peer stop landing.
Uses the yellow palette already used by ReticulumPnHostingDangerZone,
which sits directly above it in the network panel, and drops the danger
prop so ConfirmModal picks its yellow confirm button instead of red.
* fix(reticulum): collapse the RNode flasher by default
Flashing firmware is a rare, one-off task, but its section occupied the
top of the Administration tab and pushed the factory reset danger zone
down the page. Collapsing it makes the tab's remaining content visible
without scrolling.
Uses the same <details> disclosure pattern as RadioPanel and
ReticulumInterfacesPanel via the shared DetailsChevron. A closed
<details> still mounts its children, so RNodeFlasherSection keeps the
pairing session and flash/provision marks it deliberately preserves
across Admin remounts; conditional rendering would have discarded them.
The test records that so the distinction is not lost.
* refactor(reticulum): move blocked contacts to the Network tab
The blocklist is Reticulum-specific but sat in App Settings between the
database export/import and Appearance, where it was both hard to find
and surprising to encounter — the section hides itself unless the
hydrated identity is Reticulum, so most users saw a gap there instead.
The Network tab already holds the rest of the Reticulum-scoped controls.
Presented as a collapsible section like its neighbors, which supplies
the heading, so the component's own now-duplicate h3 is dropped.
Deliberately not gated on sidecarApiReady, unlike the surrounding
sections: the blocklist is local SQLite state hydrated on identityId
alone, so gating would hide it whenever the stack was stopped — the
point at which pruning it before reconnecting is most useful.
The protocol scope check moves into blockStore as
useReticulumBlocklistIdentityId rather than travelling into the panel as
a raw compare. AppPanel was grandfathered onto the
check-protocol-string-gates allowlist; ReticulumNetworkPanel is not, and
widening that legacy list is the wrong direction when the store is the
right home for knowing which protocol it currently holds.
i18n keys stay under appPanel.reticulumBlocklist.* to avoid rewriting
all 16 locale files for a move.
* feat(rrc): prompt when a pasted Reticulum hash is ambiguous
A bare 32-hex hash could be a Nomad node or an LXMF peer, and assuming a DM
sent people to the wrong place. Clicking one now opens a small dialog to pick
Nomad page or direct message; explicit `:/page` and lxmf:// forms still act
immediately.
* chore(deps): update dependencies and move off deprecated jest-dom 6.10.0
`pnpm run update` picked up systeminformation, @testing-library/react, and
@vitejs/plugin-react patch releases. @testing-library/jest-dom 6.10.0 was
deprecated upstream as a bad minor (it shipped the Node >=22 and
@testing-library/dom peer requirements intended for 7.x), so move to ^7.0.1
rather than pinning back to 6.9.1. All three of its requirements were already
satisfied, so no test migration was needed.
* feat(games): add Four in a Row board
The Games board dispatch was a binary chess-or-else fallback, so
`four_in_a_row` sessions rendered through TicTacToeBoard — a 42-cell board
truncated to 9 cells and moves sent as `{ i }` instead of `{ c }`. The sidecar
has registered the app for a while, so these sessions were reachable and
broken, not merely unimplemented.
Implemented against lrgp-rs SPEC.md appendix C: 7x6 gravity board, 42 cells
row-major (`row * 7 + column`), theme-neutral `A`/`B` markers, outgoing payload
exactly `{ c }` with the sidecar deriving move number, landing cell, and
terminal claims. Adds an optimistic column drop matching the existing TTT and
chess pattern so the disc paints before the sidecar confirms, with win and draw
detection for rollback.
The whole column is the button rather than each cell, since gravity means the
player picks a column. Discs are aria-hidden and the column button carries the
readable state ("Column 3, bottom to top: A, B. Drop here"), giving screen
reader users the board contents without 42 focus stops.
Seven of the eleven new strings are identical to existing gamesPanel.ttt values,
so those reuse the reviewed per-locale translations; the four new keys are
translated for all 15 locales.
Bumps the Ratspeak games-parity reviewed-ref to v1.0.31 (reviewed: voice
message reliability only, no Games API delta) and updates the parity matrix.
Wire interop rows stay `todo` pending the manual gold test against Ratspeak.
* fix(main): release BLE connect queue on wait timeout and always quit after BLE shutdown
BLE connect installs a fresh `connectQueue` promise before awaiting the previous
holder, but that await sat outside the try whose finally calls releaseQueue().
When the wait timed out, the promise just installed was never resolved, so every
later connect() awaited a promise that could never settle — each one timing out
and installing another dead queue. BLE connect stayed wedged for the rest of the
process lifetime rather than recovering. Release the slot on the timeout path.
The before-quit BLE branch awaited shutdownAppResources() inside a finally with
no catch, so a rejection escaped the async IIFE as an unhandled rejection and
app.quit() never ran. Since the handler had already called preventDefault(),
that left the app running with no path to exit. The non-BLE branch below already
handled this; make the two consistent.
Adds a behavior test that wedges the queue behind a holder that never releases
and asserts a follow-up connect still succeeds. Verified to fail without the fix.
* fix(main): gate MQTT handlers on client identity and harden sidecar stop race
Stale mqtt.js clients keep emitting close/error/offline after end(true). With the
handlers bound to whatever `this.client` currently points at, a replaced client's
close cleared the live client's connack timer, inflated its retry count, and
scheduled a reconnect that tore down a healthy connection. Both adapters now gate
every handler on client identity; the MeshCore adapter already used this idiom for
packetsend/packetreceive, so this extends it to the rest.
The Reticulum sidecar could also report a stopped process as running: stop() kills
the child during the health poll, so a response landing just before the kill marked
a dead PID running, connected a WS to a dead port, and armed a watchdog for a gone
process. Re-check abort and process identity after the poll, and stop the watchdog
on exit so its interval does not leak across the next spawn.
Also fold in verified low-risk audit findings:
- Normalize six Meshtastic node-id renders through formatMeshtasticNodeId so a
negative int32 cannot render as a 9-char hex id (MeshCore panels use a separate
id space and are left alone).
- Guard six fire-and-forget ipcMain.on handlers with validateIpcSender, plus a test
that enumerates every ipcMain.on channel so new ones cannot ship unguarded.
- Add object-src/base-uri/form-action to the renderer CSP (frame-ancestors is
omitted: the spec ignores it in <meta> and the renderer is never framed).
- Fix ru common.save/common.unknown wording and de "Stornieren" -> "Abbrechen",
and canonicalize the eight "Hops" keys per locale (ru mixed three spellings and
reticulumPeers.hops was untranslated in every locale).
- Correct docs drift: relative CI link, orphaned docs/index.md entries, the
useLongSessionMaintenance Noble condition, and the src/main/ipc module list.
* fix(i18n): add missing Slavic plural forms so counts stop falling back to English
Russian, Ukrainian, Polish and Czech define plural categories English does not.
Only _one/_other existed, so i18next found no match for `few` (2-4) or `many`
(0, 5-20) and fell through the fallback chain to English: ru at count=3 rendered
"3 nodes", not "3 узла". This affected every counted string in four locales at
the most common counts.
Add the 320 missing forms across 47 plural families: Russian `few` in the
genitive singular, uk/pl/cs `few` in the nominative plural, and genitive plural
for `many`. Czech `many` is deliberately absent (fractions-only, unreachable for
integer counts), as is Romance `many` (starts at 1,000,000).
Also translate the pl/cs "config issue" and "hop" families, which were still
literal English in _one/_other and so leaked regardless of category.
check:i18n now requires every plural category a locale actually selects for
counts 0-200, scoped to families that locale has already started so a wholly
untranslated locale still reports as the existing "missing key(s)" warning.
Verified: rendering all 47 families at counts 0-22 across the four locales now
produces zero English fallbacks, down from 320.
* chore(reticulum): retire rsReticulum LinkClient Nomad overlay
Upstream main now resolves the remote pubkey via handler-free
resolve_destination_on_transport, with no HasPath gate and no temporary
announce handlers, so the overlay's apply script had already degraded to a
no-op. ratspeak/rsReticulum#14 was closed as superseded rather than merged.
Removes the patch, apply script, and its test, drops the entries from
RS_RETICULUM_APPLY_SCRIPTS and RATSPEAK_PATCH_ENTRIES so pnpm run update
stops tracking it, and updates the docs to point at floated origin/main.
* fix(rrc): resolve nicklist hashes to names and surface dropped /who replies
rrcd JOINED rosters carry no nicknames, and the once-per-room auto /who gate
survived reconnects, so a re-joined room showed bare hex forever. Re-arm the
gate on the handshake, fall back to nicks learned anywhere in the hub's
transcripts, and warn when a forced /who reply never arrives instead of
looking ignored.
* feat(games): allow challenging peers to Four in a Row
The FourInARowBoard added in be5ae05 was unreachable. A challenge is the only
way to create a session, and both challenge entry points offered just ttt and
chess, so no session could ever carry app_id four_in_a_row and the board branch
in GamesPanel was dead code.
Add the app id to GamesAppId and move the menu list into a single exported
GAMES_CHALLENGE_APPS. It had been duplicated verbatim in GamesPanel and
ReticulumGameChallengeButton, which is what let both menus drift out of sync
with the board dispatch in the first place.
The list stays curated rather than derived from the sidecar app manifest:
manifest app_id is an open string and the board dispatch falls through to
Tic-Tac-Toe for anything it does not recognize, so a manifest-driven menu could
offer a game that renders the wrong board. No sidecar change is needed -- it
already registers four_in_a_row and passes the app id through unvalidated.
Locale labels use each language's established name for the game rather than a
literal translation ("Vier gewinnt", "Puissance 4", "Forza 4", "四子棋").
The picker test asserts the full option list, so adding a future board without a
menu entry fails instead of silently shipping another unreachable game.
* feat(rrc): cache hub nicks in SQLite so names survive transcript clears
Oversized /who replies are dropped by the hub, so the nicklist depended on
whatever transcript happened to be loaded. Persist every nick sighting per
hub (chat, /who row, JOINED advisory) and hydrate it once per session, so a
peer who spoke only once still gets a name after a restart or history clear.
* fix(nomad): sanitize Micron page text at the innerHTML sink
The Micron editor added on this branch feeds a textarea's value straight into
the parser, which CodeQL flags as js/xss-through-dom: appendOutput writes page
text into span.innerHTML, and the only DOMPurify pass ran afterwards over the
whole document in convertMicronToHtml.
Route the three markup-building innerHTML writes (text spans, style-run spans,
link labels) through setSanitizedHtml so the barrier sits at the sink. In
practice wrapWord already escapes `<`, `>` and `&` when force-monospace is on,
so rendering is unchanged and tag-like text stays inert literal text; the sink
sanitize covers the non-monospace path and any future part producer that does
not escape.
Tests assert tag-like page text and link labels never become elements, and that
literal angle brackets and ampersands still render as typed.
Sign up for freeto 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.

writeH1 calls socket.setTypeOfService() unconditionally — uncaught EINVAL on macOS crashes the process

3 participants

@mcollina@codecov-commenter@ronag
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

fix(h1): ignore type of service errors - #5547

Merged
mcollina merged 2 commits into
mainfrom
fix-h1-type-of-service-crash
Jul 12, 2026
Merged

fix(h1): ignore type of service errors#5547
mcollina merged 2 commits into
mainfrom
fix-h1-type-of-service-crash

Conversation

@mcollina

Copy link
Copy Markdown
Member

This relates to...

Fixes#5544.

Rationale

socket.setTypeOfService() is best-effort QoS marking. It can throw synchronously on some platforms depending on socket state, and the default request path should not touch it when no ToS value was explicitly requested.

Changes

Features

N/A

Bug Fixes

  • Skip setTypeOfService(0) for fresh HTTP/1.1 sockets when typeOfService was not explicitly provided.
  • Track the last ToS value applied to each socket and avoid redundant calls.
  • Reset a reused socket back to ToS 0 after an explicitly prioritized request.
  • Ignore synchronous setTypeOfService() failures so they do not abort requests or crash the process.
  • Add regression coverage for default requests and thrown setTypeOfService() errors.

Breaking Changes and Deprecations

N/A

Status

Tested with:

  • npx borp -p "test/ip-prioritization.js"
  • npm run lint

@codecov-commenter

codecov-commenter commented Jul 11, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 86.20690% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 93.44%. Comparing base (c7c7e7f) to head (794f99f).
⚠️ Report is 6 commits behind head on main.

Files with missing linesPatch %Lines
lib/dispatcher/client-h1.js85.71%4 Missing ⚠️
Additional details and impacted files
@@ Coverage Diff @@## main #5547 +/- ##
==========================================
- Coverage 93.45% 93.44% -0.01% 
==========================================
Files 110 110 Lines 37376 37443 +67 ==========================================
+ Hits 34928 34987 +59 - Misses 2448 2456 +8 

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@mcollina
mcollina merged commit 197a83d into mainJul 12, 2026
36 checks passed
@mcollina
mcollina deleted the fix-h1-type-of-service-crash branch July 12, 2026 13:13
@github-actionsgithub-actionsBot mentioned this pull request Jul 20, 2026
rinchen added a commit to Colorado-Mesh/mesh-client that referenced this pull request Aug 29, 2026
…ocklist tools, Four in a Row, RRC linkify + nick cache, plus MQTT/BLE/undici stability fixes (#912)
* feat(reticulum): in-app Micron page editor for Nomad My Pages
Hosting a Nomad site previously required editing .mu files on disk in an
external editor. My Pages now offers New/Edit/Delete on the hosted-page
list, opening a split-pane editor with the raw Micron source plus a
formatting toolbar on the left and a live preview on the right. Saves
write into the watched folder, so the existing FS watcher re-indexes them.
Authoring is gated on a configured content source rather than on active
serving, because page mutations only need a live stack and resolved roots.
The sidecar previously stringified NomadError straight to the renderer,
so write failures surfaced as untranslatable English prose interpolating
paths and byte counts. page_error_code now maps them to stable snake_case
codes (page_too_large, page_not_found, invalid_page_path, page_io_error,
page_not_utf8, page_write_failed) and logs the original detail, letting
nomadPageErrorHumanize translate what the user sees.
File uploads and the local-files list remain intentionally absent.
* feat(reticulum): add Serial, AX.25 KISS and Local interfaces via a shared catalog
Adding one Reticulum interface type previously took ~20 coordinated edits split
across the Rust sidecar and the renderer with no compiler link between them, so
the two sides could silently disagree about supported types, default modes or
flow-control policy.
Introduce src/shared/reticulumInterfaceCatalog.json as the single source of
truth. The sidecar compiles it in via include_str! and the renderer imports the
same file, making that drift structurally impossible rather than checked after
the fact. Both sides assert every configType is a name the upstream factory
synthesize_interface actually accepts.
On top of that, add SerialInterface, AX25KISSInterface and LocalInterface. A
catalog-driven ReticulumInterfaceFieldSet renders their forms, and add and edit
share one serialization path instead of two hand-maintained branches.
Also fixes a latent bug: ui_type_to_config echoed unknown types verbatim, so an
unmapped type was written to disk and then dropped on the next parse, leaving a
config block the user could see but never edit or delete. Unknown types are now
rejected up front.
The nine existing types keep their bespoke controls and hand-written branches;
the goal is that a new type needs no new branch, not that every old one is
rewritten. check-reticulum-interface-modes.mjs was scraping match arms that no
longer exist and is repointed at the mode list, the ap/gw aliases, and the
invariant that both sides still read the catalog.
* feat(rrc): linkify Nomad page addresses and LXMF hashes in chat
Reticulum addresses pasted into RRC rooms were inert text, so reaching a
shared page or peer meant copying the hash into another tab by hand.
Addresses now render as inline links: `<hash>:/page/foo.mu` switches to the
Nomad tab and loads the page, while a bare or lxmf:// hash opens a Chat DM.
* fix(reticulum): render Micron editor preview like the Nomad browser
The editor mounted parsed Micron into a bare div, so it missed the
nomad-micron-page class that supplies white-space: pre and the bundled
MeshClientNomadMono font. HTML collapsed the interior padding spaces and
the fallback UI font kept borders from joining, which scrambled Unicode
and ASCII box-drawing art that renders correctly in the browser.
Render the preview through NomadMicronPageView itself rather than copying
its class list, which would silently drift. Links get no-op handlers since
the modal has no browsing context, and the pane scrolls on both axes so
wide art stays reachable.
Fit-width wraps long lines and breaks wide art even once the class is
restored, so the editor gains the same toggle as the browser, backed by a
shared nomadPageFitWidth helper reading the one storage key so the two
surfaces always agree.
* feat(reticulum): clear path table, blocklist import/export, auto-resend on announce
Three MeshChatX parity gaps, each independently useful:
Clear path table — a stale route can keep sending to a next hop that no
longer works, with no way to force rediscovery short of restarting the
stack. Uses the upstream TransportQuery::DropPathTable verb (a single
control query, not a per-entry DropPath loop) and also resets the
path-request backoff gate so peers that exhausted their retry budget can
be discovered again. Local caches are dropped even when the query times
out, since the transport-side clear may still have applied.
Blocklist import/export — a blocklist was previously trapped in one
install's SQLite. Import validates strictly rather than reusing the
lenient normalizer, whose documented non-hex fallback would otherwise
persist junk as blocked hashes, and uses its own SQL so imported-vs-
skipped counts are accurate (db:blockContact always reports 1 change).
Auto-resend on announce — an announce means a path may exist again, so
failed sends to that peer are worth one retry. Off by default; guarded by
a per-destination cooldown, an in-flight lock, and a per-announce cap so
an announce burst cannot fan out into a send storm.
Also corrects the parity notes: the claim that no bulk path-table clear
verb existed was wrong, and adds the deferred transport-blackhole
write-up (it keys on identity hashes, not the destination hashes our
blocklist stores, so existing rows cannot be mirrored).
* fix(main): stop setTypeOfService EINVAL from crashing the main process
Link-preview fetches use undici, which set the advisory IP ToS byte on every
outbound HTTP/1.1 socket. macOS rejects that with EINVAL on some network
configurations, and because the call happens inside a socket connect listener
the throw escaped fetchLinkPreview's try/catch and surfaced as an "Unexpected
Error" dialog. Depend on undici ^8.10.0 directly for the upstream guard
(nodejs/undici#5547) — it was previously an undeclared transitive dependency
capped at 7.x — and keep a scoped undici@<7.29.0 floor so jsdom and node-gyp
stay on 7.29.0. A narrow uncaughtException/unhandledRejection filter now
tolerates the failure even if a future socket option regresses.
Add check:pinned-majors, run from pnpm run update, so an override that falls
behind a newer npm major surfaces instead of silently withholding fixes like
this one; caps that are correct carry a documented reason.
* fix(reticulum): style clear path table as caution, not danger
The action was neutral slate with a red confirm button, which read as
destructive. Clearing the path table only forces route rediscovery — RNS
repopulates the entries on its own — so red overstated the risk on an
action users should reach for when messages to a peer stop landing.
Uses the yellow palette already used by ReticulumPnHostingDangerZone,
which sits directly above it in the network panel, and drops the danger
prop so ConfirmModal picks its yellow confirm button instead of red.
* fix(reticulum): collapse the RNode flasher by default
Flashing firmware is a rare, one-off task, but its section occupied the
top of the Administration tab and pushed the factory reset danger zone
down the page. Collapsing it makes the tab's remaining content visible
without scrolling.
Uses the same <details> disclosure pattern as RadioPanel and
ReticulumInterfacesPanel via the shared DetailsChevron. A closed
<details> still mounts its children, so RNodeFlasherSection keeps the
pairing session and flash/provision marks it deliberately preserves
across Admin remounts; conditional rendering would have discarded them.
The test records that so the distinction is not lost.
* refactor(reticulum): move blocked contacts to the Network tab
The blocklist is Reticulum-specific but sat in App Settings between the
database export/import and Appearance, where it was both hard to find
and surprising to encounter — the section hides itself unless the
hydrated identity is Reticulum, so most users saw a gap there instead.
The Network tab already holds the rest of the Reticulum-scoped controls.
Presented as a collapsible section like its neighbors, which supplies
the heading, so the component's own now-duplicate h3 is dropped.
Deliberately not gated on sidecarApiReady, unlike the surrounding
sections: the blocklist is local SQLite state hydrated on identityId
alone, so gating would hide it whenever the stack was stopped — the
point at which pruning it before reconnecting is most useful.
The protocol scope check moves into blockStore as
useReticulumBlocklistIdentityId rather than travelling into the panel as
a raw compare. AppPanel was grandfathered onto the
check-protocol-string-gates allowlist; ReticulumNetworkPanel is not, and
widening that legacy list is the wrong direction when the store is the
right home for knowing which protocol it currently holds.
i18n keys stay under appPanel.reticulumBlocklist.* to avoid rewriting
all 16 locale files for a move.
* feat(rrc): prompt when a pasted Reticulum hash is ambiguous
A bare 32-hex hash could be a Nomad node or an LXMF peer, and assuming a DM
sent people to the wrong place. Clicking one now opens a small dialog to pick
Nomad page or direct message; explicit `:/page` and lxmf:// forms still act
immediately.
* chore(deps): update dependencies and move off deprecated jest-dom 6.10.0
`pnpm run update` picked up systeminformation, @testing-library/react, and
@vitejs/plugin-react patch releases. @testing-library/jest-dom 6.10.0 was
deprecated upstream as a bad minor (it shipped the Node >=22 and
@testing-library/dom peer requirements intended for 7.x), so move to ^7.0.1
rather than pinning back to 6.9.1. All three of its requirements were already
satisfied, so no test migration was needed.
* feat(games): add Four in a Row board
The Games board dispatch was a binary chess-or-else fallback, so
`four_in_a_row` sessions rendered through TicTacToeBoard — a 42-cell board
truncated to 9 cells and moves sent as `{ i }` instead of `{ c }`. The sidecar
has registered the app for a while, so these sessions were reachable and
broken, not merely unimplemented.
Implemented against lrgp-rs SPEC.md appendix C: 7x6 gravity board, 42 cells
row-major (`row * 7 + column`), theme-neutral `A`/`B` markers, outgoing payload
exactly `{ c }` with the sidecar deriving move number, landing cell, and
terminal claims. Adds an optimistic column drop matching the existing TTT and
chess pattern so the disc paints before the sidecar confirms, with win and draw
detection for rollback.
The whole column is the button rather than each cell, since gravity means the
player picks a column. Discs are aria-hidden and the column button carries the
readable state ("Column 3, bottom to top: A, B. Drop here"), giving screen
reader users the board contents without 42 focus stops.
Seven of the eleven new strings are identical to existing gamesPanel.ttt values,
so those reuse the reviewed per-locale translations; the four new keys are
translated for all 15 locales.
Bumps the Ratspeak games-parity reviewed-ref to v1.0.31 (reviewed: voice
message reliability only, no Games API delta) and updates the parity matrix.
Wire interop rows stay `todo` pending the manual gold test against Ratspeak.
* fix(main): release BLE connect queue on wait timeout and always quit after BLE shutdown
BLE connect installs a fresh `connectQueue` promise before awaiting the previous
holder, but that await sat outside the try whose finally calls releaseQueue().
When the wait timed out, the promise just installed was never resolved, so every
later connect() awaited a promise that could never settle — each one timing out
and installing another dead queue. BLE connect stayed wedged for the rest of the
process lifetime rather than recovering. Release the slot on the timeout path.
The before-quit BLE branch awaited shutdownAppResources() inside a finally with
no catch, so a rejection escaped the async IIFE as an unhandled rejection and
app.quit() never ran. Since the handler had already called preventDefault(),
that left the app running with no path to exit. The non-BLE branch below already
handled this; make the two consistent.
Adds a behavior test that wedges the queue behind a holder that never releases
and asserts a follow-up connect still succeeds. Verified to fail without the fix.
* fix(main): gate MQTT handlers on client identity and harden sidecar stop race
Stale mqtt.js clients keep emitting close/error/offline after end(true). With the
handlers bound to whatever `this.client` currently points at, a replaced client's
close cleared the live client's connack timer, inflated its retry count, and
scheduled a reconnect that tore down a healthy connection. Both adapters now gate
every handler on client identity; the MeshCore adapter already used this idiom for
packetsend/packetreceive, so this extends it to the rest.
The Reticulum sidecar could also report a stopped process as running: stop() kills
the child during the health poll, so a response landing just before the kill marked
a dead PID running, connected a WS to a dead port, and armed a watchdog for a gone
process. Re-check abort and process identity after the poll, and stop the watchdog
on exit so its interval does not leak across the next spawn.
Also fold in verified low-risk audit findings:
- Normalize six Meshtastic node-id renders through formatMeshtasticNodeId so a
negative int32 cannot render as a 9-char hex id (MeshCore panels use a separate
id space and are left alone).
- Guard six fire-and-forget ipcMain.on handlers with validateIpcSender, plus a test
that enumerates every ipcMain.on channel so new ones cannot ship unguarded.
- Add object-src/base-uri/form-action to the renderer CSP (frame-ancestors is
omitted: the spec ignores it in <meta> and the renderer is never framed).
- Fix ru common.save/common.unknown wording and de "Stornieren" -> "Abbrechen",
and canonicalize the eight "Hops" keys per locale (ru mixed three spellings and
reticulumPeers.hops was untranslated in every locale).
- Correct docs drift: relative CI link, orphaned docs/index.md entries, the
useLongSessionMaintenance Noble condition, and the src/main/ipc module list.
* fix(i18n): add missing Slavic plural forms so counts stop falling back to English
Russian, Ukrainian, Polish and Czech define plural categories English does not.
Only _one/_other existed, so i18next found no match for `few` (2-4) or `many`
(0, 5-20) and fell through the fallback chain to English: ru at count=3 rendered
"3 nodes", not "3 узла". This affected every counted string in four locales at
the most common counts.
Add the 320 missing forms across 47 plural families: Russian `few` in the
genitive singular, uk/pl/cs `few` in the nominative plural, and genitive plural
for `many`. Czech `many` is deliberately absent (fractions-only, unreachable for
integer counts), as is Romance `many` (starts at 1,000,000).
Also translate the pl/cs "config issue" and "hop" families, which were still
literal English in _one/_other and so leaked regardless of category.
check:i18n now requires every plural category a locale actually selects for
counts 0-200, scoped to families that locale has already started so a wholly
untranslated locale still reports as the existing "missing key(s)" warning.
Verified: rendering all 47 families at counts 0-22 across the four locales now
produces zero English fallbacks, down from 320.
* chore(reticulum): retire rsReticulum LinkClient Nomad overlay
Upstream main now resolves the remote pubkey via handler-free
resolve_destination_on_transport, with no HasPath gate and no temporary
announce handlers, so the overlay's apply script had already degraded to a
no-op. ratspeak/rsReticulum#14 was closed as superseded rather than merged.
Removes the patch, apply script, and its test, drops the entries from
RS_RETICULUM_APPLY_SCRIPTS and RATSPEAK_PATCH_ENTRIES so pnpm run update
stops tracking it, and updates the docs to point at floated origin/main.
* fix(rrc): resolve nicklist hashes to names and surface dropped /who replies
rrcd JOINED rosters carry no nicknames, and the once-per-room auto /who gate
survived reconnects, so a re-joined room showed bare hex forever. Re-arm the
gate on the handshake, fall back to nicks learned anywhere in the hub's
transcripts, and warn when a forced /who reply never arrives instead of
looking ignored.
* feat(games): allow challenging peers to Four in a Row
The FourInARowBoard added in be5ae05 was unreachable. A challenge is the only
way to create a session, and both challenge entry points offered just ttt and
chess, so no session could ever carry app_id four_in_a_row and the board branch
in GamesPanel was dead code.
Add the app id to GamesAppId and move the menu list into a single exported
GAMES_CHALLENGE_APPS. It had been duplicated verbatim in GamesPanel and
ReticulumGameChallengeButton, which is what let both menus drift out of sync
with the board dispatch in the first place.
The list stays curated rather than derived from the sidecar app manifest:
manifest app_id is an open string and the board dispatch falls through to
Tic-Tac-Toe for anything it does not recognize, so a manifest-driven menu could
offer a game that renders the wrong board. No sidecar change is needed -- it
already registers four_in_a_row and passes the app id through unvalidated.
Locale labels use each language's established name for the game rather than a
literal translation ("Vier gewinnt", "Puissance 4", "Forza 4", "四子棋").
The picker test asserts the full option list, so adding a future board without a
menu entry fails instead of silently shipping another unreachable game.
* feat(rrc): cache hub nicks in SQLite so names survive transcript clears
Oversized /who replies are dropped by the hub, so the nicklist depended on
whatever transcript happened to be loaded. Persist every nick sighting per
hub (chat, /who row, JOINED advisory) and hydrate it once per session, so a
peer who spoke only once still gets a name after a restart or history clear.
* fix(nomad): sanitize Micron page text at the innerHTML sink
The Micron editor added on this branch feeds a textarea's value straight into
the parser, which CodeQL flags as js/xss-through-dom: appendOutput writes page
text into span.innerHTML, and the only DOMPurify pass ran afterwards over the
whole document in convertMicronToHtml.
Route the three markup-building innerHTML writes (text spans, style-run spans,
link labels) through setSanitizedHtml so the barrier sits at the sink. In
practice wrapWord already escapes `<`, `>` and `&` when force-monospace is on,
so rendering is unchanged and tag-like text stays inert literal text; the sink
sanitize covers the non-monospace path and any future part producer that does
not escape.
Tests assert tag-like page text and link labels never become elements, and that
literal angle brackets and ampersands still render as typed.
Sign up for freeto 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.

writeH1 calls socket.setTypeOfService() unconditionally — uncaught EINVAL on macOS crashes the process

3 participants

@mcollina@codecov-commenter@ronag
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(h1): ignore type of service errors - #5547

Merged
mcollina merged 2 commits into
mainfrom
fix-h1-type-of-service-crash
Jul 12, 2026
Merged

fix(h1): ignore type of service errors#5547
mcollina merged 2 commits into
mainfrom
fix-h1-type-of-service-crash

Conversation

@mcollina

Copy link
Copy Markdown
Member

This relates to...

Fixes#5544.

Rationale

socket.setTypeOfService() is best-effort QoS marking. It can throw synchronously on some platforms depending on socket state, and the default request path should not touch it when no ToS value was explicitly requested.

Changes

Features

N/A

Bug Fixes

  • Skip setTypeOfService(0) for fresh HTTP/1.1 sockets when typeOfService was not explicitly provided.
  • Track the last ToS value applied to each socket and avoid redundant calls.
  • Reset a reused socket back to ToS 0 after an explicitly prioritized request.
  • Ignore synchronous setTypeOfService() failures so they do not abort requests or crash the process.
  • Add regression coverage for default requests and thrown setTypeOfService() errors.

Breaking Changes and Deprecations

N/A

Status

Tested with:

  • npx borp -p "test/ip-prioritization.js"
  • npm run lint

@codecov-commenter

codecov-commenter commented Jul 11, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 86.20690% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 93.44%. Comparing base (c7c7e7f) to head (794f99f).
⚠️ Report is 6 commits behind head on main.

Files with missing linesPatch %Lines
lib/dispatcher/client-h1.js85.71%4 Missing ⚠️
Additional details and impacted files
@@ Coverage Diff @@## main #5547 +/- ##
==========================================
- Coverage 93.45% 93.44% -0.01% 
==========================================
Files 110 110 Lines 37376 37443 +67 ==========================================
+ Hits 34928 34987 +59 - Misses 2448 2456 +8 

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@mcollina
mcollina merged commit 197a83d into mainJul 12, 2026
36 checks passed
@mcollina
mcollina deleted the fix-h1-type-of-service-crash branch July 12, 2026 13:13
@github-actionsgithub-actionsBot mentioned this pull request Jul 20, 2026
rinchen added a commit to Colorado-Mesh/mesh-client that referenced this pull request Aug 29, 2026
…ocklist tools, Four in a Row, RRC linkify + nick cache, plus MQTT/BLE/undici stability fixes (#912)
* feat(reticulum): in-app Micron page editor for Nomad My Pages
Hosting a Nomad site previously required editing .mu files on disk in an
external editor. My Pages now offers New/Edit/Delete on the hosted-page
list, opening a split-pane editor with the raw Micron source plus a
formatting toolbar on the left and a live preview on the right. Saves
write into the watched folder, so the existing FS watcher re-indexes them.
Authoring is gated on a configured content source rather than on active
serving, because page mutations only need a live stack and resolved roots.
The sidecar previously stringified NomadError straight to the renderer,
so write failures surfaced as untranslatable English prose interpolating
paths and byte counts. page_error_code now maps them to stable snake_case
codes (page_too_large, page_not_found, invalid_page_path, page_io_error,
page_not_utf8, page_write_failed) and logs the original detail, letting
nomadPageErrorHumanize translate what the user sees.
File uploads and the local-files list remain intentionally absent.
* feat(reticulum): add Serial, AX.25 KISS and Local interfaces via a shared catalog
Adding one Reticulum interface type previously took ~20 coordinated edits split
across the Rust sidecar and the renderer with no compiler link between them, so
the two sides could silently disagree about supported types, default modes or
flow-control policy.
Introduce src/shared/reticulumInterfaceCatalog.json as the single source of
truth. The sidecar compiles it in via include_str! and the renderer imports the
same file, making that drift structurally impossible rather than checked after
the fact. Both sides assert every configType is a name the upstream factory
synthesize_interface actually accepts.
On top of that, add SerialInterface, AX25KISSInterface and LocalInterface. A
catalog-driven ReticulumInterfaceFieldSet renders their forms, and add and edit
share one serialization path instead of two hand-maintained branches.
Also fixes a latent bug: ui_type_to_config echoed unknown types verbatim, so an
unmapped type was written to disk and then dropped on the next parse, leaving a
config block the user could see but never edit or delete. Unknown types are now
rejected up front.
The nine existing types keep their bespoke controls and hand-written branches;
the goal is that a new type needs no new branch, not that every old one is
rewritten. check-reticulum-interface-modes.mjs was scraping match arms that no
longer exist and is repointed at the mode list, the ap/gw aliases, and the
invariant that both sides still read the catalog.
* feat(rrc): linkify Nomad page addresses and LXMF hashes in chat
Reticulum addresses pasted into RRC rooms were inert text, so reaching a
shared page or peer meant copying the hash into another tab by hand.
Addresses now render as inline links: `<hash>:/page/foo.mu` switches to the
Nomad tab and loads the page, while a bare or lxmf:// hash opens a Chat DM.
* fix(reticulum): render Micron editor preview like the Nomad browser
The editor mounted parsed Micron into a bare div, so it missed the
nomad-micron-page class that supplies white-space: pre and the bundled
MeshClientNomadMono font. HTML collapsed the interior padding spaces and
the fallback UI font kept borders from joining, which scrambled Unicode
and ASCII box-drawing art that renders correctly in the browser.
Render the preview through NomadMicronPageView itself rather than copying
its class list, which would silently drift. Links get no-op handlers since
the modal has no browsing context, and the pane scrolls on both axes so
wide art stays reachable.
Fit-width wraps long lines and breaks wide art even once the class is
restored, so the editor gains the same toggle as the browser, backed by a
shared nomadPageFitWidth helper reading the one storage key so the two
surfaces always agree.
* feat(reticulum): clear path table, blocklist import/export, auto-resend on announce
Three MeshChatX parity gaps, each independently useful:
Clear path table — a stale route can keep sending to a next hop that no
longer works, with no way to force rediscovery short of restarting the
stack. Uses the upstream TransportQuery::DropPathTable verb (a single
control query, not a per-entry DropPath loop) and also resets the
path-request backoff gate so peers that exhausted their retry budget can
be discovered again. Local caches are dropped even when the query times
out, since the transport-side clear may still have applied.
Blocklist import/export — a blocklist was previously trapped in one
install's SQLite. Import validates strictly rather than reusing the
lenient normalizer, whose documented non-hex fallback would otherwise
persist junk as blocked hashes, and uses its own SQL so imported-vs-
skipped counts are accurate (db:blockContact always reports 1 change).
Auto-resend on announce — an announce means a path may exist again, so
failed sends to that peer are worth one retry. Off by default; guarded by
a per-destination cooldown, an in-flight lock, and a per-announce cap so
an announce burst cannot fan out into a send storm.
Also corrects the parity notes: the claim that no bulk path-table clear
verb existed was wrong, and adds the deferred transport-blackhole
write-up (it keys on identity hashes, not the destination hashes our
blocklist stores, so existing rows cannot be mirrored).
* fix(main): stop setTypeOfService EINVAL from crashing the main process
Link-preview fetches use undici, which set the advisory IP ToS byte on every
outbound HTTP/1.1 socket. macOS rejects that with EINVAL on some network
configurations, and because the call happens inside a socket connect listener
the throw escaped fetchLinkPreview's try/catch and surfaced as an "Unexpected
Error" dialog. Depend on undici ^8.10.0 directly for the upstream guard
(nodejs/undici#5547) — it was previously an undeclared transitive dependency
capped at 7.x — and keep a scoped undici@<7.29.0 floor so jsdom and node-gyp
stay on 7.29.0. A narrow uncaughtException/unhandledRejection filter now
tolerates the failure even if a future socket option regresses.
Add check:pinned-majors, run from pnpm run update, so an override that falls
behind a newer npm major surfaces instead of silently withholding fixes like
this one; caps that are correct carry a documented reason.
* fix(reticulum): style clear path table as caution, not danger
The action was neutral slate with a red confirm button, which read as
destructive. Clearing the path table only forces route rediscovery — RNS
repopulates the entries on its own — so red overstated the risk on an
action users should reach for when messages to a peer stop landing.
Uses the yellow palette already used by ReticulumPnHostingDangerZone,
which sits directly above it in the network panel, and drops the danger
prop so ConfirmModal picks its yellow confirm button instead of red.
* fix(reticulum): collapse the RNode flasher by default
Flashing firmware is a rare, one-off task, but its section occupied the
top of the Administration tab and pushed the factory reset danger zone
down the page. Collapsing it makes the tab's remaining content visible
without scrolling.
Uses the same <details> disclosure pattern as RadioPanel and
ReticulumInterfacesPanel via the shared DetailsChevron. A closed
<details> still mounts its children, so RNodeFlasherSection keeps the
pairing session and flash/provision marks it deliberately preserves
across Admin remounts; conditional rendering would have discarded them.
The test records that so the distinction is not lost.
* refactor(reticulum): move blocked contacts to the Network tab
The blocklist is Reticulum-specific but sat in App Settings between the
database export/import and Appearance, where it was both hard to find
and surprising to encounter — the section hides itself unless the
hydrated identity is Reticulum, so most users saw a gap there instead.
The Network tab already holds the rest of the Reticulum-scoped controls.
Presented as a collapsible section like its neighbors, which supplies
the heading, so the component's own now-duplicate h3 is dropped.
Deliberately not gated on sidecarApiReady, unlike the surrounding
sections: the blocklist is local SQLite state hydrated on identityId
alone, so gating would hide it whenever the stack was stopped — the
point at which pruning it before reconnecting is most useful.
The protocol scope check moves into blockStore as
useReticulumBlocklistIdentityId rather than travelling into the panel as
a raw compare. AppPanel was grandfathered onto the
check-protocol-string-gates allowlist; ReticulumNetworkPanel is not, and
widening that legacy list is the wrong direction when the store is the
right home for knowing which protocol it currently holds.
i18n keys stay under appPanel.reticulumBlocklist.* to avoid rewriting
all 16 locale files for a move.
* feat(rrc): prompt when a pasted Reticulum hash is ambiguous
A bare 32-hex hash could be a Nomad node or an LXMF peer, and assuming a DM
sent people to the wrong place. Clicking one now opens a small dialog to pick
Nomad page or direct message; explicit `:/page` and lxmf:// forms still act
immediately.
* chore(deps): update dependencies and move off deprecated jest-dom 6.10.0
`pnpm run update` picked up systeminformation, @testing-library/react, and
@vitejs/plugin-react patch releases. @testing-library/jest-dom 6.10.0 was
deprecated upstream as a bad minor (it shipped the Node >=22 and
@testing-library/dom peer requirements intended for 7.x), so move to ^7.0.1
rather than pinning back to 6.9.1. All three of its requirements were already
satisfied, so no test migration was needed.
* feat(games): add Four in a Row board
The Games board dispatch was a binary chess-or-else fallback, so
`four_in_a_row` sessions rendered through TicTacToeBoard — a 42-cell board
truncated to 9 cells and moves sent as `{ i }` instead of `{ c }`. The sidecar
has registered the app for a while, so these sessions were reachable and
broken, not merely unimplemented.
Implemented against lrgp-rs SPEC.md appendix C: 7x6 gravity board, 42 cells
row-major (`row * 7 + column`), theme-neutral `A`/`B` markers, outgoing payload
exactly `{ c }` with the sidecar deriving move number, landing cell, and
terminal claims. Adds an optimistic column drop matching the existing TTT and
chess pattern so the disc paints before the sidecar confirms, with win and draw
detection for rollback.
The whole column is the button rather than each cell, since gravity means the
player picks a column. Discs are aria-hidden and the column button carries the
readable state ("Column 3, bottom to top: A, B. Drop here"), giving screen
reader users the board contents without 42 focus stops.
Seven of the eleven new strings are identical to existing gamesPanel.ttt values,
so those reuse the reviewed per-locale translations; the four new keys are
translated for all 15 locales.
Bumps the Ratspeak games-parity reviewed-ref to v1.0.31 (reviewed: voice
message reliability only, no Games API delta) and updates the parity matrix.
Wire interop rows stay `todo` pending the manual gold test against Ratspeak.
* fix(main): release BLE connect queue on wait timeout and always quit after BLE shutdown
BLE connect installs a fresh `connectQueue` promise before awaiting the previous
holder, but that await sat outside the try whose finally calls releaseQueue().
When the wait timed out, the promise just installed was never resolved, so every
later connect() awaited a promise that could never settle — each one timing out
and installing another dead queue. BLE connect stayed wedged for the rest of the
process lifetime rather than recovering. Release the slot on the timeout path.
The before-quit BLE branch awaited shutdownAppResources() inside a finally with
no catch, so a rejection escaped the async IIFE as an unhandled rejection and
app.quit() never ran. Since the handler had already called preventDefault(),
that left the app running with no path to exit. The non-BLE branch below already
handled this; make the two consistent.
Adds a behavior test that wedges the queue behind a holder that never releases
and asserts a follow-up connect still succeeds. Verified to fail without the fix.
* fix(main): gate MQTT handlers on client identity and harden sidecar stop race
Stale mqtt.js clients keep emitting close/error/offline after end(true). With the
handlers bound to whatever `this.client` currently points at, a replaced client's
close cleared the live client's connack timer, inflated its retry count, and
scheduled a reconnect that tore down a healthy connection. Both adapters now gate
every handler on client identity; the MeshCore adapter already used this idiom for
packetsend/packetreceive, so this extends it to the rest.
The Reticulum sidecar could also report a stopped process as running: stop() kills
the child during the health poll, so a response landing just before the kill marked
a dead PID running, connected a WS to a dead port, and armed a watchdog for a gone
process. Re-check abort and process identity after the poll, and stop the watchdog
on exit so its interval does not leak across the next spawn.
Also fold in verified low-risk audit findings:
- Normalize six Meshtastic node-id renders through formatMeshtasticNodeId so a
negative int32 cannot render as a 9-char hex id (MeshCore panels use a separate
id space and are left alone).
- Guard six fire-and-forget ipcMain.on handlers with validateIpcSender, plus a test
that enumerates every ipcMain.on channel so new ones cannot ship unguarded.
- Add object-src/base-uri/form-action to the renderer CSP (frame-ancestors is
omitted: the spec ignores it in <meta> and the renderer is never framed).
- Fix ru common.save/common.unknown wording and de "Stornieren" -> "Abbrechen",
and canonicalize the eight "Hops" keys per locale (ru mixed three spellings and
reticulumPeers.hops was untranslated in every locale).
- Correct docs drift: relative CI link, orphaned docs/index.md entries, the
useLongSessionMaintenance Noble condition, and the src/main/ipc module list.
* fix(i18n): add missing Slavic plural forms so counts stop falling back to English
Russian, Ukrainian, Polish and Czech define plural categories English does not.
Only _one/_other existed, so i18next found no match for `few` (2-4) or `many`
(0, 5-20) and fell through the fallback chain to English: ru at count=3 rendered
"3 nodes", not "3 узла". This affected every counted string in four locales at
the most common counts.
Add the 320 missing forms across 47 plural families: Russian `few` in the
genitive singular, uk/pl/cs `few` in the nominative plural, and genitive plural
for `many`. Czech `many` is deliberately absent (fractions-only, unreachable for
integer counts), as is Romance `many` (starts at 1,000,000).
Also translate the pl/cs "config issue" and "hop" families, which were still
literal English in _one/_other and so leaked regardless of category.
check:i18n now requires every plural category a locale actually selects for
counts 0-200, scoped to families that locale has already started so a wholly
untranslated locale still reports as the existing "missing key(s)" warning.
Verified: rendering all 47 families at counts 0-22 across the four locales now
produces zero English fallbacks, down from 320.
* chore(reticulum): retire rsReticulum LinkClient Nomad overlay
Upstream main now resolves the remote pubkey via handler-free
resolve_destination_on_transport, with no HasPath gate and no temporary
announce handlers, so the overlay's apply script had already degraded to a
no-op. ratspeak/rsReticulum#14 was closed as superseded rather than merged.
Removes the patch, apply script, and its test, drops the entries from
RS_RETICULUM_APPLY_SCRIPTS and RATSPEAK_PATCH_ENTRIES so pnpm run update
stops tracking it, and updates the docs to point at floated origin/main.
* fix(rrc): resolve nicklist hashes to names and surface dropped /who replies
rrcd JOINED rosters carry no nicknames, and the once-per-room auto /who gate
survived reconnects, so a re-joined room showed bare hex forever. Re-arm the
gate on the handshake, fall back to nicks learned anywhere in the hub's
transcripts, and warn when a forced /who reply never arrives instead of
looking ignored.
* feat(games): allow challenging peers to Four in a Row
The FourInARowBoard added in be5ae05 was unreachable. A challenge is the only
way to create a session, and both challenge entry points offered just ttt and
chess, so no session could ever carry app_id four_in_a_row and the board branch
in GamesPanel was dead code.
Add the app id to GamesAppId and move the menu list into a single exported
GAMES_CHALLENGE_APPS. It had been duplicated verbatim in GamesPanel and
ReticulumGameChallengeButton, which is what let both menus drift out of sync
with the board dispatch in the first place.
The list stays curated rather than derived from the sidecar app manifest:
manifest app_id is an open string and the board dispatch falls through to
Tic-Tac-Toe for anything it does not recognize, so a manifest-driven menu could
offer a game that renders the wrong board. No sidecar change is needed -- it
already registers four_in_a_row and passes the app id through unvalidated.
Locale labels use each language's established name for the game rather than a
literal translation ("Vier gewinnt", "Puissance 4", "Forza 4", "四子棋").
The picker test asserts the full option list, so adding a future board without a
menu entry fails instead of silently shipping another unreachable game.
* feat(rrc): cache hub nicks in SQLite so names survive transcript clears
Oversized /who replies are dropped by the hub, so the nicklist depended on
whatever transcript happened to be loaded. Persist every nick sighting per
hub (chat, /who row, JOINED advisory) and hydrate it once per session, so a
peer who spoke only once still gets a name after a restart or history clear.
* fix(nomad): sanitize Micron page text at the innerHTML sink
The Micron editor added on this branch feeds a textarea's value straight into
the parser, which CodeQL flags as js/xss-through-dom: appendOutput writes page
text into span.innerHTML, and the only DOMPurify pass ran afterwards over the
whole document in convertMicronToHtml.
Route the three markup-building innerHTML writes (text spans, style-run spans,
link labels) through setSanitizedHtml so the barrier sits at the sink. In
practice wrapWord already escapes `<`, `>` and `&` when force-monospace is on,
so rendering is unchanged and tag-like text stays inert literal text; the sink
sanitize covers the non-monospace path and any future part producer that does
not escape.
Tests assert tag-like page text and link labels never become elements, and that
literal angle brackets and ampersands still render as typed.
Sign up for freeto 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.

writeH1 calls socket.setTypeOfService() unconditionally — uncaught EINVAL on macOS crashes the process

3 participants

@mcollina@codecov-commenter@ronag
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(h1): ignore type of service errors - #5547

Merged
mcollina merged 2 commits into
mainfrom
fix-h1-type-of-service-crash
Jul 12, 2026
Merged

fix(h1): ignore type of service errors#5547
mcollina merged 2 commits into
mainfrom
fix-h1-type-of-service-crash

Conversation

@mcollina

Copy link
Copy Markdown
Member

This relates to...

Fixes#5544.

Rationale

socket.setTypeOfService() is best-effort QoS marking. It can throw synchronously on some platforms depending on socket state, and the default request path should not touch it when no ToS value was explicitly requested.

Changes

Features

N/A

Bug Fixes

  • Skip setTypeOfService(0) for fresh HTTP/1.1 sockets when typeOfService was not explicitly provided.
  • Track the last ToS value applied to each socket and avoid redundant calls.
  • Reset a reused socket back to ToS 0 after an explicitly prioritized request.
  • Ignore synchronous setTypeOfService() failures so they do not abort requests or crash the process.
  • Add regression coverage for default requests and thrown setTypeOfService() errors.

Breaking Changes and Deprecations

N/A

Status

Tested with:

  • npx borp -p "test/ip-prioritization.js"
  • npm run lint

@codecov-commenter

codecov-commenter commented Jul 11, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 86.20690% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 93.44%. Comparing base (c7c7e7f) to head (794f99f).
⚠️ Report is 6 commits behind head on main.

Files with missing linesPatch %Lines
lib/dispatcher/client-h1.js85.71%4 Missing ⚠️
Additional details and impacted files
@@ Coverage Diff @@## main #5547 +/- ##
==========================================
- Coverage 93.45% 93.44% -0.01% 
==========================================
Files 110 110 Lines 37376 37443 +67 ==========================================
+ Hits 34928 34987 +59 - Misses 2448 2456 +8 

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@mcollina
mcollina merged commit 197a83d into mainJul 12, 2026
36 checks passed
@mcollina
mcollina deleted the fix-h1-type-of-service-crash branch July 12, 2026 13:13
@github-actionsgithub-actionsBot mentioned this pull request Jul 20, 2026
rinchen added a commit to Colorado-Mesh/mesh-client that referenced this pull request Aug 29, 2026
…ocklist tools, Four in a Row, RRC linkify + nick cache, plus MQTT/BLE/undici stability fixes (#912)
* feat(reticulum): in-app Micron page editor for Nomad My Pages
Hosting a Nomad site previously required editing .mu files on disk in an
external editor. My Pages now offers New/Edit/Delete on the hosted-page
list, opening a split-pane editor with the raw Micron source plus a
formatting toolbar on the left and a live preview on the right. Saves
write into the watched folder, so the existing FS watcher re-indexes them.
Authoring is gated on a configured content source rather than on active
serving, because page mutations only need a live stack and resolved roots.
The sidecar previously stringified NomadError straight to the renderer,
so write failures surfaced as untranslatable English prose interpolating
paths and byte counts. page_error_code now maps them to stable snake_case
codes (page_too_large, page_not_found, invalid_page_path, page_io_error,
page_not_utf8, page_write_failed) and logs the original detail, letting
nomadPageErrorHumanize translate what the user sees.
File uploads and the local-files list remain intentionally absent.
* feat(reticulum): add Serial, AX.25 KISS and Local interfaces via a shared catalog
Adding one Reticulum interface type previously took ~20 coordinated edits split
across the Rust sidecar and the renderer with no compiler link between them, so
the two sides could silently disagree about supported types, default modes or
flow-control policy.
Introduce src/shared/reticulumInterfaceCatalog.json as the single source of
truth. The sidecar compiles it in via include_str! and the renderer imports the
same file, making that drift structurally impossible rather than checked after
the fact. Both sides assert every configType is a name the upstream factory
synthesize_interface actually accepts.
On top of that, add SerialInterface, AX25KISSInterface and LocalInterface. A
catalog-driven ReticulumInterfaceFieldSet renders their forms, and add and edit
share one serialization path instead of two hand-maintained branches.
Also fixes a latent bug: ui_type_to_config echoed unknown types verbatim, so an
unmapped type was written to disk and then dropped on the next parse, leaving a
config block the user could see but never edit or delete. Unknown types are now
rejected up front.
The nine existing types keep their bespoke controls and hand-written branches;
the goal is that a new type needs no new branch, not that every old one is
rewritten. check-reticulum-interface-modes.mjs was scraping match arms that no
longer exist and is repointed at the mode list, the ap/gw aliases, and the
invariant that both sides still read the catalog.
* feat(rrc): linkify Nomad page addresses and LXMF hashes in chat
Reticulum addresses pasted into RRC rooms were inert text, so reaching a
shared page or peer meant copying the hash into another tab by hand.
Addresses now render as inline links: `<hash>:/page/foo.mu` switches to the
Nomad tab and loads the page, while a bare or lxmf:// hash opens a Chat DM.
* fix(reticulum): render Micron editor preview like the Nomad browser
The editor mounted parsed Micron into a bare div, so it missed the
nomad-micron-page class that supplies white-space: pre and the bundled
MeshClientNomadMono font. HTML collapsed the interior padding spaces and
the fallback UI font kept borders from joining, which scrambled Unicode
and ASCII box-drawing art that renders correctly in the browser.
Render the preview through NomadMicronPageView itself rather than copying
its class list, which would silently drift. Links get no-op handlers since
the modal has no browsing context, and the pane scrolls on both axes so
wide art stays reachable.
Fit-width wraps long lines and breaks wide art even once the class is
restored, so the editor gains the same toggle as the browser, backed by a
shared nomadPageFitWidth helper reading the one storage key so the two
surfaces always agree.
* feat(reticulum): clear path table, blocklist import/export, auto-resend on announce
Three MeshChatX parity gaps, each independently useful:
Clear path table — a stale route can keep sending to a next hop that no
longer works, with no way to force rediscovery short of restarting the
stack. Uses the upstream TransportQuery::DropPathTable verb (a single
control query, not a per-entry DropPath loop) and also resets the
path-request backoff gate so peers that exhausted their retry budget can
be discovered again. Local caches are dropped even when the query times
out, since the transport-side clear may still have applied.
Blocklist import/export — a blocklist was previously trapped in one
install's SQLite. Import validates strictly rather than reusing the
lenient normalizer, whose documented non-hex fallback would otherwise
persist junk as blocked hashes, and uses its own SQL so imported-vs-
skipped counts are accurate (db:blockContact always reports 1 change).
Auto-resend on announce — an announce means a path may exist again, so
failed sends to that peer are worth one retry. Off by default; guarded by
a per-destination cooldown, an in-flight lock, and a per-announce cap so
an announce burst cannot fan out into a send storm.
Also corrects the parity notes: the claim that no bulk path-table clear
verb existed was wrong, and adds the deferred transport-blackhole
write-up (it keys on identity hashes, not the destination hashes our
blocklist stores, so existing rows cannot be mirrored).
* fix(main): stop setTypeOfService EINVAL from crashing the main process
Link-preview fetches use undici, which set the advisory IP ToS byte on every
outbound HTTP/1.1 socket. macOS rejects that with EINVAL on some network
configurations, and because the call happens inside a socket connect listener
the throw escaped fetchLinkPreview's try/catch and surfaced as an "Unexpected
Error" dialog. Depend on undici ^8.10.0 directly for the upstream guard
(nodejs/undici#5547) — it was previously an undeclared transitive dependency
capped at 7.x — and keep a scoped undici@<7.29.0 floor so jsdom and node-gyp
stay on 7.29.0. A narrow uncaughtException/unhandledRejection filter now
tolerates the failure even if a future socket option regresses.
Add check:pinned-majors, run from pnpm run update, so an override that falls
behind a newer npm major surfaces instead of silently withholding fixes like
this one; caps that are correct carry a documented reason.
* fix(reticulum): style clear path table as caution, not danger
The action was neutral slate with a red confirm button, which read as
destructive. Clearing the path table only forces route rediscovery — RNS
repopulates the entries on its own — so red overstated the risk on an
action users should reach for when messages to a peer stop landing.
Uses the yellow palette already used by ReticulumPnHostingDangerZone,
which sits directly above it in the network panel, and drops the danger
prop so ConfirmModal picks its yellow confirm button instead of red.
* fix(reticulum): collapse the RNode flasher by default
Flashing firmware is a rare, one-off task, but its section occupied the
top of the Administration tab and pushed the factory reset danger zone
down the page. Collapsing it makes the tab's remaining content visible
without scrolling.
Uses the same <details> disclosure pattern as RadioPanel and
ReticulumInterfacesPanel via the shared DetailsChevron. A closed
<details> still mounts its children, so RNodeFlasherSection keeps the
pairing session and flash/provision marks it deliberately preserves
across Admin remounts; conditional rendering would have discarded them.
The test records that so the distinction is not lost.
* refactor(reticulum): move blocked contacts to the Network tab
The blocklist is Reticulum-specific but sat in App Settings between the
database export/import and Appearance, where it was both hard to find
and surprising to encounter — the section hides itself unless the
hydrated identity is Reticulum, so most users saw a gap there instead.
The Network tab already holds the rest of the Reticulum-scoped controls.
Presented as a collapsible section like its neighbors, which supplies
the heading, so the component's own now-duplicate h3 is dropped.
Deliberately not gated on sidecarApiReady, unlike the surrounding
sections: the blocklist is local SQLite state hydrated on identityId
alone, so gating would hide it whenever the stack was stopped — the
point at which pruning it before reconnecting is most useful.
The protocol scope check moves into blockStore as
useReticulumBlocklistIdentityId rather than travelling into the panel as
a raw compare. AppPanel was grandfathered onto the
check-protocol-string-gates allowlist; ReticulumNetworkPanel is not, and
widening that legacy list is the wrong direction when the store is the
right home for knowing which protocol it currently holds.
i18n keys stay under appPanel.reticulumBlocklist.* to avoid rewriting
all 16 locale files for a move.
* feat(rrc): prompt when a pasted Reticulum hash is ambiguous
A bare 32-hex hash could be a Nomad node or an LXMF peer, and assuming a DM
sent people to the wrong place. Clicking one now opens a small dialog to pick
Nomad page or direct message; explicit `:/page` and lxmf:// forms still act
immediately.
* chore(deps): update dependencies and move off deprecated jest-dom 6.10.0
`pnpm run update` picked up systeminformation, @testing-library/react, and
@vitejs/plugin-react patch releases. @testing-library/jest-dom 6.10.0 was
deprecated upstream as a bad minor (it shipped the Node >=22 and
@testing-library/dom peer requirements intended for 7.x), so move to ^7.0.1
rather than pinning back to 6.9.1. All three of its requirements were already
satisfied, so no test migration was needed.
* feat(games): add Four in a Row board
The Games board dispatch was a binary chess-or-else fallback, so
`four_in_a_row` sessions rendered through TicTacToeBoard — a 42-cell board
truncated to 9 cells and moves sent as `{ i }` instead of `{ c }`. The sidecar
has registered the app for a while, so these sessions were reachable and
broken, not merely unimplemented.
Implemented against lrgp-rs SPEC.md appendix C: 7x6 gravity board, 42 cells
row-major (`row * 7 + column`), theme-neutral `A`/`B` markers, outgoing payload
exactly `{ c }` with the sidecar deriving move number, landing cell, and
terminal claims. Adds an optimistic column drop matching the existing TTT and
chess pattern so the disc paints before the sidecar confirms, with win and draw
detection for rollback.
The whole column is the button rather than each cell, since gravity means the
player picks a column. Discs are aria-hidden and the column button carries the
readable state ("Column 3, bottom to top: A, B. Drop here"), giving screen
reader users the board contents without 42 focus stops.
Seven of the eleven new strings are identical to existing gamesPanel.ttt values,
so those reuse the reviewed per-locale translations; the four new keys are
translated for all 15 locales.
Bumps the Ratspeak games-parity reviewed-ref to v1.0.31 (reviewed: voice
message reliability only, no Games API delta) and updates the parity matrix.
Wire interop rows stay `todo` pending the manual gold test against Ratspeak.
* fix(main): release BLE connect queue on wait timeout and always quit after BLE shutdown
BLE connect installs a fresh `connectQueue` promise before awaiting the previous
holder, but that await sat outside the try whose finally calls releaseQueue().
When the wait timed out, the promise just installed was never resolved, so every
later connect() awaited a promise that could never settle — each one timing out
and installing another dead queue. BLE connect stayed wedged for the rest of the
process lifetime rather than recovering. Release the slot on the timeout path.
The before-quit BLE branch awaited shutdownAppResources() inside a finally with
no catch, so a rejection escaped the async IIFE as an unhandled rejection and
app.quit() never ran. Since the handler had already called preventDefault(),
that left the app running with no path to exit. The non-BLE branch below already
handled this; make the two consistent.
Adds a behavior test that wedges the queue behind a holder that never releases
and asserts a follow-up connect still succeeds. Verified to fail without the fix.
* fix(main): gate MQTT handlers on client identity and harden sidecar stop race
Stale mqtt.js clients keep emitting close/error/offline after end(true). With the
handlers bound to whatever `this.client` currently points at, a replaced client's
close cleared the live client's connack timer, inflated its retry count, and
scheduled a reconnect that tore down a healthy connection. Both adapters now gate
every handler on client identity; the MeshCore adapter already used this idiom for
packetsend/packetreceive, so this extends it to the rest.
The Reticulum sidecar could also report a stopped process as running: stop() kills
the child during the health poll, so a response landing just before the kill marked
a dead PID running, connected a WS to a dead port, and armed a watchdog for a gone
process. Re-check abort and process identity after the poll, and stop the watchdog
on exit so its interval does not leak across the next spawn.
Also fold in verified low-risk audit findings:
- Normalize six Meshtastic node-id renders through formatMeshtasticNodeId so a
negative int32 cannot render as a 9-char hex id (MeshCore panels use a separate
id space and are left alone).
- Guard six fire-and-forget ipcMain.on handlers with validateIpcSender, plus a test
that enumerates every ipcMain.on channel so new ones cannot ship unguarded.
- Add object-src/base-uri/form-action to the renderer CSP (frame-ancestors is
omitted: the spec ignores it in <meta> and the renderer is never framed).
- Fix ru common.save/common.unknown wording and de "Stornieren" -> "Abbrechen",
and canonicalize the eight "Hops" keys per locale (ru mixed three spellings and
reticulumPeers.hops was untranslated in every locale).
- Correct docs drift: relative CI link, orphaned docs/index.md entries, the
useLongSessionMaintenance Noble condition, and the src/main/ipc module list.
* fix(i18n): add missing Slavic plural forms so counts stop falling back to English
Russian, Ukrainian, Polish and Czech define plural categories English does not.
Only _one/_other existed, so i18next found no match for `few` (2-4) or `many`
(0, 5-20) and fell through the fallback chain to English: ru at count=3 rendered
"3 nodes", not "3 узла". This affected every counted string in four locales at
the most common counts.
Add the 320 missing forms across 47 plural families: Russian `few` in the
genitive singular, uk/pl/cs `few` in the nominative plural, and genitive plural
for `many`. Czech `many` is deliberately absent (fractions-only, unreachable for
integer counts), as is Romance `many` (starts at 1,000,000).
Also translate the pl/cs "config issue" and "hop" families, which were still
literal English in _one/_other and so leaked regardless of category.
check:i18n now requires every plural category a locale actually selects for
counts 0-200, scoped to families that locale has already started so a wholly
untranslated locale still reports as the existing "missing key(s)" warning.
Verified: rendering all 47 families at counts 0-22 across the four locales now
produces zero English fallbacks, down from 320.
* chore(reticulum): retire rsReticulum LinkClient Nomad overlay
Upstream main now resolves the remote pubkey via handler-free
resolve_destination_on_transport, with no HasPath gate and no temporary
announce handlers, so the overlay's apply script had already degraded to a
no-op. ratspeak/rsReticulum#14 was closed as superseded rather than merged.
Removes the patch, apply script, and its test, drops the entries from
RS_RETICULUM_APPLY_SCRIPTS and RATSPEAK_PATCH_ENTRIES so pnpm run update
stops tracking it, and updates the docs to point at floated origin/main.
* fix(rrc): resolve nicklist hashes to names and surface dropped /who replies
rrcd JOINED rosters carry no nicknames, and the once-per-room auto /who gate
survived reconnects, so a re-joined room showed bare hex forever. Re-arm the
gate on the handshake, fall back to nicks learned anywhere in the hub's
transcripts, and warn when a forced /who reply never arrives instead of
looking ignored.
* feat(games): allow challenging peers to Four in a Row
The FourInARowBoard added in be5ae05 was unreachable. A challenge is the only
way to create a session, and both challenge entry points offered just ttt and
chess, so no session could ever carry app_id four_in_a_row and the board branch
in GamesPanel was dead code.
Add the app id to GamesAppId and move the menu list into a single exported
GAMES_CHALLENGE_APPS. It had been duplicated verbatim in GamesPanel and
ReticulumGameChallengeButton, which is what let both menus drift out of sync
with the board dispatch in the first place.
The list stays curated rather than derived from the sidecar app manifest:
manifest app_id is an open string and the board dispatch falls through to
Tic-Tac-Toe for anything it does not recognize, so a manifest-driven menu could
offer a game that renders the wrong board. No sidecar change is needed -- it
already registers four_in_a_row and passes the app id through unvalidated.
Locale labels use each language's established name for the game rather than a
literal translation ("Vier gewinnt", "Puissance 4", "Forza 4", "四子棋").
The picker test asserts the full option list, so adding a future board without a
menu entry fails instead of silently shipping another unreachable game.
* feat(rrc): cache hub nicks in SQLite so names survive transcript clears
Oversized /who replies are dropped by the hub, so the nicklist depended on
whatever transcript happened to be loaded. Persist every nick sighting per
hub (chat, /who row, JOINED advisory) and hydrate it once per session, so a
peer who spoke only once still gets a name after a restart or history clear.
* fix(nomad): sanitize Micron page text at the innerHTML sink
The Micron editor added on this branch feeds a textarea's value straight into
the parser, which CodeQL flags as js/xss-through-dom: appendOutput writes page
text into span.innerHTML, and the only DOMPurify pass ran afterwards over the
whole document in convertMicronToHtml.
Route the three markup-building innerHTML writes (text spans, style-run spans,
link labels) through setSanitizedHtml so the barrier sits at the sink. In
practice wrapWord already escapes `<`, `>` and `&` when force-monospace is on,
so rendering is unchanged and tag-like text stays inert literal text; the sink
sanitize covers the non-monospace path and any future part producer that does
not escape.
Tests assert tag-like page text and link labels never become elements, and that
literal angle brackets and ampersands still render as typed.
Sign up for freeto 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.

writeH1 calls socket.setTypeOfService() unconditionally — uncaught EINVAL on macOS crashes the process

3 participants

@mcollina@codecov-commenter@ronag
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

fix(h1): ignore type of service errors - #5547

Merged
mcollina merged 2 commits into
mainfrom
fix-h1-type-of-service-crash
Jul 12, 2026
Merged

fix(h1): ignore type of service errors#5547
mcollina merged 2 commits into
mainfrom
fix-h1-type-of-service-crash

Conversation

@mcollina

Copy link
Copy Markdown
Member

This relates to...

Fixes#5544.

Rationale

socket.setTypeOfService() is best-effort QoS marking. It can throw synchronously on some platforms depending on socket state, and the default request path should not touch it when no ToS value was explicitly requested.

Changes

Features

N/A

Bug Fixes

  • Skip setTypeOfService(0) for fresh HTTP/1.1 sockets when typeOfService was not explicitly provided.
  • Track the last ToS value applied to each socket and avoid redundant calls.
  • Reset a reused socket back to ToS 0 after an explicitly prioritized request.
  • Ignore synchronous setTypeOfService() failures so they do not abort requests or crash the process.
  • Add regression coverage for default requests and thrown setTypeOfService() errors.

Breaking Changes and Deprecations

N/A

Status

Tested with:

  • npx borp -p "test/ip-prioritization.js"
  • npm run lint

@codecov-commenter

codecov-commenter commented Jul 11, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 86.20690% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 93.44%. Comparing base (c7c7e7f) to head (794f99f).
⚠️ Report is 6 commits behind head on main.

Files with missing linesPatch %Lines
lib/dispatcher/client-h1.js85.71%4 Missing ⚠️
Additional details and impacted files
@@ Coverage Diff @@## main #5547 +/- ##
==========================================
- Coverage 93.45% 93.44% -0.01% 
==========================================
Files 110 110 Lines 37376 37443 +67 ==========================================
+ Hits 34928 34987 +59 - Misses 2448 2456 +8 

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@mcollina
mcollina merged commit 197a83d into mainJul 12, 2026
36 checks passed
@mcollina
mcollina deleted the fix-h1-type-of-service-crash branch July 12, 2026 13:13
@github-actionsgithub-actionsBot mentioned this pull request Jul 20, 2026
rinchen added a commit to Colorado-Mesh/mesh-client that referenced this pull request Aug 29, 2026
…ocklist tools, Four in a Row, RRC linkify + nick cache, plus MQTT/BLE/undici stability fixes (#912)
* feat(reticulum): in-app Micron page editor for Nomad My Pages
Hosting a Nomad site previously required editing .mu files on disk in an
external editor. My Pages now offers New/Edit/Delete on the hosted-page
list, opening a split-pane editor with the raw Micron source plus a
formatting toolbar on the left and a live preview on the right. Saves
write into the watched folder, so the existing FS watcher re-indexes them.
Authoring is gated on a configured content source rather than on active
serving, because page mutations only need a live stack and resolved roots.
The sidecar previously stringified NomadError straight to the renderer,
so write failures surfaced as untranslatable English prose interpolating
paths and byte counts. page_error_code now maps them to stable snake_case
codes (page_too_large, page_not_found, invalid_page_path, page_io_error,
page_not_utf8, page_write_failed) and logs the original detail, letting
nomadPageErrorHumanize translate what the user sees.
File uploads and the local-files list remain intentionally absent.
* feat(reticulum): add Serial, AX.25 KISS and Local interfaces via a shared catalog
Adding one Reticulum interface type previously took ~20 coordinated edits split
across the Rust sidecar and the renderer with no compiler link between them, so
the two sides could silently disagree about supported types, default modes or
flow-control policy.
Introduce src/shared/reticulumInterfaceCatalog.json as the single source of
truth. The sidecar compiles it in via include_str! and the renderer imports the
same file, making that drift structurally impossible rather than checked after
the fact. Both sides assert every configType is a name the upstream factory
synthesize_interface actually accepts.
On top of that, add SerialInterface, AX25KISSInterface and LocalInterface. A
catalog-driven ReticulumInterfaceFieldSet renders their forms, and add and edit
share one serialization path instead of two hand-maintained branches.
Also fixes a latent bug: ui_type_to_config echoed unknown types verbatim, so an
unmapped type was written to disk and then dropped on the next parse, leaving a
config block the user could see but never edit or delete. Unknown types are now
rejected up front.
The nine existing types keep their bespoke controls and hand-written branches;
the goal is that a new type needs no new branch, not that every old one is
rewritten. check-reticulum-interface-modes.mjs was scraping match arms that no
longer exist and is repointed at the mode list, the ap/gw aliases, and the
invariant that both sides still read the catalog.
* feat(rrc): linkify Nomad page addresses and LXMF hashes in chat
Reticulum addresses pasted into RRC rooms were inert text, so reaching a
shared page or peer meant copying the hash into another tab by hand.
Addresses now render as inline links: `<hash>:/page/foo.mu` switches to the
Nomad tab and loads the page, while a bare or lxmf:// hash opens a Chat DM.
* fix(reticulum): render Micron editor preview like the Nomad browser
The editor mounted parsed Micron into a bare div, so it missed the
nomad-micron-page class that supplies white-space: pre and the bundled
MeshClientNomadMono font. HTML collapsed the interior padding spaces and
the fallback UI font kept borders from joining, which scrambled Unicode
and ASCII box-drawing art that renders correctly in the browser.
Render the preview through NomadMicronPageView itself rather than copying
its class list, which would silently drift. Links get no-op handlers since
the modal has no browsing context, and the pane scrolls on both axes so
wide art stays reachable.
Fit-width wraps long lines and breaks wide art even once the class is
restored, so the editor gains the same toggle as the browser, backed by a
shared nomadPageFitWidth helper reading the one storage key so the two
surfaces always agree.
* feat(reticulum): clear path table, blocklist import/export, auto-resend on announce
Three MeshChatX parity gaps, each independently useful:
Clear path table — a stale route can keep sending to a next hop that no
longer works, with no way to force rediscovery short of restarting the
stack. Uses the upstream TransportQuery::DropPathTable verb (a single
control query, not a per-entry DropPath loop) and also resets the
path-request backoff gate so peers that exhausted their retry budget can
be discovered again. Local caches are dropped even when the query times
out, since the transport-side clear may still have applied.
Blocklist import/export — a blocklist was previously trapped in one
install's SQLite. Import validates strictly rather than reusing the
lenient normalizer, whose documented non-hex fallback would otherwise
persist junk as blocked hashes, and uses its own SQL so imported-vs-
skipped counts are accurate (db:blockContact always reports 1 change).
Auto-resend on announce — an announce means a path may exist again, so
failed sends to that peer are worth one retry. Off by default; guarded by
a per-destination cooldown, an in-flight lock, and a per-announce cap so
an announce burst cannot fan out into a send storm.
Also corrects the parity notes: the claim that no bulk path-table clear
verb existed was wrong, and adds the deferred transport-blackhole
write-up (it keys on identity hashes, not the destination hashes our
blocklist stores, so existing rows cannot be mirrored).
* fix(main): stop setTypeOfService EINVAL from crashing the main process
Link-preview fetches use undici, which set the advisory IP ToS byte on every
outbound HTTP/1.1 socket. macOS rejects that with EINVAL on some network
configurations, and because the call happens inside a socket connect listener
the throw escaped fetchLinkPreview's try/catch and surfaced as an "Unexpected
Error" dialog. Depend on undici ^8.10.0 directly for the upstream guard
(nodejs/undici#5547) — it was previously an undeclared transitive dependency
capped at 7.x — and keep a scoped undici@<7.29.0 floor so jsdom and node-gyp
stay on 7.29.0. A narrow uncaughtException/unhandledRejection filter now
tolerates the failure even if a future socket option regresses.
Add check:pinned-majors, run from pnpm run update, so an override that falls
behind a newer npm major surfaces instead of silently withholding fixes like
this one; caps that are correct carry a documented reason.
* fix(reticulum): style clear path table as caution, not danger
The action was neutral slate with a red confirm button, which read as
destructive. Clearing the path table only forces route rediscovery — RNS
repopulates the entries on its own — so red overstated the risk on an
action users should reach for when messages to a peer stop landing.
Uses the yellow palette already used by ReticulumPnHostingDangerZone,
which sits directly above it in the network panel, and drops the danger
prop so ConfirmModal picks its yellow confirm button instead of red.
* fix(reticulum): collapse the RNode flasher by default
Flashing firmware is a rare, one-off task, but its section occupied the
top of the Administration tab and pushed the factory reset danger zone
down the page. Collapsing it makes the tab's remaining content visible
without scrolling.
Uses the same <details> disclosure pattern as RadioPanel and
ReticulumInterfacesPanel via the shared DetailsChevron. A closed
<details> still mounts its children, so RNodeFlasherSection keeps the
pairing session and flash/provision marks it deliberately preserves
across Admin remounts; conditional rendering would have discarded them.
The test records that so the distinction is not lost.
* refactor(reticulum): move blocked contacts to the Network tab
The blocklist is Reticulum-specific but sat in App Settings between the
database export/import and Appearance, where it was both hard to find
and surprising to encounter — the section hides itself unless the
hydrated identity is Reticulum, so most users saw a gap there instead.
The Network tab already holds the rest of the Reticulum-scoped controls.
Presented as a collapsible section like its neighbors, which supplies
the heading, so the component's own now-duplicate h3 is dropped.
Deliberately not gated on sidecarApiReady, unlike the surrounding
sections: the blocklist is local SQLite state hydrated on identityId
alone, so gating would hide it whenever the stack was stopped — the
point at which pruning it before reconnecting is most useful.
The protocol scope check moves into blockStore as
useReticulumBlocklistIdentityId rather than travelling into the panel as
a raw compare. AppPanel was grandfathered onto the
check-protocol-string-gates allowlist; ReticulumNetworkPanel is not, and
widening that legacy list is the wrong direction when the store is the
right home for knowing which protocol it currently holds.
i18n keys stay under appPanel.reticulumBlocklist.* to avoid rewriting
all 16 locale files for a move.
* feat(rrc): prompt when a pasted Reticulum hash is ambiguous
A bare 32-hex hash could be a Nomad node or an LXMF peer, and assuming a DM
sent people to the wrong place. Clicking one now opens a small dialog to pick
Nomad page or direct message; explicit `:/page` and lxmf:// forms still act
immediately.
* chore(deps): update dependencies and move off deprecated jest-dom 6.10.0
`pnpm run update` picked up systeminformation, @testing-library/react, and
@vitejs/plugin-react patch releases. @testing-library/jest-dom 6.10.0 was
deprecated upstream as a bad minor (it shipped the Node >=22 and
@testing-library/dom peer requirements intended for 7.x), so move to ^7.0.1
rather than pinning back to 6.9.1. All three of its requirements were already
satisfied, so no test migration was needed.
* feat(games): add Four in a Row board
The Games board dispatch was a binary chess-or-else fallback, so
`four_in_a_row` sessions rendered through TicTacToeBoard — a 42-cell board
truncated to 9 cells and moves sent as `{ i }` instead of `{ c }`. The sidecar
has registered the app for a while, so these sessions were reachable and
broken, not merely unimplemented.
Implemented against lrgp-rs SPEC.md appendix C: 7x6 gravity board, 42 cells
row-major (`row * 7 + column`), theme-neutral `A`/`B` markers, outgoing payload
exactly `{ c }` with the sidecar deriving move number, landing cell, and
terminal claims. Adds an optimistic column drop matching the existing TTT and
chess pattern so the disc paints before the sidecar confirms, with win and draw
detection for rollback.
The whole column is the button rather than each cell, since gravity means the
player picks a column. Discs are aria-hidden and the column button carries the
readable state ("Column 3, bottom to top: A, B. Drop here"), giving screen
reader users the board contents without 42 focus stops.
Seven of the eleven new strings are identical to existing gamesPanel.ttt values,
so those reuse the reviewed per-locale translations; the four new keys are
translated for all 15 locales.
Bumps the Ratspeak games-parity reviewed-ref to v1.0.31 (reviewed: voice
message reliability only, no Games API delta) and updates the parity matrix.
Wire interop rows stay `todo` pending the manual gold test against Ratspeak.
* fix(main): release BLE connect queue on wait timeout and always quit after BLE shutdown
BLE connect installs a fresh `connectQueue` promise before awaiting the previous
holder, but that await sat outside the try whose finally calls releaseQueue().
When the wait timed out, the promise just installed was never resolved, so every
later connect() awaited a promise that could never settle — each one timing out
and installing another dead queue. BLE connect stayed wedged for the rest of the
process lifetime rather than recovering. Release the slot on the timeout path.
The before-quit BLE branch awaited shutdownAppResources() inside a finally with
no catch, so a rejection escaped the async IIFE as an unhandled rejection and
app.quit() never ran. Since the handler had already called preventDefault(),
that left the app running with no path to exit. The non-BLE branch below already
handled this; make the two consistent.
Adds a behavior test that wedges the queue behind a holder that never releases
and asserts a follow-up connect still succeeds. Verified to fail without the fix.
* fix(main): gate MQTT handlers on client identity and harden sidecar stop race
Stale mqtt.js clients keep emitting close/error/offline after end(true). With the
handlers bound to whatever `this.client` currently points at, a replaced client's
close cleared the live client's connack timer, inflated its retry count, and
scheduled a reconnect that tore down a healthy connection. Both adapters now gate
every handler on client identity; the MeshCore adapter already used this idiom for
packetsend/packetreceive, so this extends it to the rest.
The Reticulum sidecar could also report a stopped process as running: stop() kills
the child during the health poll, so a response landing just before the kill marked
a dead PID running, connected a WS to a dead port, and armed a watchdog for a gone
process. Re-check abort and process identity after the poll, and stop the watchdog
on exit so its interval does not leak across the next spawn.
Also fold in verified low-risk audit findings:
- Normalize six Meshtastic node-id renders through formatMeshtasticNodeId so a
negative int32 cannot render as a 9-char hex id (MeshCore panels use a separate
id space and are left alone).
- Guard six fire-and-forget ipcMain.on handlers with validateIpcSender, plus a test
that enumerates every ipcMain.on channel so new ones cannot ship unguarded.
- Add object-src/base-uri/form-action to the renderer CSP (frame-ancestors is
omitted: the spec ignores it in <meta> and the renderer is never framed).
- Fix ru common.save/common.unknown wording and de "Stornieren" -> "Abbrechen",
and canonicalize the eight "Hops" keys per locale (ru mixed three spellings and
reticulumPeers.hops was untranslated in every locale).
- Correct docs drift: relative CI link, orphaned docs/index.md entries, the
useLongSessionMaintenance Noble condition, and the src/main/ipc module list.
* fix(i18n): add missing Slavic plural forms so counts stop falling back to English
Russian, Ukrainian, Polish and Czech define plural categories English does not.
Only _one/_other existed, so i18next found no match for `few` (2-4) or `many`
(0, 5-20) and fell through the fallback chain to English: ru at count=3 rendered
"3 nodes", not "3 узла". This affected every counted string in four locales at
the most common counts.
Add the 320 missing forms across 47 plural families: Russian `few` in the
genitive singular, uk/pl/cs `few` in the nominative plural, and genitive plural
for `many`. Czech `many` is deliberately absent (fractions-only, unreachable for
integer counts), as is Romance `many` (starts at 1,000,000).
Also translate the pl/cs "config issue" and "hop" families, which were still
literal English in _one/_other and so leaked regardless of category.
check:i18n now requires every plural category a locale actually selects for
counts 0-200, scoped to families that locale has already started so a wholly
untranslated locale still reports as the existing "missing key(s)" warning.
Verified: rendering all 47 families at counts 0-22 across the four locales now
produces zero English fallbacks, down from 320.
* chore(reticulum): retire rsReticulum LinkClient Nomad overlay
Upstream main now resolves the remote pubkey via handler-free
resolve_destination_on_transport, with no HasPath gate and no temporary
announce handlers, so the overlay's apply script had already degraded to a
no-op. ratspeak/rsReticulum#14 was closed as superseded rather than merged.
Removes the patch, apply script, and its test, drops the entries from
RS_RETICULUM_APPLY_SCRIPTS and RATSPEAK_PATCH_ENTRIES so pnpm run update
stops tracking it, and updates the docs to point at floated origin/main.
* fix(rrc): resolve nicklist hashes to names and surface dropped /who replies
rrcd JOINED rosters carry no nicknames, and the once-per-room auto /who gate
survived reconnects, so a re-joined room showed bare hex forever. Re-arm the
gate on the handshake, fall back to nicks learned anywhere in the hub's
transcripts, and warn when a forced /who reply never arrives instead of
looking ignored.
* feat(games): allow challenging peers to Four in a Row
The FourInARowBoard added in be5ae05 was unreachable. A challenge is the only
way to create a session, and both challenge entry points offered just ttt and
chess, so no session could ever carry app_id four_in_a_row and the board branch
in GamesPanel was dead code.
Add the app id to GamesAppId and move the menu list into a single exported
GAMES_CHALLENGE_APPS. It had been duplicated verbatim in GamesPanel and
ReticulumGameChallengeButton, which is what let both menus drift out of sync
with the board dispatch in the first place.
The list stays curated rather than derived from the sidecar app manifest:
manifest app_id is an open string and the board dispatch falls through to
Tic-Tac-Toe for anything it does not recognize, so a manifest-driven menu could
offer a game that renders the wrong board. No sidecar change is needed -- it
already registers four_in_a_row and passes the app id through unvalidated.
Locale labels use each language's established name for the game rather than a
literal translation ("Vier gewinnt", "Puissance 4", "Forza 4", "四子棋").
The picker test asserts the full option list, so adding a future board without a
menu entry fails instead of silently shipping another unreachable game.
* feat(rrc): cache hub nicks in SQLite so names survive transcript clears
Oversized /who replies are dropped by the hub, so the nicklist depended on
whatever transcript happened to be loaded. Persist every nick sighting per
hub (chat, /who row, JOINED advisory) and hydrate it once per session, so a
peer who spoke only once still gets a name after a restart or history clear.
* fix(nomad): sanitize Micron page text at the innerHTML sink
The Micron editor added on this branch feeds a textarea's value straight into
the parser, which CodeQL flags as js/xss-through-dom: appendOutput writes page
text into span.innerHTML, and the only DOMPurify pass ran afterwards over the
whole document in convertMicronToHtml.
Route the three markup-building innerHTML writes (text spans, style-run spans,
link labels) through setSanitizedHtml so the barrier sits at the sink. In
practice wrapWord already escapes `<`, `>` and `&` when force-monospace is on,
so rendering is unchanged and tag-like text stays inert literal text; the sink
sanitize covers the non-monospace path and any future part producer that does
not escape.
Tests assert tag-like page text and link labels never become elements, and that
literal angle brackets and ampersands still render as typed.
Sign up for freeto 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.

writeH1 calls socket.setTypeOfService() unconditionally — uncaught EINVAL on macOS crashes the process

3 participants

@mcollina@codecov-commenter@ronag
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(h1): ignore type of service errors - #5547

Merged
mcollina merged 2 commits into
mainfrom
fix-h1-type-of-service-crash
Jul 12, 2026
Merged

fix(h1): ignore type of service errors#5547
mcollina merged 2 commits into
mainfrom
fix-h1-type-of-service-crash

Conversation

@mcollina

Copy link
Copy Markdown
Member

This relates to...

Fixes#5544.

Rationale

socket.setTypeOfService() is best-effort QoS marking. It can throw synchronously on some platforms depending on socket state, and the default request path should not touch it when no ToS value was explicitly requested.

Changes

Features

N/A

Bug Fixes

  • Skip setTypeOfService(0) for fresh HTTP/1.1 sockets when typeOfService was not explicitly provided.
  • Track the last ToS value applied to each socket and avoid redundant calls.
  • Reset a reused socket back to ToS 0 after an explicitly prioritized request.
  • Ignore synchronous setTypeOfService() failures so they do not abort requests or crash the process.
  • Add regression coverage for default requests and thrown setTypeOfService() errors.

Breaking Changes and Deprecations

N/A

Status

Tested with:

  • npx borp -p "test/ip-prioritization.js"
  • npm run lint

@codecov-commenter

codecov-commenter commented Jul 11, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 86.20690% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 93.44%. Comparing base (c7c7e7f) to head (794f99f).
⚠️ Report is 6 commits behind head on main.

Files with missing linesPatch %Lines
lib/dispatcher/client-h1.js85.71%4 Missing ⚠️
Additional details and impacted files
@@ Coverage Diff @@## main #5547 +/- ##
==========================================
- Coverage 93.45% 93.44% -0.01% 
==========================================
Files 110 110 Lines 37376 37443 +67 ==========================================
+ Hits 34928 34987 +59 - Misses 2448 2456 +8 

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@mcollina
mcollina merged commit 197a83d into mainJul 12, 2026
36 checks passed
@mcollina
mcollina deleted the fix-h1-type-of-service-crash branch July 12, 2026 13:13
@github-actionsgithub-actionsBot mentioned this pull request Jul 20, 2026
rinchen added a commit to Colorado-Mesh/mesh-client that referenced this pull request Aug 29, 2026
…ocklist tools, Four in a Row, RRC linkify + nick cache, plus MQTT/BLE/undici stability fixes (#912)
* feat(reticulum): in-app Micron page editor for Nomad My Pages
Hosting a Nomad site previously required editing .mu files on disk in an
external editor. My Pages now offers New/Edit/Delete on the hosted-page
list, opening a split-pane editor with the raw Micron source plus a
formatting toolbar on the left and a live preview on the right. Saves
write into the watched folder, so the existing FS watcher re-indexes them.
Authoring is gated on a configured content source rather than on active
serving, because page mutations only need a live stack and resolved roots.
The sidecar previously stringified NomadError straight to the renderer,
so write failures surfaced as untranslatable English prose interpolating
paths and byte counts. page_error_code now maps them to stable snake_case
codes (page_too_large, page_not_found, invalid_page_path, page_io_error,
page_not_utf8, page_write_failed) and logs the original detail, letting
nomadPageErrorHumanize translate what the user sees.
File uploads and the local-files list remain intentionally absent.
* feat(reticulum): add Serial, AX.25 KISS and Local interfaces via a shared catalog
Adding one Reticulum interface type previously took ~20 coordinated edits split
across the Rust sidecar and the renderer with no compiler link between them, so
the two sides could silently disagree about supported types, default modes or
flow-control policy.
Introduce src/shared/reticulumInterfaceCatalog.json as the single source of
truth. The sidecar compiles it in via include_str! and the renderer imports the
same file, making that drift structurally impossible rather than checked after
the fact. Both sides assert every configType is a name the upstream factory
synthesize_interface actually accepts.
On top of that, add SerialInterface, AX25KISSInterface and LocalInterface. A
catalog-driven ReticulumInterfaceFieldSet renders their forms, and add and edit
share one serialization path instead of two hand-maintained branches.
Also fixes a latent bug: ui_type_to_config echoed unknown types verbatim, so an
unmapped type was written to disk and then dropped on the next parse, leaving a
config block the user could see but never edit or delete. Unknown types are now
rejected up front.
The nine existing types keep their bespoke controls and hand-written branches;
the goal is that a new type needs no new branch, not that every old one is
rewritten. check-reticulum-interface-modes.mjs was scraping match arms that no
longer exist and is repointed at the mode list, the ap/gw aliases, and the
invariant that both sides still read the catalog.
* feat(rrc): linkify Nomad page addresses and LXMF hashes in chat
Reticulum addresses pasted into RRC rooms were inert text, so reaching a
shared page or peer meant copying the hash into another tab by hand.
Addresses now render as inline links: `<hash>:/page/foo.mu` switches to the
Nomad tab and loads the page, while a bare or lxmf:// hash opens a Chat DM.
* fix(reticulum): render Micron editor preview like the Nomad browser
The editor mounted parsed Micron into a bare div, so it missed the
nomad-micron-page class that supplies white-space: pre and the bundled
MeshClientNomadMono font. HTML collapsed the interior padding spaces and
the fallback UI font kept borders from joining, which scrambled Unicode
and ASCII box-drawing art that renders correctly in the browser.
Render the preview through NomadMicronPageView itself rather than copying
its class list, which would silently drift. Links get no-op handlers since
the modal has no browsing context, and the pane scrolls on both axes so
wide art stays reachable.
Fit-width wraps long lines and breaks wide art even once the class is
restored, so the editor gains the same toggle as the browser, backed by a
shared nomadPageFitWidth helper reading the one storage key so the two
surfaces always agree.
* feat(reticulum): clear path table, blocklist import/export, auto-resend on announce
Three MeshChatX parity gaps, each independently useful:
Clear path table — a stale route can keep sending to a next hop that no
longer works, with no way to force rediscovery short of restarting the
stack. Uses the upstream TransportQuery::DropPathTable verb (a single
control query, not a per-entry DropPath loop) and also resets the
path-request backoff gate so peers that exhausted their retry budget can
be discovered again. Local caches are dropped even when the query times
out, since the transport-side clear may still have applied.
Blocklist import/export — a blocklist was previously trapped in one
install's SQLite. Import validates strictly rather than reusing the
lenient normalizer, whose documented non-hex fallback would otherwise
persist junk as blocked hashes, and uses its own SQL so imported-vs-
skipped counts are accurate (db:blockContact always reports 1 change).
Auto-resend on announce — an announce means a path may exist again, so
failed sends to that peer are worth one retry. Off by default; guarded by
a per-destination cooldown, an in-flight lock, and a per-announce cap so
an announce burst cannot fan out into a send storm.
Also corrects the parity notes: the claim that no bulk path-table clear
verb existed was wrong, and adds the deferred transport-blackhole
write-up (it keys on identity hashes, not the destination hashes our
blocklist stores, so existing rows cannot be mirrored).
* fix(main): stop setTypeOfService EINVAL from crashing the main process
Link-preview fetches use undici, which set the advisory IP ToS byte on every
outbound HTTP/1.1 socket. macOS rejects that with EINVAL on some network
configurations, and because the call happens inside a socket connect listener
the throw escaped fetchLinkPreview's try/catch and surfaced as an "Unexpected
Error" dialog. Depend on undici ^8.10.0 directly for the upstream guard
(nodejs/undici#5547) — it was previously an undeclared transitive dependency
capped at 7.x — and keep a scoped undici@<7.29.0 floor so jsdom and node-gyp
stay on 7.29.0. A narrow uncaughtException/unhandledRejection filter now
tolerates the failure even if a future socket option regresses.
Add check:pinned-majors, run from pnpm run update, so an override that falls
behind a newer npm major surfaces instead of silently withholding fixes like
this one; caps that are correct carry a documented reason.
* fix(reticulum): style clear path table as caution, not danger
The action was neutral slate with a red confirm button, which read as
destructive. Clearing the path table only forces route rediscovery — RNS
repopulates the entries on its own — so red overstated the risk on an
action users should reach for when messages to a peer stop landing.
Uses the yellow palette already used by ReticulumPnHostingDangerZone,
which sits directly above it in the network panel, and drops the danger
prop so ConfirmModal picks its yellow confirm button instead of red.
* fix(reticulum): collapse the RNode flasher by default
Flashing firmware is a rare, one-off task, but its section occupied the
top of the Administration tab and pushed the factory reset danger zone
down the page. Collapsing it makes the tab's remaining content visible
without scrolling.
Uses the same <details> disclosure pattern as RadioPanel and
ReticulumInterfacesPanel via the shared DetailsChevron. A closed
<details> still mounts its children, so RNodeFlasherSection keeps the
pairing session and flash/provision marks it deliberately preserves
across Admin remounts; conditional rendering would have discarded them.
The test records that so the distinction is not lost.
* refactor(reticulum): move blocked contacts to the Network tab
The blocklist is Reticulum-specific but sat in App Settings between the
database export/import and Appearance, where it was both hard to find
and surprising to encounter — the section hides itself unless the
hydrated identity is Reticulum, so most users saw a gap there instead.
The Network tab already holds the rest of the Reticulum-scoped controls.
Presented as a collapsible section like its neighbors, which supplies
the heading, so the component's own now-duplicate h3 is dropped.
Deliberately not gated on sidecarApiReady, unlike the surrounding
sections: the blocklist is local SQLite state hydrated on identityId
alone, so gating would hide it whenever the stack was stopped — the
point at which pruning it before reconnecting is most useful.
The protocol scope check moves into blockStore as
useReticulumBlocklistIdentityId rather than travelling into the panel as
a raw compare. AppPanel was grandfathered onto the
check-protocol-string-gates allowlist; ReticulumNetworkPanel is not, and
widening that legacy list is the wrong direction when the store is the
right home for knowing which protocol it currently holds.
i18n keys stay under appPanel.reticulumBlocklist.* to avoid rewriting
all 16 locale files for a move.
* feat(rrc): prompt when a pasted Reticulum hash is ambiguous
A bare 32-hex hash could be a Nomad node or an LXMF peer, and assuming a DM
sent people to the wrong place. Clicking one now opens a small dialog to pick
Nomad page or direct message; explicit `:/page` and lxmf:// forms still act
immediately.
* chore(deps): update dependencies and move off deprecated jest-dom 6.10.0
`pnpm run update` picked up systeminformation, @testing-library/react, and
@vitejs/plugin-react patch releases. @testing-library/jest-dom 6.10.0 was
deprecated upstream as a bad minor (it shipped the Node >=22 and
@testing-library/dom peer requirements intended for 7.x), so move to ^7.0.1
rather than pinning back to 6.9.1. All three of its requirements were already
satisfied, so no test migration was needed.
* feat(games): add Four in a Row board
The Games board dispatch was a binary chess-or-else fallback, so
`four_in_a_row` sessions rendered through TicTacToeBoard — a 42-cell board
truncated to 9 cells and moves sent as `{ i }` instead of `{ c }`. The sidecar
has registered the app for a while, so these sessions were reachable and
broken, not merely unimplemented.
Implemented against lrgp-rs SPEC.md appendix C: 7x6 gravity board, 42 cells
row-major (`row * 7 + column`), theme-neutral `A`/`B` markers, outgoing payload
exactly `{ c }` with the sidecar deriving move number, landing cell, and
terminal claims. Adds an optimistic column drop matching the existing TTT and
chess pattern so the disc paints before the sidecar confirms, with win and draw
detection for rollback.
The whole column is the button rather than each cell, since gravity means the
player picks a column. Discs are aria-hidden and the column button carries the
readable state ("Column 3, bottom to top: A, B. Drop here"), giving screen
reader users the board contents without 42 focus stops.
Seven of the eleven new strings are identical to existing gamesPanel.ttt values,
so those reuse the reviewed per-locale translations; the four new keys are
translated for all 15 locales.
Bumps the Ratspeak games-parity reviewed-ref to v1.0.31 (reviewed: voice
message reliability only, no Games API delta) and updates the parity matrix.
Wire interop rows stay `todo` pending the manual gold test against Ratspeak.
* fix(main): release BLE connect queue on wait timeout and always quit after BLE shutdown
BLE connect installs a fresh `connectQueue` promise before awaiting the previous
holder, but that await sat outside the try whose finally calls releaseQueue().
When the wait timed out, the promise just installed was never resolved, so every
later connect() awaited a promise that could never settle — each one timing out
and installing another dead queue. BLE connect stayed wedged for the rest of the
process lifetime rather than recovering. Release the slot on the timeout path.
The before-quit BLE branch awaited shutdownAppResources() inside a finally with
no catch, so a rejection escaped the async IIFE as an unhandled rejection and
app.quit() never ran. Since the handler had already called preventDefault(),
that left the app running with no path to exit. The non-BLE branch below already
handled this; make the two consistent.
Adds a behavior test that wedges the queue behind a holder that never releases
and asserts a follow-up connect still succeeds. Verified to fail without the fix.
* fix(main): gate MQTT handlers on client identity and harden sidecar stop race
Stale mqtt.js clients keep emitting close/error/offline after end(true). With the
handlers bound to whatever `this.client` currently points at, a replaced client's
close cleared the live client's connack timer, inflated its retry count, and
scheduled a reconnect that tore down a healthy connection. Both adapters now gate
every handler on client identity; the MeshCore adapter already used this idiom for
packetsend/packetreceive, so this extends it to the rest.
The Reticulum sidecar could also report a stopped process as running: stop() kills
the child during the health poll, so a response landing just before the kill marked
a dead PID running, connected a WS to a dead port, and armed a watchdog for a gone
process. Re-check abort and process identity after the poll, and stop the watchdog
on exit so its interval does not leak across the next spawn.
Also fold in verified low-risk audit findings:
- Normalize six Meshtastic node-id renders through formatMeshtasticNodeId so a
negative int32 cannot render as a 9-char hex id (MeshCore panels use a separate
id space and are left alone).
- Guard six fire-and-forget ipcMain.on handlers with validateIpcSender, plus a test
that enumerates every ipcMain.on channel so new ones cannot ship unguarded.
- Add object-src/base-uri/form-action to the renderer CSP (frame-ancestors is
omitted: the spec ignores it in <meta> and the renderer is never framed).
- Fix ru common.save/common.unknown wording and de "Stornieren" -> "Abbrechen",
and canonicalize the eight "Hops" keys per locale (ru mixed three spellings and
reticulumPeers.hops was untranslated in every locale).
- Correct docs drift: relative CI link, orphaned docs/index.md entries, the
useLongSessionMaintenance Noble condition, and the src/main/ipc module list.
* fix(i18n): add missing Slavic plural forms so counts stop falling back to English
Russian, Ukrainian, Polish and Czech define plural categories English does not.
Only _one/_other existed, so i18next found no match for `few` (2-4) or `many`
(0, 5-20) and fell through the fallback chain to English: ru at count=3 rendered
"3 nodes", not "3 узла". This affected every counted string in four locales at
the most common counts.
Add the 320 missing forms across 47 plural families: Russian `few` in the
genitive singular, uk/pl/cs `few` in the nominative plural, and genitive plural
for `many`. Czech `many` is deliberately absent (fractions-only, unreachable for
integer counts), as is Romance `many` (starts at 1,000,000).
Also translate the pl/cs "config issue" and "hop" families, which were still
literal English in _one/_other and so leaked regardless of category.
check:i18n now requires every plural category a locale actually selects for
counts 0-200, scoped to families that locale has already started so a wholly
untranslated locale still reports as the existing "missing key(s)" warning.
Verified: rendering all 47 families at counts 0-22 across the four locales now
produces zero English fallbacks, down from 320.
* chore(reticulum): retire rsReticulum LinkClient Nomad overlay
Upstream main now resolves the remote pubkey via handler-free
resolve_destination_on_transport, with no HasPath gate and no temporary
announce handlers, so the overlay's apply script had already degraded to a
no-op. ratspeak/rsReticulum#14 was closed as superseded rather than merged.
Removes the patch, apply script, and its test, drops the entries from
RS_RETICULUM_APPLY_SCRIPTS and RATSPEAK_PATCH_ENTRIES so pnpm run update
stops tracking it, and updates the docs to point at floated origin/main.
* fix(rrc): resolve nicklist hashes to names and surface dropped /who replies
rrcd JOINED rosters carry no nicknames, and the once-per-room auto /who gate
survived reconnects, so a re-joined room showed bare hex forever. Re-arm the
gate on the handshake, fall back to nicks learned anywhere in the hub's
transcripts, and warn when a forced /who reply never arrives instead of
looking ignored.
* feat(games): allow challenging peers to Four in a Row
The FourInARowBoard added in be5ae05 was unreachable. A challenge is the only
way to create a session, and both challenge entry points offered just ttt and
chess, so no session could ever carry app_id four_in_a_row and the board branch
in GamesPanel was dead code.
Add the app id to GamesAppId and move the menu list into a single exported
GAMES_CHALLENGE_APPS. It had been duplicated verbatim in GamesPanel and
ReticulumGameChallengeButton, which is what let both menus drift out of sync
with the board dispatch in the first place.
The list stays curated rather than derived from the sidecar app manifest:
manifest app_id is an open string and the board dispatch falls through to
Tic-Tac-Toe for anything it does not recognize, so a manifest-driven menu could
offer a game that renders the wrong board. No sidecar change is needed -- it
already registers four_in_a_row and passes the app id through unvalidated.
Locale labels use each language's established name for the game rather than a
literal translation ("Vier gewinnt", "Puissance 4", "Forza 4", "四子棋").
The picker test asserts the full option list, so adding a future board without a
menu entry fails instead of silently shipping another unreachable game.
* feat(rrc): cache hub nicks in SQLite so names survive transcript clears
Oversized /who replies are dropped by the hub, so the nicklist depended on
whatever transcript happened to be loaded. Persist every nick sighting per
hub (chat, /who row, JOINED advisory) and hydrate it once per session, so a
peer who spoke only once still gets a name after a restart or history clear.
* fix(nomad): sanitize Micron page text at the innerHTML sink
The Micron editor added on this branch feeds a textarea's value straight into
the parser, which CodeQL flags as js/xss-through-dom: appendOutput writes page
text into span.innerHTML, and the only DOMPurify pass ran afterwards over the
whole document in convertMicronToHtml.
Route the three markup-building innerHTML writes (text spans, style-run spans,
link labels) through setSanitizedHtml so the barrier sits at the sink. In
practice wrapWord already escapes `<`, `>` and `&` when force-monospace is on,
so rendering is unchanged and tag-like text stays inert literal text; the sink
sanitize covers the non-monospace path and any future part producer that does
not escape.
Tests assert tag-like page text and link labels never become elements, and that
literal angle brackets and ampersands still render as typed.
Sign up for freeto 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.

writeH1 calls socket.setTypeOfService() unconditionally — uncaught EINVAL on macOS crashes the process

3 participants

@mcollina@codecov-commenter@ronag
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(h1): ignore type of service errors - #5547

Merged
mcollina merged 2 commits into
mainfrom
fix-h1-type-of-service-crash
Jul 12, 2026
Merged

fix(h1): ignore type of service errors#5547
mcollina merged 2 commits into
mainfrom
fix-h1-type-of-service-crash

Conversation

@mcollina

Copy link
Copy Markdown
Member

This relates to...

Fixes#5544.

Rationale

socket.setTypeOfService() is best-effort QoS marking. It can throw synchronously on some platforms depending on socket state, and the default request path should not touch it when no ToS value was explicitly requested.

Changes

Features

N/A

Bug Fixes

  • Skip setTypeOfService(0) for fresh HTTP/1.1 sockets when typeOfService was not explicitly provided.
  • Track the last ToS value applied to each socket and avoid redundant calls.
  • Reset a reused socket back to ToS 0 after an explicitly prioritized request.
  • Ignore synchronous setTypeOfService() failures so they do not abort requests or crash the process.
  • Add regression coverage for default requests and thrown setTypeOfService() errors.

Breaking Changes and Deprecations

N/A

Status

Tested with:

  • npx borp -p "test/ip-prioritization.js"
  • npm run lint

@codecov-commenter

codecov-commenter commented Jul 11, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 86.20690% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 93.44%. Comparing base (c7c7e7f) to head (794f99f).
⚠️ Report is 6 commits behind head on main.

Files with missing linesPatch %Lines
lib/dispatcher/client-h1.js85.71%4 Missing ⚠️
Additional details and impacted files
@@ Coverage Diff @@## main #5547 +/- ##
==========================================
- Coverage 93.45% 93.44% -0.01% 
==========================================
Files 110 110 Lines 37376 37443 +67 ==========================================
+ Hits 34928 34987 +59 - Misses 2448 2456 +8 

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@mcollina
mcollina merged commit 197a83d into mainJul 12, 2026
36 checks passed
@mcollina
mcollina deleted the fix-h1-type-of-service-crash branch July 12, 2026 13:13
@github-actionsgithub-actionsBot mentioned this pull request Jul 20, 2026
rinchen added a commit to Colorado-Mesh/mesh-client that referenced this pull request Aug 29, 2026
…ocklist tools, Four in a Row, RRC linkify + nick cache, plus MQTT/BLE/undici stability fixes (#912)
* feat(reticulum): in-app Micron page editor for Nomad My Pages
Hosting a Nomad site previously required editing .mu files on disk in an
external editor. My Pages now offers New/Edit/Delete on the hosted-page
list, opening a split-pane editor with the raw Micron source plus a
formatting toolbar on the left and a live preview on the right. Saves
write into the watched folder, so the existing FS watcher re-indexes them.
Authoring is gated on a configured content source rather than on active
serving, because page mutations only need a live stack and resolved roots.
The sidecar previously stringified NomadError straight to the renderer,
so write failures surfaced as untranslatable English prose interpolating
paths and byte counts. page_error_code now maps them to stable snake_case
codes (page_too_large, page_not_found, invalid_page_path, page_io_error,
page_not_utf8, page_write_failed) and logs the original detail, letting
nomadPageErrorHumanize translate what the user sees.
File uploads and the local-files list remain intentionally absent.
* feat(reticulum): add Serial, AX.25 KISS and Local interfaces via a shared catalog
Adding one Reticulum interface type previously took ~20 coordinated edits split
across the Rust sidecar and the renderer with no compiler link between them, so
the two sides could silently disagree about supported types, default modes or
flow-control policy.
Introduce src/shared/reticulumInterfaceCatalog.json as the single source of
truth. The sidecar compiles it in via include_str! and the renderer imports the
same file, making that drift structurally impossible rather than checked after
the fact. Both sides assert every configType is a name the upstream factory
synthesize_interface actually accepts.
On top of that, add SerialInterface, AX25KISSInterface and LocalInterface. A
catalog-driven ReticulumInterfaceFieldSet renders their forms, and add and edit
share one serialization path instead of two hand-maintained branches.
Also fixes a latent bug: ui_type_to_config echoed unknown types verbatim, so an
unmapped type was written to disk and then dropped on the next parse, leaving a
config block the user could see but never edit or delete. Unknown types are now
rejected up front.
The nine existing types keep their bespoke controls and hand-written branches;
the goal is that a new type needs no new branch, not that every old one is
rewritten. check-reticulum-interface-modes.mjs was scraping match arms that no
longer exist and is repointed at the mode list, the ap/gw aliases, and the
invariant that both sides still read the catalog.
* feat(rrc): linkify Nomad page addresses and LXMF hashes in chat
Reticulum addresses pasted into RRC rooms were inert text, so reaching a
shared page or peer meant copying the hash into another tab by hand.
Addresses now render as inline links: `<hash>:/page/foo.mu` switches to the
Nomad tab and loads the page, while a bare or lxmf:// hash opens a Chat DM.
* fix(reticulum): render Micron editor preview like the Nomad browser
The editor mounted parsed Micron into a bare div, so it missed the
nomad-micron-page class that supplies white-space: pre and the bundled
MeshClientNomadMono font. HTML collapsed the interior padding spaces and
the fallback UI font kept borders from joining, which scrambled Unicode
and ASCII box-drawing art that renders correctly in the browser.
Render the preview through NomadMicronPageView itself rather than copying
its class list, which would silently drift. Links get no-op handlers since
the modal has no browsing context, and the pane scrolls on both axes so
wide art stays reachable.
Fit-width wraps long lines and breaks wide art even once the class is
restored, so the editor gains the same toggle as the browser, backed by a
shared nomadPageFitWidth helper reading the one storage key so the two
surfaces always agree.
* feat(reticulum): clear path table, blocklist import/export, auto-resend on announce
Three MeshChatX parity gaps, each independently useful:
Clear path table — a stale route can keep sending to a next hop that no
longer works, with no way to force rediscovery short of restarting the
stack. Uses the upstream TransportQuery::DropPathTable verb (a single
control query, not a per-entry DropPath loop) and also resets the
path-request backoff gate so peers that exhausted their retry budget can
be discovered again. Local caches are dropped even when the query times
out, since the transport-side clear may still have applied.
Blocklist import/export — a blocklist was previously trapped in one
install's SQLite. Import validates strictly rather than reusing the
lenient normalizer, whose documented non-hex fallback would otherwise
persist junk as blocked hashes, and uses its own SQL so imported-vs-
skipped counts are accurate (db:blockContact always reports 1 change).
Auto-resend on announce — an announce means a path may exist again, so
failed sends to that peer are worth one retry. Off by default; guarded by
a per-destination cooldown, an in-flight lock, and a per-announce cap so
an announce burst cannot fan out into a send storm.
Also corrects the parity notes: the claim that no bulk path-table clear
verb existed was wrong, and adds the deferred transport-blackhole
write-up (it keys on identity hashes, not the destination hashes our
blocklist stores, so existing rows cannot be mirrored).
* fix(main): stop setTypeOfService EINVAL from crashing the main process
Link-preview fetches use undici, which set the advisory IP ToS byte on every
outbound HTTP/1.1 socket. macOS rejects that with EINVAL on some network
configurations, and because the call happens inside a socket connect listener
the throw escaped fetchLinkPreview's try/catch and surfaced as an "Unexpected
Error" dialog. Depend on undici ^8.10.0 directly for the upstream guard
(nodejs/undici#5547) — it was previously an undeclared transitive dependency
capped at 7.x — and keep a scoped undici@<7.29.0 floor so jsdom and node-gyp
stay on 7.29.0. A narrow uncaughtException/unhandledRejection filter now
tolerates the failure even if a future socket option regresses.
Add check:pinned-majors, run from pnpm run update, so an override that falls
behind a newer npm major surfaces instead of silently withholding fixes like
this one; caps that are correct carry a documented reason.
* fix(reticulum): style clear path table as caution, not danger
The action was neutral slate with a red confirm button, which read as
destructive. Clearing the path table only forces route rediscovery — RNS
repopulates the entries on its own — so red overstated the risk on an
action users should reach for when messages to a peer stop landing.
Uses the yellow palette already used by ReticulumPnHostingDangerZone,
which sits directly above it in the network panel, and drops the danger
prop so ConfirmModal picks its yellow confirm button instead of red.
* fix(reticulum): collapse the RNode flasher by default
Flashing firmware is a rare, one-off task, but its section occupied the
top of the Administration tab and pushed the factory reset danger zone
down the page. Collapsing it makes the tab's remaining content visible
without scrolling.
Uses the same <details> disclosure pattern as RadioPanel and
ReticulumInterfacesPanel via the shared DetailsChevron. A closed
<details> still mounts its children, so RNodeFlasherSection keeps the
pairing session and flash/provision marks it deliberately preserves
across Admin remounts; conditional rendering would have discarded them.
The test records that so the distinction is not lost.
* refactor(reticulum): move blocked contacts to the Network tab
The blocklist is Reticulum-specific but sat in App Settings between the
database export/import and Appearance, where it was both hard to find
and surprising to encounter — the section hides itself unless the
hydrated identity is Reticulum, so most users saw a gap there instead.
The Network tab already holds the rest of the Reticulum-scoped controls.
Presented as a collapsible section like its neighbors, which supplies
the heading, so the component's own now-duplicate h3 is dropped.
Deliberately not gated on sidecarApiReady, unlike the surrounding
sections: the blocklist is local SQLite state hydrated on identityId
alone, so gating would hide it whenever the stack was stopped — the
point at which pruning it before reconnecting is most useful.
The protocol scope check moves into blockStore as
useReticulumBlocklistIdentityId rather than travelling into the panel as
a raw compare. AppPanel was grandfathered onto the
check-protocol-string-gates allowlist; ReticulumNetworkPanel is not, and
widening that legacy list is the wrong direction when the store is the
right home for knowing which protocol it currently holds.
i18n keys stay under appPanel.reticulumBlocklist.* to avoid rewriting
all 16 locale files for a move.
* feat(rrc): prompt when a pasted Reticulum hash is ambiguous
A bare 32-hex hash could be a Nomad node or an LXMF peer, and assuming a DM
sent people to the wrong place. Clicking one now opens a small dialog to pick
Nomad page or direct message; explicit `:/page` and lxmf:// forms still act
immediately.
* chore(deps): update dependencies and move off deprecated jest-dom 6.10.0
`pnpm run update` picked up systeminformation, @testing-library/react, and
@vitejs/plugin-react patch releases. @testing-library/jest-dom 6.10.0 was
deprecated upstream as a bad minor (it shipped the Node >=22 and
@testing-library/dom peer requirements intended for 7.x), so move to ^7.0.1
rather than pinning back to 6.9.1. All three of its requirements were already
satisfied, so no test migration was needed.
* feat(games): add Four in a Row board
The Games board dispatch was a binary chess-or-else fallback, so
`four_in_a_row` sessions rendered through TicTacToeBoard — a 42-cell board
truncated to 9 cells and moves sent as `{ i }` instead of `{ c }`. The sidecar
has registered the app for a while, so these sessions were reachable and
broken, not merely unimplemented.
Implemented against lrgp-rs SPEC.md appendix C: 7x6 gravity board, 42 cells
row-major (`row * 7 + column`), theme-neutral `A`/`B` markers, outgoing payload
exactly `{ c }` with the sidecar deriving move number, landing cell, and
terminal claims. Adds an optimistic column drop matching the existing TTT and
chess pattern so the disc paints before the sidecar confirms, with win and draw
detection for rollback.
The whole column is the button rather than each cell, since gravity means the
player picks a column. Discs are aria-hidden and the column button carries the
readable state ("Column 3, bottom to top: A, B. Drop here"), giving screen
reader users the board contents without 42 focus stops.
Seven of the eleven new strings are identical to existing gamesPanel.ttt values,
so those reuse the reviewed per-locale translations; the four new keys are
translated for all 15 locales.
Bumps the Ratspeak games-parity reviewed-ref to v1.0.31 (reviewed: voice
message reliability only, no Games API delta) and updates the parity matrix.
Wire interop rows stay `todo` pending the manual gold test against Ratspeak.
* fix(main): release BLE connect queue on wait timeout and always quit after BLE shutdown
BLE connect installs a fresh `connectQueue` promise before awaiting the previous
holder, but that await sat outside the try whose finally calls releaseQueue().
When the wait timed out, the promise just installed was never resolved, so every
later connect() awaited a promise that could never settle — each one timing out
and installing another dead queue. BLE connect stayed wedged for the rest of the
process lifetime rather than recovering. Release the slot on the timeout path.
The before-quit BLE branch awaited shutdownAppResources() inside a finally with
no catch, so a rejection escaped the async IIFE as an unhandled rejection and
app.quit() never ran. Since the handler had already called preventDefault(),
that left the app running with no path to exit. The non-BLE branch below already
handled this; make the two consistent.
Adds a behavior test that wedges the queue behind a holder that never releases
and asserts a follow-up connect still succeeds. Verified to fail without the fix.
* fix(main): gate MQTT handlers on client identity and harden sidecar stop race
Stale mqtt.js clients keep emitting close/error/offline after end(true). With the
handlers bound to whatever `this.client` currently points at, a replaced client's
close cleared the live client's connack timer, inflated its retry count, and
scheduled a reconnect that tore down a healthy connection. Both adapters now gate
every handler on client identity; the MeshCore adapter already used this idiom for
packetsend/packetreceive, so this extends it to the rest.
The Reticulum sidecar could also report a stopped process as running: stop() kills
the child during the health poll, so a response landing just before the kill marked
a dead PID running, connected a WS to a dead port, and armed a watchdog for a gone
process. Re-check abort and process identity after the poll, and stop the watchdog
on exit so its interval does not leak across the next spawn.
Also fold in verified low-risk audit findings:
- Normalize six Meshtastic node-id renders through formatMeshtasticNodeId so a
negative int32 cannot render as a 9-char hex id (MeshCore panels use a separate
id space and are left alone).
- Guard six fire-and-forget ipcMain.on handlers with validateIpcSender, plus a test
that enumerates every ipcMain.on channel so new ones cannot ship unguarded.
- Add object-src/base-uri/form-action to the renderer CSP (frame-ancestors is
omitted: the spec ignores it in <meta> and the renderer is never framed).
- Fix ru common.save/common.unknown wording and de "Stornieren" -> "Abbrechen",
and canonicalize the eight "Hops" keys per locale (ru mixed three spellings and
reticulumPeers.hops was untranslated in every locale).
- Correct docs drift: relative CI link, orphaned docs/index.md entries, the
useLongSessionMaintenance Noble condition, and the src/main/ipc module list.
* fix(i18n): add missing Slavic plural forms so counts stop falling back to English
Russian, Ukrainian, Polish and Czech define plural categories English does not.
Only _one/_other existed, so i18next found no match for `few` (2-4) or `many`
(0, 5-20) and fell through the fallback chain to English: ru at count=3 rendered
"3 nodes", not "3 узла". This affected every counted string in four locales at
the most common counts.
Add the 320 missing forms across 47 plural families: Russian `few` in the
genitive singular, uk/pl/cs `few` in the nominative plural, and genitive plural
for `many`. Czech `many` is deliberately absent (fractions-only, unreachable for
integer counts), as is Romance `many` (starts at 1,000,000).
Also translate the pl/cs "config issue" and "hop" families, which were still
literal English in _one/_other and so leaked regardless of category.
check:i18n now requires every plural category a locale actually selects for
counts 0-200, scoped to families that locale has already started so a wholly
untranslated locale still reports as the existing "missing key(s)" warning.
Verified: rendering all 47 families at counts 0-22 across the four locales now
produces zero English fallbacks, down from 320.
* chore(reticulum): retire rsReticulum LinkClient Nomad overlay
Upstream main now resolves the remote pubkey via handler-free
resolve_destination_on_transport, with no HasPath gate and no temporary
announce handlers, so the overlay's apply script had already degraded to a
no-op. ratspeak/rsReticulum#14 was closed as superseded rather than merged.
Removes the patch, apply script, and its test, drops the entries from
RS_RETICULUM_APPLY_SCRIPTS and RATSPEAK_PATCH_ENTRIES so pnpm run update
stops tracking it, and updates the docs to point at floated origin/main.
* fix(rrc): resolve nicklist hashes to names and surface dropped /who replies
rrcd JOINED rosters carry no nicknames, and the once-per-room auto /who gate
survived reconnects, so a re-joined room showed bare hex forever. Re-arm the
gate on the handshake, fall back to nicks learned anywhere in the hub's
transcripts, and warn when a forced /who reply never arrives instead of
looking ignored.
* feat(games): allow challenging peers to Four in a Row
The FourInARowBoard added in be5ae05 was unreachable. A challenge is the only
way to create a session, and both challenge entry points offered just ttt and
chess, so no session could ever carry app_id four_in_a_row and the board branch
in GamesPanel was dead code.
Add the app id to GamesAppId and move the menu list into a single exported
GAMES_CHALLENGE_APPS. It had been duplicated verbatim in GamesPanel and
ReticulumGameChallengeButton, which is what let both menus drift out of sync
with the board dispatch in the first place.
The list stays curated rather than derived from the sidecar app manifest:
manifest app_id is an open string and the board dispatch falls through to
Tic-Tac-Toe for anything it does not recognize, so a manifest-driven menu could
offer a game that renders the wrong board. No sidecar change is needed -- it
already registers four_in_a_row and passes the app id through unvalidated.
Locale labels use each language's established name for the game rather than a
literal translation ("Vier gewinnt", "Puissance 4", "Forza 4", "四子棋").
The picker test asserts the full option list, so adding a future board without a
menu entry fails instead of silently shipping another unreachable game.
* feat(rrc): cache hub nicks in SQLite so names survive transcript clears
Oversized /who replies are dropped by the hub, so the nicklist depended on
whatever transcript happened to be loaded. Persist every nick sighting per
hub (chat, /who row, JOINED advisory) and hydrate it once per session, so a
peer who spoke only once still gets a name after a restart or history clear.
* fix(nomad): sanitize Micron page text at the innerHTML sink
The Micron editor added on this branch feeds a textarea's value straight into
the parser, which CodeQL flags as js/xss-through-dom: appendOutput writes page
text into span.innerHTML, and the only DOMPurify pass ran afterwards over the
whole document in convertMicronToHtml.
Route the three markup-building innerHTML writes (text spans, style-run spans,
link labels) through setSanitizedHtml so the barrier sits at the sink. In
practice wrapWord already escapes `<`, `>` and `&` when force-monospace is on,
so rendering is unchanged and tag-like text stays inert literal text; the sink
sanitize covers the non-monospace path and any future part producer that does
not escape.
Tests assert tag-like page text and link labels never become elements, and that
literal angle brackets and ampersands still render as typed.
Sign up for freeto 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.

writeH1 calls socket.setTypeOfService() unconditionally — uncaught EINVAL on macOS crashes the process

3 participants

@mcollina@codecov-commenter@ronag
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

fix(h1): ignore type of service errors - #5547

Merged
mcollina merged 2 commits into
mainfrom
fix-h1-type-of-service-crash
Jul 12, 2026
Merged

fix(h1): ignore type of service errors#5547
mcollina merged 2 commits into
mainfrom
fix-h1-type-of-service-crash

Conversation

@mcollina

Copy link
Copy Markdown
Member

This relates to...

Fixes#5544.

Rationale

socket.setTypeOfService() is best-effort QoS marking. It can throw synchronously on some platforms depending on socket state, and the default request path should not touch it when no ToS value was explicitly requested.

Changes

Features

N/A

Bug Fixes

  • Skip setTypeOfService(0) for fresh HTTP/1.1 sockets when typeOfService was not explicitly provided.
  • Track the last ToS value applied to each socket and avoid redundant calls.
  • Reset a reused socket back to ToS 0 after an explicitly prioritized request.
  • Ignore synchronous setTypeOfService() failures so they do not abort requests or crash the process.
  • Add regression coverage for default requests and thrown setTypeOfService() errors.

Breaking Changes and Deprecations

N/A

Status

Tested with:

  • npx borp -p "test/ip-prioritization.js"
  • npm run lint

@codecov-commenter

codecov-commenter commented Jul 11, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 86.20690% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 93.44%. Comparing base (c7c7e7f) to head (794f99f).
⚠️ Report is 6 commits behind head on main.

Files with missing linesPatch %Lines
lib/dispatcher/client-h1.js85.71%4 Missing ⚠️
Additional details and impacted files
@@ Coverage Diff @@## main #5547 +/- ##
==========================================
- Coverage 93.45% 93.44% -0.01% 
==========================================
Files 110 110 Lines 37376 37443 +67 ==========================================
+ Hits 34928 34987 +59 - Misses 2448 2456 +8 

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@mcollina
mcollina merged commit 197a83d into mainJul 12, 2026
36 checks passed
@mcollina
mcollina deleted the fix-h1-type-of-service-crash branch July 12, 2026 13:13
@github-actionsgithub-actionsBot mentioned this pull request Jul 20, 2026
rinchen added a commit to Colorado-Mesh/mesh-client that referenced this pull request Aug 29, 2026
…ocklist tools, Four in a Row, RRC linkify + nick cache, plus MQTT/BLE/undici stability fixes (#912)
* feat(reticulum): in-app Micron page editor for Nomad My Pages
Hosting a Nomad site previously required editing .mu files on disk in an
external editor. My Pages now offers New/Edit/Delete on the hosted-page
list, opening a split-pane editor with the raw Micron source plus a
formatting toolbar on the left and a live preview on the right. Saves
write into the watched folder, so the existing FS watcher re-indexes them.
Authoring is gated on a configured content source rather than on active
serving, because page mutations only need a live stack and resolved roots.
The sidecar previously stringified NomadError straight to the renderer,
so write failures surfaced as untranslatable English prose interpolating
paths and byte counts. page_error_code now maps them to stable snake_case
codes (page_too_large, page_not_found, invalid_page_path, page_io_error,
page_not_utf8, page_write_failed) and logs the original detail, letting
nomadPageErrorHumanize translate what the user sees.
File uploads and the local-files list remain intentionally absent.
* feat(reticulum): add Serial, AX.25 KISS and Local interfaces via a shared catalog
Adding one Reticulum interface type previously took ~20 coordinated edits split
across the Rust sidecar and the renderer with no compiler link between them, so
the two sides could silently disagree about supported types, default modes or
flow-control policy.
Introduce src/shared/reticulumInterfaceCatalog.json as the single source of
truth. The sidecar compiles it in via include_str! and the renderer imports the
same file, making that drift structurally impossible rather than checked after
the fact. Both sides assert every configType is a name the upstream factory
synthesize_interface actually accepts.
On top of that, add SerialInterface, AX25KISSInterface and LocalInterface. A
catalog-driven ReticulumInterfaceFieldSet renders their forms, and add and edit
share one serialization path instead of two hand-maintained branches.
Also fixes a latent bug: ui_type_to_config echoed unknown types verbatim, so an
unmapped type was written to disk and then dropped on the next parse, leaving a
config block the user could see but never edit or delete. Unknown types are now
rejected up front.
The nine existing types keep their bespoke controls and hand-written branches;
the goal is that a new type needs no new branch, not that every old one is
rewritten. check-reticulum-interface-modes.mjs was scraping match arms that no
longer exist and is repointed at the mode list, the ap/gw aliases, and the
invariant that both sides still read the catalog.
* feat(rrc): linkify Nomad page addresses and LXMF hashes in chat
Reticulum addresses pasted into RRC rooms were inert text, so reaching a
shared page or peer meant copying the hash into another tab by hand.
Addresses now render as inline links: `<hash>:/page/foo.mu` switches to the
Nomad tab and loads the page, while a bare or lxmf:// hash opens a Chat DM.
* fix(reticulum): render Micron editor preview like the Nomad browser
The editor mounted parsed Micron into a bare div, so it missed the
nomad-micron-page class that supplies white-space: pre and the bundled
MeshClientNomadMono font. HTML collapsed the interior padding spaces and
the fallback UI font kept borders from joining, which scrambled Unicode
and ASCII box-drawing art that renders correctly in the browser.
Render the preview through NomadMicronPageView itself rather than copying
its class list, which would silently drift. Links get no-op handlers since
the modal has no browsing context, and the pane scrolls on both axes so
wide art stays reachable.
Fit-width wraps long lines and breaks wide art even once the class is
restored, so the editor gains the same toggle as the browser, backed by a
shared nomadPageFitWidth helper reading the one storage key so the two
surfaces always agree.
* feat(reticulum): clear path table, blocklist import/export, auto-resend on announce
Three MeshChatX parity gaps, each independently useful:
Clear path table — a stale route can keep sending to a next hop that no
longer works, with no way to force rediscovery short of restarting the
stack. Uses the upstream TransportQuery::DropPathTable verb (a single
control query, not a per-entry DropPath loop) and also resets the
path-request backoff gate so peers that exhausted their retry budget can
be discovered again. Local caches are dropped even when the query times
out, since the transport-side clear may still have applied.
Blocklist import/export — a blocklist was previously trapped in one
install's SQLite. Import validates strictly rather than reusing the
lenient normalizer, whose documented non-hex fallback would otherwise
persist junk as blocked hashes, and uses its own SQL so imported-vs-
skipped counts are accurate (db:blockContact always reports 1 change).
Auto-resend on announce — an announce means a path may exist again, so
failed sends to that peer are worth one retry. Off by default; guarded by
a per-destination cooldown, an in-flight lock, and a per-announce cap so
an announce burst cannot fan out into a send storm.
Also corrects the parity notes: the claim that no bulk path-table clear
verb existed was wrong, and adds the deferred transport-blackhole
write-up (it keys on identity hashes, not the destination hashes our
blocklist stores, so existing rows cannot be mirrored).
* fix(main): stop setTypeOfService EINVAL from crashing the main process
Link-preview fetches use undici, which set the advisory IP ToS byte on every
outbound HTTP/1.1 socket. macOS rejects that with EINVAL on some network
configurations, and because the call happens inside a socket connect listener
the throw escaped fetchLinkPreview's try/catch and surfaced as an "Unexpected
Error" dialog. Depend on undici ^8.10.0 directly for the upstream guard
(nodejs/undici#5547) — it was previously an undeclared transitive dependency
capped at 7.x — and keep a scoped undici@<7.29.0 floor so jsdom and node-gyp
stay on 7.29.0. A narrow uncaughtException/unhandledRejection filter now
tolerates the failure even if a future socket option regresses.
Add check:pinned-majors, run from pnpm run update, so an override that falls
behind a newer npm major surfaces instead of silently withholding fixes like
this one; caps that are correct carry a documented reason.
* fix(reticulum): style clear path table as caution, not danger
The action was neutral slate with a red confirm button, which read as
destructive. Clearing the path table only forces route rediscovery — RNS
repopulates the entries on its own — so red overstated the risk on an
action users should reach for when messages to a peer stop landing.
Uses the yellow palette already used by ReticulumPnHostingDangerZone,
which sits directly above it in the network panel, and drops the danger
prop so ConfirmModal picks its yellow confirm button instead of red.
* fix(reticulum): collapse the RNode flasher by default
Flashing firmware is a rare, one-off task, but its section occupied the
top of the Administration tab and pushed the factory reset danger zone
down the page. Collapsing it makes the tab's remaining content visible
without scrolling.
Uses the same <details> disclosure pattern as RadioPanel and
ReticulumInterfacesPanel via the shared DetailsChevron. A closed
<details> still mounts its children, so RNodeFlasherSection keeps the
pairing session and flash/provision marks it deliberately preserves
across Admin remounts; conditional rendering would have discarded them.
The test records that so the distinction is not lost.
* refactor(reticulum): move blocked contacts to the Network tab
The blocklist is Reticulum-specific but sat in App Settings between the
database export/import and Appearance, where it was both hard to find
and surprising to encounter — the section hides itself unless the
hydrated identity is Reticulum, so most users saw a gap there instead.
The Network tab already holds the rest of the Reticulum-scoped controls.
Presented as a collapsible section like its neighbors, which supplies
the heading, so the component's own now-duplicate h3 is dropped.
Deliberately not gated on sidecarApiReady, unlike the surrounding
sections: the blocklist is local SQLite state hydrated on identityId
alone, so gating would hide it whenever the stack was stopped — the
point at which pruning it before reconnecting is most useful.
The protocol scope check moves into blockStore as
useReticulumBlocklistIdentityId rather than travelling into the panel as
a raw compare. AppPanel was grandfathered onto the
check-protocol-string-gates allowlist; ReticulumNetworkPanel is not, and
widening that legacy list is the wrong direction when the store is the
right home for knowing which protocol it currently holds.
i18n keys stay under appPanel.reticulumBlocklist.* to avoid rewriting
all 16 locale files for a move.
* feat(rrc): prompt when a pasted Reticulum hash is ambiguous
A bare 32-hex hash could be a Nomad node or an LXMF peer, and assuming a DM
sent people to the wrong place. Clicking one now opens a small dialog to pick
Nomad page or direct message; explicit `:/page` and lxmf:// forms still act
immediately.
* chore(deps): update dependencies and move off deprecated jest-dom 6.10.0
`pnpm run update` picked up systeminformation, @testing-library/react, and
@vitejs/plugin-react patch releases. @testing-library/jest-dom 6.10.0 was
deprecated upstream as a bad minor (it shipped the Node >=22 and
@testing-library/dom peer requirements intended for 7.x), so move to ^7.0.1
rather than pinning back to 6.9.1. All three of its requirements were already
satisfied, so no test migration was needed.
* feat(games): add Four in a Row board
The Games board dispatch was a binary chess-or-else fallback, so
`four_in_a_row` sessions rendered through TicTacToeBoard — a 42-cell board
truncated to 9 cells and moves sent as `{ i }` instead of `{ c }`. The sidecar
has registered the app for a while, so these sessions were reachable and
broken, not merely unimplemented.
Implemented against lrgp-rs SPEC.md appendix C: 7x6 gravity board, 42 cells
row-major (`row * 7 + column`), theme-neutral `A`/`B` markers, outgoing payload
exactly `{ c }` with the sidecar deriving move number, landing cell, and
terminal claims. Adds an optimistic column drop matching the existing TTT and
chess pattern so the disc paints before the sidecar confirms, with win and draw
detection for rollback.
The whole column is the button rather than each cell, since gravity means the
player picks a column. Discs are aria-hidden and the column button carries the
readable state ("Column 3, bottom to top: A, B. Drop here"), giving screen
reader users the board contents without 42 focus stops.
Seven of the eleven new strings are identical to existing gamesPanel.ttt values,
so those reuse the reviewed per-locale translations; the four new keys are
translated for all 15 locales.
Bumps the Ratspeak games-parity reviewed-ref to v1.0.31 (reviewed: voice
message reliability only, no Games API delta) and updates the parity matrix.
Wire interop rows stay `todo` pending the manual gold test against Ratspeak.
* fix(main): release BLE connect queue on wait timeout and always quit after BLE shutdown
BLE connect installs a fresh `connectQueue` promise before awaiting the previous
holder, but that await sat outside the try whose finally calls releaseQueue().
When the wait timed out, the promise just installed was never resolved, so every
later connect() awaited a promise that could never settle — each one timing out
and installing another dead queue. BLE connect stayed wedged for the rest of the
process lifetime rather than recovering. Release the slot on the timeout path.
The before-quit BLE branch awaited shutdownAppResources() inside a finally with
no catch, so a rejection escaped the async IIFE as an unhandled rejection and
app.quit() never ran. Since the handler had already called preventDefault(),
that left the app running with no path to exit. The non-BLE branch below already
handled this; make the two consistent.
Adds a behavior test that wedges the queue behind a holder that never releases
and asserts a follow-up connect still succeeds. Verified to fail without the fix.
* fix(main): gate MQTT handlers on client identity and harden sidecar stop race
Stale mqtt.js clients keep emitting close/error/offline after end(true). With the
handlers bound to whatever `this.client` currently points at, a replaced client's
close cleared the live client's connack timer, inflated its retry count, and
scheduled a reconnect that tore down a healthy connection. Both adapters now gate
every handler on client identity; the MeshCore adapter already used this idiom for
packetsend/packetreceive, so this extends it to the rest.
The Reticulum sidecar could also report a stopped process as running: stop() kills
the child during the health poll, so a response landing just before the kill marked
a dead PID running, connected a WS to a dead port, and armed a watchdog for a gone
process. Re-check abort and process identity after the poll, and stop the watchdog
on exit so its interval does not leak across the next spawn.
Also fold in verified low-risk audit findings:
- Normalize six Meshtastic node-id renders through formatMeshtasticNodeId so a
negative int32 cannot render as a 9-char hex id (MeshCore panels use a separate
id space and are left alone).
- Guard six fire-and-forget ipcMain.on handlers with validateIpcSender, plus a test
that enumerates every ipcMain.on channel so new ones cannot ship unguarded.
- Add object-src/base-uri/form-action to the renderer CSP (frame-ancestors is
omitted: the spec ignores it in <meta> and the renderer is never framed).
- Fix ru common.save/common.unknown wording and de "Stornieren" -> "Abbrechen",
and canonicalize the eight "Hops" keys per locale (ru mixed three spellings and
reticulumPeers.hops was untranslated in every locale).
- Correct docs drift: relative CI link, orphaned docs/index.md entries, the
useLongSessionMaintenance Noble condition, and the src/main/ipc module list.
* fix(i18n): add missing Slavic plural forms so counts stop falling back to English
Russian, Ukrainian, Polish and Czech define plural categories English does not.
Only _one/_other existed, so i18next found no match for `few` (2-4) or `many`
(0, 5-20) and fell through the fallback chain to English: ru at count=3 rendered
"3 nodes", not "3 узла". This affected every counted string in four locales at
the most common counts.
Add the 320 missing forms across 47 plural families: Russian `few` in the
genitive singular, uk/pl/cs `few` in the nominative plural, and genitive plural
for `many`. Czech `many` is deliberately absent (fractions-only, unreachable for
integer counts), as is Romance `many` (starts at 1,000,000).
Also translate the pl/cs "config issue" and "hop" families, which were still
literal English in _one/_other and so leaked regardless of category.
check:i18n now requires every plural category a locale actually selects for
counts 0-200, scoped to families that locale has already started so a wholly
untranslated locale still reports as the existing "missing key(s)" warning.
Verified: rendering all 47 families at counts 0-22 across the four locales now
produces zero English fallbacks, down from 320.
* chore(reticulum): retire rsReticulum LinkClient Nomad overlay
Upstream main now resolves the remote pubkey via handler-free
resolve_destination_on_transport, with no HasPath gate and no temporary
announce handlers, so the overlay's apply script had already degraded to a
no-op. ratspeak/rsReticulum#14 was closed as superseded rather than merged.
Removes the patch, apply script, and its test, drops the entries from
RS_RETICULUM_APPLY_SCRIPTS and RATSPEAK_PATCH_ENTRIES so pnpm run update
stops tracking it, and updates the docs to point at floated origin/main.
* fix(rrc): resolve nicklist hashes to names and surface dropped /who replies
rrcd JOINED rosters carry no nicknames, and the once-per-room auto /who gate
survived reconnects, so a re-joined room showed bare hex forever. Re-arm the
gate on the handshake, fall back to nicks learned anywhere in the hub's
transcripts, and warn when a forced /who reply never arrives instead of
looking ignored.
* feat(games): allow challenging peers to Four in a Row
The FourInARowBoard added in be5ae05 was unreachable. A challenge is the only
way to create a session, and both challenge entry points offered just ttt and
chess, so no session could ever carry app_id four_in_a_row and the board branch
in GamesPanel was dead code.
Add the app id to GamesAppId and move the menu list into a single exported
GAMES_CHALLENGE_APPS. It had been duplicated verbatim in GamesPanel and
ReticulumGameChallengeButton, which is what let both menus drift out of sync
with the board dispatch in the first place.
The list stays curated rather than derived from the sidecar app manifest:
manifest app_id is an open string and the board dispatch falls through to
Tic-Tac-Toe for anything it does not recognize, so a manifest-driven menu could
offer a game that renders the wrong board. No sidecar change is needed -- it
already registers four_in_a_row and passes the app id through unvalidated.
Locale labels use each language's established name for the game rather than a
literal translation ("Vier gewinnt", "Puissance 4", "Forza 4", "四子棋").
The picker test asserts the full option list, so adding a future board without a
menu entry fails instead of silently shipping another unreachable game.
* feat(rrc): cache hub nicks in SQLite so names survive transcript clears
Oversized /who replies are dropped by the hub, so the nicklist depended on
whatever transcript happened to be loaded. Persist every nick sighting per
hub (chat, /who row, JOINED advisory) and hydrate it once per session, so a
peer who spoke only once still gets a name after a restart or history clear.
* fix(nomad): sanitize Micron page text at the innerHTML sink
The Micron editor added on this branch feeds a textarea's value straight into
the parser, which CodeQL flags as js/xss-through-dom: appendOutput writes page
text into span.innerHTML, and the only DOMPurify pass ran afterwards over the
whole document in convertMicronToHtml.
Route the three markup-building innerHTML writes (text spans, style-run spans,
link labels) through setSanitizedHtml so the barrier sits at the sink. In
practice wrapWord already escapes `<`, `>` and `&` when force-monospace is on,
so rendering is unchanged and tag-like text stays inert literal text; the sink
sanitize covers the non-monospace path and any future part producer that does
not escape.
Tests assert tag-like page text and link labels never become elements, and that
literal angle brackets and ampersands still render as typed.
Sign up for freeto 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.

writeH1 calls socket.setTypeOfService() unconditionally — uncaught EINVAL on macOS crashes the process

3 participants

@mcollina@codecov-commenter@ronag