Skip to content

Commit 64fdb9a

Browse files
committed
feat(pat inject): Better error handling.
Error handling: In a case of injection error, search for a configurable CSS id selector in the error response. If it is found, inject the rendered error page into the document. If no valid error response is found fall back to pre-configured error pages. This allows for some more informative error pages than a very generic one. The CSS id selector is defined by an optional URL fragment in the pre-configured error pages.
1 parent 3039a4f commit 64fdb9a

File tree

2 files changed

+187
-24
lines changed

2 files changed

+187
-24
lines changed

src/pat/inject/inject.js

Lines changed: 34 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,7 @@ const inject = {
216216
const cfgs = parser.parse($el, opts, true);
217217
cfgs.forEach((cfg) => {
218218
cfg.$context = $el;
219-
// opts and cfg have priority, fallback to href/action
219+
// opts and cfg have priority, fall back to href/action
220220
cfg.url =
221221
opts.url ||
222222
cfg.url ||
@@ -628,7 +628,6 @@ const inject = {
628628

629629
async _onInjectError($el, cfgs, event) {
630630
let explanation = "";
631-
let fallback;
632631
const status = event.jqxhr.status;
633632
const timestamp = new Date();
634633
if (status % 100 == 4) {
@@ -642,21 +641,36 @@ const inject = {
642641
"It seems, the server is down. Please make a screenshot and contact support. Thank you!";
643642
}
644643

644+
let error_page;
645+
let error_page_fragment;
645646
const url_params = new URLSearchParams(window.location.search);
647+
if (url_params.get("pat-inject-errorhandler.off") === null) {
648+
// Prepare a error page to be injected into the document.
649+
650+
// Try to get a suitable error message from pre-configured error pages.
651+
const error_page_url = document
652+
.querySelector(`meta[name^=pat-inject-status-${status}]`)
653+
?.getAttribute("content", false);
654+
error_page_fragment = error_page_url?.split("#")[1];
655+
error_page_fragment = error_page_fragment ? `#${error_page_fragment}` : null;
656+
657+
if (error_page_fragment) {
658+
error_page = document.createElement("html");
659+
error_page.innerHTML = event.jqxhr.responseText;
660+
error_page = error_page.querySelector(error_page_fragment);
661+
}
646662

647-
const fallback_url = document
648-
.querySelector(`meta[name=pat-inject-status-${status}]`)
649-
?.getAttribute("content", false);
650-
if (fallback_url && url_params.get("pat-inject-errorhandler.off") === null) {
651-
try {
652-
const fallback_response = await fetch(fallback_url, {
653-
method: "GET",
654-
});
655-
fallback = document.createElement("html");
656-
fallback.innerHTML = await fallback_response.text();
657-
fallback = fallback.querySelector("body");
658-
} catch {
659-
// fallback to standard error message and ignore.
663+
if (!error_page && error_page_url) {
664+
try {
665+
const error_page_response = await fetch(error_page_url, {
666+
method: "GET",
667+
});
668+
error_page = document.createElement("html");
669+
error_page.innerHTML = await error_page_response.text();
670+
error_page = error_page.querySelector(error_page_fragment || "body");
671+
} catch {
672+
// fall back to standard error message and ignore.
673+
}
660674
}
661675
}
662676

@@ -671,12 +685,12 @@ const inject = {
671685
$el.off("pat-ajax-success.pat-inject");
672686
$el.off("pat-ajax-error.pat-inject");
673687

674-
if (fallback) {
675-
document.body.innerHTML = fallback.innerHTML;
688+
if (error_page) {
689+
const error_zone = document.querySelector(error_page_fragment || "body");
690+
error_zone.innerHTML = error_page.innerHTML;
691+
registry.scan(error_zone); // initialize any patterns in error page
676692
} else {
677-
const msg_attr =
678-
fallback ||
679-
`${explanation} Status is ${status} ${event.jqxhr.statusText}, time was ${timestamp}. You can click to close this.`;
693+
const msg_attr = `${explanation} Status is ${status} ${event.jqxhr.statusText}, time was ${timestamp}. You can click to close this.`;
680694
$("body").attr("data-error-message", msg_attr);
681695
$("body").on("click", () => {
682696
$("body").removeAttr("data-error-message");

src/pat/inject/inject.test.js

Lines changed: 153 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1033,13 +1033,16 @@ describe("pat-inject", function () {
10331033

10341034
describe("Error handling", () => {
10351035
let $a;
1036-
let $div;
10371036

10381037
beforeEach(() => {
10391038
jest.spyOn($, "ajax").mockImplementation(() => deferred);
1040-
$a = $('<a class="pat-inject" href="test.html#someid">link</a>');
1041-
$div = $('<div id="someid" />');
1042-
$("#lab").append($a).append($div);
1039+
document.body.innerhtml = `
1040+
<div id="lab">
1041+
<a class="pat-inject" href="test.html#someid">link</a>
1042+
<div id="someid" />
1043+
</div>
1044+
`;
1045+
$a = $(".pat-inject");
10431046
});
10441047

10451048
afterEach(() => {
@@ -1128,6 +1131,7 @@ describe("pat-inject", function () {
11281131
});
11291132

11301133
it("Doesnt get error page from meta tags if query string present", async () => {
1134+
const _window_location = global.window.location;
11311135
delete global.window.location;
11321136
global.window.location = {
11331137
search: "?something=nothing&pat-inject-errorhandler.off",
@@ -1164,6 +1168,151 @@ describe("pat-inject", function () {
11641168
// In this case, the normal error reporting is used
11651169
expect(document.body.hasAttribute("data-error-message")).toBeTruthy();
11661170

1171+
global.fetch.mockClear();
1172+
delete global.fetch;
1173+
global.window.location = _window_location;
1174+
});
1175+
1176+
it("Injects an error message from the error response.", async () => {
1177+
// In this test the error message from the error reponse is used instead of the error template.
1178+
// No need to mock fetch which would get the error template.
1179+
1180+
// Configure fallback error page with a error zone selector #error-message
1181+
document.head.innerHTML = `
1182+
<meta name="pat-inject-status-404" content="/404.html#error-message" />
1183+
`;
1184+
1185+
// Add body with a error zone (#error-message)
1186+
document.body.innerHTML = `
1187+
<a class="pat-inject" href="test.html#someid">link</a>
1188+
<div id="error-message"></div>
1189+
`;
1190+
1191+
$a = $(".pat-inject");
1192+
1193+
pattern.init($a);
1194+
1195+
// Invoke error case
1196+
pattern._onInjectError($a, [], {
1197+
jqxhr: {
1198+
status: 404,
1199+
responseText: `
1200+
<!DOCTYPE html>
1201+
<html>
1202+
<head>
1203+
<title>404</title>
1204+
</head>
1205+
<body>
1206+
<section id="error-message">
1207+
<h1>oh no, what did you do?!</h1>
1208+
</section>
1209+
</body>
1210+
</html>
1211+
`,
1212+
},
1213+
});
1214+
await utils.timeout(1); // wait a tick for async to settle.
1215+
1216+
expect(document.querySelector("#error-message").innerHTML.trim()).toEqual(
1217+
"<h1>oh no, what did you do?!</h1>"
1218+
);
1219+
});
1220+
1221+
it("Injects an error message from the error template.", async () => {
1222+
// Let the error response contain a error zone section with an ID as configured further below.
1223+
global.fetch = jest.fn().mockImplementation(
1224+
mockFetch(`
1225+
<!DOCTYPE html>
1226+
<html>
1227+
<head>
1228+
<title>404</title>
1229+
</head>
1230+
<body>
1231+
<section id="error-message">
1232+
<h1>this is a message from your operator.</h1>
1233+
</section>
1234+
</body>
1235+
</html>
1236+
`)
1237+
);
1238+
1239+
// Configure fallback error page with a error zone selector #error-message
1240+
document.head.innerHTML = `
1241+
<meta name="pat-inject-status-404" content="/404.html#error-message"/>
1242+
`;
1243+
1244+
// Add body with a error zone (#error-message)
1245+
document.body.innerHTML = `
1246+
<a class="pat-inject" href="test.html#someid">link</a>
1247+
<div id="error-message"></div>
1248+
`;
1249+
1250+
$a = $(".pat-inject");
1251+
1252+
pattern.init($a);
1253+
1254+
// Invoke error case
1255+
pattern._onInjectError($a, [], {
1256+
jqxhr: {
1257+
status: 404,
1258+
responseText: `
1259+
<!DOCTYPE html>
1260+
<html>
1261+
<head>
1262+
<title>404</title>
1263+
</head>
1264+
<body>
1265+
<section id="error-message-not-to-be-found">
1266+
<h1>oh no, what did you do?!</h1>
1267+
</section>
1268+
</body>
1269+
</html>
1270+
`,
1271+
},
1272+
});
1273+
await utils.timeout(1); // wait a tick for async to settle.
1274+
1275+
expect(document.querySelector("#error-message").innerHTML.trim()).toEqual(
1276+
"<h1>this is a message from your operator.</h1>"
1277+
);
1278+
1279+
global.fetch.mockClear();
1280+
delete global.fetch;
1281+
});
1282+
1283+
it("Falls back to data-error-message attribute if no error page can be found.", async () => {
1284+
global.fetch = jest.fn().mockImplementation(mockFetch(""));
1285+
1286+
// Configure fallback error page with a error zone selector #error-message
1287+
document.head.innerHTML = `
1288+
<meta name="pat-inject-status-404" content="/404.html#error-message"/>
1289+
`;
1290+
1291+
// Add body with a error zone (#error-message)
1292+
document.body.innerHTML = `
1293+
<a class="pat-inject" href="test.html#someid">link</a>
1294+
<div id="error-message"></div>
1295+
`;
1296+
1297+
$a = $(".pat-inject");
1298+
1299+
pattern.init($a);
1300+
1301+
// Invoke error case
1302+
pattern._onInjectError($a, [], {
1303+
jqxhr: {
1304+
status: 404,
1305+
responseText: "",
1306+
},
1307+
});
1308+
await utils.timeout(1); // wait a tick for async to settle.
1309+
1310+
expect(document.querySelector("#error-message").innerHTML.trim()).toEqual(
1311+
""
1312+
);
1313+
1314+
expect(document.body.hasAttribute("data-error-message")).toBeTruthy();
1315+
11671316
global.fetch.mockClear();
11681317
delete global.fetch;
11691318
});

0 commit comments

Comments
 (0)