Resolving Relative URLs Against a Base URL
Relative URL resolution is how a browser converts href="../about" to an absolute URL given the current page's address. The algorithm is defined in RFC 3986 Section 5.21 and is implemented by every major URL parsing library. Understanding it matters when writing web crawlers, processing HTML links, implementing redirects, or building link resolvers for documentation systems.
The resolution algorithm has four cases: a reference starting with a scheme is used as-is (absolute URL); one starting with // inherits the base scheme (protocol-relative); one starting with / inherits the base scheme and authority (root-relative); everything else is resolved relative to the base path directory after removing the base filename.
The four reference types
An absolute reference such as https://other.com/page is returned as-is without any modification. A scheme-relative reference like //cdn.example.com/file.js inherits the base scheme and becomes https://cdn.example.com/file.js when the base is HTTPS. A root-relative reference like /images/logo.png inherits the base scheme and host, producing https://example.com/images/logo.png regardless of the current page depth. Consequently, a path-relative reference such as ../about or contact.html is resolved against the base path directory2. Building on this, the base path directory is everything up to and including the last / in the base path, so /blog/post/ has directory /blog/post/ while /blog/post has directory /blog/. Choosing the right reference type for each use case keeps your URLs portable and your resolution logic predictable across every environment.
Dot-segment resolution
Path-relative references may contain dot-segments: . (current directory) and .. (parent directory). The resolution algorithm removes them: /a/b/../c becomes /a/c, and /a/./b becomes /a/b1. Applying this to a relative reference ../images/logo.png against base https://example.com/docs/guide/: combine base directory /docs/guide/ with the reference to get /docs/guide/../images/logo.png, then resolve the dot-segment to produce /docs/images/logo.png. Consequently, a reference of ../../home against the same base produces /home by stepping two levels up from /docs/guide/. The algorithm processes each dot-segment in order from left to right, collapsing the path until no dot-segments remain, which guarantees a deterministic result regardless of how many . or .. tokens the input contains.
Implementation across languages
Every major URL library implements RFC 3986 Section 5.2, so the same relative reference resolved against the same base URL produces the same absolute URL regardless of which language you use. In JavaScript, new URL(relative, base) resolves the reference2. In Python, urllib.parse.urljoin(base, relative) does the same3. In Go, baseUrl.ResolveReference(relativeUrl)4. Building on this, all three follow the same algorithm and produce identical results for well-formed inputs.
Edge cases every implementation must handle
Edge cases to test include: an empty string reference returns the base URL with the fragment removed, a reference of ? inherits the base authority and path but starts a new query, and a reference of # inherits everything except the fragment5. When you need to resolve URLs in a language not listed here, look for a library that explicitly cites RFC 3986 Section 5.2 in its documentation because that spec compliance is what guarantees cross-language consistency.
Resolving relative URLs in web crawlers and HTML processors
Web crawlers encounter relative URLs in href, src, and action attributes. The resolution algorithm from RFC 3986 Section 5.2 converts each relative reference to an absolute URL using the document's base URL. Building on this, the base URL comes from the document's <base href="..."> tag if present, or from the document's own URL. A crawler must track the current base URL as it processes each page and resolve every extracted link against that base. For HTML processing in Python, the BeautifulSoup library does not resolve relative URLs automatically; you must call urljoin(base, href) on each extracted link. In JavaScript DOM processing, anchor.href returns the resolved absolute URL directly, which is one advantage of browser-based link extraction.
Handling redirect chains that mix absolute and relative Location headers
HTTP redirect responses include a Location header that may be absolute or relative. When a crawler follows a redirect chain, each relative Location must be resolved against the URL of the redirecting response. A chain of A → B → C where B returns a relative Location like ../new requires resolving against A's URL to find C. Building on this, tracking the current URL at each hop is essential because resolving a relative Location against the wrong base silently redirects the crawler to an unrelated page. Libraries like Python's requests and Node's follow-redirects handle this automatically, but custom redirect-following code must resolve each Location header against the previous response URL before issuing the next request.
Protocol-relative URLs and when to avoid them
Protocol-relative URLs (starting with //) inherit the scheme from the base URL. They were commonly used to serve resources on the same scheme as the parent page: //cdn.example.com/script.js loads via HTTP on HTTP pages and HTTPS on HTTPS pages. Building on this, protocol-relative URLs are now considered an anti-pattern6. The parent page's scheme may be HTTP (insecure), causing the resource to load over HTTP even when HTTPS is available. Modern best practice is to avoid loading a CDN script over plain HTTP by always writing explicit https:// URLs for external resources, since the Content Security Policy header upgrade-insecure-requests can upgrade HTTP requests to HTTPS but is less reliable than stating the scheme yourself. When you encounter protocol-relative URLs in legacy code, replace them with https:// equivalents during your next maintenance pass.
Edge cases in dot-segment resolution with trailing slashes
Trailing slashes change how relative references resolve because they determine whether the base path's final segment is treated as a file or a directory. Given base https://example.com/docs/guide/, the reference ../about resolves to /docs/about because the base directory is /docs/guide/. Given base https://example.com/docs/guide (no trailing slash), the same reference resolves to /about because the base directory is /docs/. Building on this, a base URL of /a/b/c/ with reference ../../x resolves to /x, while /a/b/c with the same reference resolves to /a/x. The trailing slash is the only thing that determines which directory the resolution starts from, so normalizing trailing slashes before resolving relative URLs prevents inconsistent results across environments.
Practically, this means you should decide on the directory form of every base URL once, at the boundary where you receive it, rather than re-deriving it on each resolution call. A helper that appends a slash when the path has no trailing slash and is not obviously a file (no extension) makes the resolution start point deterministic. That single normalization step removes the most common source of off-by-one-directory bugs in crawlers and link generators.
Testing relative URL resolution across languages
When your system involves multiple languages parsing and resolving URLs, test that they produce identical results. JavaScript's new URL(relative, base), Python's urljoin(base, relative), Go's base.ResolveReference(ref), and Java's base.resolve(relative) all implement RFC 3986 Section 5.2, but edge cases may produce different results. Building on this, create a shared test suite with inputs like empty references, query-only references (?page=2), fragment-only references (#section), and references with encoded characters. Run the same test cases in every language your system uses. Differences in dot-segment resolution, empty reference handling, or percent-encoding normalization can cause subtle bugs in distributed systems where one service resolves a URL and another fetches it. Document any known differences between the URL libraries in your system.
When to use this
Resolve relative URLs whenever you process links from HTML documents, sitemap files, API responses with href fields, or redirect chains because any source that delivers URLs without a scheme or host requires resolution against a known base.
Examples
Resolve relative URLs against a base in JavaScript
const base = "https://example.com/docs/guide/"; console.log(new URL("../images/logo.png", base).href); // https://example.com/docs/images/logo.png console.log(new URL("/about", base).href); // https://example.com/about console.log(new URL("//cdn.example.com/script.js", base).href); // https://cdn.example.com/script.js console.log(new URL("https://other.com/page", base).href); // https://other.com/page
Resolve relative URLs in Python
from urllib.parse import urljoin base = "https://example.com/docs/guide/" print(urljoin(base, "../images/logo.png")) # https://example.com/docs/images/logo.png print(urljoin(base, "/about")) # https://example.com/about print(urljoin(base, "contact.html")) # https://example.com/docs/guide/contact.html
- 1.
T. Berners-Lee, R. Fielding, and L. Masinter, "Uniform Resource Identifier (URI): Generic Syntax," RFC 3986, IETF, January 2005. https://www.rfc-editor.org/rfc/rfc3986.html
- 2.
Mozilla Developer Network, "Resolving relative references to a URL," developer.mozilla.org, accessed June 2026. https://developer.mozilla.org/en-US/docs/Web/API/URL_API/Resolving_relative_references
- 3.
Python Software Foundation, "urllib.parse — Parse URLs into components," docs.python.org, accessed June 2026. https://docs.python.org/3/library/urllib.parse.html
- 4.
Go team, "url package — net/url," pkg.go.dev, accessed June 2026. https://pkg.go.dev/net/url#URL.ResolveReference
- 5.
Stack Overflow, "Is Java's URI.resolve incompatible with RFC 3986 when the relative URI contains an empty path?," stackoverflow.com, accessed June 2026. https://stackoverflow.com/questions/22203111/is-javas-uri-resolve-incompatible-with-rfc-3986-when-the-relative-uri-contains
- 6.
Google for Developers, "Push notifications for the web," developers.google.com, accessed June 2026. https://developers.google.com/web/shows/lazyweb/2015/episode-3