Skip to content

fix(http): guard relative baseURL — closes queue #21#77

Merged
Goosterhof merged 5 commits into
mainfrom
medic/queue-21-relative-baseurl-guard
Jun 1, 2026
Merged

fix(http): guard relative baseURL — closes queue #21#77
Goosterhof merged 5 commits into
mainfrom
medic/queue-21-relative-baseurl-guard

Conversation

@Goosterhof
Copy link
Copy Markdown
Contributor

@Goosterhof Goosterhof commented May 8, 2026

Summary

  • Wrap new URL(baseURL) in createHttpService with a library-attributed error so relative paths like '/api' fail fast with a message that names the package, names the function, explains what's needed, and echoes the offending value — instead of the opaque native TypeError: Invalid URL that points at fs-http internals.
  • Bumps @script-development/fs-http 0.4.0 → 0.4.1 (patch — additive guard on previously-undefined behavior, no API change).
  • Closes enforcement queue #21.

Seed Incident

Latent for 6 days on entreezuil (PR #40 adoption → PR #96 fix). Integration tests mock @script-development/fs-http, so the real factory never ran in CI — the bug surfaced only at runtime in production. Library-side fail-fast prevents the class for every future fs-http adopter.

Error Message Shape

[@script-development/fs-http] createHttpService requires an absolute baseURL (e.g. `${location.origin}/api`). Received: "/api"

Names the package, names the function, gives an actionable example, echoes the offending value (via JSON.stringify so empty strings, whitespace, and quoting are visible).

Out of Scope (per orders)

  • Auto-coercing relative URLs to absolute — silently wrong direction; chose fail-fast.
  • Adding an oxlint call-site rule — alternative path noted in queue chore(deps-dev): bump the oxc group with 2 updates #21; library-layer guard is the path chosen here.
  • Touching streamRequest, withXSRFToken, withCredentials, or any other surface (Armorer is on a parallel docs-only XSRF PR; staying out of that lane).
  • Cascading peer-range bumps — patch bump on fs-http stays within consumer ^0.4.0 ranges; verified with grep on packages/adapter-store/package.json + packages/loading/package.json.

Stash-and-rerun Proof

Pre-fix run of the 4 new guard tests against unmodified source:

× throws a library-attributed error when called with a relative path  → 'Invalid URL'
× throws a library-attributed error when called with an empty string  → 'Invalid URL'
× throws a library-attributed error for malformed URL strings         → 'Invalid URL'
✓ does NOT throw for valid absolute URLs (happy-path regression guard)

Post-fix: 4/4 pass. Bug reproduced as documented; fix verified to be what made the difference.

CI Gates (8/8 PASS locally)

# Gate Result
1 npm audit 0 vulnerabilities
2 npm run format:check clean (133 files)
3 npm run lint 0 warnings / 0 errors (95 rules, 83 files)
4 npm run build dual ESM + CJS produced for fs-http (and all 10 packages)
5 npm run typecheck clean across all packages
6 npm run lint:pkg publint + attw zero advisories on all 10 packages
7 npm run test:coverage 400/400 pass (was 396, +4 new), 100% statements/branches/functions/lines
8 npx stryker run (packages/http) 97.44% mutation (was 97.30% pre-fix), 76/78 killed, 2 surviving — both pre-existing on unregister's index > -1 check, unrelated. No regression of pre-existing survivors.

Lockfile sanity: package-lock.json regenerated; all 10 node_modules/@script-development/* resolve to workspace symlinks ("link": true); no nested registry copies.

Test Plan

  • Pre-fix: 3 negative cases fail with native TypeError: Invalid URL (proof captured)
  • Post-fix: 4 new guard tests pass; existing 45 tests still pass
  • Coverage: 100% maintained (400/400 tests, 100% across all metrics)
  • Mutation: ≥90% maintained (97.44%); pre-existing survivors not regressed
  • All 8 CI gates green locally

🤖 Generated with Claude Code

@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented May 8, 2026

Deploying fs-packages with  Cloudflare Pages  Cloudflare Pages

Latest commit: 222991f
Status: ✅  Deploy successful!
Preview URL: https://fe9d1f89.fs-packages.pages.dev
Branch Preview URL: https://medic-queue-21-relative-base.fs-packages.pages.dev

View logs

Goosterhof and others added 3 commits May 8, 2026 12:47
createHttpService(baseURL) previously called `new URL(baseURL)` directly,
which throws an opaque native `TypeError: Invalid URL` on relative paths
like '/api'. The error pointed at fs-http internals rather than the
consumer's call site. This was latent for 6 days on entreezuil
(PR #40 adoption -> PR #96 fix) because integration tests mock
@script-development/fs-http and the real factory never ran in CI.

Wrap the URL parse in a `parseBaseURL` helper that catches the native
TypeError and re-throws a library-attributed Error naming the package
(@script-development/fs-http), the function (createHttpService), the
expectation (absolute baseURL with example), and the offending value
(JSON.stringify of the input).

Library-side fail-fast prevents the class for every fs-http adopter.
Closes enforcement queue #21.

Tests: 4 new guard cases (relative '/api', empty string, malformed
strings 'not a url' / 'http://', happy-path regression for absolute
URLs). Pre-fix all 3 negative cases failed with 'Invalid URL'; post-fix
all 4 pass. Stash-and-rerun proof captured.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Patch bump — additive guard on previously-undefined behavior, no API
change. Existing absolute-URL behavior unchanged. Consumer peer ranges
already include `^0.3.0` (verified packages/adapter-store/package.json
+ packages/loading/package.json) — no peer-range cascade required.

Lockfile regenerated; all @script-development/* still resolve to
workspace symlinks (no nested registry copies).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
One-liner clarification on the existing baseURL row of the
createHttpService API Reference table. The library-attributed error
message remains the primary documentation surface for the failure
path; this is the static-page corollary so consumers reading the API
table before writing the call site see the constraint up front.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@Goosterhof Goosterhof force-pushed the medic/queue-21-relative-baseurl-guard branch from 41373b5 to 22da1c0 Compare May 8, 2026 10:48
@Goosterhof
Copy link
Copy Markdown
Contributor Author

Library-attributed error met de echte call-site is de goede keuze ipv stille coercion — de entreezuil-incident class is hiermee voor elke adopter weggevangen. CHANGELOG noemt het pakket, de functie, het echo'd de bad input, en attribueert de latency naar de mock-in-tests root cause. Happy-path regression test in dezelfde describe-block is mooi.

CONFLICTING op de moment — pls rebase op main na #95 (npm audit) + #88 (sideEffects) landen, dan kan deze door.

@Goosterhof Goosterhof requested a review from jasperboerhof May 21, 2026 10:19
@jasperboerhof
Copy link
Copy Markdown
Contributor

PR Reviewer · 9/10 · PASS

Findings

  • MINOR · packages/http/src/http.ts:44 — The catch drops the original TypeError (bare catch {) instead of chaining it via new Error(msg, {cause: e}). The new message is self-sufficient (names package + function + requirement + echoes the offending value via JSON.stringify), and new URL's native TypeError carries no extra actionable detail, so this is genuinely minor. Optional: capture catch (cause) and pass {cause} so the stack-trace chain is preserved for any future debugging that wants the original throw site.

Action

merge-ready

@Goosterhof Goosterhof added the Agent Review Requested Requesting review of specialized AI review agents. label May 29, 2026
…ve-baseurl-guard

# Conflicts:
#	docs/packages/http.md
#	package-lock.json
#	packages/http/CHANGELOG.md
#	packages/http/package.json
Copy link
Copy Markdown
Contributor Author

@Goosterhof Goosterhof left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No blockers. One concern, two nits, and one load-bearing decision worth naming. This is a tight, well-scoped patch that does exactly what queue #21 asked.

Concern

The malformed-URL guard catches more than "relative baseURL" — and that's good, but the framing undersells it. parseBaseURL (packages/http/src/http.ts:39-47) wraps any new URL(baseURL) failure, so it also fires on 'not a url', 'http://', and '' — all confirmed in tests/http.spec.ts:60-74. The error message, however, is hard-coded to requires an absolute baseURL. For 'http://' (a malformed-but-arguably-absolute input) the message is slightly misleading — the real failure is "unparseable", not "relative". This is defensible: the dominant real-world failure mode is the relative path, and a single clear message beats branching on URL failure subtypes that the platform doesn't cleanly expose. Flagging it only so the choice is conscious — if a consumer ever passes 'http://' and reads "requires an absolute baseURL", the message points them slightly off. Not worth a code change unless you want to soften the wording to "requires a valid absolute baseURL".

Nits

The PR body Summary says the bump is 0.3.0 → 0.3.1, but the actual diff bumps 0.4.0 → 0.4.1 (packages/http/package.json:3, package-lock.json:10231) — verified against origin/main, which sits at 0.4.0. The diff is correct; the body text is stale. Worth fixing the description so the changelog narrative and the PR record agree.

The malformed-URL test (tests/http.spec.ts:60-74) asserts the package-name prefix on every case but only spot-checks absolute baseURL on the relative-path and empty-string cases, not on 'http://'. If the message ever diverges by input class (per the concern above), that case wouldn't catch it. Minor — the current single-message implementation makes this redundant, not wrong.

Praise

The 6-day-latency provenance baked into the parseBaseURL docblock (packages/http/src/http.ts:32-38) is the right call — it records why the guard exists (consumers mock @script-development/fs-http in integration tests, so the real factory never runs in CI; the bug only surfaces at runtime) directly at the code that would otherwise look like defensive over-engineering. That comment is what stops a future contributor from deleting the guard as "the platform already throws on bad URLs." It does throw — opaquely, pointed at library internals — and the comment captures exactly that distinction. Library-layer fail-fast over consumer-side lint is also the correct lever for a defect class that recurs across every adopter.

Verdict: COMMENT — mergeable as-is. The version-number discrepancy in the body is cosmetic (the diff is authoritative and correct); fix it before merge for record hygiene, but it doesn't block.

Copy link
Copy Markdown
Contributor

@jasperboerhof jasperboerhof left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved — reviewed, no blockers.

@Goosterhof Goosterhof merged commit fdba944 into main Jun 1, 2026
2 checks passed
@Goosterhof Goosterhof deleted the medic/queue-21-relative-baseurl-guard branch June 1, 2026 10:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Agent Review Requested Requesting review of specialized AI review agents.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants