fix: surface a clear error when requestUrl() itself rejects with HTML content

parseJson() already handles the case where a response resolves normally
but its .json getter throws on an HTML body (e.g. a login/SSO redirect
or proxy error page). But some Obsidian versions eagerly parse the
response body as JSON inside requestUrl() itself, so the same failure
can instead reject the whole call with a raw SyntaxError — landing in
safeRequest's outer catch, before there's even a response object to
inspect. That catch just rethrew the error verbatim, surfacing the
literal "Unexpected token '<', \"<!DOCTYPE \"... is not valid JSON"
message to the user (as seen in the reported "Failed to refresh" case,
against a self-hosted GitLab instance).

safeRequest's outer catch now recognizes this error's known V8
phrasings and throws the same friendly "received an HTML page" message
parseJson() already produces for the other code path.

Closes #31
This commit is contained in:
ClaudiaFang 2026-07-13 13:46:38 +00:00
parent ef238cea59
commit a86721752a
2 changed files with 48 additions and 1 deletions

View file

@ -78,8 +78,20 @@ export abstract class BaseGitService {
response = await requestUrl(options); response = await requestUrl(options);
} catch (error) { } catch (error) {
// Network-level failure (DNS, offline, TLS, etc.) // Network-level failure (DNS, offline, TLS, etc.) — but some Obsidian
// versions eagerly parse the response body as JSON inside requestUrl()
// itself, before `throw: false` or our own status check ever run. If a
// proxy/login page returns HTML instead of JSON, that eager parse
// throws here as a raw "Unexpected token '<' ... is not valid JSON"
// error rather than surfacing as a normal response we could inspect.
if (!silent) logger.error('Git Service Request Failed:', error); if (!silent) logger.error('Git Service Request Failed:', error);
if (this.looksLikeJsonParseOfHtmlError(error)) {
throw new Error(
'Expected a JSON response from the Git server but received an HTML page ' +
'(likely a login, SSO redirect, or proxy/error page). ' +
'Please check the server URL, your access token, and any network proxy or firewall.'
);
}
if (error instanceof Error) throw error; if (error instanceof Error) throw error;
throw new Error(`Network error or unexpected failure: ${String(error)}`); throw new Error(`Network error or unexpected failure: ${String(error)}`);
} }
@ -102,6 +114,17 @@ export abstract class BaseGitService {
protected abstract addAuthHeader(headers: Record<string, string>): void; protected abstract addAuthHeader(headers: Record<string, string>): void;
/**
* Detects V8's JSON.parse error for a response starting with '<' (an HTML
* page), across its known message phrasings, e.g.:
* "Unexpected token '<', "<!DOCTYPE "... is not valid JSON"
* "Unexpected token < in JSON at position 0"
*/
private looksLikeJsonParseOfHtmlError(error: unknown): boolean {
if (!(error instanceof Error)) return false;
return /unexpected token/i.test(error.message) && /json/i.test(error.message) && error.message.includes('<');
}
/** /**
* Safely parses a response body as JSON. * Safely parses a response body as JSON.
* *

View file

@ -77,6 +77,30 @@ describe('BaseGitService', () => {
}); });
}); });
describe('safeRequest when requestUrl() itself throws a JSON-parse-of-HTML error', () => {
// Some Obsidian versions eagerly parse the response body as JSON inside
// requestUrl() itself, so a proxy/login HTML page can make the whole
// call reject with a raw SyntaxError — before `throw: false` or our own
// status/content-type checks ever get a response object to inspect.
it('wraps the raw "Unexpected token \'<\'... is not valid JSON" rejection with a clear message', async () => {
vi.mocked(requestUrl).mockRejectedValue(
new SyntaxError('Unexpected token \'<\', "<!DOCTYPE "... is not valid JSON')
);
await expect(service.listFiles('main')).rejects.toThrow(/received an HTML page/);
await expect(service.listFiles('main')).rejects.not.toThrow(/Unexpected token/);
});
it('wraps the older V8 phrasing ("Unexpected token < in JSON at position 0") too', async () => {
vi.mocked(requestUrl).mockRejectedValue(new SyntaxError('Unexpected token < in JSON at position 0'));
await expect(service.listFiles('main')).rejects.toThrow(/received an HTML page/);
});
it('does not misclassify an unrelated JSON syntax error as an HTML response', async () => {
vi.mocked(requestUrl).mockRejectedValue(new SyntaxError('Unexpected end of JSON input'));
await expect(service.listFiles('main')).rejects.toThrow('Unexpected end of JSON input');
});
});
describe('parseJson with non-JSON (HTML) responses', () => { describe('parseJson with non-JSON (HTML) responses', () => {
// Simulates Obsidian's real RequestUrlResponse.json getter, which throws // Simulates Obsidian's real RequestUrlResponse.json getter, which throws
// SyntaxError when the body is not valid JSON (e.g. an HTML page). // SyntaxError when the body is not valid JSON (e.g. an HTML page).