Repository navigation
Abort the inflight fetch() when networkTimeoutSeconds is reached #3075
Description
Library Affected:
"workbox-background-sync": "^6.5.3",
"workbox-core": "^6.5.3",
"workbox-precaching": "^6.5.3",
"workbox-window": "^6.5.3"
Browser & Platform:
all browsers
Issue or Feature Request Description:
I have noticed 2 issues with handlerDidError callback.
Base code:
import { Queue } from "workbox-background-sync";
import { Strategy, NetworkOnly } from "workbox-strategies";
const queue = new Queue("myQueueName", {
onSync: async ({ queue }) => {
let entry;
while ((entry = await queue.shiftRequest())) {
try {
var response = await fetch(entry.request.clone());
} catch (error) {
await queue.unshiftRequest(entry);
throw error;
}
}
},
});
const statusPlugin = {
handlerWillRespond: async ({ request, response, event, state }) => {
console.log("handlerWillRespond");
return response;
},
fetchDidFail: async ({ originalRequest, request, error, event }) => {
console.log("fetchDidFail");
//console.log(await originalRequest.clone().json()); // works
await queue.pushRequest({ request: originalRequest });
},
fetchDidSucceed: async ({ response }) => {
console.log("fetchDidSucceed");
//console.log(await response.clone().json()); // works
return response;
},
handlerDidError: async ({ error, event, request, state }) => {
console.log("handlerDidError");
//console.log(await request.clone().json()); // TypeError: Failed to execute 'clone' on 'Request': Request body is already used
},
};
registerRoute(
new RegExp(".*/Exercises/.*"),
new NetworkOnly({
networkTimeoutSeconds: 3,
plugins: [statusPlugin],
}),
"POST"
);Issue 1: Unable to read the request data
Following code:
handlerDidError: async ({ error, event, request, state }) => {
console.log("handlerDidError");
console.log(await request.clone().json()); // TypeError: Failed to execute 'clone' on 'Request': Request body is already used
},will throw:
TypeError: Failed to execute 'clone' on 'Request': Request body is already used
Issue 2: handlerDidError runs before fetchDidFail (subjective)
Current callback flow is:
- client makes a request
- network is unavailable
handlerDidErroris consoled- clients gets the response
fetchDidFailis consoled
I would expect all the pipelines related to errors to be executed before client sees the response.
Temporary solution
I have managed to fix both of the problems by using the custom Network strategy
class NewStrategy extends NetworkOnly {
async _handle(request, handler) {
return await handler.fetch(request.clone());
}
}
See that I have cloned the request before passing it to handler. This will allow us to clone the request later in handlerDidError callback. The second "issue" is also solved. Now the flow is:
- client makes a request
- network is unavailable
fetchDidFailis consoledhandlerDidErroris consoled- clients gets the response
See that the handlerDidError is actually run After the fetchDidFail. This makes sense because the handlerDidError is supposed to be the last logical thing in the pipeline: https://developer.chrome.com/docs/workbox/using-plugins/
handlerDidError: Called if the handler can't provide a valid response from from any source, which is the optimal time to provide some sort of fallback response as an alternative to failing outright.
// Returnresponse, a differentResponseobject as a fallback, ornull.
I might be wrong about the pipeline order thingy (it just looks more logical to me) but the "cloning" issue seems to be a valid one regardless.
Activity
jeffposnick commentedon May 2, 2022
Generally speaking,
fetchDidFailshould fire beforehandlerDidError. You can see this happen at, e.g., https://sudden-futuristic-health.glitch.me/ where there's a request that fails because the domain name resolution is unsuccessful.One exception, that you might be bumping up into, is when the overall strategy fails due to a timeout imposed by
networkTimeoutSeconds, without a valid cached response to fall back on. In that scenario, I do see howhandlerDidErrorwould end up firing first, before eitherfetchDidSucceedorfetchDidFail(depending on whether the underlying request, which is make independently from the timeout, ended up succeeding or not). The cleanest solution there might be to use abortablefetch(), which is now widely supported, and actually terminate the associatedfetch()whennetworkTimeoutSecondsis reached. At the same time, we should not trigger eitherfetchDidFailorfetchDidSucceedwhen that happens. Maybe there should be afetchDidAbortevent that gets triggered, though? We can leave this issue open to track that.Regarding the other issue, I would recommend working around that by saving a clone of the outgoing request in the
stateparameter that's passed to therequestWillFetchcallback, and then referencing it later on in, e.g.,handlerDidError. You're guaranteed to get the samestatevalue passed to all of the callbacks associated with the samefetchevent.[-]handlerDidError multiple issues[/-][+]Abort the inflight fetch() when networkTimeoutSeconds is reached[/+]pavlexander commentedon May 3, 2022
You are correct. I have just tested with and without
networkTimeoutSecondsparameter and thefetchDidFailis only triggered beforehandlerDidErrorwhen no value is set.. I would be very careful with abortable fetch option, given that it hasn't been around for long enough, unless there is a way to make it be backwards compatible with non-abortable fetch.. Otherwise the suggestion is nice.It works! Now I know what the state parameter could/should be used for :) I guess it's a nice workaround.. So is this correct that the body of the request is not accessible in the
handlerDidErrorcallback? Personally I have no problems with that, given that we could use the state to pass the data around across the pipeline.Thank you for your reply.
jeffposnick commentedon May 3, 2022
I've filed GoogleChrome/developer.chrome.com#2751 to move that context about using
stateto store a clonedrequestto our official docs.tomayac commentedon Apr 25, 2024
Hi there,
Workbox is moving to a new engineering team within Google. As part of this move, we're declaring a partial bug bankruptcy to allow the new team to start fresh. We realize this isn't optimal, but realistically, this is the only way we see it working. For transparency, here're the criteria we applied:
v7branch.Thanks, and we hope for your understanding!
The Workbox team