-
Notifications
You must be signed in to change notification settings - Fork 2.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Don't cache HTTP errors in the module map #10327
base: main
Are you sure you want to change the base?
Don't cache HTTP errors in the module map #10327
Conversation
e7b9493
to
b94700d
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good to me modulo nits.
cc @whatwg/modules
Multiple parallel imports will still only fetch once, and if the fetch fails, will both error. A subsequent import (dynamic import, a dynamically inserted via `<script src="..." type="module">`, or a static import from either of these) to the same specifier will result in a re-fetch if the previous fetch failed.
b94700d
to
728c268
Compare
source
Outdated
<li><p>If <var>moduleMap</var>[(<var>url</var>, <var>moduleType</var>)] <span | ||
data-x="map exists">exists</span>, run <var>onComplete</var> given | ||
<var>moduleMap</var>[(<var>url</var>, <var>moduleType</var>)], and return.</p></li> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need to handle the case in which moduleMap[(url, moduleType)]
exists and is "fetching"
, since we should not pass "fetching"
to onComplete
.
Test case:
await Promise.all([
/* 1 */ import("./network-error.js").catch(() =>
/* 3 */ import("./network-error.js").catch(() => {})
),
/* 2 */ import("./network-error.js").catch(() => {})
]);
This is what I think is happening (but please somebody with better understanding of event loops confirm):
/* 1 */
setsmoduleMap["./network-error.js"]
to"fetching"
, and performs the fetch. When that fetch is done, a task T1 is scheduled to the networking task source./* 2 */
sees thatmoduleMap["./network-error.js"]
is"fetching"
, and waits.- At some point the event loop runs T1, which will run the
processResponseConsumeBody
steps of fetch a single module script:- It changes
moduleMap[( url , moduleType )]
(it removes it), so a task T2 is scheduled to continue running/* 2 */
. - It runs
onComplete
withnull
, which will callFinishLoadingImportedModule
with an error completion in the last step ofHostLoadImportedModule
. - This will then reject the dynamic import promise in
ContinueDynamicImport
, thus queueing a microtask (in HostEnqueuePromiseJob) to run/* 1 */
's.catch()
callback - Once this is done, we drain the microtask queue and thus execute
/* 3 */
:moduleMap
doen't have a"./network-error.js"
entry anymore, so it setsmoduleMap["./network-error.js"]
to"fetching"
, and performs the fetch.
- It changes
- At this point we are done running T1, and we can start running T2.
moduleMap["./network-error.js"]
exists (and has value"fetching"
), so we runonComplete
with"fetching"
which is wrong.
Once this is fixed the code above should probably be used for a WPT test.
The current safari behavior is that in this case: import("./network-error.js");
import("./network-error.js"); after the first import fails, it performs a second fetch for the second one (rather than deduplicating them and immediately erroring for both.) And in this case: import("./network-error.js");
import("./network-error.js");
import("./network-error.js"); it performs three fetches serially. |
I have addressed @nicolo-ribaudo's review comment by removing the race condition between notification and read of the value in the module map. PTAL again @annevk. Note that the behaviour here is not identical to Safari in the case where you a parallel dynamic import for the same specifier. In this PR, both dynamic imports will always resolve / reject with the same value. For dynamic imports started at the same time, you can't resolve one, but reject another. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM, thanks!
One change introduced by this PR is that now this code: import("x").then(() => {
console.log(1);
Promise.resolve().then(() => console.log(2));
});
import("x").then(() => {
console.log(3);
Promise.resolve().then(() => console.log(4));
}); is guaranteed to log (on success) Previously, the possible behaviors where I think this change is ok/good, but if we want to revert it it can be done by scheduling tasks to call the various |
I'm working on getting a Chromium position on this. I am hopeful it will be positive. However, we'll likely need help with someone updating web platform tests, and adding new ones, including adding ones for the new scenario @nicolo-ribaudo points out, and for the differences between this and current Safari behavior. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
From an editor perspective, these changes LGTM, although I've also asked if we can get @hiroshige-g's time to review.
However, I'm unsure why we're counting MIME type mismatches as permanent. I agree they're less likely to be transient in practice, but it seems a bit strange to have one class of bad response treated so differently than all others.
I'm generally supportive of the proposal. I think this change would steer the module fetching behavior more toward availability (vs determinism which was prioritized when the feature was originally designed), but that seems well-aligned with the current web author needs. My only question is about the introduction of the callbacks. Would you mind elaborating on the context of the change (why is the old |
I think the PR works consistently (at least basically) with the high-level semantics of:
I haven't investigated the behavoir/impact on the Chromium implementation though, as the behavior/invariants in the current Chromium/V8 are different from the current spec. Specific cases and behaviors follows below (which is probably OK?). 1: never re-request a module within a single module graph
This mechanism doesn't match with the current Chromium implementation that doesn't early-exit on failure, but probably this is implementable. 2: MIGHT re-request submodules across module graphs
for: The
3: failure order not guaranteed
for:
4: consistency over timeI was concerned that re-requesting would break consistency/invariants and cause crashes by chaning module scripts from null to non-null over time (e.g. module graphs look different over time and causes obsolete/inconsistent pointer access and state transition). But so far this doesn't look the case and the PR works consistently:
(Similar consistency/invariant checks must be done in Chromium/V8 implementation side when implementing.) |
@domenic I have updated the change to also not cache of mime type errors. @nyaxt Thanks for the feedback. The change to callback was necessary to be able to accurately reason about the state of each invocation. If implemented without the callbacks, there was a race condition where between the "in parallel wait until change" and the actual read of the With the callback change, the new moduleMap value for this entry, for this onCompleted call is passed explicitly to the onCompleted callback - thus removing the race condition. @hiroshige-g Thanks for the elaborate notes. This matches my mental model. |
Multiple parallel imports will still only fetch once, and if the fetch
fails, will both error. A subsequent import (dynamic import, a
dynamically inserted via
<script src="..." type="module">
, or a staticimport from either of these) to the same specifier will result in a
re-fetch if the previous fetch failed.
Fixes #6768
(See WHATWG Working Mode: Changes for more details.)
/acknowledgements.html ( diff )
/webappapis.html ( diff )