@samitouri / QOSami-HFS / commits / e85820b0

fix: (regression 3.0.3) ERR_UNESCAPED_CHARACTERS #1175

Massimo Melina committed Feb 28, 2026 at 18:54 UTC e85820b0cbcc967483a0c6f91308deedaa12f124
2 files changed +8 -2
src/util-http.ts
+2 -1
@@ -144,9 +144,10 @@ export function parseHttpUrl(url: string) {
144 const options = urlToHttpOptions(parsed)
145 const withoutHash = url.split('#', 1)[0]!
146 const authority = /^[a-z][a-z\d+.-]*:\/\/[^/?#]*/i.exec(withoutHash)?.[0]
147 + const unresolvedPath = !authority ? '/' : enforceStarting('/', withoutHash.slice(authority.length))
148 return {
149 ...options,
150 host: parsed.host,
150 - path: !authority ? '/' : enforceStarting('/', withoutHash.slice(authority.length)), // unresolved paths are useful in our tests
151 + path: unresolvedPath.replace(/[\u0000-\u0020\u0100-\u{10FFFF}]/gu, encodeURIComponent), // keep unresolved paths for tests, but escape characters that Node request rejects as unescaped
152 }
153 }
tests/test.ts
+6 -1
@@ -7,7 +7,7 @@ import { exec } from 'child_process'
7 import _ from 'lodash'
8 import yaml from 'yaml'
9 import { findDefined, pathEncode, randomId, try_, tryJson, UPLOAD_TEMP_HASH, wait } from '../src/cross'
10 -import { httpStream, stream2string, XRequestOptions } from '../src/util-http'
10 +import { httpStream, parseHttpUrl, stream2string, XRequestOptions } from '../src/util-http'
11 import { ThrottledStream, ThrottleGroup } from '../src/ThrottledStream'
12 import { mkdir, rm, rename, writeFile, access } from 'fs/promises'
13 import { Readable } from 'stream'
@@ -44,6 +44,11 @@ let defaultBaseUrl = BASE_URL
44 const execP = (cmd: string) => promisify(exec)(cmd).then(x => x.stdout)
45
46 describe('basics', () => {
47 + test('parseHttpUrl.path escapes invalid chars and keeps unresolved segments', () => {
48 + const parsedPath = parseHttpUrl('https://example.com/a/../репо with space/%2e%2e/file').path
49 + if (parsedPath !== '/a/../%D1%80%D0%B5%D0%BF%D0%BE%20with%20space/%2e%2e/file')
50 + throw Error('unexpected path: ' + parsedPath)
51 + })
52 //before(async () => appStarted)
53 test('frontend', req('/', /<body>/, { headers: { accept: '*/*' } })) // workaround: 'accept' is necessary when running server-for-test-dev, still don't know why
54 test('force slash', req('/f1', 302, { noRedirect: true }))