Skip to content

Validate device names on rename so users don't brick a device profile - #143

Merged
jherforth merged 2 commits into
jherforth:mainfrom
mrramam:feature/device-name-validation
Aug 31, 2026
Merged

Validate device names on rename so users don't brick a device profile#143
jherforth merged 2 commits into
jherforth:mainfrom
mrramam:feature/device-name-validation

Conversation

@mrramam

@mrramam mrramam commented Aug 29, 2026

Copy link
Copy Markdown
Contributor

A device name containing / bricks that device: the dashboard stops rendering, and the device cannot be deleted through the UI either.

The failure

The client escapes the name correctly — getDeviceApiBase uses encodeURIComponent, so / becomes %2F. nginx decodes it before proxying, so /api/devices/<name>/settings splits into a path that matches no route, and every request for that device 404s. Deleting it goes through the same path shape, so the UI offers no way out.

Reproduced on a stock instance:

PUT /api/devices/Kitchen%20Display/settings   -> 200   (a space is fine)
PUT /api/devices/a%2Fb/settings               -> 404   (the bug)

The fix, and one thing it deliberately does not do

Validation happens on rename only — the one place a human chooses a name. Reads are untouched, and ensureDeviceExists is unchanged.

That restraint is the important part. An earlier revision of this change validated on the lookup path too, which meant an existing device called Kitchen Display would start failing on upgrade — breaking displays that work today in order to fix ones that are already broken. An existing row's name is a fact about the world; rejecting a name the application previously accepted and stored is not a fix.

For the same reason there is no migration and no sweep. Repairing an install that already holds a bad name would require rewriting the name server-side and in every browser's localStorage, since the client holds its own identity and would keep requesting the old one. That is a permanent cost on every page load of every install, to fix a rare condition. getDeviceName still returns exactly what is stored.

The rule

An allow-list rather than a deny-list, so it stays correct if HomeGlow is fronted by Caddy or Apache, or by nginx with different merge_slashes settings:

/^[\p{L}\p{N} _\-.'()]+$/u

Unicode letters and digits, space, and the punctuation people actually use for a screen in their house — Kitchen Display, Nan's iPad, Playroom (up), José. Characters carrying URL meaning (/ \ % ? # & + : @) are excluded even where the current nginx tolerates them.

Plus: NFC-normalised so an accented name typed two ways cannot produce two devices that look identical; 1–64 characters after trimming; at least one letter or digit; no ...

The charset is intentionally broader than the bug strictly requires. Only / is known to break today — but %, ? and # have URL meaning that a different front end may well act on, and a device name has no need of them.

Client side

The rename dialog validates as you type, disabling the confirm action with inline helper text, so the server 400 is a backstop rather than the user's first encounter with the rule. Both implementations are checked against each other; an earlier revision diverged because RegExp.prototype.test() coerces its argument, so isValidDeviceName(undefined) tested the string "undefined" and returned true on the client while the server returned false.

Testing

server/tests/deviceName.test.js and client/src/utils/deviceName.test.js cover the valid names above, /, .., whitespace-only, pure punctuation, over-length, and non-string inputs — plus a case asserting the two NFC spellings of an accented name resolve to the same device.

Full suites on Node 20 (matching ci-tests.yml): backend 190 passing, frontend 120 passing, check:i18n 715/715, client build clean.

A device name containing `/` bricks that device. The client escapes it
correctly with encodeURIComponent, but nginx decodes `%2F` before proxying, so
`/api/devices/<name>` splits into a path that matches no route. The dashboard
stops rendering, and the device cannot be deleted through the UI either — that
request takes the same shape.

Validate the name where a human chooses one: the rename route. Reads are
deliberately left alone. An existing row's name is a fact about the world, and
rejecting a name we previously accepted would break displays that work today —
including on upgrade, which is the worst possible moment.

The rule is an allow-list rather than a deny-list, so it stays correct if
HomeGlow is fronted by Caddy or Apache, or by nginx with different
merge_slashes settings:

    /^[\p{L}\p{N} _\-.'()]+$/u

Unicode letters and digits, space, and the punctuation people actually use
when naming a screen in their house — "Kitchen Display", "Nan's iPad",
"Playroom (up)", "José". Characters carrying URL meaning (/ \ % ? # & + : @)
are excluded even where the current nginx tolerates them. Names are
NFC-normalised so an accented name typed two ways cannot produce two devices
that look identical, must be 1-64 characters after trimming, must contain at
least one letter or digit, and must not contain ".." .
Mirrors the server rule in the rename dialog so the 400 is a backstop rather
than the user's first experience of the constraint: the confirm action is
disabled and inline helper text explains what is allowed.

The two implementations are checked against each other for agreement — an
earlier revision diverged because `RegExp.test()` coerces its argument, so
`isValidDeviceName(undefined)` tested the string "undefined" and returned true
on the client while the server returned false.

getDeviceName is deliberately unchanged: it returns exactly what is stored and
never rewrites it. Repairing an install that already holds an invalid name
would mean mutating every client's stored identity on every page load, forever,
to fix a rare condition — so this prevents new breakage rather than attempting
to repair old.
@mrramam
mrramam force-pushed the feature/device-name-validation branch from caa542f to af88508 Compare August 29, 2026 15:59
@mrramam mrramam changed the title Validate device names on rename so a users don't brick a device profile Validate device names on rename so users don't brick a device profile Aug 29, 2026
@jherforth
jherforth merged commit 50a192e into jherforth:main Aug 31, 2026
@github-project-automation github-project-automation Bot moved this from Backlog to Done in HomeGlow Kanban Aug 31, 2026
jherforth added a commit that referenced this pull request Aug 31, 2026
…ollow-up)

The allow-list added in #143 is letters, digits and a little punctuation. Plenty
of scripts do not fit that: Devanagari, Thai, Punjabi, Sinhala and Khmer carry
their vowel signs and viramas in the Unicode Mark category, so a perfectly
ordinary "kitchen" in Hindi or Thai was rejected on rename.

Adding \p{M} to the allow-list fixes those without loosening anything that
matters. Slash, percent, question mark, hash and dot-dot stay rejected, and so
do zero-width joiners and bidi overrides, which are format characters (\p{Cf})
rather than marks.

DEVICE_NAME_HAS_ALNUM is deliberately left alone, so a name still has to contain
a real letter or digit and a string of bare combining marks is not a name.

Tests on both sides cover the three scripts and pin the controls that must stay
out. 208 server, 122 client, build clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@jherforth

Copy link
Copy Markdown
Owner

Merged — nice piece of work. The reasoning about validating on rename only, and deliberately not shipping a migration, is the right call and well argued.

One little tweak pushed on top in b7951d2: the allow-list was \p{L}\p{N}, which quietly excludes any script that keeps its vowel signs in the Mark category — so Hindi and Thai device names bounced. Added \p{M}, which lets those through while /, %, ?, # and .. stay rejected, and zero-width joiners and bidi overrides stay out too since they're \p{Cf} rather than marks. Left DEVICE_NAME_HAS_ALNUM alone so a string of bare combining marks still isn't a name.

Admittedly this only unblocks our enormous install base across India and Thailand, who I'm sure have been queuing up to name a kitchen tablet — but the fix was one character class, so it seemed rude not to.

Tests added both sides. 208 backend / 122 frontend, build clean.

@jherforth jherforth added this to the 1.8 milestone Aug 31, 2026
@jherforth
jherforth self-requested a review August 31, 2026 14:17
@jherforth jherforth added the bug Something isn't working label Aug 31, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

2 participants