Porównaj commity

...

4 Commity

Autor SHA1 Wiadomość Data
Michał 'rysiek' Woźniak 562a62df11 documentation updated (ref. #36) 2024-03-13 04:07:29 +00:00
Michał 'rysiek' Woźniak c573810762 additional test for activate event handling 2024-03-13 03:35:19 +00:00
Michał 'rysiek' Woźniak 935b9c27ac additional tests for handling HTTP errors from plugins (ref. #36) 2024-03-13 03:20:10 +00:00
Michał 'rysiek' Woźniak e4db403d62 additional tests for handling errors/rejections in plugins (ref. #36) 2024-03-13 02:48:29 +00:00
2 zmienionych plików z 384 dodań i 2 usunięć

Wyświetl plik

@ -232,9 +232,9 @@ beforeAll(async ()=>{
postMessage: window.clients.prototypePostMessage
}
},
claim: async () => {
claim: spy(async () => {
return undefined
},
}),
// the actual spy function must be possible to reference
// but we want spy data per test, so we set it properly in beforeEach()
prototypePostMessage: null
@ -443,6 +443,14 @@ describe('service-worker', async () => {
window.test_id = 0
it("should call clients.claim() when activated", async () => {
await import("../../service-worker.js?" + window.test_id);
await self.dispatchEvent(new Event('install'))
await self.waitForSWInstall()
await self.dispatchEvent(new Event('activate'))
assertSpyCalls(window.clients.claim, 1)
})
it("should use default LibResilientConfig values when config.json is missing", async () => {
let mock_response_data = {
@ -1592,6 +1600,246 @@ describe('service-worker', async () => {
assertEquals(await response.json(), { test: "success" })
});
it("should use return a 4xx error directly from the last plugin, regardless of previous plugin errors or rejection", async () => {
window.LibResilientConfig = {
plugins: [{
name: 'reject-all'
},{
name: 'error-out'
},{
name: 'return-418'
}],
loggedComponents: [
'service-worker'
]
}
let rejectingFetch = spy(
(request, init)=>{ return Promise.reject('reject-all rejecting a request for: ' + request); }
)
window.LibResilientPluginConstructors.set('reject-all', ()=>{
return {
name: 'reject-all',
description: 'Reject all requests.',
version: '0.0.1',
fetch: rejectingFetch
}
})
let throwingFetch = spy(
(request, init)=>{ throw new Error('error-out throwing an Error for: ' + request); }
)
window.LibResilientPluginConstructors.set('error-out', ()=>{
return {
name: 'error-out',
description: 'Throws.',
version: '0.0.1',
fetch: throwingFetch
}
})
let mock_response_data = {
data: JSON.stringify({text: "success"}),
status: 418,
statusText: "Im A Teapot"
}
window.fetch = spy(window.getMockedFetch(mock_response_data))
window.LibResilientPluginConstructors.set('return-418', ()=>{
return {
name: 'return-418',
description: 'Return 418 HTTP Error.',
version: '0.0.1',
fetch: fetch
}
})
await import("../../service-worker.js?" + window.test_id);
await self.dispatchEvent(new Event('install'))
await self.waitForSWInstall()
let fetch_event = new FetchEvent('test.json')
window.dispatchEvent(fetch_event)
let response = await fetch_event.waitForResponse()
assertSpyCalls(window.fetch, 2); // two, because the first one is for config.json
assertSpyCalls(rejectingFetch, 1);
assertSpyCalls(throwingFetch, 1);
assertSpyCall(window.fetch, 1, { args: [
"https://test.resilient.is/test.json",
{
cache: undefined,
integrity: undefined,
method: "GET",
redirect: "follow",
referrer: undefined,
}]
})
assertEquals(response.status, 418)
assertEquals(response.statusText, 'Im A Teapot')
assertEquals(await response.json(), { text: "success" })
});
it("should use return a 4xx error directly from a plugin, regardless of any following plugins", async () => {
window.LibResilientConfig = {
plugins: [{
name: 'return-418'
},{
name: 'reject-all'
},{
name: 'error-out'
}],
loggedComponents: [
'service-worker'
]
}
let rejectingFetch = spy(
(request, init)=>{ return Promise.reject('reject-all rejecting a request for: ' + request); }
)
window.LibResilientPluginConstructors.set('reject-all', ()=>{
return {
name: 'reject-all',
description: 'Reject all requests.',
version: '0.0.1',
fetch: rejectingFetch
}
})
let throwingFetch = spy(
(request, init)=>{ throw new Error('error-out throwing an Error for: ' + request); }
)
window.LibResilientPluginConstructors.set('error-out', ()=>{
return {
name: 'error-out',
description: 'Throws.',
version: '0.0.1',
fetch: throwingFetch
}
})
let mock_response_data = {
data: JSON.stringify({text: "success"}),
status: 418,
statusText: "Im A Teapot"
}
window.fetch = spy(window.getMockedFetch(mock_response_data))
window.LibResilientPluginConstructors.set('return-418', ()=>{
return {
name: 'return-418',
description: 'Return 418 HTTP Error.',
version: '0.0.1',
fetch: fetch
}
})
await import("../../service-worker.js?" + window.test_id);
await self.dispatchEvent(new Event('install'))
await self.waitForSWInstall()
let fetch_event = new FetchEvent('test.json')
window.dispatchEvent(fetch_event)
let response = await fetch_event.waitForResponse()
assertSpyCalls(window.fetch, 2); // two, because the first one is for config.json
assertSpyCalls(rejectingFetch, 0);
assertSpyCalls(throwingFetch, 0);
assertSpyCall(window.fetch, 1, { args: [
"https://test.resilient.is/test.json",
{
cache: undefined,
integrity: undefined,
method: "GET",
redirect: "follow",
referrer: undefined,
}]
})
assertEquals(response.status, 418)
assertEquals(response.statusText, 'Im A Teapot')
assertEquals(await response.json(), { text: "success" })
});
it("should use treat a 5xx error from a plugin as internal error and try following plugins", async () => {
window.LibResilientConfig = {
plugins: [{
name: 'return-500'
},{
name: 'reject-all'
},{
name: 'error-out'
}],
loggedComponents: [
'service-worker'
]
}
let rejectingFetch = spy(
(request, init)=>{ return Promise.reject('reject-all rejecting a request for: ' + request); }
)
window.LibResilientPluginConstructors.set('reject-all', ()=>{
return {
name: 'reject-all',
description: 'Reject all requests.',
version: '0.0.1',
fetch: rejectingFetch
}
})
let throwingFetch = spy(
(request, init)=>{ throw new Error('error-out throwing an Error for: ' + request); }
)
window.LibResilientPluginConstructors.set('error-out', ()=>{
return {
name: 'error-out',
description: 'Throws.',
version: '0.0.1',
fetch: throwingFetch
}
})
let mock_response_data = {
data: JSON.stringify({text: "success"}),
status: 500,
statusText: "Internal Server Error"
}
window.fetch = spy(window.getMockedFetch(mock_response_data))
window.LibResilientPluginConstructors.set('return-500', ()=>{
return {
name: 'return-500',
description: 'Return 500 HTTP Error.',
version: '0.0.1',
fetch: fetch
}
})
await import("../../service-worker.js?" + window.test_id);
await self.dispatchEvent(new Event('install'))
await self.waitForSWInstall()
let fetch_event = new FetchEvent('test.json')
window.dispatchEvent(fetch_event)
let response = fetch_event.waitForResponse()
assertRejects( async () => {
return await response
})
// wait for the response to resolve
await response.catch((e)=>{})
assertSpyCalls(window.fetch, 2);
assertSpyCalls(rejectingFetch, 1);
assertSpyCalls(throwingFetch, 1);
});
it("should normalize query params in requested URLs by default", async () => {
console.log(self.LibResilientConfig)
@ -2882,4 +3130,110 @@ describe('service-worker', async () => {
assertEquals(await response.json(), { test: "success" })
})
it("should return a 404 Not Found HTTP response object with an error screen when handling a rejected navigation request", async () => {
window.LibResilientConfig = {
plugins: [{
name: 'reject-all'
}],
loggedComponents: [
'service-worker'
]
}
let rejectingFetch = spy(
(request, init)=>{ return Promise.reject('reject-all rejecting a request for: ' + request); }
)
window.LibResilientPluginConstructors.set('reject-all', ()=>{
return {
name: 'reject-all',
description: 'Reject all requests.',
version: '0.0.1',
fetch: rejectingFetch
}
})
await import("../../service-worker.js?" + window.test_id);
await self.dispatchEvent(new Event('install'))
await self.waitForSWInstall()
let fetch_event = new FetchEvent(window.location.origin + 'test.json', {mode: "navigate"})
window.dispatchEvent(fetch_event)
let response = await fetch_event.waitForResponse()
assertEquals(response.status, 404)
assertEquals(response.statusText, 'Not Found')
assertEquals(response.headers.get('content-type'), 'text/html')
assertEquals((await response.text()).slice(0, 57), '<!DOCTYPE html><html><head><title>Loading failed.</title>')
})
it("should not return a 404 Not Found HTTP response object with an error screen when handling a rejected non-navigation request", async () => {
window.LibResilientConfig = {
plugins: [{
name: 'reject-all'
}],
loggedComponents: [
'service-worker'
]
}
let rejectingFetch = spy(
(request, init)=>{ return Promise.reject('reject-all rejecting a request for: ' + request); }
)
window.LibResilientPluginConstructors.set('reject-all', ()=>{
return {
name: 'reject-all',
description: 'Reject all requests.',
version: '0.0.1',
fetch: rejectingFetch
}
})
await import("../../service-worker.js?" + window.test_id);
await self.dispatchEvent(new Event('install'))
await self.waitForSWInstall()
let fetch_event = new FetchEvent(window.location.origin + 'test.json')
window.dispatchEvent(fetch_event)
assertRejects(async ()=>{ await fetch_event.waitForResponse() })
})
it("should not return a 404 Not Found HTTP response object with an error screen when handling a non-navigation request that throws an error", async () => {
window.LibResilientConfig = {
plugins: [{
name: 'error-out'
}],
loggedComponents: [
'service-worker'
]
}
let throwingFetch = spy(
(request, init)=>{ throw new Error('error-out throwing an Error for: ' + request); }
)
window.LibResilientPluginConstructors.set('error-out', ()=>{
return {
name: 'error-out',
description: 'Throws.',
version: '0.0.1',
fetch: throwingFetch
}
})
await import("../../service-worker.js?" + window.test_id);
await self.dispatchEvent(new Event('install'))
await self.waitForSWInstall()
let fetch_event = new FetchEvent(window.location.origin + 'test.json')
window.dispatchEvent(fetch_event)
assertRejects(async ()=>{
await fetch_event.waitForResponse()
},
Error,
'error-out throwing an Error for: https://test.resilient.is/test.json'
)
})
})

Wyświetl plik

@ -219,3 +219,31 @@ The still-loading screen is *only* displayed when *all* of these conditions are
The reason why a stashing plugin needs to be configured and enabled is to avoid loops. Consider a scenario, where a visitor is navigating to a page, and the request is taking very long. The still-loading screen is displayed (by way of the service worker returning the relevant HTML in response to the request). Eventually, the request completes in the background, but the response is discarded due to lack of a stashing plugin.
In such a case reloading the page will cause a repeat: request, still-loading screen, request completes in the background (and the result is discarded). The visitor would be stuck in a loop. If a stashing plugin (like `cache`) is enabled, this loop can be expected not to emerge, since the second request would quickly return the cached response.
## Error handling
LibResilient's error handling focuses on attempting to "do the right thing". In some cases this means passing a HTTP error response from a plugin directly to the browser to be displayed to the user; in other cases it means ignoring HTTP error response from a plugin so that other plugins can attempt to retrieve a given resource.
In general:
- **A response with status code value of `499` or lower is passed immediately to the browser**
no other plugins are used to try to retrieve the content; if a stashing plugin is configured, the response might be cached locally.
- **A response with status code value of `500` or higher is treated as a plugin error**
if other plugins are configured, they will be used to try to retrieve the content; if a stashing plugin is configured it will not stash that response.
- **Any exception thrown in a plugin will be caught and treated as a plugin error**
if other plugins are configured, they will be used to try to retrieve the content; there is no response object, so there is nothing to stash.
- **If a plugin rejects for whatever reason, it is treated as a plugin error**
if other plugins are configured, they will be used to try to retrieve the content; there is no response object, so there is nothing to stash.
All plugin errors (`5xx` HTTP responses, thrown exceptions, rejections) are logged internally. This data is printed in the console (if `loggedComponents` config field contains `service-worker`), and sent to the client using `Client.postMessage()`, to simplify debugging.
If all plugins fail in case of a navigate request, a Request object is created with a `404 Not Found` HTTP status, containing a simple HTML error page (similar to the still-loading screen mentioned above) to be displayed to the user. If the request is not a navigate request, the rejected promise is returned directly.
Mapping plugin errors onto HTTP errors is not always going to be trivial. For example, an IPFS-based transport plugin could in some circumstances return a `404 Not Found` HTTP error, but the `any-of` plugin necessarily has to ignore any HTTP errors it receives from plugins it is configured to use, while waiting for one to potentially return the resource successfully. If all of the configured plugins fail, with different HTTP errors, which one should the `any-of` plugin return itself?..
At the same time, returning HTTP errors makes sense, as it allows the browser and the user to properly interpret well-understood errors. So, the `fetch` plugin will return any `4xx` HTTP error it receives, for example, and the service worker will in turn treat that as a successfully completed retrieval and return that to the browser to be displayed to the user.
Plugin authors should consider this carefully. If in doubt, it's probably better to throw an exception or reject the promise with a meaningful error message, than to try to fit a potentially complex failure mode into the limited and rigit contraints of HTTP error codes.