Frontend engineer and QA reviewer inspecting JavaScript URL resolution cases and API path-boundary failures on a development monitor

Your API Base URL Does Not Keep Every Request Under Its Path

Thu, Oct 15, 2026

A client is configured with https://api.example.test/tenant-a/v1/. A caller supplies /users, the JavaScript URL constructor accepts it, and the resulting pathname is /users at the site root. Nothing failed at the parsing layer. The failure is in the application assumption that a base URL acts as a directory restriction. It does not.

A useful endpoint contract must say more than “the URL is valid” or “the origin matches.” For this fixture, the approved destination has one HTTPS origin, one tenant API prefix, and a deliberately small endpoint language. The accepted endpoint users/42 must become exactly https://api.example.test/tenant-a/v1/users/42; leading slashes, parent references, authorities, absolute URLs, query strings, fragments, encoded dot forms, backslashes, and repeated separators are outside the admitted grammar.

This playbook qualifies that boundary before any network request exists. Seven controls expose trailing-slash, root-relative, parent-relative, network-path, absolute-reference, and lookalike-prefix behavior. Four more cases qualify the narrow helper, its invalid bases, and its returned-value ownership. The evidence is local URL tuples and explicit exceptions, not traffic, server routing, tenant isolation, or a security guarantee. The operational decisions are REPAIR_CONSTRUCTION, REJECT_UNSUPPORTED_REFERENCE, ACCEPT_BOUNDED, and HOLD.

Treat this as a pre-transport review: first establish the destination string, then decide whether the application admits it. A successful diagnostic may deliberately confirm an unacceptable construction, so every result needs both an observation and an operational decision.

Define the approved origin and directory prefix

The contract starts with constants, not with whatever string a caller happens to provide. The approved origin is https://api.example.test. The approved directory prefix is /tenant-a/v1/. Together they form the normalized base https://api.example.test/tenant-a/v1/. The final slash is part of the reviewed configuration because relative resolution gives it meaning.

Review the constructed result as a tuple rather than as one reassuring string. href is the complete serialized URL used for the literal oracle. For these HTTPS URLs, origin identifies the scheme and host, including a nondefault port when present; pathname isolates the path. These are documented URL properties. Exact href equality fixes the full result, while the separate fields explain why an origin or prefix check passes. They are diagnostic views of one parsed object, not independent transport observations.

For this client, an admitted endpoint is one or more nonempty ASCII segments. Each segment may contain letters, digits, underscore, or hyphen, and a single slash may separate adjacent segments. Thus users and users/42 are in scope. An empty string, /users, ../users, users//42, or users?active=1 is not. The helper also excludes fragments, user information, and caller-selected origins. Those exclusions are product decisions for this fixture, not universal rules for every API.

The postcondition is literal: an accepted endpoint must produce the approved origin and a pathname equal to PREFIX + endpoint, with no query, fragment, username, or password. That makes the intended path membership reviewable. Teams still need broader frontend engineering and JavaScript foundations for component design and API integration, but this article stays on the narrower construction boundary.

Use ACCEPT_BOUNDED only when the exact tuple and interface checks pass. Use REPAIR_CONSTRUCTION when JavaScript constructs a valid URL that falls outside the selected origin or prefix. Use REJECT_UNSUPPORTED_REFERENCE when an input is intentionally outside the endpoint language. Use HOLD for claims about transport, server routing, authorization, or a wider input domain that this local fixture does not test.

Distinguish reference resolution from string concatenation

The two-argument form new URL(reference, base) does not simply append the first string to the second. It parses the inputs and resolves the reference according to URL rules. The Node.js v22.16.0 URL documentation explains relative resolution and destination selection by absolute inputs. The WHATWG URL Standard supplies the normative parsing and serialization model. It is a living specification consulted on 15 October 2026; the runtime observation here remains pinned to Node.js v22.16.0.

The same distinction appears in MDN’s URL constructor reference: relative references need a base, while an absolute URL supplies its own destination. Neither reference resolution nor successful parsing establishes the application’s allowed origin or path. There is also a qualification to “ignores the base”: a supplied base is still parsed, so an invalid base can throw even when the input is absolute. The seven controls below use valid bases and test destination selection, not tolerance of invalid base syntax.

That distinction explains all seven diagnostic controls. A plain users reference is current-directory relative. A leading slash is root-relative. ../users is parent-relative. A leading // is a network-path reference that supplies an authority while inheriting the base scheme. A complete https://... reference is absolute. Each can be syntactically acceptable while producing a destination that the application did not approve.

The base form matters too. With a trailing slash, /tenant-a/v1/ is the current directory for a plain relative reference. Without that slash, the final v1 component is treated as the last component of the base path and is replaced by users. This is documented resolution behavior, not string loss, random normalization, or a Node defect.

Separate URL validity from the application’s destination rule

A successful constructor call establishes that the implementation produced a URL object. It does not establish that the result belongs to the client’s approved endpoint space. https://api.example.test/users is a valid same-origin URL, but it is outside /tenant-a/v1/. https://other.example.test/users is also a valid URL, but it changes the origin entirely.

Keep three decisions separate. Syntax answers whether the URL can be parsed. Application admission answers whether the origin, pathname, and excluded components match the reviewed client contract. Service authorization answers whether a particular principal may perform a particular operation after a request reaches a service. A constructor test can support the first two only when the contract is explicit; it supplies no evidence for the third.

Freeze the eleven-case tuple manifest

Write the expected tuple manifest before implementing the helper. Otherwise, an implementation can define its own success after the fact. U1 through U7 call new URL() directly and record href, origin, and pathname. U8 through U11 exercise the bounded interface. Every case starts as FAIL, changes to PASS only after all assertions in that case complete, and remains in the report even when a later case throws.

The supplied commissioning record reports PASS_11_CASES, with timestamps 2026-10-15T06:49:14.541Z and 2026-10-15T06:49:14.564Z. A separate execution of the byte-matched driver also recorded PASS_11_CASES: Node.js v22.16.0, V8 12.4.254.21-node.26, Linux release 6.18.44. Its report records 2026-10-15T07:59:50.409Z as the start and 2026-10-15T07:59:50.417Z as the finish. The source SHA-256 is 335a148a0c41b8ac41896471ff7dce7ea995d22c891156c4946c8907cd08adf8.

Keep the supplied observation distinct from the independently reproduced result. Neither is a benchmark, a release announcement, a production incident, or a cross-platform guarantee. The recorded finish timestamp is set before report writing, so these fields are not an end-to-end timing measurement. A future rerun needs its own versions, timestamps, source digest, and retained report.

Freeze expected values as authored data rather than copying the first run’s output. The manifest below spells out the complete expected href, origin, and pathname. U2 alone removes the base’s final slash; the other constructor controls use BASE. U8 expects users/42 to return the bounded URL, U9 expects ten endpoint rejections, U10 expects six base rejections, and U11 checks returned-value ownership and non-string rejection.

U1–U7 expected tuple manifest

Case

Expected URL tuple

U1

href: https://api.example.test/tenant-a/v1/users
origin: https://api.example.test
pathname: /tenant-a/v1/users

U2

href: https://api.example.test/tenant-a/users
origin: https://api.example.test
pathname: /tenant-a/users

U3

href: https://api.example.test/users
origin: https://api.example.test
pathname: /users

U4

href: https://api.example.test/tenant-a/users
origin: https://api.example.test
pathname: /tenant-a/users

U5

href: https://other.example.test/users
origin: https://other.example.test
pathname: /users

U6

href: https://other.example.test/users
origin: https://other.example.test
pathname: /users

U7

href: https://api.example.test/tenant-a/v10/users
origin: https://api.example.test
pathname: /tenant-a/v10/users

Keep constructor evidence separate from network evidence

No server, DNS resolver, browser, proxy, service worker, redirect handler, or authentication system participates in this fixture. Its example.test names belong to the reserved .test namespace and remain data inside URL objects. The driver never calls fetch, opens a socket, follows a redirect, or presents a token. Domain reservation is not the mechanism keeping this test offline; the absence of network operations is.

Therefore, report that a URL was constructed and that its tuple matched or violated the local contract. Do not report that an endpoint was reached, a tenant boundary was enforced, or a request was blocked. Those are later integration and service claims. Keeping this boundary explicit prevents a clean constructor report from being mistaken for evidence about the behavior of a deployed system.

Run the complete local URL qualification driver

Save the following source as url_base_fixture.mjs in a directory the current user can write. It creates a new sibling evidence directory with mkdtempSync, writes results.json with the wx exclusive-creation flag, and prints the report path on successful persistence. It makes no request. The source is included in full, preserving the commissioning driver rather than substituting an unseen harness.

url_base_fixture.mjs

```javascript
// WHATWG URL construction only: no network, browser, credentials, or live API.
import {readFileSync, writeFileSync, mkdtempSync} from 'node:fs';
import {createHash} from 'node:crypto';
import {fileURLToPath} from 'node:url';
import {dirname, join} from 'node:path';
import {release} from 'node:os';
const ORIGIN = '[https://api.example.test](https://api.example.test)';
const PREFIX = '/tenant-a/v1/';
const BASE = ORIGIN + PREFIX;
const source = fileURLToPath(import.meta.url);
const root = mkdtempSync(join(dirname(source), 'url-base-'));
const report = {status: 'FAIL', node: process.version, v8: process.versions.v8, os: release(), started_utc: new Date().toISOString(), cases: [], source_sha256: createHash('sha256').update(readFileSync(source)).digest('hex')};
function require(ok, message) { if (!ok) throw new Error(message); }
function tuple(u) { return {href:u.href, origin:u.origin, pathname:u.pathname}; }
function endpointURL(base, endpoint) {
  if (typeof base !== 'string' || typeof endpoint !== 'string') {
    throw new TypeError('String inputs required');
  }
  const b = new URL(base);
  if (b.origin !== ORIGIN || b.pathname !== PREFIX || [b.search](http://b.search) || b.hash || b.username || b.password) throw new TypeError('Unapproved base');
  // This application accepts only ASCII path segments, not arbitrary URL references.
  if (!/^[A-Za-z0-9\_-]+(?:\\/[A-Za-z0-9\_-]+)\*$/.test(endpoint)) {
    throw new TypeError('Unsupported endpoint path');
  }
  const u = new URL(endpoint, b);
  require(u.origin === ORIGIN && u.pathname === PREFIX + endpoint && \![u.search](http://u.search) && !u.hash && !u.username && !u.password, 'Postcondition failed');
  return u.href; // Publish a string, not an owner-shared mutable URL instance.
}
function caseRun(id, action) {
  const row = {id, status:'FAIL'};
  report.cases.push(row);
  action(row);
  row\.status = 'PASS';
}
function rejection(base, path) {
  try { endpointURL(base, path); return null; } catch (e) { return [e.name](http://e.name); }
}
try {
  const diagnostic = [
    ['U1', BASE, 'users', BASE+'users'],
    ['U2', BASE.slice(0,-1), 'users', ORIGIN+'/tenant-a/users'],
    ['U3', BASE, '/users', ORIGIN+'/users'],
    ['U4', BASE, '../users', ORIGIN+'/tenant-a/users'],
    ['U5', BASE, '//other.example.test/users', '[https://other.example.test/users](https://other.example.test/users)'],
    ['U6', BASE, '[https://other.example.test/users](https://other.example.test/users)', '[https://other.example.test/users](https://other.example.test/users)'],
    ['U7', BASE, '/tenant-a/v10/users', ORIGIN+'/tenant-a/v10/users'],
  ];
  for (const [id, base, reference, expected] of diagnostic) {
    caseRun(id, row => {
      const u = new URL(reference, base);
      row\.base = base; row\.reference = reference; row\.expected_href = expected;
      row\.actual = tuple(u);
      row\.origin_only_passes = u.origin === ORIGIN;
      row\.segment_prefix_passes = u.pathname.startsWith(PREFIX);
      require(u.href === expected, 'Resolution baseline changed');
      if (id === 'U7') {
        row\.unsafe_string_prefix_passes = u.pathname.startsWith('/tenant-a/v1');
        require(row\.unsafe_string_prefix_passes && !row\.segment_prefix_passes, 'Prefix lookalike control failed');
      }
    });
  }
  caseRun('U8', row => {
    row\.actual = endpointURL(BASE, 'users/42');
    require(row\.actual === BASE+'users/42', 'Valid endpoint failed');
  });
  caseRun('U9', row => {
    row\.rejections = ['', '/users', '../users', '//other.example.test/users', '[https://other.example.test/users](https://other.example.test/users)', 'users?x=1', 'users#x', '%2e%2e/users', 'users\\\42', 'users//42'].map(input => ({input, error:rejection(BASE, input)}));
    require(row\.rejections.every(r => r.error === 'TypeError'), 'Unsupported input admitted');
  });
  caseRun('U10', row => {
    row\.rejections = [BASE.slice(0,-1), BASE+'?x=1', BASE+'#x', '[https://[email protected]'+PREFIX](https://[email protected]'+PREFIX), ORIGIN+'/tenant-a/v10/', '[https://other.example.test](https://other.example.test)'+PREFIX].map(base => ({base, error:rejection(base, 'users')}));
    require(row\.rejections.every(r => r.error === 'TypeError'), 'Unapproved base admitted');
  });
  caseRun('U11', row => {
    const published = endpointURL(BASE, 'users');
    const consumer = new URL(published);
    consumer.pathname = '/elsewhere';
    row\.consumer = consumer.href; row\.published = published;
    row\.non_string_error = rejection(BASE, {toString:()=> 'users'});
    require(published === BASE+'users' && row\.consumer === ORIGIN+'/elsewhere', 'String ownership control failed');
    require(row\.non_string_error === 'TypeError', 'Input coercion escaped admission');
  });
  require(report.cases.length === 11, 'Incomplete cases');
  report.status = 'PASS_11_CASES';
} catch (error) {
  report.error = {name:[error.name](http://error.name), message:error.message};
  process.exitCode = 1;
} finally {
  report.finished_utc = new Date().toISOString();
  const result = join(root, 'results.json');
  writeFileSync(result, JSON.stringify(report, null, 2)+'\n', {flag:'wx'});
  console.log(result);
}
```

Run the driver with the versioned Node.js executable under qualification:

node url_base_fixture.mjs

Open the printed results.json, not a remembered console summary. Its directory suffix is newly generated on each run. The top-level status starts as FAIL and becomes PASS_11_CASES only after all eleven case actions complete and the inventory length is eleven. An assertion failure inside the try block records an error, sets a nonzero process exit code, and still attempts report writing. A missing report, missing case, unexpected error name, or unexpected exception is not success.

Setup and persistence have their own failure modes. Directory creation and source reading occur before the main try; report writing occurs in finally. Either stage can fail without leaving a complete report. Check process completion and the saved JSON together. A status value assigned in memory is not evidence that the file was successfully written.

The first oracle is the literal serialized href. U1 must be https://api.example.test/tenant-a/v1/users; U2 must be https://api.example.test/tenant-a/users; U3 must be https://api.example.test/users; U4 must be https://api.example.test/tenant-a/users; U5 and U6 must both be https://other.example.test/users; U7 must be https://api.example.test/tenant-a/v10/users. These expected strings make the resolution baseline independent of the helper under test.

Do not replace those literals with an expectation calculated by another call to new URL() inside the test. That would ask the implementation to generate both the result and its oracle. Likewise, do not compare only the final pathname if an origin change is possible, and do not compare only the origin when a path escape is the defect under review. A useful oracle is intentionally boring: fixed input, fixed complete output, and separately inspected tuple fields.

The report also preserves per-case status. If U6 throws unexpectedly, U1 through U5 remain recorded with their completed observations, U6 remains FAIL, and later cases are absent because execution stopped. That partial evidence can help diagnosis, but it does not satisfy the inventory requirement. The top-level failure-default design prevents a truncated run from looking like a successful subset merely because every row that happened to be written says PASS.

The report also separates the tuple for diagnosis. origin_only_passes compares the parsed origin with https://api.example.test; segment_prefix_passes checks whether the pathname begins with /tenant-a/v1/. These booleans are recorded, not asserted against a full expected boolean matrix. Review them alongside the literal manifest. U7 additionally asserts the unsafe check pathname.startsWith('/tenant-a/v1') and the failure of the slash-terminated prefix, exposing the lookalike /tenant-a/v10/users.

The helper then applies an admission rule before construction. It requires primitive strings, parses and normalizes the base, checks every approved base component, tests the endpoint against the narrow ASCII-segment regular expression, constructs the URL, and rechecks the exact postcondition. The postcondition is deliberately redundant. It turns assumptions about the parser and the grammar into an explicit assertion at the interface boundary.

In the recorded rerun, U8 matched the positive path users/42; U9 recorded TypeError for ten unsupported endpoint forms; U10 recorded TypeError for six unapproved bases. U11 confirmed the bounded ownership behavior and rejected a coercible object. Its consumer mutates a separately constructed URL object, not the previously returned string. The rejection helper records error.name; it does not retain stack traces or compare exception constructor identity. These are the assertions this fixture actually makes.

Treat the driver hash as a source-identity aid, not a trust certificate. Recompute it after edits, including whitespace-only changes. A modified file needs a separately identified run even when its results look unchanged. A matching recorded digest can link a report’s source label to retained file bytes; the digest alone does not establish that those bytes were executed, that the report is authentic, or that any claim outside the assertions is true.

Compare a directory base with the same text lacking its slash

U1 and U2 vary one character in the base and keep the reference users unchanged. U1 uses https://api.example.test/tenant-a/v1/ and produces https://api.example.test/tenant-a/v1/users. Its origin is https://api.example.test, and its pathname is /tenant-a/v1/users. Both the origin check and the complete prefix check pass.

U2 removes the final slash: https://api.example.test/tenant-a/v1. The result is https://api.example.test/tenant-a/users. The origin is still approved, but the pathname no longer begins with /tenant-a/v1/. The constructor has not rejected the base, and it has not “forgotten” a directory at random. It resolved the plain relative reference against the current directory derived from the base.

The MDN relative-reference guide describes the current directory as the base URL through its last forward slash. In U2, the text after that slash is v1, so users replaces it. This is why a base URL without a trailing slash can behave like a file-shaped base even when an application author intended it as a directory label.

Notice that both results are deterministic and syntactically valid. A test that only expects the constructor not to throw will mark U2 green; a check of only the origin will also pass. Comparing the expected complete href or pathname reveals the defect. That is the practical reason to freeze the literal tuple before repairing configuration.

A missing slash changes the directory context

The repair is not “append a slash to every URL string.” Define the approved normalized base components and enforce the endpoint language at the interface. Blind concatenation is not a substitute: joining the slash-terminated BASE and /users creates a doubled separator in the path. Removing that separator fixes one spelling, but does not define an admission rule for parent references, authorities, or other unsupported forms.

Also avoid importing filesystem language too literally. URL paths and local filesystem paths are different grammars with different parsers. The useful point is narrower: for hierarchical URLs, the final slash changes the directory context used by relative resolution. U1 is ACCEPT_BOUNDED; U2 is REPAIR_CONSTRUCTION until the configuration is restored to the exact approved base.

Follow root-relative, parent-relative and absolute references

U3 supplies /users. A leading slash makes the reference root-relative, so the base pathname is replaced and the result is https://api.example.test/users. The origin remains approved, but the tenant API prefix is gone. U3 is a valid URL construction and a REPAIR_CONSTRUCTION decision for this client.

U4 supplies ../users. Starting from the current directory /tenant-a/v1/, the parent reference removes the v1/ segment and appends users. The exact result is https://api.example.test/tenant-a/users. Again, parsing succeeds and the origin matches, while the approved prefix fails.

U5 supplies //other.example.test/users. This network-path reference inherits the HTTPS scheme from the base but supplies a new authority, producing https://other.example.test/users. U6 supplies that destination as a complete absolute HTTPS reference. For this absolute input, the base does not confine the destination. The versioned constructor documentation describes that behavior; it does not mean an invalid supplied base becomes acceptable.

These are constructor outcomes, not redirects. No server returned a 3xx status and no client decided whether to preserve a method or body. That separate transport question belongs in the separate behavior of HTTP redirects. Here, U3 through U6 are all REPAIR_CONSTRUCTION as raw constructor uses, while the bounded helper later classifies the corresponding reference forms as REJECT_UNSUPPORTED_REFERENCE.

The distinction between U5 and U6 is still useful. U5 shows that an input need not contain a scheme to replace the authority; the inherited scheme can make the final URL look superficially related to the base. U6 shows the simpler absolute-reference rule. Keeping both controls prevents a repair that rejects https:// text but still admits a network-path authority beginning with //.

Test why an origin check and a naive prefix check can pass

An origin check is necessary for this contract, but it is not sufficient. U2, U3, and U4 all keep https://api.example.test. The MDN origin property reference describes origin as the serialized origin of the represented URL; pathname membership is a separate property. Therefore, code that accepts any URL with the expected origin also accepts /tenant-a/users and /users.

A naive pathname prefix can be equally suggestive. U7 constructs https://api.example.test/tenant-a/v10/users. The pathname /tenant-a/v10/users starts with the character sequence /tenant-a/v1, so pathname.startsWith('/tenant-a/v1') returns true. But it does not start with the complete directory prefix /tenant-a/v1/. The extra slash is a segment boundary, not decoration.

This counterexample is worth keeping even after the repair. It prevents a reviewer from replacing the exact postcondition with a shorter but weaker check. It also demonstrates why the independent tuple manifest matters: the diagnostic can be a technical PASS because it produced the expected unsafe result, while the application decision is still REPAIR_CONSTRUCTION.

Require the exact bounded postcondition rather than a suggestive prefix

For admitted endpoints, the helper requires u.origin === ORIGIN and u.pathname === PREFIX + endpoint. It also requires empty search, hash, username, and password fields. Equality is practical here because the endpoint grammar has already excluded dot segments, percent forms, empty segments, backslashes, queries, fragments, and authorities.

That postcondition is stronger than the unsafe U7 prefix check, but its strength is scoped. It does not sanitize arbitrary URL input, prove how a server maps paths, or establish that two differently serialized paths are equivalent in every intermediary. It qualifies one plain-segment language against one exact base. Wider semantics need a separate contract and new negative controls.

Build only the endpoint language the client actually needs

endpointURL(base, endpoint) accepts two primitive strings. Inspection of its type guard shows that arrays, URL objects, boxed strings, and objects with a toString() method are outside the interface. U11 exercises one coercible object as the named negative control. The native constructor’s string coercion is deliberately not exposed by this helper: endpoint admission operates on the submitted primitive string.

The function parses base and checks its normalized properties: exact origin and pathname, plus empty search, hash, username, and password getters. The missing-slash base fails its pathname check. The base ending in ?x=1 fails its query check despite the correct pathname, and the nonempty username in U10 fails the userinfo check.

These are component checks, not byte-for-byte canonical-base admission. Alternative spellings may normalize to the same components. The WHATWG getter definitions also return empty search or hash for a bare trailing ? or #, so those spellings are not rejected by this base guard. For the admitted nonempty endpoints, those empty components do not survive into the returned href. U10 qualifies its six named violations, not every noncanonical base spelling; a stricter serialization rule needs additional tests.

Only after the base passes does the endpoint regular expression run: ^[A-Za-z0-9_-]+(?:\/[A-Za-z0-9_-]+)*$. It admits one or more nonempty segments separated by single forward slashes. It does not decode or normalize caller input. users/42 is accepted as authored; %2e%2e/users, users\42, and users//42 are rejected as unsupported syntax rather than transformed into something else.

The helper then constructs with new URL(endpoint, b) and checks the exact origin, pathname, and excluded-component postcondition before returning href. This order leaves construction with the native parser without making that parser the admission policy. The complete returned string is compared with its literal expectation in U8 and U11. The repair remains smaller than a general-purpose URL validator because this interface needs endpoint names, not arbitrary references.

The grammar is also easy to review against call sites. A component that needs users/42 can pass that exact endpoint. A component that needs a search query must use a separately designed interface rather than smuggling ? into this one. A complete URL is outside the admitted grammar. The postcondition makes a mismatch in its checked components fail before the helper returns a value.

Exercise unsupported references and invalid bases

U9 freezes ten endpoint rejections. The empty string has no segment. /users is root-relative. ../users is parent-relative. //other.example.test/users supplies an authority. https://other.example.test/users is absolute. users?x=1 and users#x introduce components that this endpoint grammar does not own. %2e%2e/users introduces a percent form. users\42 uses a backslash. users//42 contains an empty segment.

Every one of those calls is expected to throw TypeError, and the independently recorded rerun matched all ten expected error names. The decision is REJECT_UNSUPPORTED_REFERENCE, not “repair the string automatically.” Silent normalization would widen the public interface beyond the reviewed examples and could collapse distinct caller mistakes into the same destination.

U10 freezes six base rejections: missing trailing slash, nonempty query, nonempty fragment, nonempty username, wrong API version prefix, and wrong origin. Each produced the expected TypeError in the recorded rerun before a usable endpoint string was returned. These are configuration defects, so the operational response is REPAIR_CONSTRUCTION and another manifest run, not weakening the checks until the current value passes.

Queries are not inherently invalid for APIs; they are simply outside this helper. A future client that needs them should design a separate key-and-value contract and test its encoding behavior. The related article on preserving query values under a distinct contract addresses that different boundary. It is not evidence that arbitrary query text belongs in this endpoint parameter.

U9 endpoint rejection inventory

Form

Input

Recorded error name

empty string

''

TypeError

root-relative

/users

TypeError

parent-relative

../users

TypeError

network-path

//other.example.test/users

TypeError

absolute

https://other.example.test/users

TypeError

query-bearing

users?x=1

TypeError

fragment-bearing

users#x

TypeError

percent form

%2e%2e/users

TypeError

backslash

users\42

TypeError

repeated slash

users//42

TypeError

U10 base rejection inventory

Base defect

Input

Recorded error name

missing final slash

https://api.example.test/tenant-a/v1

TypeError

nonempty query

https://api.example.test/tenant-a/v1/?x=1

TypeError

nonempty fragment

https://api.example.test/tenant-a/v1/#x

TypeError

nonempty username

https://[email protected]/tenant-a/v1/

TypeError

wrong prefix

https://api.example.test/tenant-a/v10/

TypeError

wrong origin

https://other.example.test/tenant-a/v1/

TypeError

Publish an immutable string without confusing it with authorization

U11 calls endpointURL(BASE, 'users') and receives the primitive string https://api.example.test/tenant-a/v1/users. A consumer constructs a new URL object from that string and changes its pathname to /elsewhere. The consumer object becomes https://api.example.test/elsewhere, while the published string remains unchanged. The helper has not shared an owner-controlled mutable URL instance with the consumer.

The same case passes an object whose toString() returns users. This helper rejects that object with TypeError because the interface requires a primitive string. The earlier constructor documentation explains why relying on native coercion would expose a wider interface. The explicit guard keeps this helper’s endpoint grammar focused on the submitted string rather than an object’s conversion behavior.

This is a small interface ownership control, not a general immutability theorem. JavaScript strings are primitive values, but a caller can store another value, construct another URL, or ignore the helper. Likewise, the destination contract is independent of validating the fields in a request body. A correct URL string says nothing about whether a later FormData object contains the intended controls.

A fixed local result does not constrain later consumer behavior

After endpointURL() returns, a caller can discard the string, concatenate something else, select another request target, or follow transport behavior that changes the eventual destination. The helper cannot police code that does not use its result. It also cannot authorize a user, choose an HTTP method, validate a body, or force an intermediary to route a path in a particular way.

For those reasons, do not market the helper as a tenant security boundary or a complete SSRF defense. It reduces one construction ambiguity for one client interface. The correct decision for the local interface is ACCEPT_BOUNDED; the decision for actual service authorization, redirects, DNS behavior, and downstream routing remains HOLD until those layers have their own evidence.

Reconcile every constructed tuple and admission decision

The reconciliation matrix must preserve cases that parse successfully but violate the application contract. A green diagnostic assertion means the observed result matched the expected result for that case. It does not mean every result is an acceptable endpoint. This is especially important for U2 through U7, whose purpose is to prove predictable drift.

Read the matrix in two passes. First, compare the exact tuple or exception with the authored oracle. Second, apply the application decision. U1, U8, and U11 support ACCEPT_BOUNDED within their stated scope. U2 through U7 require REPAIR_CONSTRUCTION when used as unrestricted constructor inputs. U9 supports REJECT_UNSUPPORTED_REFERENCE. U10 identifies bases that must be repaired before use.

PASS_11_CASES reports completion of this driver’s assertions, not report authenticity or blanket endpoint approval. The constructor controls assert href and record origin and pathname for reconciliation; U7 also asserts its prefix counterexample. Check the eleven distinct IDs as well as the top-level status, because the final length assertion alone would not detect duplicated IDs in an edited driver. Missing rows, unexpected error names, changed literals, or failed report writing require HOLD.

Eleven-case reconciliation matrix

Case

Recorded result and contract interpretation

Decision

U1

Exact U1 tuple matches the manifest.
Origin and segment prefix pass.

ACCEPT_BOUNDED

U2

Exact U2 tuple matches the manifest.
Same origin; /tenant-a/users fails the prefix.

REPAIR_CONSTRUCTION

U3

Exact U3 tuple matches the manifest.
Same origin; /users fails the prefix.

REPAIR_CONSTRUCTION

U4

Exact U4 tuple matches the manifest.
Same origin; /tenant-a/users fails the prefix.

REPAIR_CONSTRUCTION

U5

Exact U5 tuple matches the manifest.
Different origin; /users fails the prefix.

REPAIR_CONSTRUCTION

U6

Exact U6 tuple matches the manifest.
Absolute HTTPS input selects the other origin.

REPAIR_CONSTRUCTION

U7

Exact U7 tuple matches the manifest.
/tenant-a/v10/users passes the unsafe prefix,
but fails /tenant-a/v1/.

REPAIR_CONSTRUCTION

U8

users/42 returns the literal bounded href
https://api.example.test/tenant-a/v1/users/42

ACCEPT_BOUNDED

U9

Ten endpoint forms are rejected; each recorded
error.name is TypeError.

REJECT_UNSUPPORTED_REFERENCE

U10

Six base violations are rejected; each recorded
error.name is TypeError.

REPAIR_CONSTRUCTION

U11

Published string stays unchanged; consumer URL
mutates separately; non-string input is rejected.

ACCEPT_BOUNDED
Transport: HOLD

Recover the client configuration without claiming server rollback

When the evidence shows construction drift, repair the approved base or the caller’s reference shape at the client boundary. Restore the normalized trailing-slash base. Route plain endpoint segments through the bounded helper. Remove callers that pass root-relative, parent-relative, network-path, absolute, query-bearing, fragment-bearing, or otherwise unsupported references into that parameter.

Then rerun the full tuple manifest. Do not rerun only the formerly failing example, because a repair to base normalization or admission logic can change another case. Keep the new source digest, runtime versions, timestamps, report path, and eleven-row inventory. After the local result is stable, proceed to separate integration tests such as browser request-routing coverage where the test can observe which request a browser actually emits.

Local construction evidence cannot reconstruct effects from requests that might already have been sent to the wrong path or origin. It cannot prove whether a service read or changed data, whether a redirect occurred, or whether a retry duplicated an operation. Any rollback, deletion, reconciliation, or incident closure needs request logs and service-owned evidence. The local decision can be REPAIR_CONSTRUCTION while the operational impact remains HOLD.

A wider endpoint requirement needs a new explicit contract

Some clients legitimately need query parameters, Unicode identifiers, encoded segments, optional tenant prefixes, or multiple approved origins. That requirement should produce a new grammar, new normalization rules, new tuple oracles, and new negative controls. Do not remove the guard and call the result flexible. Do not silently decode, trim, collapse, or rewrite unsupported input until examples define the intended meaning.

The current helper is valuable because its domain is small enough to reason about. A wider contract may still use new URL(), but it must specify who owns each component and how boundary comparisons are made. Until that review exists, the wider requirement is HOLD, not an implicit extension of ACCEPT_BOUNDED.

Assign the base, endpoint and integration owners

The API contract owner defines approved origins, tenant prefixes, version segments, and whether queries or other components belong in the endpoint interface. That owner also resolves changes such as a migration from /tenant-a/v1/ to another prefix rather than leaving clients to infer compatibility from string similarity.

The client maintainer owns normalization of the configured base, the admitted endpoint language, the exact postcondition, and the choice to return a string. QA owns the literal tuple manifest, negative controls, error-type checks, inventory completeness, runtime labels, and source digest. The service owner retains authorization, routing, tenancy, redirect, and operational-impact responsibilities.

Reopen qualification when URL-building code changes, the input grammar expands, the approved origin or prefix changes, or the runtime is upgraded. A pinned Node.js observation is useful regression evidence, but it is not a universal promise for every runtime and platform. Preserve the old report, create a new one, and compare named cases rather than overwriting history.

A practical handoff includes four artifacts: the approved origin and prefix decision, the source file and digest, the machine-readable report, and the reconciliation matrix that maps technical observations to operational decisions. None substitutes for the others. The configuration owner can approve a prefix without proving the code used it; the code can pass without proving the service route; and a report without its source identity cannot establish which assertions actually ran.

Build dependable API clients through frontend practice

Reliable frontend API work depends on treating browser and JavaScript semantics as testable contracts. The exercise above combines ES module code, the WHATWG URL interface, API-integration reasoning, deterministic fixtures, and explicit QA decisions without pretending that local construction proves transport or service behavior.

For structured practice, explore Refonte Learning’s Frontend Development programme. Its official page lists JavaScript ES6+, APIs and AJAX, React, Redux, Git, accessibility, performance optimization, and a capstone, with a stated dedication of 10–12 hours per week. These broader subjects provide a learning bridge to maintainable client interfaces; specialized production policies still need their own contract review and evidence.

Define the endpoint language, preserve counterexamples, and compare literal tuples. This qualifies client configuration before transport without claiming service routing or authorization.