Skip to content

feat: auto-reload Apple webviews on content process termination#15162

Open
polw1 wants to merge 3 commits intotauri-apps:devfrom
polw1:feat/ios-black-screen
Open

feat: auto-reload Apple webviews on content process termination#15162
polw1 wants to merge 3 commits intotauri-apps:devfrom
polw1:feat/ios-black-screen

Conversation

@polw1
Copy link
Copy Markdown

@polw1 polw1 commented Mar 27, 2026

Summary

Fixes #14371

On iOS (and macOS), the WebKit content process can be terminated by the OS (e.g. due to memory pressure or backgrounding). When this happens, the webview goes blank and becomes unresponsive. This PR adds automatic recovery by reloading the webview when the content process terminates.

  • Automatically reload macOS/iOS webviews on content process termination when no custom handler is set
  • Add a sliding-window rate limiter (3 attempts per 10 seconds) to prevent infinite reload loops if the page itself causes crashes
  • Expose on_web_content_process_terminate on both WebviewBuilder and WebviewWindowBuilder for custom handling
  • All new code paths are platform-gated to macOS/iOS only

Test plan

  • cargo fmt -p tauri -- --check passes
  • cargo clippy -p tauri -- -W warnings passes
  • All 9 unit tests pass (cargo test -p tauri --lib webview::tests)
    • Rate limiter: allows up to limit, blocks after limit, re-allows after window expires
    • Handler mapping: default handler created, no handler when disabled, custom handler preferred
  • Manual: on iOS Simulator, background the app, kill the WebContent process, bring app to foreground — webview should reload automatically
  • Manual: verify on macOS that content process termination triggers reload
  • Manual: verify Linux/Windows/Android builds are unaffected (no compilation errors from cfg gating)

@polw1 polw1 requested a review from a team as a code owner March 27, 2026 10:25
@github-project-automation github-project-automation bot moved this to 📬Proposal in Roadmap Mar 27, 2026
@Legend-Master Legend-Master added the ai-slop Low effort content, see https://github.com/tauri-apps/tauri?tab=contributing-ov-file#ai-tool-policy label Mar 27, 2026
@polw1 polw1 force-pushed the feat/ios-black-screen branch from 8d9c907 to bd371cd Compare March 27, 2026 12:55
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 27, 2026

Package Changes Through aa8fc7e

There are 9 changes which include tauri-macos-sign with patch, tauri-build with patch, tauri with minor, tauri-bundler with minor, tauri-cli with minor, @tauri-apps/cli with minor, tauri-runtime with minor, tauri-runtime-wry with minor, tauri-utils with minor

Planned Package Versions

The following package releases are the planned based on the context of changes in this pull request.

package current next
tauri-utils 2.8.3 2.9.0
tauri-macos-sign 2.3.3 2.3.4
tauri-bundler 2.8.1 2.9.0
tauri-runtime 2.10.1 2.11.0
tauri-runtime-wry 2.10.1 2.11.0
tauri-codegen 2.5.5 2.5.6
tauri-macros 2.5.5 2.5.6
tauri-plugin 2.5.4 2.5.5
tauri-build 2.5.6 2.5.7
tauri 2.10.3 2.11.0
@tauri-apps/cli 2.10.1 2.11.0
tauri-cli 2.10.1 2.11.0

Add another change file through the GitHub UI by following this link.


Read about change files or the docs at github.com/jbolda/covector

@FabianLars FabianLars removed the ai-slop Low effort content, see https://github.com/tauri-apps/tauri?tab=contributing-ov-file#ai-tool-policy label Mar 27, 2026
@polw1 polw1 force-pushed the feat/ios-black-screen branch from bd371cd to 26c539e Compare March 27, 2026 17:05
@FabianLars
Copy link
Copy Markdown
Member

Thanks for the PR! I didn't take a proper look yet but i think we should expose this on tauri::Builder as well (or only there?) so that windows created in JS or tauri.conf can be handled as well.

@polw1 polw1 force-pushed the feat/ios-black-screen branch from 26c539e to aa8fc7e Compare March 28, 2026 11:57
@polw1
Copy link
Copy Markdown
Author

polw1 commented Mar 28, 2026

Thank you, @FabianLars

Good point about webviews created through JS or tauri.conf. I do not see a strong reason for this behavior to differ per webview, so tauri::Builder seems like the right place for this.

@velocitysystems
Copy link
Copy Markdown
Contributor

Following @FabianLars' feedback — here's a proposal for how to plumb the handler globally.

Priority chain:

  1. Per-webview handler (via WebviewBuilder / WebviewWindowBuilder) — highest priority
  2. Global handler (via tauri::Builder) — fallback for all webviews
  3. Default auto-reload with rate limiting — when nothing is configured

Touch points:

// 1. Builder<R> — new field + setter (crates/tauri/src/app.rs)
#[cfg(any(target_os = "macos", target_os = "ios"))]
on_web_content_process_terminate: Option<Arc<OnWebContentProcessTerminateHandler<R>>>,

// 2. WebviewManager — store the global handler (crates/tauri/src/manager/webview.rs)
//    Passed through Builder::build() → AppManager::with_handlers() → WebviewManager
#[cfg(any(target_os = "macos", target_os = "ios"))]
pub on_web_content_process_terminate: Option<Arc<OnWebContentProcessTerminateHandler<R>>>,

// 3. Webview creation — resolve the priority chain (crates/tauri/src/webview/mod.rs)
//    In into_pending_webview():
let handler = per_webview_handler
    .or_else(|| global_handler_from_webview_manager.clone());

pending.handler = match handler {
    Some(h) => wrap_custom_handler(manager, label, h),
    None    => build_default_reload_handler(manager, label),
};

// 4. Simplify map_web_content_process_terminate_handler — remove the
//    install_default_reload_handler bool param (always true today)

This follows the same pattern as on_page_load on Builder. The install_default_reload_handler parameter can be removed since the fallback chain handles it naturally.

What are your thoughts @FabianLars?

@FabianLars
Copy link
Copy Markdown
Member

I agree with your proposal.
if we wanted to keep it simple we could skip the webview specific api since the builder on gives you the webview object but your on_page_load argument is a good call. I don't mind either way but since you already implemented the webview apis we might as well keep them.

@JeffTsang
Copy link
Copy Markdown

It looks like this PR is built on top of an existing open PR (#14523) that already reloads the webview automatically. Even the comments are the same.

Are we going to make progress on the original PR (#14523) or will this be the new PR for fixing this issue?

@JeffTsang
Copy link
Copy Markdown

So I dug deeper and found out that @velocitysystems and @polw1 are collaborators in repos under the organization @silvermine.

The weird part is that @velocitysystems started a review of the original PR (#14523) earlier this month, then went cold until this PR (with most of the code of the original PR copied over) popped up a few days ago.

This is surely not in the spirit of open source, but if this is how you want to manage contributions to Tauri, I'm sure you're free to do so.

@FabianLars
Copy link
Copy Markdown
Member

This is totally my bad, i didn't want to give the impression that this would simply discard your PR.

For a bit more transparency, this is their internally used/tested version of your PR and they asked me whether it made sense to show it quickly like this instead of keep adding comments on your PR which sounded reasonable to me, way quicker to take a quick look than discussing it for days.
That said, i have to admit that i expected more drastic differences to your PR in the code logic.

I wouldn't mind pulling in the changes in this PR in yours first and then merge yours in the main branch (or attribute you as a Co-authored-by here). Completely discarding #15162 would be a bad move too considering it does add value in the retry logic and the unit tests.

Also, with the comments above, both PRs are missing the tauri::Builder hook.

@velocitysystems
Copy link
Copy Markdown
Contributor

velocitysystems commented Mar 31, 2026

@JeffTsang Our apologies for any confusion. To be fully transparent: this issue is a priority for us to resolve, and after reviewing your PR I had concerns about the reload() vs navigate() approach and the absence of tests/rate-limiting. Rather than continuing to add review comments, we wanted to demonstrate an alternative approach so the differences would be concrete — quicker than going back and forth. There was no intention to sideline your work.

That said, your PR is the foundation — you identified the root cause, got the wry dependency landed, and wired up the handler. Per @FabianLars's suggestion, I propose we combine efforts: pull the rate-limiting logic, tests, and get the tauri::Builder hook into yours and get a single PR merged.

@polw1 has been working on the builder pattern and will push these changes shortly to this branch.

@JeffTsang
Copy link
Copy Markdown

Clearly, the proper way forward would be to finish the original PR (#14523) where I already reverted to reloading as requested. But you can do as you wish.

In writing the original PR, I followed the conventions of the existing code. If there were unit tests for similar functions, I would have added some myself. If the tests were really necessary, it would have been trivially easy to add them.

The tauri::Builder hook would have also been trivially easy to add, but it was never requested in the original PR.

With regards to this PR, it assumes that multiple reloads with rate limiting works where a single reload fails. Have you had this experience with an app in production that was tested over an extended period of time?

I have macOS and iOS apps that have been in production for almost a year now, and I solved the blank webview problem by loading a url instead of reloading.

@JeffTsang
Copy link
Copy Markdown

If you haven't actually experienced an endless loop of web content process terminations, you're adding a lot of needless complexity that won't accomplish anything.

Also, the logging isn't particularly useful. In fact, none of the other functions in webview/mod.rs generate any messages in the logs. But I guess it's up to the Tauri team to decide if they want to have all this extra logging for one function.

In the original PR (#14523), I placed the default handler in tauri-runtime-wry. This is the most appropriate place for the default handler because it is the final place where the handler is actually added to the webview.

@JeffTsang
Copy link
Copy Markdown

Just a heads up that I've moved the function to tauri::Builder in the original PR (#14523).

If there's anything else that needs to be done on the original PR, let me know.

@polw1 polw1 force-pushed the feat/ios-black-screen branch from aa8fc7e to b959470 Compare April 1, 2026 10:10
@polw1
Copy link
Copy Markdown
Author

polw1 commented Apr 1, 2026

Thanks for the detailed feedback, @JeffTsang . Your original PR was important in identifying and moving this issue forward.

Talking a bit about the design decisions made here:

Rate limiting and complexity
I understand the concern about unnecessary complexity, but the implementation here is intentionally small. The core sliding-window logic is only a few lines, plus a minimal integration in the default handler. Given that this is a framework-level default, having a lightweight guard against unbounded retry loops felt like a reasonable safety measure. This kind of guard is also a fairly common pattern in similar scenarios

Why this policy lives in the Tauri layer
With the introduction of a priority chain (per-webview handler → global Builder handler → default fallback), the decision of which handler to use becomes part of the Tauri orchestration layer. The runtime still performs the final wiring into WKWebView, but Tauri determines the policy that drives that wiring.

Logging
The logs were originally limited to exceptional paths (reload failure or retry budget exhausted). That said, after reviewing it again, I agree with you — I’ve reduced it so that we only log actual error conditions (e.g. reload failure or retry budget exhaustion), keeping the normal path quiet and consistent with the rest of the module.

Tests
I might be missing your point here, but these are not end-to-end tests.
They were added during the implementation to validate the logic while building it, so I would not need to repeatedly verify these behaviors manually.
Removing them just because adjacent code does not have similar coverage does not seem necessary.
Additionally, they help ensure the behavior remains stable over time during future refactors.

I’m happy to keep iterating if needed. It’s completely fine if we disagree on some of these points — healthy discussion helps us arrive at better solutions.

@JeffTsang
Copy link
Copy Markdown

The discussion ended up being continued in the original PR (#14523 (comment)).

When I was talking about testing, I was referring to the lack of tests in the original PR. The tests you wrote for the rate-limited reloads are fine.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: 📬Proposal

Development

Successfully merging this pull request may close these issues.

[bug] iOS webview becomes blank and unresponsive when resuming from the background

5 participants