Skip to content

Implement document preloaded resources#39794

Merged
TimvdLippe merged 1 commit intoservo:mainfrom
TimvdLippe:implement-document-preloaded-resources
Oct 20, 2025
Merged

Implement document preloaded resources#39794
TimvdLippe merged 1 commit intoservo:mainfrom
TimvdLippe:implement-document-preloaded-resources

Conversation

@TimvdLippe
Copy link
Copy Markdown
Contributor

@TimvdLippe TimvdLippe commented Oct 11, 2025

This aligns the request object more with the specification,
since the spec now has a traversable_for_user_prompts and
a separate field for the client. Before, they were present
in the same enum.

In doing so, new structs are added that are all required in
the new spec. With this we can add support for preloaded
resources in this client, which are only populated when
we have an applicable Global.

Since the spec moved things around a bit, it now has a
dedicated method to populate the client from the request.

Unfortunately none of the WPT preload tests pass, since
the requests are received out-of-order. The specification
requires us to wait for that to settle, but I haven't figured
out yet how to do that. Given that this PR is already quite
large, opted to do that in a follow-up.

Part of #35035

@TimvdLippe TimvdLippe force-pushed the implement-document-preloaded-resources branch 2 times, most recently from dc571ee to 219f724 Compare October 13, 2025 10:55
@TimvdLippe TimvdLippe added the T-linux-wpt Do a try run of the WPT label Oct 13, 2025
@github-actions github-actions bot removed the T-linux-wpt Do a try run of the WPT label Oct 13, 2025
@github-actions
Copy link
Copy Markdown

🔨 Triggering try run (#18463506639) for Linux (WPT)

@github-actions
Copy link
Copy Markdown

Test results for linux-wpt from try job (#18463506639):

Flaky unexpected result (34)
  • OK /FileAPI/url/url-with-fetch.any.worker.html (#21517)
    • FAIL [expected PASS] subtest: Revoke blob URL after calling fetch, fetch should succeed

      promise_test: Unhandled rejection with value: object "TypeError: Network error occurred"
      

  • OK /IndexedDB/idbfactory_open.any.html
    • FAIL [expected PASS] subtest: Calling open() with version argument 1.5 should not throw.

      assert_equals: version expected 1 but got 9007199254740991
      

  • FAIL [expected PASS] /_mozilla/css/iframe/hide_and_show.html (#15265)
  • TIMEOUT [expected PASS] /_mozilla/shadow-dom/move-element-with-ua-shadow-tree-crash.html (#39473)
  • OK /content-security-policy/frame-ancestors/frame-ancestors-path-ignored.window.html (#36468)
    • PASS [expected FAIL] subtest: A 'frame-ancestors' CSP directive with a URL that includes a path should be ignored.
  • FAIL [expected PASS] /css/CSS2/tables/table-height-algorithm-008b.xht
  • FAIL [expected PASS] /css/WOFF2/tabledata-decompressed-length-004.xht
  • OK /css/css-animations/event-order.tentative.html (#39000)
    • PASS [expected FAIL] subtest: Same events on pseudo-elements follow the prescribed order
  • PASS [expected FAIL] /css/css-fonts/font-palette-18.html (#39630)
  • FAIL [expected PASS] /css/css-fonts/font-palette.html
  • FAIL [expected PASS] /css/css-fonts/font-variation-settings-descriptor-03.html
  • PASS [expected FAIL] /css/css-fonts/system-ui-ja-vs-zh.html (#39601)
  • PASS [expected FAIL] /css/css-fonts/system-ui-ja.html (#39685)
  • PASS [expected FAIL] /css/css-grid/grid-items/grid-auto-margin-and-replaced-item-001.html (#37162)
  • FAIL [expected PASS] /css/css-sizing/intrinsic-percent-non-replaced-001.html
  • OK /fetch/content-length/api-and-duplicate-headers.any.html (#35873)
    • FAIL [expected PASS] subtest: fetch() and duplicate Content-Length/Content-Type headers

      promise_test: Unhandled rejection with value: object "TypeError: Network error occurred"
      

  • OK /html/browsers/browsing-the-web/navigating-across-documents/009.html (#24456)
    • PASS [expected FAIL] subtest: Link with onclick form submit to javascript url with document.write and href navigation
  • OK /html/browsers/browsing-the-web/navigating-across-documents/initial-empty-document/iframe-src-aboutblank-navigate-immediately.html (#29048)
    • FAIL [expected PASS] subtest: Navigating to a different document with location.href

      assert_equals: expected "http://web-platform.test:8000/common/blank.html?1" but got "about:blank"
      

    • FAIL [expected PASS] subtest: Navigating to a different document with location.assign

      assert_equals: expected "http://web-platform.test:8000/common/blank.html?1" but got "about:blank"
      

  • OK /html/browsers/browsing-the-web/navigating-across-documents/navigation-unload-same-origin.window.html (#29049)
    • PASS [expected FAIL] subtest: Same-origin navigation started from unload handler must be ignored
  • OK /html/browsers/browsing-the-web/navigating-across-documents/refresh/same-document-refresh.html (#34597)
    • FAIL [expected PASS] subtest: Same-Document Referrer from Refresh

      assert_equals: original page loads expected "http://web-platform.test:8000/html/browsers/browsing-the-web/navigating-across-documents/refresh/resources/refresh-with-section.sub.html?url=%23section" but got "http://web-platform.test:8000/html/browsers/browsing-the-web/navigating-across-documents/refresh/resources/refresh-with-section.sub.html?url=%23section#section"
      

  • OK /html/browsers/history/the-history-interface/traverse_the_history_4.html (#21383)
    • PASS [expected FAIL] subtest: Multiple history traversals, last would be aborted
  • OK /html/browsers/history/the-history-interface/traverse_the_history_5.html (#21383)
    • PASS [expected FAIL] subtest: Multiple history traversals, last would be aborted
  • TIMEOUT [expected OK] /html/interaction/focus/the-autofocus-attribute/autofocus-dialog.html (#29087)
    • TIMEOUT [expected FAIL] subtest: <dialog>-contained autofocus element gets focused when the dialog is shown

      Test timed out
      

  • TIMEOUT [expected OK] /html/interaction/focus/the-autofocus-attribute/update-the-rendering.html (#24145)
    • TIMEOUT [expected FAIL] subtest: "Flush autofocus candidates" should be happen before a scroll event and animation frame callbacks

      Test timed out
      

  • OK /html/semantics/embedded-content/the-iframe-element/iframe-loading-lazy-nav-location-assign.html (#32863)
    • FAIL [expected PASS] subtest: Navigating iframe loading='lazy' before it is loaded: location.assign

      uncaught exception: Error: assert_equals: expected "http://web-platform.test:8000/html/semantics/embedded-content/the-iframe-element/support/blank.htm?nav" but got "http://web-platform.test:8000/html/semantics/embedded-content/the-iframe-element/support/blank.htm?src"
      

  • CRASH [expected OK] /html/semantics/embedded-content/the-iframe-element/iframe_sandbox_popups_nonescaping-1.html (#24066)
  • OK [expected CRASH] /html/semantics/embedded-content/the-iframe-element/iframe_sandbox_popups_nonescaping-3.html (#24066)
  • OK /html/semantics/forms/form-submission-0/multipart-formdata.window.html (#28725)
    • PASS [expected FAIL] subtest: multipart/form-data: Basic test (normal form)
  • OK /html/webappapis/user-prompts/print-during-unload.html (#35944)
    • PASS [expected FAIL] subtest: print() during unload
  • OK /preload/prefetch-document.html (#37210)
    • FAIL [expected PASS] subtest: different-site document prefetch with 'as=document' should not be consumed

      assert_equals: expected 2 but got 1
      

  • OK /preload/preload-invalid-resources.html (#39091)
    • PASS [expected FAIL] subtest: Preloading an invalid image (missing) should preload and not re-fetch
  • TIMEOUT [expected OK] /resource-timing/nested-context-navigations-iframe.html (#24311)
    • TIMEOUT [expected PASS] subtest: Test that crossorigin iframe navigations are not observable by the parent, even after history navigations by the parent

      Test timed out
      

    • NOTRUN [expected PASS] subtest: Test that cross-site iframe navigations are not observable by the parent, even after history navigations by the parent
    • NOTRUN [expected PASS] subtest: Test that iframe navigations are not observable by the parent
    • NOTRUN [expected PASS] subtest: Test that crossorigin iframe navigations are not observable by the parent
    • NOTRUN [expected PASS] subtest: Test that cross-site iframe navigations are not observable by the parent
    • NOTRUN [expected PASS] subtest: Test that iframe refreshes are not observable by the parent
    • NOTRUN [expected PASS] subtest: Test that crossorigin iframe refreshes are not observable by the parent
    • NOTRUN [expected PASS] subtest: Test that cross-site iframe refreshes are not observable by the parent
  • ERROR /service-workers/idlharness.https.any.html (#36250)
    • PASS [expected TIMEOUT] subtest: ServiceWorkerContainer interface: operation register((TrustedScriptURL or USVString), optional RegistrationOptions)
    • PASS [expected TIMEOUT] subtest: NavigationPreloadManager interface: operation enable()
    • PASS [expected TIMEOUT] subtest: NavigationPreloadManager interface: operation disable()
    • PASS [expected TIMEOUT] subtest: NavigationPreloadManager interface: operation setHeaderValue(ByteString)
    • PASS [expected TIMEOUT] subtest: NavigationPreloadManager interface: operation getState()
  • TIMEOUT [expected OK] /webstorage/localstorage-about-blank-3P-iframe-opens-3P-window.partitioned.html (#29053)
    • TIMEOUT [expected PASS] subtest: StorageKey: test 3P about:blank window opened from a 3P iframe

      Test timed out
      

Stable unexpected results that are known to be intermittent (26)
  • OK /IndexedDB/idbcursor-continuePrimaryKey-exceptions.any.worker.html (#39277)
    • FAIL [expected PASS] subtest: IDBCursor continuePrimaryKey() on object store cursor

      assert_throws_dom: continuePrimaryKey() should throw if source is not an index function "function() {
              cursor.continuePrimaryKey(2, 2);
            }" threw object "TypeError: cursor.continuePrimaryKey is not a function" that is not a DOMException InvalidAccessError: property "code" is equal to undefined, expected 15
      

  • OK /IndexedDB/idbobjectstore_getAll.any.html (#39276)
    • PASS [expected FAIL] subtest: Get all values with transaction.commit()
  • OK /IndexedDB/idbobjectstore_getAll.any.worker.html (#39400)
    • PASS [expected FAIL] subtest: Get all values with transaction.commit()
  • OK /IndexedDB/key-conversion-exceptions.any.html (#39305)
    • FAIL [expected PASS] subtest: IDBCursor continue() method with throwing/invalid keys

      assert_throws_exactly: key conversion with throwing getter should rethrow function "() => {
            receiver[method](key);
          }" threw object "TypeError: receiver[method] is not a function" but we expected it to throw object "getter: throwing from getter"
      

  • FAIL [expected PASS] /_mozilla/mozilla/sslfail.html (#10760)
  • TIMEOUT [expected OK] /_mozilla/mozilla/window_resize_event.html (#36741)
    • TIMEOUT [expected PASS] subtest: Popup onresize event fires after resizeTo

      Test timed out
      

  • OK /css/css-cascade/layer-font-face-override.html (#35935)
    • FAIL [expected PASS] subtest: @font-face override update with appended sheet 1

      assert_equals: expected "80px" but got "38.3166666666667px"
      

    • FAIL [expected PASS] subtest: @font-face override update with appended sheet 2

      assert_equals: expected "80px" but got "38.3166666666667px"
      

  • OK /custom-elements/form-associated/form-disabled-callback.html (#38843)
    • PASS [expected FAIL] subtest: A disabled form-associated custom element should not submit an entry for it
  • TIMEOUT [expected FAIL] /dom/xslt/large-cdata.html (#38029)
  • OK /fetch/metadata/generated/css-font-face.https.sub.tentative.html (#32732)
    • PASS [expected FAIL] subtest: sec-fetch-site - Same origin
  • OK /fetch/metadata/generated/css-font-face.sub.tentative.html (#34624)
    • PASS [expected FAIL] subtest: sec-fetch-storage-access - Not sent to non-trustworthy same-site destination
  • ERROR /fetch/metadata/generated/serviceworker.https.sub.html (#36247)
    • FAIL [expected PASS] subtest: sec-fetch-site - Same origin, no options - registration

      promise_test: Unhandled rejection with value: object "Error: Failed to query for recorded headers."
      

  • OK /html/browsers/browsing-the-web/navigating-across-documents/008.html (#24456)
    • FAIL [expected PASS] subtest: Link with onclick form submit to javascript url and href navigation

      assert_equals: expected "href" but got "click"
      

  • OK /html/browsers/browsing-the-web/navigating-across-documents/empty-iframe-load-event.html (#29066)
    • PASS [expected FAIL] subtest: Check execution order on load handler
    • PASS [expected FAIL] subtest: Check execution order from nested timeout
  • OK /html/browsers/browsing-the-web/navigating-across-documents/navigation-unload-same-origin-fragment.html (#20768)
    • FAIL [expected PASS] subtest: Tests that a fragment navigation in the unload handler will not block the initial navigation

      assert_equals: expected "" but got "#fragment"
      

  • OK /html/browsers/windows/embedded-opener-remove-frame.html (#23867)
    • FAIL [expected PASS] subtest: opener of discarded auxiliary browsing context

      assert_object_equals: property "get" expected function "function opener() {
          [native code]
      }" got function "function opener() {
          [native code]
      }"
      

  • OK [expected TIMEOUT] /html/interaction/focus/the-autofocus-attribute/supported-elements.html (#24145)
    • PASS [expected NOTRUN] subtest: Non-HTMLElement should not support autofocus
    • FAIL [expected NOTRUN] subtest: Host element with delegatesFocus should support autofocus

      assert_equals: expected Element node <div autofocus=""></div> but got Element node <body><div autofocus=""></div></body>
      

    • FAIL [expected NOTRUN] subtest: Host element with delegatesFocus including no focusable descendants should be skipped

      assert_equals: expected Element node <input autofocus=""></input> but got Element node <body><div autofocus=""></div><input autofocus=""></body>
      

    • FAIL [expected NOTRUN] subtest: Area element should support autofocus

      promise_test: Unhandled rejection with value: object "TypeError: can't access property "appendChild", w.document.querySelector(...) is null"
      

  • CRASH [expected OK] /html/semantics/embedded-content/the-iframe-element/iframe_sandbox_popups_escaping-2.html (#22667)
  • OK [expected CRASH] /html/semantics/embedded-content/the-iframe-element/iframe_sandbox_popups_escaping-3.html (#24057)
  • OK /html/semantics/forms/form-submission-0/form-submit-iframe-then-location-navigate.html (#29634)
    • PASS [expected FAIL] subtest: Verifies that location navigations take precedence when following form submissions.
  • OK [expected CRASH] /html/semantics/forms/the-fieldset-element/disabled-003.html (#31730, #39631)
  • OK /navigation-timing/test-navigation-type-reload.html (#33334)
    • FAIL [expected PASS] subtest: Reload domComplete > Original domComplete

      assert_true: Reload domComplete > Original domComplete expected true got false
      

    • FAIL [expected PASS] subtest: Reload domContentLoadedEventEnd > Original domContentLoadedEventEnd

      assert_true: Reload domContentLoadedEventEnd > Original domContentLoadedEventEnd expected true got false
      

    • FAIL [expected PASS] subtest: Reload loadEventEnd > Original loadEventEnd

      assert_true: Reload loadEventEnd > Original loadEventEnd expected true got false
      

    • FAIL [expected PASS] subtest: Reload loadEventStart > Original loadEventStart

      assert_true: Reload loadEventStart > Original loadEventStart expected true got false
      

  • OK /preload/preload-error.sub.html (#37177)
    • FAIL [expected PASS] subtest: success (fetch): main

      assert_greater_than: http://web-platform.test:8000/preload/resources/dummy.xml?label=fetch should be loaded expected a number greater than 0 but got 0
      

    • FAIL [expected PASS] subtest: 404 (fetch): main

      assert_greater_than: http://web-platform.test:8000/preload/resources/dummy.xml?pipe=status%28404%29&label=fetch should be loaded expected a number greater than 0 but got 0
      

    • PASS [expected FAIL] subtest: CORS (fetch): main
  • OK /trusted-types/trusted-types-navigation.html?01-05 (#38975)
    • PASS [expected FAIL] subtest: Navigate a window via anchor with javascript:-urls in enforcing mode.
  • OK /trusted-types/trusted-types-navigation.html?26-30 (#38807)
    • FAIL [expected PASS] subtest: Navigate a window via form-submission with javascript:-urls in report-only mode.

      promise_test: Unhandled rejection with value: "Unexpected message received: \"No securitypolicyviolation reported!\""
      

  • OK [expected ERROR] /webxr/render_state_update.https.html (#27535)

@github-actions
Copy link
Copy Markdown

✨ Try run (#18463506639) succeeded.

@TimvdLippe TimvdLippe force-pushed the implement-document-preloaded-resources branch from 219f724 to 0a59149 Compare October 13, 2025 12:55
@TimvdLippe TimvdLippe marked this pull request as ready for review October 13, 2025 12:55
@servo-highfive servo-highfive added the S-awaiting-review There is new code that needs to be reviewed. label Oct 13, 2025
Comment on lines +435 to +438
// Step 1. Wait until fetchParams’s preloaded response candidate is not "pending".
// TODO
// Step 2. Assert: fetchParams’s preloaded response candidate is a response.
// TODO
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reason WPT isn't passing more tests is because of this here. I don't know how we can change the structs to be able to handle this case. I think it needs a separate PreloadEntry with enum on how to store this, but I am not sure how we can pass in the fetch_params into that.

Any guidance on how I can implement this in Rust would be great. I did see .await in other parts of the codebase, so I assume that Futures are the appropriate language construct to implement this, are they not?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, futures are probably the way to go here. One possibility would be to use a channel for preload responses, so this code could just call await on the receiver, and the code that produces the preload response would be responsible for sending the complete response object over the corresponding sender.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool. I think I know already how to tackle that, so will implement that in the next PR

Copy link
Copy Markdown
Member

@jdm jdm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a really useful step forward!

Comment on lines +435 to +438
// Step 1. Wait until fetchParams’s preloaded response candidate is not "pending".
// TODO
// Step 2. Assert: fetchParams’s preloaded response candidate is a response.
// TODO
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, futures are probably the way to go here. One possibility would be to use a channel for preload responses, so this code could just call await on the receiver, and the code that produces the preload response would be responsible for sending the complete response object over the corresponding sender.

global: self.global.clone(),
}));
let global = self.global.root();
let _realm = enter_realm(&*global);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why this change?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah this should have been part of 9042b68#diff-7732144f217b054c2148f8efd84259dfba8526ca07db0019691d994445c5195e Previously, we would call GlobalScope::current() which would require us to enter the realm here. Now, we pass in the global as an argument, hence this isn't required anymore.

pub struct RequestClient {
/// <https://html.spec.whatwg.org/multipage/#map-of-preloaded-resources>
#[conditional_malloc_size_of]
#[serde(skip)] // TODO: Figure out what we need to do here to serialize this map
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What doesn't work? IndexMap?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah. It does support serialization, but it doesn't guarantee ordering. Ordering is important per the spec, so if we were to serialize as-is, then the ordering would be broken and we would violate the spec.

That said, I don't think in the current setup we need serialization for these requests, as we don't transfer them between threads. But I don't know when we will and how to support that, given that the default serialization isn't feasible.


/// <https://html.spec.whatwg.org/multipage/#traversable-navigable>
#[derive(Clone, Copy, Default, MallocSizeOf, PartialEq)]
pub struct TraversableNavigable {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suspect we will want this to live somewhere else and only include a few relevant fields in the net-related data structures (particularly after the TODOs), but I'm fine with dealing with that later.

Copy link
Copy Markdown
Member

@jdm jdm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm willing to merge this with only minor changes to avoid this turning into a sprawling PR of doom.

@servo-highfive servo-highfive added S-needs-rebase There are merge conflict errors. and removed S-awaiting-review There is new code that needs to be reviewed. labels Oct 17, 2025
This aligns the request object more with the specification,
since the spec now has a `traversable_for_user_prompts` and
a separate field for the client. Before, they were present
in the same enum.

In doing so, new structs are added that are all required in
the new spec. Doing so, we can add support for preloaded
resources in this client, which are only populated when
we have an applicable Global.

Since the spec moved things around a bit, it now has a
dedicated method to populate the client from the request.

Part of servo#35035

Signed-off-by: Tim van der Lippe <[email protected]>
@TimvdLippe TimvdLippe force-pushed the implement-document-preloaded-resources branch from 0a59149 to 412d562 Compare October 20, 2025 12:48
@servo-highfive servo-highfive added S-awaiting-review There is new code that needs to be reviewed. and removed S-needs-rebase There are merge conflict errors. labels Oct 20, 2025
@TimvdLippe TimvdLippe enabled auto-merge October 20, 2025 12:48
@TimvdLippe TimvdLippe added this pull request to the merge queue Oct 20, 2025
@servo-highfive servo-highfive added the S-awaiting-merge The PR is in the process of compiling and running tests on the automated CI. label Oct 20, 2025
Merged via the queue into servo:main with commit 93e58de Oct 20, 2025
33 checks passed
@TimvdLippe TimvdLippe deleted the implement-document-preloaded-resources branch October 20, 2025 14:08
@servo-highfive servo-highfive removed the S-awaiting-merge The PR is in the process of compiling and running tests on the automated CI. label Oct 20, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

S-awaiting-review There is new code that needs to be reviewed.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants