Skip to content

Implement usage of custom protocol handlers#40803

Merged
TimvdLippe merged 1 commit intoservo:mainfrom
TimvdLippe:use-protocol-handler
Dec 2, 2025
Merged

Implement usage of custom protocol handlers#40803
TimvdLippe merged 1 commit intoservo:mainfrom
TimvdLippe:use-protocol-handler

Conversation

@TimvdLippe
Copy link
Copy Markdown
Contributor

@TimvdLippe TimvdLippe commented Nov 21, 2025

This implements a dummy mailto: protocol handler in
Servoshell. It registers a new custom handler that
fetches content by substituting the registered url
for the protocol handler.

This does not pass any WPT tests, as that requires
using the document.protocol_handler_automation_mode
which will be part of a follow-up PR.

Part of #40615

@TimvdLippe TimvdLippe marked this pull request as ready for review November 22, 2025 12:13
@TimvdLippe TimvdLippe requested a review from atbrakhi as a code owner November 22, 2025 12:13
@servo-highfive servo-highfive added the S-awaiting-review There is new code that needs to be reviewed. label Nov 22, 2025
@TimvdLippe TimvdLippe changed the title Implement using page content protocol handlers Implement usage of custom protocol handlers Nov 22, 2025
@codecov-commenter
Copy link
Copy Markdown

⚠️ JUnit XML file not found

The CLI was unable to find any JUnit XML files to upload.
For more help, visit our troubleshooting guide.

@servo-highfive servo-highfive added the S-needs-rebase There are merge conflict errors. label Nov 22, 2025
@servo-highfive servo-highfive removed the S-needs-rebase There are merge conflict errors. label Nov 22, 2025
@TimvdLippe TimvdLippe added the T-linux-wpt Do a try run of the WPT label Nov 22, 2025
@github-actions github-actions bot removed the T-linux-wpt Do a try run of the WPT label Nov 22, 2025
@github-actions
Copy link
Copy Markdown

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

@github-actions
Copy link
Copy Markdown

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

Flaky unexpected result (41)
  • 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
      

  • OK /_mozilla/mozilla/getBoundingClientRect.html (#39668)
    • FAIL [expected PASS] subtest: getBoundingClientRect 1

      assert_equals: expected 62 but got 60.35
      

  • OK /_mozilla/webxr/create_session.https.html
    • FAIL [expected PASS] subtest: create_session

      can't access property "simulateDeviceConnection", navigator.xr.test is undefined
      

  • OK /_mozilla/webxr/obtain_frame.https.html
    • FAIL [expected PASS] subtest: obtain_frame

      promise_test: Unhandled rejection with value: object "TypeError: can't access property "simulateDeviceConnection", navigator.xr.test is undefined"
      

  • ERROR [expected TIMEOUT] /_mozilla/webxr/sessionavailable.https.html
  • CRASH [expected OK] /_webgl/conformance2/wasm/readpixels-2gb-in-4gb-wasm-memory.html
  • OK /content-security-policy/frame-ancestors/frame-ancestors-path-ignored.window.html (#36468)
    • FAIL [expected PASS] subtest: A 'frame-ancestors' CSP directive with a URL that includes a path should be ignored.

      assert_unreached: The IFrame should have been blocked (or cross-origin). It wasn't. Reached unreachable code
      

  • TIMEOUT /content-security-policy/inheritance/location-reload.html (#38983)
    • PASS [expected FAIL] subtest: location.reload() of empty iframe.
  • FAIL [expected PASS] /css/css-backgrounds/background-size-042.html
  • CRASH [expected PASS] /css/css-text-decor/text-decoration-line-011.xht
  • OK /custom-elements/form-associated/ElementInternals-setFormValue.html (#29174)
    • PASS [expected FAIL] subtest: Single value - empty name exists
    • PASS [expected FAIL] subtest: Multiple values - name content attribute is ignored
  • CRASH [expected ERROR] /fetch/api/basic/stream-safe-creation.any.serviceworker.html
  • CRASH [expected ERROR] /fetch/api/credentials/authentication-basic.any.serviceworker.html
  • CRASH [expected OK] /fetch/api/policies/referrer-unsafe-url-service-worker.https.html
  • CRASH [expected OK] /fetch/compression-dictionary/dictionary-fetch-with-link-header.tentative.https.html
  • CRASH [expected TIMEOUT] /fetch/metadata/generated/css-images.https.sub.tentative.html
  • CRASH [expected OK] /fetch/metadata/generated/window-location.sub.html
  • TIMEOUT [expected CRASH] /fetch/metadata/window-open.https.sub.html (#40339)
  • OK /html/browsers/browsing-the-web/navigating-across-documents/initial-empty-document/load-pageshow-events-iframe-contentWindow.html (#28681)
    • FAIL [expected PASS] subtest: load & pageshow events do not fire on contentWindow of <iframe> element created with src='about:blank'

      assert_unreached: load should not be fired Reached unreachable code
      

  • 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/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"
      

  • CRASH [expected OK] /html/browsers/sandboxing/sandbox-initial-empty-document-toward-same-origin.html (#35948)
  • CRASH [expected OK] /html/browsers/the-window-object/open-close/open-features-tokenization-width-height.html
  • CRASH [expected OK] /html/browsers/windows/browsing-context-window.html
  • OK /html/semantics/embedded-content/the-iframe-element/iframe-loading-lazy-nav-location-replace-set-src.html (#32697)
    • PASS [expected FAIL] subtest: Navigating iframe loading='lazy' and then setting src: location.replace
  • OK /html/semantics/forms/form-submission-0/jsurl-form-submit.tentative.html (#36489)
    • PASS [expected FAIL] subtest: Verifies that form submissions scheduled inside javascript: urls take precedence over the javascript: url's return value.
  • OK /html/semantics/forms/form-submission-0/multipart-formdata.window.html (#28725)
    • PASS [expected FAIL] subtest: multipart/form-data: 0x00 in name (normal form)
    • PASS [expected FAIL] subtest: multipart/form-data: 0x00 in value (formdata event)
  • OK /html/semantics/forms/form-submission-0/text-plain.window.html (#28687)
    • FAIL [expected PASS] subtest: text/plain: Basic test (formdata event)

      assert_equals: expected "basic=test\r\n" but got ""
      

    • FAIL [expected PASS] subtest: text/plain: Basic File test (normal form)

      assert_equals: expected "basic=file-test.txt\r\n" but got ""
      

    • PASS [expected FAIL] subtest: text/plain: backslash in name (formdata event)
  • OK [expected CRASH] /html/semantics/forms/the-fieldset-element/disabled-003.html (#31730, #39631)
  • OK [expected ERROR] /html/semantics/forms/the-input-element/click-user-gesture.html (#40512)
  • TIMEOUT /resource-timing/test_resource_timing.html (#25720)
    • PASS [expected FAIL] subtest: PerformanceEntry has correct name, initiatorType, startTime, and duration (img)
  • CRASH [expected OK] /trusted-types/eval-csp-no-tt.html
  • CRASH [expected OK] /trusted-types/get-trusted-types-compliant-attribute-value.html
  • TIMEOUT /trusted-types/trusted-types-navigation.html?06-10 (#37920)
    • TIMEOUT [expected FAIL] subtest: Navigate a frame via anchor with javascript:-urls in report-only mode.

      Test timed out
      

    • NOTRUN [expected TIMEOUT] subtest: Navigate a frame via anchor with javascript:-urls w/ default policy in report-only mode.
  • TIMEOUT /trusted-types/trusted-types-navigation.html?31-35 (#38034)
    • TIMEOUT [expected PASS] subtest: Navigate a frame via form-submission with javascript:-urls in report-only mode.

      Test timed out
      

    • NOTRUN [expected TIMEOUT] subtest: Navigate a frame via form-submission with javascript:-urls w/ default policy in report-only mode.
  • CRASH [expected OK] /uievents/idlharness.window.html
  • CRASH [expected ERROR] /wasm/webapi/empty-body.any.serviceworker.html
  • OK [expected TIMEOUT] /webstorage/localstorage-about-blank-3P-iframe-opens-3P-window.partitioned.html (#29053)
    • PASS [expected TIMEOUT] subtest: StorageKey: test 3P about:blank window opened from a 3P iframe
  • OK [expected ERROR] /webxr/render_state_update.https.html (#27535)
  • OK /webxr/xrSession_features_deviceSupport.https.html (#24357)
    • FAIL [expected PASS] subtest: Immersive XRSession requests with no supported device should reject

      assert_unreached: Should have rejected: undefined Reached unreachable code
      

  • ERROR [expected OK] /workers/baseurl/alpha/sharedworker-in-worker.html (#21315)
Stable unexpected results that are known to be intermittent (25)
  • 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()
  • 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
      

  • CRASH [expected PASS] /_mozilla/shadow-dom/move-element-with-ua-shadow-tree-crash.html (#39473)
  • OK /css/css-cascade/layer-font-face-override.html (#35935)
    • PASS [expected FAIL] subtest: @font-face override update with appended sheet 1
    • PASS [expected FAIL] subtest: @font-face override update with appended sheet 2
  • OK /css/css-fonts/generic-family-keywords-001.html (#37467)
    • FAIL [expected PASS] subtest: @font-face matching for quoted and unquoted generic(khmer-mul)

      assert_equals: quoted generic(khmer-mul) matches  @font-face rule expected 50 but got 30
      

  • OK /css/css-fonts/generic-family-keywords-003.html (#38994)
    • PASS [expected FAIL] subtest: @font-face matching for quoted and unquoted generic(fangsong) (drawing text in a canvas)
  • OK /fetch/fetch-later/send-on-discard/not-send-after-abort.https.window.html (#40696)
    • FAIL [expected PASS] subtest: A discarded document does not send an already aborted fetchLater request.

      assert_equals: Number of sent beacons does not match expected count: expected 1 but got 0
      

  • OK /fetch/metadata/generated/css-font-face.sub.tentative.html (#34624)
    • FAIL [expected PASS] subtest: sec-fetch-storage-access - Not sent to non-trustworthy cross-site destination

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

  • OK /fetch/metadata/generated/element-img-environment-change.https.sub.html (#30111)
    • FAIL [expected PASS] subtest: sec-fetch-site - Same site, no attributes

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

    • PASS [expected FAIL] subtest: sec-fetch-site - Same-Origin -> Cross-Site -> Same-Origin redirect, no attributes
    • FAIL [expected PASS] subtest: sec-fetch-site - Same-Origin -> Same-Site -> Same-Origin redirect, no attributes

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

    • PASS [expected FAIL] subtest: sec-fetch-site - Cross-Site -> Same Origin, no attributes
    • FAIL [expected PASS] subtest: sec-fetch-site - Cross-Site -> Same-Site, no attributes

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

    • PASS [expected FAIL] subtest: sec-fetch-site - Cross-Site -> Cross-Site, no attributes
    • FAIL [expected PASS] subtest: sec-fetch-site - Same-Origin -> Same Origin, no attributes

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

    • PASS [expected FAIL] subtest: sec-fetch-site - Same-Origin -> Same-Site, no attributes
    • FAIL [expected PASS] subtest: sec-fetch-site - Same-Origin -> Cross-Site, no attributes

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

    • PASS [expected FAIL] subtest: sec-fetch-site - Same-Site -> Same Origin, no attributes
    • And 3 more unexpected results...
  • OK /fetch/metadata/generated/element-img-environment-change.sub.html (#30111)
    • PASS [expected FAIL] subtest: sec-fetch-mode - Not sent to non-trustworthy same-origin destination, no attributes
    • PASS [expected FAIL] subtest: sec-fetch-dest - Not sent to non-trustworthy cross-site destination, no attributes
    • PASS [expected FAIL] subtest: sec-fetch-user - Not sent to non-trustworthy cross-site destination, no attributes
    • PASS [expected FAIL] subtest: sec-fetch-storage-access - Not sent to non-trustworthy same-origin destination, no attributes
    • FAIL [expected PASS] subtest: sec-fetch-site - HTTPS downgrade-upgrade, no attributes

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

  • OK [expected ERROR] /focus/focus-event-after-switching-iframes.sub.html (#40368)
  • CRASH [expected OK] /html/browsers/browsing-the-web/navigating-across-documents/005.html (#27062)
  • OK /html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/a-click.html (#28697)
    • PASS [expected FAIL] subtest: aElement.click() before the load event must NOT replace
  • ERROR [expected OK] /html/infrastructure/common-dom-interfaces/collections/domstringlist.html (#40665)
  • OK [expected TIMEOUT] /html/interaction/focus/the-autofocus-attribute/autofocus-dialog.html (#29087)
    • FAIL [expected TIMEOUT] subtest: <dialog>-contained autofocus element gets focused when the dialog is shown

      assert_equals: expected "DIV" but got "BODY"
      

  • OK [expected TIMEOUT] /html/interaction/focus/the-autofocus-attribute/supported-elements.html (#24145)
    • FAIL [expected TIMEOUT] subtest: Element with tabindex should support autofocus

      assert_equals: expected "SPAN" but got "BODY"
      

    • 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"
      

  • 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 /navigation-timing/test-navigation-type-reload.html (#33334)
    • PASS [expected FAIL] subtest: Reload domComplete > Original domComplete
    • PASS [expected FAIL] subtest: Reload loadEventEnd > Original loadEventEnd
    • PASS [expected FAIL] subtest: Reload loadEventStart > Original loadEventStart
  • CRASH [expected OK] /pointerevents/compat/pointerevent_touch-action_two-finger_interaction.html (#40418)
  • OK /preload/preload-error.sub.html (#37177)
    • PASS [expected FAIL] subtest: CORS (fetch): main
  • OK /trusted-types/trusted-types-navigation.html?26-30 (#38807)
    • PASS [expected FAIL] subtest: Navigate a window via form-submission with javascript:-urls in report-only mode.
    • PASS [expected FAIL] subtest: Navigate a frame via form-submission with javascript:-urls in enforcing mode.
  • OK /wasm/webapi/abort.any.html (#39966)
    • FAIL [expected PASS] subtest: instantiateStreaming() asynchronously racing with abort should succeed or reject with AbortError

      assert_equals: expected "AbortError" but got "CompileError"
      

  • ERROR [expected OK] /workers/baseurl/alpha/import-in-moduleworker.html (#21315)

@github-actions
Copy link
Copy Markdown

✨ Try run (#19595453061) succeeded.

Copy link
Copy Markdown
Member

@mrobinson mrobinson left a comment

Choose a reason for hiding this comment

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

I'll let folks more familiar with the fetch implementation review the meet of this, but I have a comment about the servoshell chage:

@servo-highfive servo-highfive added S-needs-code-changes Changes have not yet been made that were requested by a reviewer. and removed S-awaiting-review There is new code that needs to be reviewed. labels Nov 24, 2025
@nicoburns
Copy link
Copy Markdown
Contributor

nicoburns commented Nov 25, 2025

This seems useful for something like a servo:// protocol that loads things like webpages built in to the binary. But for mailto: I would expect an interface that allows for things like opening an external mail client on the user's computer.

Presumably this would be implemented by returning some kind of "cancel navigation" (where this is differentiated from an error state, but the result is that Servo does nothing), with the action actually triggered by the custom handler as a side effect.

@webbeef
Copy link
Copy Markdown
Contributor

webbeef commented Nov 25, 2025

registerProtocolHandler is a standard API - if a user is happy with some webapp taking care of all mailto: links this is their choice. Redirecting to an external helper app is a different topic (and should also cover content types that are not supported internally).

@TimvdLippe
Copy link
Copy Markdown
Contributor Author

This is indeed the standard API that dictates how to navigate these URLs.

If an embedder wants do implement custom logic for a particular scheme, they can already do so with the existing ProtocolRegistry implementation: https://doc.servo.org/net/protocols/struct.ProtocolRegistry.html#method.register Currently we already have a couple of examples in Servoshell (https://github.com/servo/servo/tree/main/ports/servoshell/desktop/protocols) and I can imagine an embedder writing one for a mailto: external client as well. In that case, the embedder can also deny any webpage content to register another mailto: handler.

@servo-highfive servo-highfive added S-awaiting-review There is new code that needs to be reviewed. and removed S-needs-code-changes Changes have not yet been made that were requested by a reviewer. labels Nov 28, 2025
@TimvdLippe TimvdLippe requested review from jdm and mrobinson November 28, 2025 14:32
@TimvdLippe
Copy link
Copy Markdown
Contributor Author

Added @jdm as reviewer for the Fetch parts

Copy link
Copy Markdown
Member

@mrobinson mrobinson left a comment

Choose a reason for hiding this comment

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

Looks good from my perspective. Thanks for reverting the servoshell bits.

@servo-highfive servo-highfive removed the S-awaiting-review There is new code that needs to be reviewed. label Nov 29, 2025
@servo-highfive servo-highfive added the S-awaiting-review There is new code that needs to be reviewed. label Nov 30, 2025
@TimvdLippe
Copy link
Copy Markdown
Contributor Author

Rebased and also replaced the .unwrap() with an .unwrap_or_else() which was quite straightforward. One less papercut 🎉

@TimvdLippe TimvdLippe force-pushed the use-protocol-handler branch from 2b73712 to f84ed36 Compare December 2, 2025 19:08
@TimvdLippe TimvdLippe requested a review from jdm December 2, 2025 19:09
This implements a dummy `mailto:` protocol handler in
Servoshell. It registers a new custom handler that
fetches content by substituting the registered url
for the protocol handler.

This does not pass any WPT tests, as that requires
using the `document.protocol_handler_automation_mode`
which will be part of a follow-up PR.

Part of servo#40615

Signed-off-by: Tim van der Lippe <[email protected]>
@TimvdLippe TimvdLippe force-pushed the use-protocol-handler branch from f84ed36 to dafb772 Compare December 2, 2025 19:15
@servo-highfive servo-highfive removed the S-awaiting-review There is new code that needs to be reviewed. label Dec 2, 2025
@TimvdLippe TimvdLippe added this pull request to the merge queue Dec 2, 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 Dec 2, 2025
Merged via the queue into servo:main with commit c4db781 Dec 2, 2025
29 checks passed
@TimvdLippe TimvdLippe deleted the use-protocol-handler branch December 2, 2025 20:36
@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 Dec 2, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants