Skip to content

Commit bbc079f

Browse files
committed
fix(browser): map non-http(s) URL rejection to exit 5 (validation error)
openInBrowser threw a plain Error for a non-http(s) URL, which fell through the top-level handler and exited 1. Route it through localValidationError so it is classified as VALIDATION_ERROR and maps to the documented exit code 5, matching every other bad-argument path. Test asserts the exit code, not the message string.
1 parent 782b3af commit bbc079f

2 files changed

Lines changed: 19 additions & 5 deletions

File tree

src/lib/browser.test.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ vi.mock('node:child_process', () => ({
99
}));
1010

1111
import { openInBrowser } from './browser.js';
12+
import { ApiError } from './errors.js';
1213

1314
describe('openInBrowser', () => {
1415
const url = 'https://portal.example.com/tests/t_123';
@@ -40,11 +41,16 @@ describe('openInBrowser', () => {
4041
expect(calls).toEqual([{ command: 'xdg-open', args: [url] }]);
4142
});
4243

43-
it('refuses a non-http(s) URL before spawning', () => {
44+
it('refuses a non-http(s) URL with exit 5 before spawning', () => {
4445
const exec = vi.fn();
45-
expect(() => openInBrowser('file:///etc/passwd', { platform: 'linux', exec })).toThrow(
46-
/non-http\(s\)/,
47-
);
46+
let error: unknown;
47+
try {
48+
openInBrowser('file:///etc/passwd', { platform: 'linux', exec });
49+
} catch (err) {
50+
error = err;
51+
}
52+
expect(error).toBeInstanceOf(ApiError);
53+
expect((error as ApiError).exitCode).toBe(5);
4854
expect(exec).not.toHaveBeenCalled();
4955
});
5056

src/lib/browser.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
* are the caller's to surface (it already printed the URL as the fallback).
1515
*/
1616
import { spawn } from 'node:child_process';
17+
import { localValidationError } from './errors.js';
1718

1819
export interface OpenInBrowserDeps {
1920
platform?: NodeJS.Platform;
@@ -24,7 +25,14 @@ export interface OpenInBrowserDeps {
2425
export function openInBrowser(url: string, deps: OpenInBrowserDeps = {}): void {
2526
const parsed = new URL(url);
2627
if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') {
27-
throw new Error(`refusing to open a non-http(s) URL (${parsed.protocol})`);
28+
// User-input error, not an internal failure: classify as VALIDATION_ERROR
29+
// so it maps to exit 5 (like every other bad-argument path), not exit 1.
30+
throw localValidationError(
31+
'url',
32+
`must be an http(s) URL (got ${parsed.protocol})`,
33+
undefined,
34+
'field',
35+
);
2836
}
2937
const platform = deps.platform ?? process.platform;
3038
const exec =

0 commit comments

Comments
 (0)