URL encoding diagnostics · Updated November 2, 2026

URL Encode Dot Segments: Why %2E%2E Still Changes the Path

Percent-encoding dots does not necessarily keep them as a literal path segment. For the HTTP/HTTPS URL parser tested here, /files/%2E%2E/item becomes /item. The mixed spellings .%2E and %2e. do the same. Inspect the parsed pathname before assuming an encoder preserved the route you intended.

This local preview uses new URL(input, 'https://example.com'). It makes no request to that address. The server observations in the table below come from a separate loopback experiment, not from your input.

Locate the boundary where the path changed

  1. Save the path string immediately after your encoding step. Keep the exact percent spelling in the record.
  2. Construct the URL the way your client actually does, and record its pathname. Compare it with the saved string before sending anything.
  3. Record the request target at your own test receiver. A parsed URL object and a raw HTTP path option are different construction paths; the matrix below runs both.
  4. Repeat with a non-special control such as report..txt. If that stays intact, do not generalize a special whole-segment rule to every dot in a filename.
  5. If the value is supposed to be an identifier, define the allowed values and route construction deliberately. An escaping function alone does not define that application policy.

Twelve paths, with 24 observed local HTTP requests

Run on November 2, 2026 using Node v24.19.0. One temporary HTTP server on 127.0.0.1 returned its literal req.url. For each fixture, the script sent one request with http.get(new URL(input, base)) and another with the original input supplied as the HTTP client's path option. Each received string was asserted. These are two client construction modes against the same server, not a survey of proxies or frameworks.

FixtureInputURL.pathnameReceiver: URL objectReceiver: raw path option
literal-parent/files/../item/item/item/files/../item
encoded-parent/files/%2E%2E/item/item/item/files/%2E%2E/item
lowercase-parent/files/%2e%2e/item/item/item/files/%2e%2e/item
mixed-left/files/.%2E/item/item/item/files/.%2E/item
mixed-right/files/%2e./item/item/item/files/%2e./item
literal-current/files/./item/files/item/files/item/files/./item
encoded-current/files/%2E/item/files/item/files/item/files/%2E/item
double-encoded/files/%252E%252E/item/files/%252E%252E/item/files/%252E%252E/item/files/%252E%252E/item
three-dots/files/.../item/files/.../item/files/.../item/files/.../item
dot-suffix/files/..;v/item/files/..;v/item/files/..;v/item/files/..;v/item
dots-in-name/files/report..txt/item/files/report..txt/item/files/report..txt/item/files/report..txt/item
encoded-slash/files/%2F../item/files/%2F../item/files/%2F../item/files/%2F../item

The useful result is the disagreement, not just the removed segment

The encoded-parent row reaches /item through the URL-object mode, but the same local receiver sees /files/%2E%2E/item through the raw-path mode. Nothing in that result says our server decoded the dots or that a reverse proxy behaved a certain way. The experiment isolates a change introduced before the first request was sent: the construction of the URL object.

The five parent variants also show why case alone is a poor diagnostic. Literal double dots, uppercase escapes, lowercase escapes, and the two mixed literal/escaped forms all shorten the path in these fixtures. Our assertion checks the final path instead of only searching the input for one particular spelling.

The single-dot controls do something different. Both /files/./item and /files/%2E/item become /files/item. They remove their own segment; they do not remove files. Putting those rows next to the parent variants catches an overly broad explanation that treats every dot segment as a parent directory.

Four controls that keep this from becoming a blanket rule

Three dots and dots inside a name: ..., ..;v, and report..txt stay in the parsed path in this run. The rule concerns particular complete segments. It is not a global replacement of two adjacent dots anywhere in the URL.

A slash-looking escape: the %2F.. fixture stays one serialized path segment here. This does not establish how your server handles encoded slashes later. That is a separate boundary, covered in the encoded-slash experiment.

Double encoding: %252E%252E remains in the parsed pathname and both observed request targets. That is an observation, not a recommended fix. An extra decode in a later system could change what the value means. Do not add a percent layer merely to make this one preview stop shortening the path.

A query value: a separate assertion parses /files/item?q=%2E%2E#view. Its preview request target is /files/item?q=%2E%2E, and its hash is #view. A query value is not a dot path segment. Keep the path, query and fragment records separate when explaining a changed URL.

Why encodeURIComponent('..') is not a route policy

const value = '..';
const escaped = encodeURIComponent(value); // '..'
const url = new URL('/files/' + escaped + '/item', 'https://example.com');
console.log(url.pathname); // '/item'

The regression checks those exact outputs. A successful call to encodeURIComponent did not promise the assembled path would retain an opaque identifier. If you expect a literal name rather than navigation syntax, choose an application representation that expresses that contract and verify the constructed URL and receiver together. This checker does not validate access permissions or certify a path as safe.

Download and rerun the evidence

Download the Node regression script, twelve fixtures, recorded request observations and shared preview core. Put the first three in downloads/ and the core in assets/ beneath one parent folder, then run:

node downloads/url-dot-segments-regression.cjs

Expected summary on the recorded runtime: PASS 12 fixtures and 24 observed loopback requests; v24.19.0. The script starts its own loopback server on an available port and closes that server after the run. It writes fresh results, checks the query/fragment control and the origin-input guard, and exits nonzero on a failed assertion. Your Node version appears in the fresh output; it need not match ours.

Does this predict the final route on my production server?

No. The experiment records the request target at one Node receiver. It does not test your CDN, web server, router or storage lookup. Reproduce those boundaries in your own authorized test setup before assigning a cause to one of them.

Return to the URL encoder for ordinary component escaping, or use the JavaScript guide for API syntax. The matrix is deliberately about path dot segments, not another general encoder.

References checked November 2, 2026: WHATWG single-dot segment, double-dot segment, and URL constructor reference. The behavior is specified; our contribution is the controlled request-construction comparison, fixtures and recorded observations.