使用Selenium与Testcontainers启动时偶发WebDriver异常求助
偶发出现启动异常,具体异常信息如下:
已发生异常:CLR/OpenQA.Selenium.WebDriverException
在WebDriver.dll中发生类型为'OpenQA.Selenium.WebDriverException'的异常,未在用户代码中处理:'向远程WebDriver服务器发送HTTP请求到URL http://localhost:4444/session时遇到未知异常。异常消息为:发送请求时发生错误。'
已找到内部异常,查看变量窗口中的$exception获取更多详细信息。
最内层异常 System.IO.IOException : 响应提前结束。
在 System.Net.Http.HttpConnection.d__61.MoveNext()
该问题在运行以下C# / xUnit.net测试构造函数时,有一半概率出现:
public DockerShould() { var gridNetwork = new NetworkBuilder() .WithName("gridNetwork") .Build(); const int SessionPort = 4444; var containerHub = new ContainerBuilder() .WithImage("selenium/hub:4.8") .WithName("selenium-hub") .WithPortBinding(4442, 4442) .WithPortBinding(4443, 4443) .WithPortBinding(SessionPort, SessionPort) .WithNetwork(gridNetwork) .Build(); var firefoxEnvironment = new Dictionary<string, string>() { {"SE_EVENT_BUS_HOST", "selenium-hub"}, {"SE_EVENT_BUS_PUBLISH_PORT", "4442"}, {"SE_EVENT_BUS_SUBSCRIBE_PORT", "4443"} }; var containerFirefox = new ContainerBuilder() .WithImage("selenium/node-firefox:4.8") .WithEnvironment(firefoxEnvironment) .WithNetwork(gridNetwork) .Build(); var firefoxVideoEnvironment = new Dictionary<string, string>() { {"DISPLAY_CONTAINER_NAME", "firefox"}, {"FILE_NAME", "firefox.mp4"} }; var containerFirefoxVideo = new ContainerBuilder() .WithImage("selenium/video:ffmpeg-4.3.1-20230210") .WithNetwork(gridNetwork) .WithEnvironment(firefoxVideoEnvironment) // .WithWaitStrategy(Wait.ForUnixContainer().UntilPortIsAvailable(SessionPort)) .Build(); gridNetwork.CreateAsync().Wait(); containerHub.StartAsync().Wait(); containerFirefox.StartAsync().Wait(); containerFirefoxVideo.StartAsync().Wait(); Thread.Sleep(5000); _remoteWebDriver = new RemoteWebDriver(new Uri("http://localhost:4444"), new FirefoxOptions()); }
异常发生在创建RemoteWebDriver实例时。已添加Thread.Sleep(5000)延迟等待容器启动,但效果不佳。求更优雅的方式确保所有容器完全启动后再创建WebDriver实例?
解决方法
1. 给Selenium Hub容器添加服务就绪等待策略
仅等待容器启动完成不够,需要确认Hub的WebDriver服务真正就绪。利用Testcontainers的WaitStrategy验证Hub的/status端点:
var containerHub = new ContainerBuilder() .WithImage("selenium/hub:4.8") .WithName("selenium-hub") .WithPortBinding(4442, 4442) .WithPortBinding(4443, 4443) .WithPortBinding(SessionPort, SessionPort) .WithNetwork(gridNetwork) // 添加等待策略:等待/status返回200,超时设为30秒 .WithWaitStrategy(Wait.ForUnixContainer() .UntilHttpRequestIsSucceeded(r => r.ForPort(SessionPort).ForPath("/status").ForStatusCode(HttpStatusCode.OK)) .WithTimeout(TimeSpan.FromSeconds(30))) .Build();
2. 等待Firefox节点完成注册到Hub
Hub启动后,还需要等待Firefox节点完成注册,否则创建会话时会因无可用节点失败。可以轮询Hub的/grid/api/hub端点检查节点状态:
// 启动所有容器后,轮询检查节点是否就绪 var httpClient = new HttpClient(); var hubStatusUrl = new Uri($"http://localhost:{SessionPort}/grid/api/hub"); var timeout = DateTime.Now.AddSeconds(30); while (DateTime.Now < timeout) { try { var response = await httpClient.GetAsync(hubStatusUrl); if (response.IsSuccessStatusCode) { var content = await response.Content.ReadAsStringAsync(); var hubStatus = JsonSerializer.Deserialize<HubStatus>(content); // 检查是否有可用的Firefox节点 if (hubStatus.Nodes?.Any(n => n.Config.Capabilities?.Any(c => c.CapabilityName == "firefox") == true) == true) { break; } } } catch { // 忽略异常,继续等待 } await Task.Delay(1000); } // 定义用于反序列化的类 public class HubStatus { public List<Node> Nodes { get; set; } } public class Node { public NodeConfig Config { get; set; } } public class NodeConfig { public List<Capability> Capabilities { get; set; } } public class Capability { [JsonPropertyName("capabilityName")] public string CapabilityName { get; set; } }
3. 替换Thread.Sleep为异步等待逻辑
避免硬编码延迟,改用异步等待,同时配合xUnit的IAsyncLifetime接口实现异步初始化:
public class DockerShould : IAsyncLifetime { private RemoteWebDriver _remoteWebDriver; private Network _gridNetwork; private IContainer _containerHub; private IContainer _containerFirefox; private IContainer _containerFirefoxVideo; public async Task InitializeAsync() { _gridNetwork = new NetworkBuilder() .WithName("gridNetwork") .Build(); const int SessionPort = 4444; _containerHub = new ContainerBuilder() .WithImage("selenium/hub:4.8") .WithName("selenium-hub") .WithPortBinding(4442, 4442) .WithPortBinding(4443, 4443) .WithPortBinding(SessionPort, SessionPort) .WithNetwork(_gridNetwork) .WithWaitStrategy(Wait.ForUnixContainer() .UntilHttpRequestIsSucceeded(r => r.ForPort(SessionPort).ForPath("/status").ForStatusCode(HttpStatusCode.OK)) .WithTimeout(TimeSpan.FromSeconds(30))) .Build(); var firefoxEnvironment = new Dictionary<string, string>() { {"SE_EVENT_BUS_HOST", "selenium-hub"}, {"SE_EVENT_BUS_PUBLISH_PORT", "4442"}, {"SE_EVENT_BUS_SUBSCRIBE_PORT", "4443"} }; _containerFirefox = new ContainerBuilder() .WithImage("selenium/node-firefox:4.8") .WithEnvironment(firefoxEnvironment) .WithNetwork(_gridNetwork) .Build(); var firefoxVideoEnvironment = new Dictionary<string, string>() { {"DISPLAY_CONTAINER_NAME", "firefox"}, {"FILE_NAME", "firefox.mp4"} }; _containerFirefoxVideo = new ContainerBuilder() .WithImage("selenium/video:ffmpeg-4.3.1-20230210") .WithNetwork(_gridNetwork) .WithEnvironment(firefoxVideoEnvironment) .Build(); await _gridNetwork.CreateAsync(); await _containerHub.StartAsync(); await _containerFirefox.StartAsync(); await _containerFirefoxVideo.StartAsync(); // 等待节点注册完成 var httpClient = new HttpClient(); var hubStatusUrl = new Uri($"http://localhost:{SessionPort}/grid/api/hub"); var timeout = DateTime.Now.AddSeconds(30); while (DateTime.Now < timeout) { try { var response = await httpClient.GetAsync(hubStatusUrl); if (response.IsSuccessStatusCode) { var content = await response.Content.ReadAsStringAsync(); var hubStatus = JsonSerializer.Deserialize<HubStatus>(content); if (hubStatus.Nodes?.Any(n => n.Config.Capabilities?.Any(c => c.CapabilityName == "firefox") == true) == true) { break; } } } catch { // 忽略异常继续等待 } await Task.Delay(1000); } _remoteWebDriver = new RemoteWebDriver(new Uri("http://localhost:4444"), new FirefoxOptions()); } public async Task DisposeAsync() { _remoteWebDriver?.Quit(); await _containerFirefoxVideo.StopAsync(); await _containerFirefox.StopAsync(); await _containerHub.StopAsync(); await _gridNetwork.DeleteAsync(); } }
4. 额外建议
- 避免给容器指定固定名称(
WithName),测试并行运行时会导致名称冲突,Testcontainers默认会生成唯一名称。 - 升级Testcontainers和Selenium到最新稳定版,可能已修复部分偶发的启动同步问题。
内容的提问来源于stack exchange,提问作者ossentoo

