Harden the dev flasher's postMessage receiver#1609
Conversation
Three receiver-side fixes for parity with the web.esphome.io review (esphome/dashboard#923), which shares this postMessage contract: - post() wraps postMessage in try/catch and falls back to '*', so a malformed origin= hash param (e.g. origin=null) can no longer throw and wedge the ready handshake before the retry interval starts. - A malformed firmware payload now calls stopReadyRetry() before bailing, so the opener stops receiving ready frames once it has clearly attached, instead of for up to 10s. - erase coerces with 'firmware.erase !== false' instead of '?? true', so a non-boolean erase (e.g. the string "false") no longer reads as truthy. handshake.test.mjs gains assertions for the first two (the erase path only runs against real hardware).
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #1609 +/- ##
=======================================
Coverage 99.54% 99.54%
=======================================
Files 226 226
Lines 17852 17852
=======================================
Hits 17771 17771
Misses 81 81
Flags with carried forward coverage won't be shown. Click here to find out more. 🚀 New features to boost your workflow:
|
|
|
esphbot
left a comment
There was a problem hiding this comment.
No blocking issues found.
There was a problem hiding this comment.
Pull request overview
Hardens the dev USB flasher’s postMessage receiver (flasher/src/main.ts) to avoid handshake wedging and to better handle malformed inputs, aligning behavior with the production receiver review findings.
Changes:
- Wrap outbound
postMessageintry/catchand fall back totargetOrigin="*"when the hash-provided origin is malformed (e.g.origin=null) so the ready handshake still proceeds. - Stop the ready re-announce interval when a nonce-valid but malformed firmware payload is received (error state), preventing spurious
readyframes after the opener has clearly attached. - Make
erasehandling explicitly boolean-safe (erasedefaults totrueunless the sender provides literalfalse).
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
| flasher/src/main.ts | Adds receiver-side hardening: safe postMessage fallback, correct ready-retry shutdown on malformed payload, and explicit erase coercion. |
| flasher/test/handshake.test.mjs | Extends puppeteer handshake assertions to cover malformed origin and “malformed payload stops ready retry” behavior. |
The malformed-origin fallback caught every postMessage failure and silently downgraded targetOrigin to '*', masking unrelated errors, and the fallback post was itself unguarded. Log the caught error before falling back (outbound frames carry no nonce, so the broader audience leaks nothing) and wrap the fallback so a dead opener doesn't throw.
|
Thanks Kōan — addressed the broad-catch finding in 70c1250: |
PR Review — Harden the dev flasher's postMessage receiverThree tightly-scoped, correct receiver-hardening fixes with matching puppeteer assertions. Merge-ready. Verified against the live file (not just the diff):
Checklist
Automated review by Kōan (Claude) |
esphbot
left a comment
There was a problem hiding this comment.
No blocking issues found.
What does this implement/fix?
Three receiver-side hardening fixes for the dev flasher's postMessage handler (
flasher/src/main.ts), for parity with the web.esphome.io receiver review (esphome/dashboard#923) that shares the same contract. The device-builder-frontend sender (src/util/usb-flasher.ts) is unaffected; these are receiver concerns.post()no longer throws on a malformed origin.targetOrigincomes from the hashoriginparam, so a value likeorigin=nullbecomes"null"and the first synchronouspost()threw before the ready-retry interval was set up, wedging the handshake.post()now wrapspostMessagein try/catch, falls back to"*", and pinstargetOrigin = "*"so subsequent frames keep flowing.isFlashPartsfailure branch set the error state and returned beforestopReadyRetry(), so the opener kept receivingreadyframes for up to 10s even though it had clearly attached and sent. It now callsstopReadyRetry()on that path, mirroring the accepted path.eraseis coerced explicitly.firmware.erase ?? trueonly defaults on null/undefined, so a non-booleanerase(e.g. the string"false") read as truthy. Changed tofirmware.erase !== false, so erase stays the safe default unless the opener explicitly sent booleanfalse.The puppeteer harness (
flasher/test/handshake.test.mjs) gains assertions for the first two: a malformedorigin=nullstill yields areadyframe, and a malformed payload stops thereadyre-announce. The erase path only runs at flash time against real hardware, so it stays code-only (consistent with the harness's existing scope). I confirmed both new assertions go red when their fix is reverted.Related issue or feature (if applicable):
Types of changes
bugfixnew-featureenhancementbreaking-changerefactordocsmaintenancecidependenciesFrontend coordination
Checklist
ruff,codespell, yaml/json/python checks).tests/where applicable.