Compare commits

...

1 Commits

Author SHA1 Message Date
Shelley Vohr
a4e9cbfe2d fix: recover network requests after Network Service restart
Co-Authored-By: inuyasha <lvfuqiang@bytedance.com>
2026-02-23 10:07:53 +01:00
15 changed files with 326 additions and 16 deletions

View File

@@ -451,6 +451,7 @@ filenames = {
"shell/browser/net/network_context_service.h",
"shell/browser/net/network_context_service_factory.cc",
"shell/browser/net/network_context_service_factory.h",
"shell/browser/net/network_service_restart_observer.h",
"shell/browser/net/node_stream_loader.cc",
"shell/browser/net/node_stream_loader.h",
"shell/browser/net/proxying_url_loader_factory.cc",

View File

@@ -30,8 +30,9 @@
#include "components/proxy_config/proxy_config_dictionary.h"
#include "components/proxy_config/proxy_config_pref_names.h"
#include "components/proxy_config/proxy_prefs.h"
#include "content/browser/gpu/compositor_util.h" // nogncheck
#include "content/browser/gpu/gpu_data_manager_impl.h" // nogncheck
#include "content/browser/gpu/compositor_util.h" // nogncheck
#include "content/browser/gpu/gpu_data_manager_impl.h" // nogncheck
#include "content/browser/network_service_instance_impl.h" // nogncheck
#include "content/public/browser/browser_accessibility_state.h"
#include "content/public/browser/browser_child_process_host.h"
#include "content/public/browser/child_process_data.h"
@@ -627,6 +628,19 @@ void App::OnPreMainMessageLoopRun() {
process_singleton_->StartWatching();
watch_singleton_socket_on_ready_ = false;
}
// Register handler for Network Service process gone events (crash/restart).
// This is used for testing Network Service restart recovery.
// Must be called on the UI thread after browser initialization is complete.
network_service_gone_subscription_ =
content::RegisterNetworkServiceProcessGoneHandler(base::BindRepeating(
[](App* app, bool crashed) {
if (crashed) {
// Emit event when Network Service crashes (for testing).
app->Emit("-network-service-crashed");
}
},
base::Unretained(this)));
}
void App::OnPreCreateThreads() {

View File

@@ -10,6 +10,7 @@
#include <string>
#include <vector>
#include "base/callback_list.h"
#include "base/containers/flat_map.h"
#include "base/task/cancelable_task_tracker.h"
#include "chrome/browser/process_singleton.h"
@@ -293,6 +294,9 @@ class App final : public gin::Wrappable<App>,
bool disable_domain_blocking_for_3DAPIs_ = false;
bool watch_singleton_socket_on_ready_ = false;
// Subscription to Network Service process gone notifications.
base::CallbackListSubscription network_service_gone_subscription_;
std::unique_ptr<content::ScopedAccessibilityMode> scoped_accessibility_mode_;
};

View File

@@ -13,7 +13,10 @@
#include "base/process/kill.h"
#include "base/process/launch.h"
#include "base/process/process.h"
#include "base/time/time.h"
#include "chrome/browser/browser_process.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/child_process_host.h"
#include "content/public/browser/service_process_host.h"
#include "content/public/common/result_codes.h"
@@ -223,6 +226,7 @@ UtilityProcessWrapper::UtilityProcessWrapper(
loader_params->url_loader_network_observer =
url_loader_network_observer_->Bind();
}
network::mojom::NetworkContext* network_context =
g_browser_process->system_network_context_manager()->GetContext();
network_context->CreateURLLoaderFactory(
@@ -238,6 +242,14 @@ UtilityProcessWrapper::UtilityProcessWrapper(
node_service_remote_->Initialize(std::move(params),
receiver_.BindNewPipeAndPassRemote());
// Subscribe to Network Service restart notifications.
if (auto* manager = g_browser_process->system_network_context_manager()) {
network_service_restart_subscription_ =
manager->AddNetworkServiceRestartCallback(base::BindRepeating(
&UtilityProcessWrapper::OnNetworkServiceRestarted,
weak_factory_.GetWeakPtr()));
}
}
UtilityProcessWrapper::~UtilityProcessWrapper() {
@@ -429,6 +441,48 @@ void UtilityProcessWrapper::OnV8FatalError(const std::string& location,
EmitWithoutEvent("error", "FatalError", location, report);
}
void UtilityProcessWrapper::OnNetworkServiceRestarted() {
if (!node_service_remote_.is_connected())
return;
content::GetUIThreadTaskRunner({})->PostTask(
FROM_HERE,
base::BindOnce(&UtilityProcessWrapper::CreateAndSendURLLoaderFactory,
weak_factory_.GetWeakPtr()));
}
void UtilityProcessWrapper::CreateAndSendURLLoaderFactory() {
if (!node_service_remote_.is_connected())
return;
auto* manager = g_browser_process->system_network_context_manager();
if (!manager)
return;
network::mojom::NetworkContext* network_context = manager->GetContext();
mojo::PendingRemote<network::mojom::URLLoaderFactory> url_loader_factory;
auto loader_params = network::mojom::URLLoaderFactoryParams::New();
// Use kBrowserProcessId to match the initial factory creation (where pid_
// was 0 before the process launched). This is required because non-browser
// process IDs require a request_initiator_origin_lock to be set.
loader_params->process_id = network::OriginatingProcess::browser();
loader_params->is_orb_enabled = false;
loader_params->is_trusted = true;
network_context->CreateURLLoaderFactory(
url_loader_factory.InitWithNewPipeAndPassReceiver(),
std::move(loader_params));
// Create host resolver from the same network context
mojo::PendingRemote<network::mojom::HostResolver> host_resolver;
network_context->CreateHostResolver(
{}, host_resolver.InitWithNewPipeAndPassReceiver());
node_service_remote_->UpdateURLLoaderFactory(std::move(url_loader_factory),
std::move(host_resolver));
}
// static
raw_ptr<UtilityProcessWrapper> UtilityProcessWrapper::FromProcessId(
base::ProcessId pid) {

View File

@@ -9,6 +9,7 @@
#include <memory>
#include <string>
#include "base/callback_list.h"
#include "base/containers/id_map.h"
#include "base/environment.h"
#include "base/memory/weak_ptr.h"
@@ -99,6 +100,13 @@ class UtilityProcessWrapper final
void OnServiceProcessDisconnected(uint32_t exit_code,
const std::string& description);
// Called when the Network Service restarts.
void OnNetworkServiceRestarted();
// Creates and sends a new URLLoaderFactory to the utility process.
// Called after Network Service restart to update the factory.
void CreateAndSendURLLoaderFactory();
base::ProcessId pid_ = base::kNullProcessId;
#if BUILDFLAG(IS_WIN)
// Non-owning handles, these will be closed when the
@@ -117,6 +125,7 @@ class UtilityProcessWrapper final
mojo::Remote<node::mojom::NodeService> node_service_remote_;
std::optional<electron::URLLoaderNetworkObserver>
url_loader_network_observer_;
base::CallbackListSubscription network_service_restart_subscription_;
base::WeakPtrFactory<UtilityProcessWrapper> weak_factory_{this};
};

View File

@@ -54,6 +54,7 @@
#include "shell/browser/file_system_access/file_system_access_permission_context_factory.h"
#include "shell/browser/media/media_device_id_salt.h"
#include "shell/browser/net/resolve_proxy_helper.h"
#include "shell/browser/net/system_network_context_manager.h"
#include "shell/browser/protocol_registry.h"
#include "shell/browser/serial/serial_chooser_context.h"
#include "shell/browser/special_storage_policy.h"
@@ -407,10 +408,20 @@ ElectronBrowserContext::ElectronBrowserContext(
extension_system->FinishInitialization();
}
#endif
// Subscribe to Network Service restart notifications to reset the cached
// URLLoaderFactory.
if (auto* manager = SystemNetworkContextManager::GetInstance()) {
network_service_restart_subscription_ =
manager->AddNetworkServiceRestartCallback(base::BindRepeating(
&ElectronBrowserContext::OnNetworkServiceRestarted,
base::Unretained(this)));
}
}
ElectronBrowserContext::~ElectronBrowserContext() {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
NotifyWillBeDestroyed();
// Notify any keyed services of browser context destruction.
@@ -568,6 +579,12 @@ content::PreconnectManager* ElectronBrowserContext::GetPreconnectManager() {
return preconnect_manager_.get();
}
void ElectronBrowserContext::OnNetworkServiceRestarted() {
// Clear the cached URLLoaderFactory so the next request creates a new one
// from the new NetworkContext.
url_loader_factory_.reset();
}
scoped_refptr<network::SharedURLLoaderFactory>
ElectronBrowserContext::GetURLLoaderFactory() {
if (url_loader_factory_)

View File

@@ -13,6 +13,7 @@
#include <variant>
#include <vector>
#include "base/callback_list.h"
#include "base/files/file_path.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/media_stream_request.h"
@@ -184,6 +185,9 @@ class ElectronBrowserContext : public content::BrowserContext {
// Initialize pref registry.
void InitPrefs();
// Called when the Network Service restarts.
void OnNetworkServiceRestarted();
scoped_refptr<ValueMapPrefStore> in_memory_pref_store_;
std::unique_ptr<CookieChangeNotifier> cookie_change_notifier_;
std::unique_ptr<PrefService> prefs_;
@@ -207,6 +211,9 @@ class ElectronBrowserContext : public content::BrowserContext {
// Shared URLLoaderFactory.
scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory_;
// Subscription to Network Service restart notifications.
base::CallbackListSubscription network_service_restart_subscription_;
network::mojom::SSLConfigPtr ssl_config_;
mojo::Remote<network::mojom::SSLConfigClient> ssl_config_client_;

View File

@@ -0,0 +1,18 @@
// Copyright (c) 2026 Microsoft GmbH.
// Use of this source code is governed by the MIT license that can be
// found in the LICENSE file.
#ifndef ELECTRON_SHELL_BROWSER_NET_NETWORK_SERVICE_RESTART_OBSERVER_H_
#define ELECTRON_SHELL_BROWSER_NET_NETWORK_SERVICE_RESTART_OBSERVER_H_
#include "base/observer_list_types.h"
// Observer interface for Network Service restart notifications.
class NetworkServiceRestartObserver : public base::CheckedObserver {
public:
// Called after the Network Service has been recreated following a crash.
// Observers should use this to refresh any cached URLLoaderFactory instances.
virtual void OnNetworkServiceRestarted() = 0;
};
#endif // ELECTRON_SHELL_BROWSER_NET_NETWORK_SERVICE_RESTART_OBSERVER_H_

View File

@@ -34,6 +34,7 @@
#include "services/network/public/cpp/shared_url_loader_factory.h"
#include "services/network/public/mojom/network_context.mojom.h"
#include "services/network/public/mojom/url_response_head.mojom.h"
#include "shell/browser/api/electron_api_app.h"
#include "shell/browser/browser.h"
#include "shell/browser/electron_browser_client.h"
#include "shell/common/application_info.h"
@@ -287,6 +288,18 @@ void SystemNetworkContextManager::OnNetworkServiceCreated(
network_service->SetEncryptionKey(OSCrypt::GetRawEncryptionKey());
#endif
}
restart_callbacks_.Notify();
// Test-only event to signal Network Service has been recreated.
if (auto* app = electron::api::App::Get())
app->Emit("-network-service-created");
}
base::CallbackListSubscription
SystemNetworkContextManager::AddNetworkServiceRestartCallback(
base::RepeatingClosure callback) {
return restart_callbacks_.Add(std::move(callback));
}
network::mojom::NetworkContextParamsPtr

View File

@@ -5,6 +5,7 @@
#ifndef ELECTRON_SHELL_BROWSER_NET_SYSTEM_NETWORK_CONTEXT_MANAGER_H_
#define ELECTRON_SHELL_BROWSER_NET_SYSTEM_NETWORK_CONTEXT_MANAGER_H_
#include "base/callback_list.h"
#include "chrome/browser/net/proxy_config_monitor.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "net/base/features.h"
@@ -83,6 +84,11 @@ class SystemNetworkContextManager {
// SystemNetworkContext, if the network service is enabled.
void OnNetworkServiceCreated(network::mojom::NetworkService* network_service);
// Subscribe to Network Service restart notifications. Returns a subscription
// that will automatically unsubscribe when destroyed.
base::CallbackListSubscription AddNetworkServiceRestartCallback(
base::RepeatingClosure callback);
private:
class URLLoaderFactoryForSystem;
@@ -102,6 +108,9 @@ class SystemNetworkContextManager {
// consumers don't all need to create their own factory.
scoped_refptr<URLLoaderFactoryForSystem> shared_url_loader_factory_;
mojo::Remote<network::mojom::URLLoaderFactory> url_loader_factory_;
// Callbacks notified when Network Service restarts.
base::RepeatingClosureList restart_callbacks_;
};
#endif // ELECTRON_SHELL_BROWSER_NET_SYSTEM_NETWORK_CONTEXT_MANAGER_H_

View File

@@ -8,13 +8,13 @@
#include <utility>
#include "base/command_line.h"
#include "base/memory/ref_counted.h"
#include "base/no_destructor.h"
#include "base/process/process.h"
#include "base/strings/utf_string_conversions.h"
#include "electron/fuses.h"
#include "electron/mas.h"
#include "net/base/network_change_notifier.h"
#include "services/network/public/cpp/wrapper_shared_url_loader_factory.h"
#include "services/network/public/mojom/host_resolver.mojom.h"
#include "services/network/public/mojom/network_context.mojom.h"
#include "shell/browser/javascript_environment.h"
@@ -56,22 +56,26 @@ void V8FatalErrorCallback(const char* location, const char* message) {
*zero = 0;
}
URLLoaderBundle::URLLoaderBundle() = default;
// URLLoaderBundle implementation
URLLoaderBundle::URLLoaderBundle() {
// Add an extra reference to prevent the singleton from ever being deleted
AddRef();
}
URLLoaderBundle::~URLLoaderBundle() = default;
URLLoaderBundle* URLLoaderBundle::GetInstance() {
static base::NoDestructor<URLLoaderBundle> instance;
return instance.get();
static URLLoaderBundle* instance = new URLLoaderBundle();
return instance;
}
void URLLoaderBundle::SetURLLoaderFactory(
mojo::PendingRemote<network::mojom::URLLoaderFactory> pending_factory,
mojo::Remote<network::mojom::HostResolver> host_resolver,
bool use_network_observer_from_url_loader_factory) {
factory_ = network::SharedURLLoaderFactory::Create(
std::make_unique<network::WrapperPendingSharedURLLoaderFactory>(
std::move(pending_factory)));
// Reset the old remote before binding the new one
factory_remote_.reset();
factory_remote_.Bind(std::move(pending_factory));
host_resolver_ = std::move(host_resolver);
should_use_network_observer_from_url_loader_factory_ =
use_network_observer_from_url_loader_factory;
@@ -79,7 +83,35 @@ void URLLoaderBundle::SetURLLoaderFactory(
scoped_refptr<network::SharedURLLoaderFactory>
URLLoaderBundle::GetSharedURLLoaderFactory() {
return factory_;
// Return ourselves since we implement SharedURLLoaderFactory
return scoped_refptr<network::SharedURLLoaderFactory>(this);
}
void URLLoaderBundle::CreateLoaderAndStart(
mojo::PendingReceiver<network::mojom::URLLoader> loader,
int32_t request_id,
uint32_t options,
const network::ResourceRequest& request,
mojo::PendingRemote<network::mojom::URLLoaderClient> client,
const net::MutableNetworkTrafficAnnotationTag& traffic_annotation) {
if (factory_remote_.is_bound() && factory_remote_.is_connected()) {
factory_remote_->CreateLoaderAndStart(std::move(loader), request_id,
options, request, std::move(client),
traffic_annotation);
}
}
void URLLoaderBundle::Clone(
mojo::PendingReceiver<network::mojom::URLLoaderFactory> receiver) {
if (factory_remote_.is_bound() && factory_remote_.is_connected()) {
factory_remote_->Clone(std::move(receiver));
}
}
std::unique_ptr<network::PendingSharedURLLoaderFactory>
URLLoaderBundle::Clone() {
// Return nullptr - callers should use GetSharedURLLoaderFactory() instead
return nullptr;
}
network::mojom::HostResolver* URLLoaderBundle::GetHostResolver() {
@@ -208,4 +240,13 @@ void NodeService::Initialize(
node_bindings_->StartPolling();
}
void NodeService::UpdateURLLoaderFactory(
mojo::PendingRemote<network::mojom::URLLoaderFactory> url_loader_factory,
mojo::PendingRemote<network::mojom::HostResolver> host_resolver) {
URLLoaderBundle::GetInstance()->SetURLLoaderFactory(
std::move(url_loader_factory), mojo::Remote(std::move(host_resolver)),
URLLoaderBundle::GetInstance()
->ShouldUseNetworkObserverfromURLLoaderFactory());
}
} // namespace electron

View File

@@ -14,7 +14,7 @@
#include "net/base/network_change_notifier.h"
#include "services/network/public/cpp/shared_url_loader_factory.h"
#include "services/network/public/mojom/host_resolver.mojom.h"
#include "services/network/public/mojom/url_loader_factory.mojom-forward.h"
#include "services/network/public/mojom/url_loader_factory.mojom.h"
#include "shell/services/node/public/mojom/node_service.mojom.h"
namespace node {
@@ -29,25 +29,40 @@ class ElectronBindings;
class JavascriptEnvironment;
class NodeBindings;
class URLLoaderBundle {
class URLLoaderBundle : public network::SharedURLLoaderFactory {
public:
URLLoaderBundle();
~URLLoaderBundle();
URLLoaderBundle(const URLLoaderBundle&) = delete;
URLLoaderBundle& operator=(const URLLoaderBundle&) = delete;
static URLLoaderBundle* GetInstance();
void SetURLLoaderFactory(
mojo::PendingRemote<network::mojom::URLLoaderFactory> factory,
mojo::Remote<network::mojom::HostResolver> host_resolver,
bool use_network_observer_from_url_loader_factory);
scoped_refptr<network::SharedURLLoaderFactory> GetSharedURLLoaderFactory();
network::mojom::HostResolver* GetHostResolver();
bool ShouldUseNetworkObserverfromURLLoaderFactory() const;
void CreateLoaderAndStart(
mojo::PendingReceiver<network::mojom::URLLoader> loader,
int32_t request_id,
uint32_t options,
const network::ResourceRequest& request,
mojo::PendingRemote<network::mojom::URLLoaderClient> client,
const net::MutableNetworkTrafficAnnotationTag& traffic_annotation)
override;
void Clone(mojo::PendingReceiver<network::mojom::URLLoaderFactory> receiver)
override;
std::unique_ptr<network::PendingSharedURLLoaderFactory> Clone() override;
private:
scoped_refptr<network::SharedURLLoaderFactory> factory_;
~URLLoaderBundle() override;
mojo::Remote<network::mojom::URLLoaderFactory> factory_remote_;
mojo::Remote<network::mojom::HostResolver> host_resolver_;
bool should_use_network_observer_from_url_loader_factory_ = false;
};
@@ -65,6 +80,9 @@ class NodeService : public node::mojom::NodeService {
void Initialize(node::mojom::NodeServiceParamsPtr params,
mojo::PendingRemote<node::mojom::NodeServiceClient>
client_pending_remote) override;
void UpdateURLLoaderFactory(
mojo::PendingRemote<network::mojom::URLLoaderFactory> url_loader_factory,
mojo::PendingRemote<network::mojom::HostResolver> host_resolver) override;
private:
// This needs to be initialized first so that it can be destroyed last

View File

@@ -28,4 +28,9 @@ interface NodeServiceClient {
interface NodeService {
Initialize(NodeServiceParams params,
pending_remote<NodeServiceClient> client_remote);
// Update the URLLoaderFactory and HostResolver after Network Service restart.
UpdateURLLoaderFactory(
pending_remote<network.mojom.URLLoaderFactory> url_loader_factory,
pending_remote<network.mojom.HostResolver> host_resolver);
};

View File

@@ -1,4 +1,4 @@
import { net, session, ClientRequest, ClientRequestConstructorOptions, utilityProcess } from 'electron/main';
import { app, net, session, ClientRequest, ClientRequestConstructorOptions, utilityProcess } from 'electron/main';
import { expect } from 'chai';
@@ -10,7 +10,7 @@ import * as path from 'node:path';
import { setTimeout } from 'node:timers/promises';
import { collectStreamBody, collectStreamBodyBuffer, getResponse, kOneKiloByte, kOneMegaByte, randomBuffer, randomString, respondNTimes, respondOnce } from './lib/net-helpers';
import { listen, defer } from './lib/spec-helpers';
import { ifit, listen, defer } from './lib/spec-helpers';
const utilityFixturePath = path.resolve(__dirname, 'fixtures', 'api', 'utility-process', 'api-net-spec.js');
const fixturesPath = path.resolve(__dirname, 'fixtures');
@@ -1688,4 +1688,80 @@ describe('net module', () => {
}
});
}
describe('Network Service crash recovery', () => {
ifit(process.platform !== 'linux')('should recover net.fetch after Network Service crash (main process)', async () => {
const serverUrl = await respondOnce.toSingleURL((request, response) => {
response.end('first');
});
const firstResponse = await net.fetch(serverUrl);
expect(firstResponse.ok).to.be.true();
expect(await firstResponse.text()).to.equal('first');
const metrics = app.getAppMetrics();
const networkServiceProcess = metrics.find(
m => m.type === 'Utility' && m.serviceName === 'network.mojom.NetworkService'
);
expect(networkServiceProcess).to.not.be.undefined();
const crashPromise = once(app, '-network-service-crashed');
const restartPromise = once(app, '-network-service-created');
process.kill(networkServiceProcess!.pid, 'SIGKILL');
await crashPromise;
await restartPromise;
const secondServerUrl = await respondOnce.toSingleURL((request, response) => {
response.end('second');
});
const secondResponse = await net.fetch(secondServerUrl);
expect(secondResponse.ok).to.be.true();
expect(await secondResponse.text()).to.equal('second');
});
ifit(process.platform !== 'linux')('should recover net.fetch after Network Service crash (utility process)', async () => {
const child = utilityProcess.fork(path.join(fixturesPath, 'api', 'utility-process', 'network-restart-test.js'));
await once(child, 'spawn');
await once(child, 'message');
const firstServerUrl = await respondOnce.toSingleURL((request, response) => {
response.end('utility-first');
});
child.postMessage({ type: 'fetch', url: firstServerUrl });
const [firstResult] = await once(child, 'message');
expect(firstResult.ok).to.be.true();
expect(firstResult.body).to.equal('utility-first');
// Find the Network Service process
const metrics = app.getAppMetrics();
const networkServiceProcess = metrics.find(
m => m.type === 'Utility' && m.serviceName === 'network.mojom.NetworkService'
);
expect(networkServiceProcess).to.not.be.undefined();
const crashPromise = once(app, '-network-service-crashed');
const restartPromise = once(app, '-network-service-created');
process.kill(networkServiceProcess!.pid, 'SIGKILL');
await crashPromise;
await restartPromise;
// Needed for UpdateURLLoaderFactory IPC to propagate to the utility process
// and for any in-flight requests to settle
await setTimeout(500);
const secondServerUrl = await respondOnce.toSingleURL((request, response) => {
response.end('utility-second');
});
child.postMessage({ type: 'fetch', url: secondServerUrl });
const [secondResult] = await once(child, 'message');
expect(secondResult.ok).to.be.true();
expect(secondResult.body).to.equal('utility-second');
child.kill();
await once(child, 'exit');
});
});
});

View File

@@ -0,0 +1,24 @@
const { net } = require('electron');
process.parentPort.on('message', async (e) => {
const { type, url } = e.data;
if (type === 'fetch') {
try {
const response = await net.fetch(url);
const body = await response.text();
process.parentPort.postMessage({
ok: response.ok,
status: response.status,
body
});
} catch (error) {
process.parentPort.postMessage({
ok: false,
error: error.message
});
}
}
});
process.parentPort.postMessage({ type: 'ready' });