You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用Selenium与Testcontainers启动时偶发WebDriver异常求助

Selenium结合Testcontainers偶发启动异常求助

偶发出现启动异常,具体异常信息如下:

已发生异常: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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.01 14:25:24