Skip to content

Commit dac8e59

Browse files
Feat: Improved error handling and response status availability (#2303)
* Add response data to route object * Display error status and description on content fetch error * Fix issue where initial site render was incomplete on content fetch error * Fix issue where empty markdown pages/routes were handled as 404 errors * Fix incorrect `notFoundPage` default value
1 parent ae71c69 commit dac8e59

File tree

10 files changed

+302
-134
lines changed

10 files changed

+302
-134
lines changed

docs/configuration.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -534,7 +534,7 @@ To disable emoji parsing of individual shorthand codes, replace `:` characters w
534534
- Type: `Boolean` | `String` | `Object`
535535
- Default: `false`
536536

537-
Display default "404 - Not found" message:
537+
Display default "404 - Not Found" message:
538538

539539
```js
540540
window.$docsify = {

src/core/config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ export default function (vm) {
3030
nativeEmoji: false,
3131
noCompileLinks: [],
3232
noEmoji: false,
33-
notFoundPage: true,
33+
notFoundPage: false,
3434
plugins: [],
3535
relativePath: false,
3636
repo: '',

src/core/fetch/index.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,8 @@ export function Fetch(Base) {
102102
this.isHTML = /\.html$/g.test(file);
103103

104104
// create a handler that should be called if content was fetched successfully
105-
const contentFetched = (text, opt) => {
105+
const contentFetched = (text, opt, response) => {
106+
this.route.response = response;
106107
this._renderMain(
107108
text,
108109
opt,
@@ -111,7 +112,8 @@ export function Fetch(Base) {
111112
};
112113

113114
// and a handler that is called if content failed to fetch
114-
const contentFailedToFetch = _error => {
115+
const contentFailedToFetch = (_error, response) => {
116+
this.route.response = response;
115117
this._fetchFallbackPage(path, qs, cb) || this._fetch404(file, qs, cb);
116118
};
117119

src/core/render/index.js

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -60,10 +60,6 @@ export function Render(Base) {
6060
return isVue2 || isVue3;
6161
};
6262

63-
if (!html) {
64-
html = /* html */ `<h1>404 - Not found</h1>`;
65-
}
66-
6763
if ('Vue' in window) {
6864
const mountedElms = dom
6965
.findAll('.markdown-section > *')
@@ -310,8 +306,12 @@ export function Render(Base) {
310306
}
311307

312308
_renderMain(text, opt = {}, next) {
313-
if (!text) {
314-
return this.#renderMain(text);
309+
const { response } = this.route;
310+
311+
// Note: It is possible for the response to be undefined in environments
312+
// where XMLHttpRequest has been modified or mocked
313+
if (response && !response.ok && (!text || response.status !== 404)) {
314+
text = `# ${response.status} - ${response.statusText}`;
315315
}
316316

317317
this.callHook('beforeEach', text, result => {

src/core/router/history/hash.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ export class HashHistory extends History {
9292
path,
9393
file: this.getFile(path, true),
9494
query: parseQuery(query),
95+
response: {},
9596
};
9697
}
9798

src/core/router/history/html5.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ export class HTML5History extends History {
5959
path,
6060
file: this.getFile(path),
6161
query: parseQuery(query),
62+
response: {},
6263
};
6364
}
6465
}

src/core/util/ajax.js

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@ import progressbar from '../render/progressbar.js';
44
import { noop } from './core.js';
55

66
/** @typedef {{updatedAt: string}} CacheOpt */
7-
87
/** @typedef {{content: string, opt: CacheOpt}} CacheItem */
9-
8+
/** @typedef {{ok: boolean, status: number, statusText: string}} ResponseStatus */
109
/** @type {Record<string, CacheItem>} */
10+
1111
const cache = {};
1212

1313
/**
@@ -37,10 +37,16 @@ export function get(url, hasBar = false, headers = {}) {
3737

3838
return {
3939
/**
40-
* @param {(text: string, opt: CacheOpt) => void} success
41-
* @param {(event: ProgressEvent<XMLHttpRequestEventTarget>) => void} error
40+
* @param {(text: string, opt: CacheOpt, response: ResponseStatus) => void} success
41+
* @param {(event: ProgressEvent<XMLHttpRequestEventTarget>, response: ResponseStatus) => void} error
4242
*/
4343
then(success, error = noop) {
44+
const getResponseStatus = event => ({
45+
ok: event.target.status >= 200 && event.target.status < 300,
46+
status: event.target.status,
47+
statusText: event.target.statusText,
48+
});
49+
4450
if (hasBar) {
4551
const id = setInterval(
4652
_ =>
@@ -57,11 +63,15 @@ export function get(url, hasBar = false, headers = {}) {
5763
});
5864
}
5965

60-
xhr.addEventListener('error', error);
66+
xhr.addEventListener('error', event => {
67+
error(event, getResponseStatus(event));
68+
});
69+
6170
xhr.addEventListener('load', event => {
6271
const target = /** @type {XMLHttpRequest} */ (event.target);
72+
6373
if (target.status >= 400) {
64-
error(event);
74+
error(event, getResponseStatus(event));
6575
} else {
6676
if (typeof target.response !== 'string') {
6777
throw new TypeError('Unsupported content type.');
@@ -74,7 +84,7 @@ export function get(url, hasBar = false, headers = {}) {
7484
},
7585
});
7686

77-
success(result.content, result.opt);
87+
success(result.content, result.opt, getResponseStatus(event));
7888
}
7989
});
8090
},

test/e2e/configuration.test.js

Lines changed: 131 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -3,64 +3,146 @@ import docsifyInit from '../helpers/docsify-init.js';
33
import { test, expect } from './fixtures/docsify-init-fixture.js';
44

55
test.describe('Configuration options', () => {
6-
test('catchPluginErrors:true (handles uncaught errors)', async ({ page }) => {
7-
let consoleMsg, errorMsg;
8-
9-
page.on('console', msg => (consoleMsg = msg.text()));
10-
page.on('pageerror', err => (errorMsg = err.message));
11-
12-
await docsifyInit({
13-
config: {
14-
catchPluginErrors: true,
15-
plugins: [
16-
function (hook, vm) {
17-
hook.init(function () {
18-
fail();
19-
});
20-
hook.beforeEach(markdown => {
21-
return `${markdown}\n\nbeforeEach`;
22-
});
23-
},
24-
],
25-
},
26-
markdown: {
27-
homepage: '# Hello World',
28-
},
29-
// _logHTML: true,
6+
test.describe('catchPluginErrors', () => {
7+
test('true (handles uncaught errors)', async ({ page }) => {
8+
let consoleMsg, errorMsg;
9+
10+
page.on('console', msg => (consoleMsg = msg.text()));
11+
page.on('pageerror', err => (errorMsg = err.message));
12+
13+
await docsifyInit({
14+
config: {
15+
catchPluginErrors: true,
16+
plugins: [
17+
function (hook, vm) {
18+
hook.init(function () {
19+
fail();
20+
});
21+
hook.beforeEach(markdown => {
22+
return `${markdown}\n\nbeforeEach`;
23+
});
24+
},
25+
],
26+
},
27+
markdown: {
28+
homepage: '# Hello World',
29+
},
30+
// _logHTML: true,
31+
});
32+
33+
const mainElm = page.locator('#main');
34+
35+
expect(errorMsg).toBeUndefined();
36+
expect(consoleMsg).toContain('Docsify plugin error');
37+
await expect(mainElm).toContainText('Hello World');
38+
await expect(mainElm).toContainText('beforeEach');
3039
});
3140

32-
const mainElm = page.locator('#main');
41+
test('false (throws uncaught errors)', async ({ page }) => {
42+
let consoleMsg, errorMsg;
43+
44+
page.on('console', msg => (consoleMsg = msg.text()));
45+
page.on('pageerror', err => (errorMsg = err.message));
3346

34-
expect(errorMsg).toBeUndefined();
35-
expect(consoleMsg).toContain('Docsify plugin error');
36-
await expect(mainElm).toContainText('Hello World');
37-
await expect(mainElm).toContainText('beforeEach');
47+
await docsifyInit({
48+
config: {
49+
catchPluginErrors: false,
50+
plugins: [
51+
function (hook, vm) {
52+
hook.ready(function () {
53+
fail();
54+
});
55+
},
56+
],
57+
},
58+
markdown: {
59+
homepage: '# Hello World',
60+
},
61+
// _logHTML: true,
62+
});
63+
64+
expect(consoleMsg).toBeUndefined();
65+
expect(errorMsg).toContain('fail');
66+
});
3867
});
3968

40-
test('catchPluginErrors:false (throws uncaught errors)', async ({ page }) => {
41-
let consoleMsg, errorMsg;
69+
test.describe('notFoundPage', () => {
70+
test.describe('renders default error content', () => {
71+
test.beforeEach(async ({ page }) => {
72+
await page.route('README.md', async route => {
73+
await route.fulfill({
74+
status: 500,
75+
});
76+
});
77+
});
78+
79+
test('false', async ({ page }) => {
80+
await docsifyInit({
81+
config: {
82+
notFoundPage: false,
83+
},
84+
});
4285

43-
page.on('console', msg => (consoleMsg = msg.text()));
44-
page.on('pageerror', err => (errorMsg = err.message));
86+
await expect(page.locator('#main')).toContainText('500');
87+
});
4588

46-
await docsifyInit({
47-
config: {
48-
catchPluginErrors: false,
49-
plugins: [
50-
function (hook, vm) {
51-
hook.ready(function () {
52-
fail();
53-
});
89+
test('true with non-404 error', async ({ page }) => {
90+
await docsifyInit({
91+
config: {
92+
notFoundPage: true,
93+
},
94+
routes: {
95+
'_404.md': '',
5496
},
55-
],
56-
},
57-
markdown: {
58-
homepage: '# Hello World',
59-
},
60-
// _logHTML: true,
97+
});
98+
99+
await expect(page.locator('#main')).toContainText('500');
100+
});
101+
102+
test('string with non-404 error', async ({ page }) => {
103+
await docsifyInit({
104+
config: {
105+
notFoundPage: '404.md',
106+
},
107+
routes: {
108+
'404.md': '',
109+
},
110+
});
111+
112+
await expect(page.locator('#main')).toContainText('500');
113+
});
61114
});
62115

63-
expect(consoleMsg).toBeUndefined();
64-
expect(errorMsg).toContain('fail');
116+
test('true: renders _404.md page', async ({ page }) => {
117+
const expectText = 'Pass';
118+
119+
await docsifyInit({
120+
config: {
121+
notFoundPage: true,
122+
},
123+
routes: {
124+
'_404.md': expectText,
125+
},
126+
});
127+
128+
await page.evaluate(() => (window.location.hash = '#/fail'));
129+
await expect(page.locator('#main')).toContainText(expectText);
130+
});
131+
132+
test('string: renders specified 404 page', async ({ page }) => {
133+
const expectText = 'Pass';
134+
135+
await docsifyInit({
136+
config: {
137+
notFoundPage: '404.md',
138+
},
139+
routes: {
140+
'404.md': expectText,
141+
},
142+
});
143+
144+
await page.evaluate(() => (window.location.hash = '#/fail'));
145+
await expect(page.locator('#main')).toContainText(expectText);
146+
});
65147
});
66148
});

0 commit comments

Comments
 (0)