<?xml version='1.0' encoding='utf-8'?>
<!DOCTYPE rfc [
  <!ENTITY nbsp    "&#160;">
  <!ENTITY zwsp   "&#8203;">
  <!ENTITY nbhy   "&#8209;">
  <!ENTITY wj     "&#8288;">
]>
<?xml-stylesheet type="text/xsl" href="rfc2629.xslt" ?>
<!-- generated by https://github.com/cabo/kramdown-rfc version 1.7.29 (Ruby 3.4.4) -->
<rfc xmlns:xi="http://www.w3.org/2001/XInclude" ipr="trust200902" docName="draft-go-protocol-00" category="info" submissionType="independent" tocInclude="true" sortRefs="true" symRefs="true" version="3">
  <!-- xml2rfc v2v3 conversion 3.31.0 -->
  <front>
    <title>A Deterministic Algorithm for Resolving Shortlinks with Internal Redirects</title>
    <seriesInfo name="Internet-Draft" value="draft-go-protocol-00"/>
    <author fullname="Ethan Davidson">
      <organization>FartLabs</organization>
      <address>
        <email>ethan.r.davidson@gmail.com</email>
      </address>
    </author>
    <date year="2025" month="November" day="02"/>
    <keyword>shortlinks</keyword>
    <keyword>URL resolution</keyword>
    <keyword>redirects</keyword>
    <keyword>link shortening</keyword>
    <abstract>
      <?line 24?>

<t>This document specifies a deterministic algorithm for resolving shortlinks
(compact string identifiers) into fully-qualified URLs. The algorithm supports
both absolute URL destinations and origin-relative internal redirects with loop
protection. It defines deterministic precedence rules for combining query
parameters and fragments from multiple sources during resolution, enabling
consistent behavior across clients, servers, and command-line tools.</t>
    </abstract>
    <note removeInRFC="true">
      <name>Discussion Venues</name>
      <t>Source for this draft and an issue tracker can be found at
    <eref target="https://github.com/EthanThatOneKid/go-protocol"/>.</t>
    </note>
  </front>
  <middle>
    <?line 33?>

<section anchor="introduction">
      <name>Introduction</name>
      <t>Shortlinks provide a mechanism for mapping compact, memorable identifiers to
longer URLs. However, without a standardized resolution algorithm, different
implementations may resolve the same shortlink to different destinations or
handle edge cases inconsistently, leading to interoperability issues.</t>
      <t>This document specifies a deterministic algorithm for resolving shortlinks that:</t>
      <ul spacing="normal">
        <li>
          <t>Supports longest-prefix key matching to enable hierarchical shortlink
structures</t>
        </li>
        <li>
          <t>Handles both absolute URL destinations and origin-relative internal redirects</t>
        </li>
        <li>
          <t>Provides loop protection for internal redirect chains</t>
        </li>
        <li>
          <t>Defines clear precedence rules for combining query parameters and fragments
from multiple sources</t>
        </li>
        <li>
          <t>Remains stateless and deterministic for consistent behavior across
implementations</t>
        </li>
      </ul>
      <t>The algorithm is designed to work with any ruleset source (database, file,
configuration, etc.) and requires no protocol-level changes or IANA
registrations.</t>
    </section>
    <section anchor="conventions-and-definitions">
      <name>Conventions and Definitions</name>
      <t>The key words "<bcp14>MUST</bcp14>", "<bcp14>MUST NOT</bcp14>", "<bcp14>REQUIRED</bcp14>", "<bcp14>SHALL</bcp14>", "<bcp14>SHALL
NOT</bcp14>", "<bcp14>SHOULD</bcp14>", "<bcp14>SHOULD NOT</bcp14>", "<bcp14>RECOMMENDED</bcp14>", "<bcp14>NOT RECOMMENDED</bcp14>",
"<bcp14>MAY</bcp14>", and "<bcp14>OPTIONAL</bcp14>" in this document are to be interpreted as
described in BCP 14 <xref target="RFC2119"/> <xref target="RFC8174"/> when, and only when, they
appear in all capitals, as shown here.</t>
      <?line -18?>

<t>The following terms are used throughout this document:</t>
      <t><strong>Shortlink</strong>: A compact string identifier that maps to a URL destination.</t>
      <t><strong>Ruleset</strong>: A collection of key-value pairs where keys are shortlink
identifiers (non-empty strings without leading "/") and values are either
absolute URLs (strings whose first four characters are "http") or
origin-relative paths (strings whose first character is "/").</t>
      <t><strong>Request URL</strong>: The incoming URL to be resolved, which may contain an embedded
shortlink in its pathname component.</t>
      <t><strong>Destination URL</strong>: The fully-qualified URL that results from resolving a
shortlink.</t>
      <t><strong>Internal Redirect</strong>: A shortlink value that is an origin-relative path (whose
first character is "/"), which requires further resolution within the same
origin.</t>
      <t><strong>Absolute Destination</strong>: A shortlink value that is a fully-qualified URL (whose
first four characters are "http", including both "http" and "https" schemes),
which serves as the final destination.</t>
      <t><strong>Longest-Prefix Match</strong>: The process of finding the matching shortlink key by
starting with the full pathname (minus leading "/") and progressively removing
the trailing path segment (everything from the last "/" character onward,
inclusive of the "/") until either a matching key is found in the Ruleset or no
"/" character remains in the candidate key.</t>
    </section>
    <section anchor="algorithm-specification">
      <name>Algorithm Specification</name>
      <section anchor="overview">
        <name>Overview</name>
        <t>The algorithm takes as input:</t>
        <ul spacing="normal">
          <li>
            <t>A Request URL (a fully-qualified URL)</t>
          </li>
          <li>
            <t>A Ruleset (a collection of shortlink mappings)</t>
          </li>
        </ul>
        <t>The algorithm produces as output:</t>
        <ul spacing="normal">
          <li>
            <t>A Destination URL (the resolved fully-qualified URL)</t>
          </li>
        </ul>
        <t>The algorithm operates in three main phases:</t>
        <ol spacing="normal" type="1"><li>
            <t><strong>Initialization</strong>: Extract embedded query parameters and fragments from the
Request URL</t>
          </li>
          <li>
            <t><strong>Resolution</strong>: Find the matching shortlink using longest-prefix matching,
handling internal redirects as needed</t>
          </li>
          <li>
            <t><strong>Construction</strong>: Build the final Destination URL by combining components
according to precedence rules</t>
          </li>
        </ol>
      </section>
      <section anchor="initialization-phase">
        <name>Initialization Phase</name>
        <t>From the Request URL, extract and preserve the following components:</t>
        <ul spacing="normal">
          <li>
            <t><strong>Origin</strong>: The scheme, host, and port components of the Request URL</t>
          </li>
          <li>
            <t><strong>Request Query</strong>: The search component of the Request URL (including the
leading "?" if present, otherwise an empty string)</t>
          </li>
          <li>
            <t><strong>Request Fragment</strong>: The hash component of the Request URL (including the
leading "#" if present, otherwise an empty string)</t>
          </li>
          <li>
            <t><strong>Pathname</strong>: The pathname component of the Request URL (the path portion,
including the leading "/")</t>
          </li>
        </ul>
        <t>These preserved components will be used during final URL construction according
to the precedence rules defined below. Initialize the following variables:</t>
        <ul spacing="normal">
          <li>
            <t><tt>relativePathname</tt> = "" (empty string)</t>
          </li>
          <li>
            <t><tt>initialQuery</tt> = "" (empty string)</t>
          </li>
          <li>
            <t><tt>initialHash</tt> = "" (empty string)</t>
          </li>
          <li>
            <t><tt>redirectCount</tt> = 0</t>
          </li>
        </ul>
      </section>
      <section anchor="resolution-phase">
        <name>Resolution Phase</name>
        <t>The resolution process begins with the pathname from the Request URL and
searches for a matching shortlink key using longest-prefix matching.</t>
        <section anchor="longest-prefix-matching">
          <name>Longest-Prefix Matching</name>
          <t>At the start of each resolution iteration, find a matching shortlink key using
the following procedure:</t>
          <ol spacing="normal" type="1"><li>
              <t>Let <tt>candidate</tt> = <tt>pathname.slice(1)</tt> (the pathname with the leading "/"
character removed). If pathname is exactly "/", then <tt>candidate</tt> = "" (empty
string).</t>
            </li>
            <li>
              <t>Check if <tt>candidate</tt> exists as a key in the Ruleset:
              </t>
              <ul spacing="normal">
                <li>
                  <t>If yes, <tt>matchedKey</tt> = <tt>candidate</tt>, proceed to "Processing Matched
Shortlinks"</t>
                </li>
                <li>
                  <t>If no, continue to step 3</t>
                </li>
              </ul>
            </li>
            <li>
              <t>While <tt>candidate.length &gt; 0</tt>: a. Find the last occurrence of "/" in
<tt>candidate</tt> using <tt>candidate.lastIndexOf("/")</tt> b. If no "/" is found (result
is -1), exit the loop, no match found c. Set <tt>candidate</tt> =
<tt>candidate.slice(0, slashIndex)</tt> (everything before the last "/", exclusive)
d. Check if <tt>candidate</tt> exists as a key in the Ruleset:
              </t>
              <ul spacing="normal">
                <li>
                  <t>If yes, <tt>matchedKey</tt> = <tt>candidate</tt>, proceed to "Processing Matched
Shortlinks"</t>
                </li>
                <li>
                  <t>If no, repeat step 3</t>
                </li>
              </ul>
            </li>
          </ol>
          <t>If no matching key is found after this process completes, the algorithm
terminates and returns the Request URL with any accumulated relativePathname
appended (see "No Match Found" below).</t>
        </section>
        <section anchor="handling-hash-and-query-in-pathname">
          <name>Handling Hash and Query in Pathname</name>
          <t>At the start of each resolution iteration (before longest-prefix matching), if
the pathname contains "#" or "?" characters, these <bcp14>MUST</bcp14> be extracted and removed
from the pathname. The extraction <bcp14>MUST</bcp14> occur in the following order:</t>
          <ol spacing="normal" type="1"><li>
              <t><strong>Hash Extraction</strong> (performed first):
              </t>
              <ul spacing="normal">
                <li>
                  <t>Find the last occurrence of "#" in <tt>pathname</tt> using
<tt>pathname.lastIndexOf("#")</tt></t>
                </li>
                <li>
                  <t>If found (result &gt;= 0):
                  </t>
                  <ul spacing="normal">
                    <li>
                      <t>If <tt>initialHash</tt> is empty (""), set <tt>initialHash</tt> =
<tt>pathname.slice(hashIdx)</tt> (everything from "#" onward, inclusive)</t>
                    </li>
                    <li>
                      <t>Set <tt>pathname</tt> = <tt>pathname.slice(0, hashIdx)</tt> (everything before "#",
exclusive)</t>
                    </li>
                  </ul>
                </li>
              </ul>
            </li>
            <li>
              <t><strong>Query Extraction</strong> (performed after hash extraction):
              </t>
              <ul spacing="normal">
                <li>
                  <t>Find the last occurrence of "?" in the updated <tt>pathname</tt> using
<tt>pathname.lastIndexOf("?")</tt></t>
                </li>
                <li>
                  <t>If found (result &gt;= 0):
                  </t>
                  <ul spacing="normal">
                    <li>
                      <t>If <tt>initialQuery</tt> is empty (""), set <tt>initialQuery</tt> =
<tt>pathname.slice(queryIdx)</tt> (everything from "?" onward, inclusive)</t>
                    </li>
                    <li>
                      <t>Set <tt>pathname</tt> = <tt>pathname.slice(0, queryIdx)</tt> (everything before "?",
exclusive)</t>
                    </li>
                  </ul>
                </li>
              </ul>
            </li>
          </ol>
          <t>After extraction, the pathname contains only path segments and is used for
longest-prefix matching. The extracted hash and query are preserved in
<tt>initialHash</tt> and <tt>initialQuery</tt> (only the first extraction of each is preserved
across iterations).</t>
        </section>
        <section anchor="processing-matched-shortlinks">
          <name>Processing Matched Shortlinks</name>
          <t>When a matching key <tt>matchedKey</tt> is found in the Ruleset:</t>
          <ol spacing="normal" type="1"><li>
              <t>Let <tt>value</tt> = the value associated with <tt>matchedKey</tt> in the Ruleset</t>
            </li>
            <li>
              <t>Calculate the remaining pathname segment:
              </t>
              <ul spacing="normal">
                <li>
                  <t>After removing the leading "/" from pathname and matching against
<tt>matchedKey</tt>, the remaining segment is calculated as: <tt>remainingPath</tt> =
<tt>pathname.slice(matchedKey.length + 1)</tt> where <tt>matchedKey.length + 1</tt>
accounts for the matched key and the "/" that follows it (or would follow
it). This extracts everything from index <tt>matchedKey.length + 1</tt> to the end
of the pathname string.</t>
                </li>
                <li>
                  <t>If <tt>pathname.length &lt;= matchedKey.length + 1</tt>, then <tt>remainingPath</tt> = ""
(empty string), meaning the pathname ended exactly at or before the end of
the matched key.</t>
                </li>
                <li>
                  <t>Prepend <tt>remainingPath</tt> to <tt>relativePathname</tt>: <tt>relativePathname</tt> =
<tt>remainingPath + relativePathname</tt> (string concatenation, preserving any
leading "/" in <tt>remainingPath</tt>)</t>
                </li>
              </ul>
            </li>
            <li>
              <t>Determine the value type:  </t>
              <t><strong>Absolute Destination</strong> (first four characters of <tt>value</tt> are "http"):
- Parse <tt>value</tt> as a URL to obtain <tt>destination</tt> URL object
- Resolution terminates immediately
- Construct final URL (see "Construction Phase" below)  </t>
              <t><strong>Internal Redirect</strong> (first character of <tt>value</tt> is "/"):
- Set <tt>pathname</tt> = <tt>value</tt> (the shortlink value becomes the new pathname)
- Increment <tt>redirectCount</tt> by 1
- If <tt>redirectCount</tt> &gt;= 256, terminate with error "too many internal
  redirects"
- Continue to next resolution iteration (return to "Handling Hash and Query
  in Pathname" at the start of the iteration)</t>
            </li>
          </ol>
        </section>
        <section anchor="no-match-found">
          <name>No Match Found</name>
          <t>If no matching key is found during longest-prefix matching:</t>
          <ul spacing="normal">
            <li>
              <t>Construct a destination URL using
<tt>new URL(pathname + relativePathname, origin)</tt> where <tt>origin</tt> is the Request
URL origin</t>
            </li>
            <li>
              <t>This destination URL's pathname becomes the final destination pathname</t>
            </li>
            <li>
              <t>Proceed to "Construction Phase" with this destination URL</t>
            </li>
          </ul>
        </section>
      </section>
      <section anchor="construction-phase">
        <name>Construction Phase</name>
        <t>After resolution completes (either by finding an absolute destination or no
match), construct the final Destination URL using the following procedure:</t>
        <section anchor="base-url-construction">
          <name>Base URL Construction</name>
          <t>The base URL components are determined as follows:</t>
          <ul spacing="normal">
            <li>
              <t><strong>Origin</strong>:
              </t>
              <ul spacing="normal">
                <li>
                  <t>If an absolute destination was found: use the origin from the parsed
destination URL</t>
                </li>
                <li>
                  <t>Otherwise (no match found): use the Request URL origin</t>
                </li>
              </ul>
            </li>
            <li>
              <t><strong>Pathname</strong>:
              </t>
              <ul spacing="normal">
                <li>
                  <t>If an absolute destination was found: start with the destination URL's
pathname component (without leading/trailing modification)</t>
                </li>
                <li>
                  <t>Otherwise: use the pathname from the constructed URL in "No Match Found"</t>
                </li>
                <li>
                  <t>In both cases: append <tt>relativePathname</tt> directly (string concatenation)
without any separator, as <tt>relativePathname</tt> already includes any necessary
leading "/"</t>
                </li>
              </ul>
            </li>
          </ul>
        </section>
        <section anchor="query-parameter-combination">
          <name>Query Parameter Combination</name>
          <t>Query parameters <bcp14>MUST</bcp14> be combined using the following procedure and precedence
order (higher precedence overwrites lower precedence for matching parameter
names):</t>
          <ol spacing="normal" type="1"><li>
              <t><strong>Base query</strong>: Start with an empty URLSearchParams object</t>
            </li>
            <li>
              <t><strong>Add destination query</strong> (lowest precedence): If an absolute destination was
found, parse <tt>destination.search</tt> (the destination URL's search component,
including leading "?" if present) as URLSearchParams and add all parameters
to the base</t>
            </li>
            <li>
              <t><strong>Add pathname-embedded query</strong> (medium precedence): If <tt>initialQuery</tt> is not
empty, parse it (including leading "?" if present) as URLSearchParams and for
each key-value pair, call <tt>URLSearchParams.set(key, value)</tt> on the base (this
overwrites any existing parameters with the same key)</t>
            </li>
            <li>
              <t><strong>Add Request URL query</strong> (highest precedence): If <tt>requestQuery</tt> is not
empty, parse it (including leading "?" if present) as URLSearchParams and for
each key-value pair, call <tt>URLSearchParams.set(key, value)</tt> on the base (this
overwrites any existing parameters with the same key)</t>
            </li>
          </ol>
          <t>Convert the final URLSearchParams to a string using
<tt>URLSearchParams.toString()</tt>. If the result is non-empty, prefix it with "?" to
form the final query string. Otherwise, the query string is "" (empty string).</t>
        </section>
        <section anchor="fragment-selection">
          <name>Fragment Selection</name>
          <t>Fragment values <bcp14>MUST</bcp14> be selected using the following precedence (higher
precedence replaces lower precedence):</t>
          <ol spacing="normal" type="1"><li>
              <t><strong>Request URL fragment</strong> (highest precedence): If <tt>requestHash</tt> is not empty,
use <tt>requestHash</tt> (which includes the leading "#")</t>
            </li>
            <li>
              <t><strong>Pathname-embedded fragment</strong> (medium precedence): If <tt>requestHash</tt> is empty
and <tt>initialHash</tt> is not empty, use <tt>initialHash</tt> (which includes the leading
"#")</t>
            </li>
            <li>
              <t><strong>Destination URL fragment</strong> (lowest precedence): If both <tt>requestHash</tt> and
<tt>initialHash</tt> are empty and an absolute destination was found, use
<tt>destination.hash</tt> (which includes the leading "#" if present, otherwise "")</t>
            </li>
            <li>
              <t><strong>No fragment</strong>: If none of the above apply, use "" (empty string)</t>
            </li>
          </ol>
          <t>Unlike query parameters, fragments are not merged; only one fragment value is
used based on the precedence order above.</t>
        </section>
        <section anchor="final-url-assembly">
          <name>Final URL Assembly</name>
          <t>Assemble the final Destination URL by concatenating the following components in
order:</t>
          <t><tt>
{origin}{destination-pathname}{relativePathname}{query-string}{fragment-string}
</tt></t>
          <t>Where:</t>
          <ul spacing="normal">
            <li>
              <t><tt>{origin}</tt> is the scheme, host, and port (e.g., "https://example.com")</t>
            </li>
            <li>
              <t><tt>{destination-pathname}</tt> is the pathname component from the destination (e.g.,
"/path/to")</t>
            </li>
            <li>
              <t><tt>{relativePathname}</tt> is the accumulated relative pathname (e.g.,
"/extra/segments")</t>
            </li>
            <li>
              <t><tt>{query-string}</tt> is the combined query string from "Query Parameter
Combination" (e.g., "?foo=bar" or "")</t>
            </li>
            <li>
              <t><tt>{fragment-string}</tt> is the selected fragment from "Fragment Selection" (e.g.,
"#section" or "")</t>
            </li>
          </ul>
          <t>No additional separators are inserted between components beyond what is already
present in the component values themselves.</t>
        </section>
      </section>
      <section anchor="algorithm-pseudocode">
        <name>Algorithm Pseudocode</name>
        <t>The following pseudocode summarizes the algorithm:</t>
        <t>```
function resolve(RequestURL, Ruleset):
  // Initialize variables
  origin = RequestURL.origin
  requestQuery = RequestURL.search  // Includes "?" if present, else ""
  requestHash = RequestURL.hash     // Includes "#" if present, else ""
  pathname = RequestURL.pathname    // Always starts with "/"</t>
        <t>relativePathname = ""
  initialQuery = ""
  initialHash = ""
  redirectCount = 0
  matchedKey = null  // Initialize to null/undefined</t>
        <t>while redirectCount &lt; 256:
    matchedKey = null  // Reset for each iteration</t>
        <artwork><![CDATA[
// Step 1: Extract embedded hash from pathname (first)
hashIdx = pathname.lastIndexOf("#")
if hashIdx >= 0:
  if initialHash == "":
    initialHash = pathname.slice(hashIdx)  // From "#" onward, inclusive
  pathname = pathname.slice(0, hashIdx)    // Before "#", exclusive

// Step 2: Extract embedded query from pathname (after hash)
queryIdx = pathname.lastIndexOf("?")
if queryIdx >= 0:
  if initialQuery == "":
    initialQuery = pathname.slice(queryIdx)  // From "?" onward, inclusive
  pathname = pathname.slice(0, queryIdx)     // Before "?", exclusive

// Step 3: Longest-prefix match
if pathname == "/":
  candidate = ""
else:
  candidate = pathname.slice(1)  // Remove leading "/"

// Try exact match first
if candidate != "" and Ruleset.hasOwnProperty(candidate):
  matchedKey = candidate
else:
  // Try progressively shorter prefixes
  while candidate.length > 0:
    slashIdx = candidate.lastIndexOf("/")
    if slashIdx == -1:
      break  // No more "/" found, no match
    candidate = candidate.slice(0, slashIdx)  // Remove last segment
    if Ruleset.hasOwnProperty(candidate):
      matchedKey = candidate
      break

// Step 4: Process match result
if matchedKey == null:
  // No match found
  destination = new URL(pathname + relativePathname, origin)
  query = combineQueries("", initialQuery, requestQuery)
  hash = selectFragment(requestHash, initialHash, "")
  return destination.origin + destination.pathname + query + hash

// Match found, process it
value = Ruleset[matchedKey]

// Calculate remaining path
if pathname.length > matchedKey.length + 1:
  remainingPath = pathname.slice(matchedKey.length + 1)
else:
  remainingPath = ""
relativePathname = remainingPath + relativePathname

// Check value type
if value.slice(0, 4) == "http":
  // Absolute destination
  destination = new URL(value)
  query = combineQueries(destination.search, initialQuery, requestQuery)
  hash = selectFragment(requestHash, initialHash, destination.hash)
  return destination.origin + destination.pathname +
         relativePathname + query + hash

if value.length > 0 and value[0] == "/":
  // Internal redirect
  pathname = value
  redirectCount++
  continue  // Next iteration
]]></artwork>
        <t>// Loop limit exceeded
  throw Error("too many internal redirects")</t>
        <t>function combineQueries(baseQuery, pathnameQuery, requestQuery):
  // baseQuery is lowest precedence (destination query)
  // pathnameQuery is medium precedence
  // requestQuery is highest precedence</t>
        <t>params = new URLSearchParams(baseQuery)</t>
        <t>// Add pathname-embedded query (overwrites base)
  if pathnameQuery != "":
    pathParams = new URLSearchParams(pathnameQuery)
    for each key-value pair (key, value) in pathParams:
      params.set(key, value)</t>
        <t>// Add request query (overwrites base and pathname)
  if requestQuery != "":
    requestParams = new URLSearchParams(requestQuery)
    for each key-value pair (key, value) in requestParams:
      params.set(key, value)</t>
        <t>result = params.toString()
  return result.length &gt; 0 ? "?" + result : ""</t>
        <t>function selectFragment(requestHash, initialHash, destinationHash):
  if requestHash != "":
    return requestHash
  if initialHash != "":
    return initialHash
  return destinationHash  // May be "" if destination has no fragment
```</t>
      </section>
      <section anchor="example-resolutions">
        <name>Example Resolutions</name>
        <section anchor="example-1-absolute-destination-with-path-appending">
          <name>Example 1: Absolute Destination with Path Appending</name>
          <t>Ruleset:</t>
          <t><tt>json
{
  "github": "https://github.com",
  "repo": "https://github.com/FartLabs/go"
}
</tt></t>
          <t>Request: <tt>https://example.com/github/ietf/guidelines</tt></t>
          <t>Resolution:</t>
          <ol spacing="normal" type="1"><li>
              <t>Pathname "/github/ietf/guidelines" - remove leading "/" to get candidate
"github/ietf/guidelines"</t>
            </li>
            <li>
              <t>Check Ruleset for "github/ietf/guidelines" - not found</t>
            </li>
            <li>
              <t>Find last "/" at index 6, candidate becomes "github"</t>
            </li>
            <li>
              <t>Check Ruleset for "github" - found: value "https://github.com"</t>
            </li>
            <li>
              <t>Calculate remainingPath: pathname is "/github/ietf/guidelines", matchedKey is
"github" (length 6), so pathname.slice(matchedKey.length + 1) =
pathname.slice(7) = "/ietf/guidelines" (from index 7, which is the "/" after
"github")</t>
            </li>
            <li>
              <t>relativePathname = "/ietf/guidelines" + "" = "/ietf/guidelines"</t>
            </li>
            <li>
              <t>Value starts with "http" → absolute destination</t>
            </li>
            <li>
              <t>Parse "https://github.com" as URL: origin = "https://github.com", pathname =
"/"</t>
            </li>
            <li>
              <t>Query: "" (no queries), Fragment: "" (no fragments)</t>
            </li>
            <li>
              <t>Result: <tt>https://github.com</tt> + <tt>/</tt> + <tt>/ietf/guidelines</tt> + <tt>+</tt> =
<tt>https://github.com/ietf/guidelines</tt></t>
            </li>
          </ol>
        </section>
        <section anchor="example-2-internal-redirect-chain">
          <name>Example 2: Internal Redirect Chain</name>
          <t>Ruleset:</t>
          <t><tt>json
{
  "docs": "/documentation",
  "docs/api": "/documentation/reference"
}
</tt></t>
          <t>Request: <tt>https://example.com/docs/api/v1/users</tt></t>
          <t>Resolution: <strong>Iteration 1:</strong></t>
          <ol spacing="normal" type="1"><li>
              <t>Pathname "/docs/api/v1/users" - remove leading "/" to get candidate
"docs/api/v1/users"</t>
            </li>
            <li>
              <t>Check Ruleset for "docs/api/v1/users" - not found</t>
            </li>
            <li>
              <t>Find last "/" at index 8, candidate becomes "docs/api"</t>
            </li>
            <li>
              <t>Check Ruleset for "docs/api" - found: value "/documentation/reference"</t>
            </li>
            <li>
              <t>Calculate remainingPath: pathname.slice(8 + 1) = pathname.slice(9) =
"/v1/users"</t>
            </li>
            <li>
              <t>relativePathname = "/v1/users" + "" = "/v1/users"</t>
            </li>
            <li>
              <t>Value starts with "/" → internal redirect</t>
            </li>
            <li>
              <t>pathname = "/documentation/reference", redirectCount = 1</t>
            </li>
          </ol>
          <t><strong>Iteration 2:</strong></t>
          <ol spacing="normal" type="1"><li>
              <t>Pathname "/documentation/reference" - remove leading "/" to get candidate
"documentation/reference"</t>
            </li>
            <li>
              <t>Check Ruleset for "documentation/reference" - not found</t>
            </li>
            <li>
              <t>Find last "/" at index 13, candidate becomes "documentation"</t>
            </li>
            <li>
              <t>Check Ruleset for "documentation" - not found</t>
            </li>
            <li>
              <t>Find last "/" at index -1 (no "/" in "documentation") - no match found</t>
            </li>
            <li>
              <t>Construct destination: new URL("/documentation/reference" + "/v1/users",
origin)</t>
            </li>
            <li>
              <t>Query: "" (no queries), Fragment: "" (no fragments)</t>
            </li>
            <li>
              <t>Result: <tt>https://example.com/documentation/reference/v1/users</tt></t>
            </li>
          </ol>
        </section>
        <section anchor="example-3-query-parameter-precedence">
          <name>Example 3: Query Parameter Precedence</name>
          <t>Ruleset:</t>
          <t><tt>json
{
  "example": "https://example.com?baz=qux"
}
</tt></t>
          <t>Request: <tt>https://example.com/example?foo=bar</tt></t>
          <t>Resolution:</t>
          <ol spacing="normal" type="1"><li>
              <t>Pathname "/example" - remove leading "/" to get candidate "example"</t>
            </li>
            <li>
              <t>Match found: key "example", value "https://example.com?baz=qux"</t>
            </li>
            <li>
              <t>No remaining pathname (matchedKey.length + 1 = 8, pathname.length = 8, so
remainingPath = "")</t>
            </li>
            <li>
              <t>Value starts with "http" → absolute destination</t>
            </li>
            <li>
              <t>Parse "https://example.com?baz=qux" as URL: destination.search = "?baz=qux"</t>
            </li>
            <li>
              <t>Combine queries:
              </t>
              <ul spacing="normal">
                <li>
                  <t>Base (destination): baz=qux</t>
                </li>
                <li>
                  <t>Pathname-embedded: (none, initialQuery = "")</t>
                </li>
                <li>
                  <t>Request: foo=bar (overwrites any existing foo, but baz remains)</t>
                </li>
                <li>
                  <t>Result: "?baz=qux&amp;foo=bar"</t>
                </li>
              </ul>
            </li>
            <li>
              <t>Fragment: requestHash = "" (no fragment in request), initialHash = "",
destinationHash = ""</t>
            </li>
            <li>
              <t>Result: <tt>https://example.com/?baz=qux&amp;foo=bar</tt></t>
            </li>
          </ol>
        </section>
        <section anchor="example-4-fragment-precedence">
          <name>Example 4: Fragment Precedence</name>
          <t>Ruleset:</t>
          <t><tt>json
{
  "example": "https://example.com#yin"
}
</tt></t>
          <t>Request: <tt>https://example.com/example#yang</tt></t>
          <t>Resolution:</t>
          <ol spacing="normal" type="1"><li>
              <t>Pathname "/example" - remove leading "/" to get candidate "example"</t>
            </li>
            <li>
              <t>Match found: key "example", value "https://example.com#yin"</t>
            </li>
            <li>
              <t>No remaining pathname (matchedKey.length + 1 = 8, pathname.length = 8, so
remainingPath = "")</t>
            </li>
            <li>
              <t>Value starts with "http" → absolute destination</t>
            </li>
            <li>
              <t>Parse "https://example.com#yin" as URL: destination.hash = "#yin"</t>
            </li>
            <li>
              <t>Query: no query parameters present, result = ""</t>
            </li>
            <li>
              <t>Fragment selection: requestHash = "#yang" (not empty) → use "#yang"</t>
            </li>
            <li>
              <t>Result: <tt>https://example.com/#yang</tt></t>
            </li>
          </ol>
        </section>
        <section anchor="example-5-no-match-returns-original">
          <name>Example 5: No Match Returns Original</name>
          <t>Ruleset:</t>
          <t><tt>json
{
  "c": "https://example.com/calendar"
}
</tt></t>
          <t>Request: <tt>https://example.com/colors</tt></t>
          <t>Resolution:</t>
          <ol spacing="normal" type="1"><li>
              <t>Pathname "/colors" - remove leading "/" to get candidate "colors"</t>
            </li>
            <li>
              <t>Check Ruleset for "colors" - not found</t>
            </li>
            <li>
              <t>Find last "/" in "colors" - not found (lastIndexOf returns -1)</t>
            </li>
            <li>
              <t>No matching key found</t>
            </li>
            <li>
              <t>Construct destination: new URL("/colors" + "", origin) =
"https://example.com/colors"</t>
            </li>
            <li>
              <t>Query: "" (no queries), Fragment: "" (no fragments)</t>
            </li>
            <li>
              <t>Result: <tt>https://example.com/colors</tt> (unchanged)</t>
            </li>
          </ol>
        </section>
      </section>
    </section>
    <section anchor="security-considerations">
      <name>Security Considerations</name>
      <section anchor="loop-protection">
        <name>Loop Protection</name>
        <t>The algorithm includes protection against infinite redirect loops by limiting
internal redirects to 256 iterations. Implementations <bcp14>MUST</bcp14> enforce this limit
and <bcp14>MUST</bcp14> reject requests that exceed it. However, 256 may be insufficient for
legitimate deeply-nested redirect structures; implementers should consider
logging or monitoring redirect counts to identify potentially problematic
rulesets.</t>
      </section>
      <section anchor="input-validation">
        <name>Input Validation</name>
        <t>Implementations <bcp14>SHOULD</bcp14> validate that:</t>
        <ul spacing="normal">
          <li>
            <t>Request URLs are well-formed and use supported schemes</t>
          </li>
          <li>
            <t>Ruleset values are valid URLs or paths</t>
          </li>
          <li>
            <t>Pathname components do not contain illegal characters</t>
          </li>
        </ul>
        <t>Malformed input may cause unpredictable behavior or security vulnerabilities.</t>
      </section>
      <section anchor="ruleset-source-security">
        <name>Ruleset Source Security</name>
        <t>The security of the resolution algorithm depends on the trustworthiness of the
ruleset source. Implementations <bcp14>SHOULD</bcp14>:</t>
        <ul spacing="normal">
          <li>
            <t>Validate ruleset integrity (e.g., checksums, signatures)</t>
          </li>
          <li>
            <t>Restrict ruleset modifications to authorized entities</t>
          </li>
          <li>
            <t>Audit ruleset changes for suspicious patterns</t>
          </li>
          <li>
            <t>Implement rate limiting on resolution requests to prevent abuse</t>
          </li>
        </ul>
      </section>
      <section anchor="information-disclosure">
        <name>Information Disclosure</name>
        <t>The resolution process may reveal information about the ruleset structure
through timing differences or error messages. Implementations <bcp14>SHOULD</bcp14> consider:</t>
        <ul spacing="normal">
          <li>
            <t>Using constant-time matching algorithms where timing attacks are a concern</t>
          </li>
          <li>
            <t>Providing uniform error responses that don't reveal whether a shortlink key
exists</t>
          </li>
        </ul>
      </section>
      <section anchor="path-traversal">
        <name>Path Traversal</name>
        <t>When appending relative pathname segments to destination URLs, implementations
<bcp14>MUST</bcp14> ensure that the resulting pathname is safe and does not allow path
traversal attacks (e.g., "../" sequences). The algorithm as specified does not
explicitly sanitize these segments, so implementations should validate or
sanitize the final constructed URL before use.</t>
      </section>
      <section anchor="query-parameter-and-fragment-handling">
        <name>Query Parameter and Fragment Handling</name>
        <t>The query parameter merging behavior means that user-supplied query parameters
from the Request URL can overwrite destination URL parameters. This may be
desired behavior, but implementers should be aware that it could be used to
modify the intended destination behavior. Similarly, fragments from the Request
URL take precedence, which may override intended anchor targets in the
destination.</t>
      </section>
      <section anchor="redirect-chains">
        <name>Redirect Chains</name>
        <t>Even with loop protection, long redirect chains can be used for:</t>
        <ul spacing="normal">
          <li>
            <t>Denial of service through excessive processing</t>
          </li>
          <li>
            <t>Obfuscation of final destination</t>
          </li>
          <li>
            <t>Tracking or analytics purposes</t>
          </li>
        </ul>
        <t>Implementations should consider logging redirect chains and potentially limiting
chain length more aggressively than the 256-iteration limit.</t>
      </section>
    </section>
    <section anchor="iana-considerations">
      <name>IANA Considerations</name>
      <t>This document has no IANA actions.</t>
    </section>
  </middle>
  <back>
    <references anchor="sec-normative-references">
      <name>Normative References</name>
      <reference anchor="RFC2119">
        <front>
          <title>Key words for use in RFCs to Indicate Requirement Levels</title>
          <author fullname="S. Bradner" initials="S." surname="Bradner"/>
          <date month="March" year="1997"/>
          <abstract>
            <t>In many standards track documents several words are used to signify the requirements in the specification. These words are often capitalized. This document defines these words as they should be interpreted in IETF documents. This document specifies an Internet Best Current Practices for the Internet Community, and requests discussion and suggestions for improvements.</t>
          </abstract>
        </front>
        <seriesInfo name="BCP" value="14"/>
        <seriesInfo name="RFC" value="2119"/>
        <seriesInfo name="DOI" value="10.17487/RFC2119"/>
      </reference>
      <reference anchor="RFC8174">
        <front>
          <title>Ambiguity of Uppercase vs Lowercase in RFC 2119 Key Words</title>
          <author fullname="B. Leiba" initials="B." surname="Leiba"/>
          <date month="May" year="2017"/>
          <abstract>
            <t>RFC 2119 specifies common key words that may be used in protocol specifications. This document aims to reduce the ambiguity by clarifying that only UPPERCASE usage of the key words have the defined special meanings.</t>
          </abstract>
        </front>
        <seriesInfo name="BCP" value="14"/>
        <seriesInfo name="RFC" value="8174"/>
        <seriesInfo name="DOI" value="10.17487/RFC8174"/>
      </reference>
    </references>
    <?line 686?>

<section anchor="acknowledgments">
      <name>Acknowledgments</name>
      <t>This document was inspired by the need for consistent shortlink resolution
across different implementations and platforms.</t>
    </section>
  </back>
  <!-- ##markdown-source:
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
wJCxL2tTGoG/sXXScbOkMHEg/SxXJKVdRP0CRFmdbE2qI2yEak89dtzaGD6y
W1TbPLZ7O5PmqtwiupVCy3zZEQSqtfNxXlbGO3/Ui7RQnMH3+UW2TOCVe90e
h/62vwosW6lxK6XKC2px6AEYpgWs9SAlZqo/HMDKYYgbisXyS3okipyfnZk9
GpBI3M2Qfa+f2xs4JnfnPQ8gxJXSbjYrpPgcUHauxptkjdrnVaUh8y/2RVJR
E9K++Y671MQ+WlQCXHE5Mak+KdRHsz9x7kTGVvOBY+dU2qWllsb7Uo56GscN
uRJAaozIgCA7bEDKr5ZaJDIJ7pQ1qeH351xcFq/ZNZvtPRXK19x+Qf/uyQSF
ob06JHwIqwppW9ewD8FJvIpmhne8cPFG2GfNrTskAUYg9bZDgm5OnOUUyxC1
zeIx7P5q/DGjRICYzjWbRaaYdqTqojUKyFuN4csphyHgy/LMLhZpnhAFPGFD
/aBSZEO2vP0C6kMEkJPggaGVb90slUikeyTlouCv/9+QKaDGtcJ3kG2kqVlI
LB7HKB0Mq/yc3o8nF1Tvlh1srM4QCaUHiDIDjIkS0XYkWZUHWFzyEOAygyRi
ztZzVuu/pKC3vQ0lpQKz9wihsOzT406tPJNWI2MdS/pk0Dpa0yaWMPD35/Qu
DaMeO2hNnS9/K7sher0I2voiSKCIH7IZ/V3zkzF3pVgn0qgg3BpN2GaedUyG
j8yQ1WjjYveC/CpPD6aMZuP9FWgiQMKUDFw7QPPxHDDw5L+byIac4LdqU9hb
RsJCBve6YIaWQVB8p7C5CdUHNqtHI7FMEIisvN1xSisy20YTLkGbMRJJhZTd
zdbgfZYmH3SnZ2PqNWzgcpEnW12sdfwnLgniNKuGIgDrAqoJokGJjXXx3T3F
AYSU0S6bzp6WJUgUJLyB/KWv676wMVNH1bwQGmJQszdwcXERfOa49PfPHiNm
xgv+/rkdWv3+mcgyY2L9/tms1zwgmFgupAh/pi4MfJs/DTRfjPV8PZ9KZ9ji
6Eh/CjFJwZMcI9oA70fQgu2Je2286osgzwOiNzrCIUdVLuA7K7Wg+7alvBYx
B5AKZ0emKCxwG/SyMG3Y2LC6XP5uBaIA2gtFR5ZSJ6s8f7QMC95gktna/HBk
N5bYSihP1rXmI29Jt0rzTOYIQL0gnKL+a2zNN4E4q0SSleDxqJ2i2mud+XK3
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
b5sFIg3YSbTLgNj/ADefV5aYYgAA

-->

</rfc>
