Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Rank #2
Sale
HTML and CSS: Design and Build Websites
  • 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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Rank #4
Sale
Web Design with HTML, CSS, JavaScript and jQuery Set
  • 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

  1. Log the exact value and type. Record typeof href, the string (without secrets), and the selected base.
  2. Classify the domain. Is it a web reference, a file: URL, or a local path? Choose one API accordingly.
  3. Check for a base. Relative values need a valid absolute base with a scheme and host where appropriate.
  4. Parse with validation. Call URL.canParse(value, base), then construct new URL(value, base).
  5. Inspect components. Check protocol, origin, pathname, search, and hash separately.
  6. Apply policy. If the URL will trigger a request, allowlist protocols (usually https: and possibly http:) 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.Support on Ko-Fi

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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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.