Skip to content

Commit fdba944

Browse files
authored
Merge pull request #77 from script-development/medic/queue-21-relative-baseurl-guard
fix(http): guard relative baseURL — closes queue #21
2 parents b5a3336 + 222991f commit fdba944

6 files changed

Lines changed: 68 additions & 4 deletions

File tree

docs/packages/http.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,7 @@ try {
222222

223223
| Parameter | Type | Description |
224224
| -------------------------- | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
225-
| `baseURL` | `string` | Base URL for all requests |
225+
| `baseURL` | `string` | Base URL for all requests. **Must be absolute** (e.g. `${location.origin}/api`); relative paths fail fast. |
226226
| `options.timeout` | `number \| undefined` | Request timeout in milliseconds (default: `30000`; pass `0` to disable) |
227227
| `options.headers` | `Record<string, string>` | Default headers |
228228
| `options.withCredentials` | `boolean` | Send cookies cross-origin (default: `true`) |

package-lock.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/http/CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# @script-development/fs-http
22

3+
## 0.4.1 — 2026-05-29
4+
5+
### Patch Changes
6+
7+
- **Fail-fast guard on relative `baseURL`.** `createHttpService('/api')` now throws a library-attributed `Error` instead of an opaque native `TypeError: Invalid URL`. The new message names the package, names the function, explains that an absolute baseURL is required, and echoes the offending value — so the failure points at the consumer's call site rather than at fs-http internals. Production-bug class previously surfaced only at runtime as opaque `TypeError: Invalid URL` and remained latent in CI when consumers mock `@script-development/fs-http` in integration tests. Closes enforcement queue #21.
8+
39
## 0.4.0 — 2026-05-15
410

511
### Breaking Changes

packages/http/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@script-development/fs-http",
3-
"version": "0.4.0",
3+
"version": "0.4.1",
44
"description": "Framework-agnostic HTTP service factory with middleware architecture",
55
"homepage": "https://packages.script.nl/packages/http",
66
"license": "MIT",

packages/http/src/http.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,26 @@ const unregister = <T>(array: T[], item: T): UnregisterMiddleware => {
3030
};
3131
};
3232

33+
/**
34+
* Parse the consumer-supplied baseURL with a library-attributed error on failure.
35+
* The native `new URL(baseURL)` throws an opaque `TypeError: Invalid URL` that
36+
* points at fs-http internals rather than the consumer's call site, latent for
37+
* 6 days on entreezuil (PR #40 adoption → PR #96 fix) because integration tests
38+
* mocked @script-development/fs-http and the real factory never ran. Fail-fast
39+
* here (vs. silent coercion to absolute) prevents the class for every adopter.
40+
*/
41+
const parseBaseURL = (baseURL: string): URL => {
42+
try {
43+
return new URL(baseURL);
44+
} catch {
45+
throw new Error(
46+
`[@script-development/fs-http] createHttpService requires an absolute baseURL (e.g. \`\${location.origin}/api\`). Received: ${JSON.stringify(baseURL)}`,
47+
);
48+
}
49+
};
50+
3351
export const createHttpService = (baseURL: string, options?: HttpServiceOptions): HttpService => {
34-
const apiUrl = new URL(baseURL);
52+
const apiUrl = parseBaseURL(baseURL);
3553

3654
const http = axios.create({
3755
baseURL: apiUrl.toString(),

packages/http/tests/http.spec.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,46 @@ describe('createHttpService', () => {
3636
});
3737
});
3838

39+
describe('baseURL guard (queue #21)', () => {
40+
// Latent for 6 days on entreezuil (PR #40 adoption → PR #96 fix) because integration
41+
// tests mock @script-development/fs-http so the real factory never ran. The opaque
42+
// native `TypeError: Invalid URL` pointed at fs-http internals rather than the
43+
// consumer's createHttpService call site. Library-side fail-fast guard prevents the
44+
// class for every future adopter.
45+
46+
it('throws a library-attributed error when called with a relative path', () => {
47+
// Arrange & Act & Assert
48+
expect(() => createHttpService('/api')).toThrow(/@script-development\/fs-http/);
49+
expect(() => createHttpService('/api')).toThrow(/createHttpService/);
50+
expect(() => createHttpService('/api')).toThrow(/absolute baseURL/);
51+
expect(() => createHttpService('/api')).toThrow(/"\/api"/);
52+
});
53+
54+
it('throws a library-attributed error when called with an empty string', () => {
55+
// Arrange & Act & Assert
56+
expect(() => createHttpService('')).toThrow(/@script-development\/fs-http/);
57+
expect(() => createHttpService('')).toThrow(/createHttpService/);
58+
expect(() => createHttpService('')).toThrow(/absolute baseURL/);
59+
expect(() => createHttpService('')).toThrow(/""/);
60+
});
61+
62+
it('throws a library-attributed error for malformed URL strings', () => {
63+
// Arrange & Act & Assert — sample of malformed inputs covered by the guard.
64+
expect(() => createHttpService('not a url')).toThrow(/@script-development\/fs-http/);
65+
expect(() => createHttpService('not a url')).toThrow(/"not a url"/);
66+
67+
expect(() => createHttpService('http://')).toThrow(/@script-development\/fs-http/);
68+
expect(() => createHttpService('http://')).toThrow(/"http:\/\/"/);
69+
});
70+
71+
it('does NOT throw for valid absolute URLs (happy-path regression guard)', () => {
72+
// Arrange & Act & Assert — these all succeed because the guard parses successfully.
73+
expect(() => createHttpService('http://localhost')).not.toThrow();
74+
expect(() => createHttpService('https://example.com/api')).not.toThrow();
75+
expect(() => createHttpService('https://api.example.com')).not.toThrow();
76+
});
77+
});
78+
3979
describe('default options', () => {
4080
it('creates axios instance with correct defaults', async () => {
4181
// Arrange

0 commit comments

Comments
 (0)