Use the WHATWG URL API—not path.normalize()—for an href. Resolve the reference against a known base URL, validate it, and serialize the resulting URL:
function normalizeHref(href, base = document.baseURI) {
if (typeof href !== 'string') throw new TypeError('href must be a string');
if (!URL.canParse(href, base)) throw new TypeError('Invalid href');
return new URL(href, base).href;
}
This handles relative links, removes dot segments such as .., and applies URL encoding. The “unsupported path format” family of errors usually means a URL reference was sent to a filesystem-path API, a relative reference was parsed without a base, or malformed input reached the parser.
First decide what you are normalizing
An href is a URL reference. A filesystem path is a location on the local machine. They look similar but have different syntax and security rules.
| Input | Correct API | Typical result |
|---|---|---|
/docs/../guide/index.html |
new URL(value, base) |
URL with /guide/index.html |
./assets/../public/app.css on disk |
path.normalize() or path.resolve() |
Platform-specific local path |
images/logo.svg |
URL plus an explicit base | Absolute URL at that origin |
Do not pass https://example.com/a to path.normalize(). On POSIX it treats the string as a filename-like value; on Windows, backslashes and drive-letter rules can produce an even more misleading result. Conversely, do not use URL parsing to decide whether a local path stays inside an allowed directory.
#1 Best Overall
Normalize an href in browser code
Resolve against the document base
The browser’s base is normally document.baseURI. It includes the effect of a page’s optional <base href> element, so it is safer than assuming the current address.
function normalizeHref(href, base = document.baseURI) {
if (typeof href !== 'string') {
throw new TypeError('href must be a string');
}
if (!URL.canParse(href, base)) {
throw new TypeError('Invalid href');
}
return new URL(href, base).href;
}
normalizeHref('/docs/../guide/index.html');
// https://example.test/guide/index.html
normalizeHref('../images/logo.svg', 'https://example.test/docs/');
// https://example.test/images/logo.svg
A leading slash is origin-relative; a value without a leading slash is relative to the base directory. A query-only reference such as ?page=2 keeps the base path, while #details keeps both the path and query and changes only the fragment.
Use the URL object when you need individual components
const u = new URL('../guide/index.html?mode=print#top', 'https://example.test/docs/');
console.log(u.protocol); // 'https:'
console.log(u.origin); // 'https://example.test'
console.log(u.pathname); // '/guide/index.html'
console.log(u.search); // '?mode=print'
console.log(u.hash); // '#top'
console.log(u.href); // serialized absolute URL
Inspecting components avoids fragile string splitting on ?, #, or :. Assigning to URL properties also applies the platform’s percent-encoding rules.
Why relative hrefs need a base
new URL('images/logo.svg') cannot determine an origin and throws. Supply a base that represents the page or site where the link is interpreted:
Windows Errors? Fix Them Before They Spread
Repair common Windows errors and clear accumulated junk for a smoother, more stable PC - no reinstall needed.Free scan · no reinstallCrashes, No Sound, or Screen Glitches?
Random freezes, missing sound and display glitches usually trace back to one bad driver. Find and replace yours safely.Free scan · under a minuteRank #2
- HTML CSS Design and Build Web Sites
- Comes with secure packaging
- It can be a gift option
const normalized = new URL('images/logo.svg', 'https://example.test/products/').href;
// https://example.test/products/images/logo.svg
In server code, choose the base deliberately. It may come from a trusted configured origin, an incoming request’s validated origin, or a site-specific setting. Do not blindly use an untrusted Host header to construct links that will be sent in security-sensitive contexts.
Validate before parsing and handle malformed input
Use URL.canParse() for expected bad data
function tryNormalizeHref(value, base) {
if (typeof value !== 'string' || !URL.canParse(value, base)) {
return null;
}
return new URL(value, base).href;
}
const result = tryNormalizeHref(userSuppliedHref, 'https://example.test/');
if (result === null) {
// Reject, report, or show a validation error.
}
URL.canParse() returns a boolean instead of throwing. If your runtime does not provide it, use a try/catch around new URL() and retain the same type check.
Reject the wrong type early
Node path methods throw a TypeError when their argument is not a string. The same explicit check is useful for hrefs because values such as null, objects, and numbers often arrive from JSON or optional form fields.
Do not confuse URL paths with filesystem paths
When path.normalize() is the right tool
import path from 'node:path';
const localPath = path.normalize('./assets/../public/app.css');
console.log(localPath); // public/app.css on POSIX; separator varies by platform
Filesystem normalization removes . and .., collapses repeated separators, and follows the host platform’s separator convention. An empty string normalizes to .; trailing-separator behavior is platform-specific. Use path.resolve() when you need an absolute path based on the process working directory.
The Tool Desk
Outbyte Driver Updater FREEFix the driver behind crashes, sound loss and screen glitchesFind Drivers →Outbyte PC Repair FREEClear out junk files and repair common Windows errorsFree Scan →Rank #3
When a URL points to a local file
Convert a file: URL with the URL-aware conversion provided by Node, then apply filesystem policy checks. Decoding an encoded dot segment can expose traversal; conversion alone is not a complete directory-traversal defense. Resolve the result and verify it remains under an explicitly allowed directory before opening it.
Never use platform separators in web hrefs
Web URL paths use forward slashes, including on Windows. A backslash may be interpreted specially by URL parsers or servers and can create inconsistent security decisions. Build URL references with URL, not by joining strings with path.join().
How URL normalization handles dots, queries, fragments, and encoding
Dot segments
During relative-reference resolution, . and .. segments are removed according to generic URI rules. This is why /a/b/../c becomes /a/c. The operation does not mean the server has authorized access to every resulting resource; authorization still belongs to the server.
Query strings and fragments
The path ends at the first ? or #. A query is sent to the server; a fragment normally is not sent in the HTTP request and is interpreted by the client. Preserve them through URL serialization rather than attempting to normalize the entire href as one filesystem string.
Quick wins for a faster PC:
Repair Windows errors before they cause bigger problemsFix Now →Scan for outdated or missing drivers - takes under a minuteDriver Scan →Rank #4
- Brand: Wiley
- Set of 2 Volumes
- A handy two-book set that uniquely combines related technologies Highly visual format and accessible language makes these books highly effective learning tools Perfect for beginning web designers and front-end developers
Percent-encoding
Characters that are not valid in a URL component are encoded by URL serialization. If you set a pathname, search parameter, or hash through the URL object, let the implementation encode it. Do not concatenate untrusted text into a URL and assume replacing spaces is sufficient; different components have different encoding rules.
const u = new URL('https://example.test/search');
u.searchParams.set('q', 'red shoes & hats');
console.log(u.href);
// https://example.test/search?q=red+shoes+%26+hats
Unsupported path format errors: a diagnostic procedure
- Log the exact value and type. Record
typeof href, the string (without secrets), and the selected base. - Classify the domain. Is it a web reference, a
file:URL, or a local path? Choose one API accordingly. - Check for a base. Relative values need a valid absolute base with a scheme and host where appropriate.
- Parse with validation. Call
URL.canParse(value, base), then constructnew URL(value, base). - Inspect components. Check
protocol,origin,pathname,search, andhashseparately. - Apply policy. If the URL will trigger a request, allowlist protocols (usually
https:and possiblyhttp:) and hosts. If it becomes a file path, enforce an allowed-directory boundary after resolution.
Common failures and precise fixes
| Symptom | Likely cause | Fix |
|---|---|---|
“Invalid URL” from new URL() |
Relative input has no base, or the value is malformed. | Provide a valid base and guard with URL.canParse(). |
| “Path must be a string” | A Node path function received null, an object, or another type. |
Validate type; use URL APIs if the value is an href. |
| Backslashes appear in links | path.join() or Windows path logic was used for a URL. |
Construct with new URL() and serialize .href. |
| Correct path, wrong directory | The base ends at a file-like segment or has an unexpected trailing slash. | Verify the exact base: /docs and /docs/ resolve descendants differently. |
| Open redirect or SSRF risk | A normalized URL was trusted without host or protocol policy. | Allowlist schemes and origins after parsing; do not rely on string prefixes. |
| Traversal remains possible for local files | Normalization was mistaken for authorization. | Resolve, compare against a canonical allowed directory, and reject escapes. |
Node.js patterns for reliable applications
Normalize links in a server-rendered page
export function absoluteHref(href, siteOrigin) {
if (typeof href !== 'string') throw new TypeError('href must be a string');
const base = new URL(siteOrigin);
if (!URL.canParse(href, base)) throw new TypeError('Invalid href');
const url = new URL(href, base);
if (url.protocol !== 'https:' && url.protocol !== 'http:') {
throw new TypeError('Unsupported protocol');
}
return url.href;
}
Keeping protocol checks beside normalization makes the trust boundary visible. Add an origin allowlist when links are required to stay on your site.
Keep URL and path tests separate
Test URL cases with relative references, query-only and fragment-only references, encoded characters, protocol changes, and malformed hosts. Test filesystem cases on every supported operating system because separators and drive-letter behavior differ.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Performance and reliability considerations
URL parsing is deterministic and inexpensive compared with network access. Normalize once at the boundary where input enters your application, then pass the resulting URL object or string through internal code. Avoid repeatedly parsing the same value in rendering loops. Do not “normalize” by making a network request: URL validity does not prove that a resource exists, responds quickly, or is safe to fetch.
Recommended Free Tools
Best Value
For bulk link processing, collect rejected values with their index and reason rather than aborting the entire batch. Preserve the original value for diagnostics, but log cautiously when it may contain credentials or tokens.
Or skip the browser setup
If your goal is to obtain a clean screenshot of a normalized URL rather than implement browser navigation yourself, ScreenshotNeo provides a single HTTP request. It accepts consent banners before capture and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each cleanup step can be disabled. Bot checks, CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and the response identifies the result with X-Page-Verdict and X-Billed headers.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
See the complete parameter reference in the ScreenshotNeo documentation. The same endpoint supports PNG, JPEG, WebP, and PDF output, full-page lazy-image loading, CSS-selector element capture, dark mode, device and viewport settings, retina scale, PDF paper and page options, custom CSS and JavaScript, clicks, selector or network-idle waits, ad/tracker/request blocking, headers, cookies, user-agent and Authorization values, timezone and geolocation, transparent backgrounds, resizing, TTL caching, signed image links, asynchronous webhooks, bulk capture of up to 100 URLs per call, a usage API, and an OpenAPI specification. Common parameter names used by other screenshot APIs also work, easing migration.
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
ScreenshotNeo also exposes an MCP server with take_screenshot, get_page_info, and capture_pdf for Claude, Cursor, and other MCP clients. The Free plan includes 1,000 shots per month without a card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account to try it.
Frequently Asked Questions
Should I call path.normalize() on an href?
No. Use new URL(href, base) for web references. Reserve path.normalize() for local filesystem paths.
Why does new URL() reject my relative link?
A relative reference has no origin by itself. Pass an absolute base such as document.baseURI or a configured site URL.
Does normalization make a URL safe?
No. It canonicalizes syntax only. Enforce protocol, host, redirect, and filesystem-boundary policies separately.
What is the difference between /docs and /docs/ as a base?
The first is treated like a file segment, so a relative child replaces it; the second is treated as a directory, so the child is appended.
Free tools Windows power users keep installed
One-click scans. No signup required.
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

