Skip to content

fix(deps): raise nicegui lower bound to >=3.10.0 — CVE-2026-39844#589

Closed
helmut-hoffer-von-ankershoffen wants to merge 1 commit intomainfrom
claude/affectionate-pascal-iHHnc
Closed

fix(deps): raise nicegui lower bound to >=3.10.0 — CVE-2026-39844#589
helmut-hoffer-von-ankershoffen wants to merge 1 commit intomainfrom
claude/affectionate-pascal-iHHnc

Conversation

@helmut-hoffer-von-ankershoffen
Copy link
Copy Markdown
Contributor

Supply-chain audit — 2026-04-25

New advisory resolved

Advisory Package Severity Fix
GHSA-w8wv-vfpc-hw2w / CVE-2026-39844 nicegui < 3.10.0 CVSS 3.1 5.9 Medium (CWE-22 path traversal) Raise lower bound → >=3.10.0

Vulnerability: NiceGUI's upload filename sanitization used PurePosixPath(filename).name, which does not strip backslashes. On Windows deployments where application code constructs paths with file.name, an attacker can write files outside the intended upload directory (arbitrary file write → potential RCE). Linux/macOS are not affected (backslash is a literal character on those platforms).

Fix available: nicegui 3.10.0 (released 2026-04-08, referenced in GitHub release). The pyproject.toml already had a forward-looking comment CVE-2026-39844 (>=3.10.0, #531) not yet merged — that PR has since landed and 3.10.0 is on PyPI.

Changes

  • pyproject.toml: nicegui[native]>=3.9.0,<4>=3.10.0,<4; comment updated to remove "not yet merged".
  • SUPPLY_CHAIN_VULNERABILITIES.md: enforced lower bounds table updated — constraint, protected CVE list, and "Since" column.
  • uv.lock: lock resolved to nicegui 3.11.0 (latest 3.x, fully fixes CVE-2026-39844).

Existing acceptances re-verified

Advisory Status
GHSA-58qw-9mgm-455v / CVE-2026-3219 — pip archive type confusion ✅ Still accepted (dev-only, fix expected in pip 26.1)

Audit summary

  • 415 packages scanned via OSV batch API from uv.lock
  • 1 new advisory → resolved by raising lower bound (this PR)
  • 1 existing acceptance re-verified

Routine daily supply-chain audit. Labels: routine:pysdk-audit-daily, skip:test:long_running.


Generated by Claude Code



NiceGUI upload filename sanitization bypass via backslashes (path traversal
on Windows) was fixed in nicegui 3.10.0. Raise the enforced lower bound from
>=3.9.0 to >=3.10.0 so all consumers resolve a fixed version. Lock file
updated to nicegui 3.11.0 (latest in 3.x line).

CVE: CVE-2026-39844 / GHSA-w8wv-vfpc-hw2w
Severity: CVSS 3.1 5.9 Medium (AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:H/A:N)
Fix: nicegui >=3.10.0

Routine audit: 2026-04-25

https://claude.ai/code/session_01Qom7RJt32JmKRtDAEufTCN
@sonarqubecloud
Copy link
Copy Markdown

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR addresses CVE-2026-39844 (NiceGUI upload filename sanitization bypass on Windows) by raising the project’s enforced lower bound for nicegui[native] and refreshing the lockfile to a non-vulnerable NiceGUI release.

Changes:

  • Raise nicegui[native] lower bound from >=3.9.0 to >=3.10.0 in pyproject.toml (and update the inline CVE annotation).
  • Update the enforced lower-bounds table in SUPPLY_CHAIN_VULNERABILITIES.md to include CVE-2026-39844 and the new “Since” entry.
  • Refresh uv.lock to reflect the new constraint and resolve NiceGUI to 3.11.0.

Reviewed changes

Copilot reviewed 2 out of 3 changed files in this pull request and generated no comments.

File Description
pyproject.toml Raises the runtime dependency lower bound for nicegui[native] to ensure the CVE fix is always included.
SUPPLY_CHAIN_VULNERABILITIES.md Updates the documented enforced-lower-bound constraint and CVE coverage list for NiceGUI.
uv.lock Updates the locked NiceGUI version and dependency graph to align with the new minimum requirement.

@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 25, 2026

❌ 3 Tests Failed:

Tests completed Failed Passed Skipped
767 3 764 15
View the top 2 failed test(s) by shortest run time
tests.aignostics.qupath.gui_test::test_gui_run_qupath_install_to_inspect
Stack Traces | 318s run time
user = <nicegui.testing.user.User object at 0x7f4d9aa77460>
runner = <typer.testing.CliRunner object at 0x7f4d98f14210>
tmp_path = PosixPath('.../pytest-of-runner/pytest-21/test_gui_run_qupath_install_to0')
silent_logging = None, qupath_teardown = None, qupath_save_restore = None
record_property = <function record_property.<locals>.append_property at 0x7f4d7f187ab0>

    @pytest.mark.e2e
    @pytest.mark.long_running
    @pytest.mark.skipif(
        (platform.system() == "Linux" and platform.machine() in {"aarch64", "arm64"}),
        reason="QuPath is not supported on ARM64 Linux",
    )
    @pytest.mark.timeout(timeout=60 * 15)
    @pytest.mark.sequential
    async def test_gui_run_qupath_install_to_inspect(  # noqa: C901, PLR0912, PLR0913, PLR0914, PLR0915, PLR0917
        user: User,
        runner: CliRunner,
        tmp_path: Path,
        silent_logging: None,
        qupath_teardown: None,
        qupath_save_restore: None,
        record_property,
    ) -> None:
        """Test installing QuPath, downloading run results, creating QuPath project from it, and inspecting results."""
        record_property("tested-item-id", "TC-QUPATH-01, SPEC-GUI-SERVICE")
    
        # Find run
        runs = Service().application_runs(
            application_id=HETA_APPLICATION_ID,
            application_version=HETA_APPLICATION_VERSION,
            external_id=SPOT_0_GS_URL,
            tags=["scheduled"],
            has_output=True,
            limit=1,
        )
        if not runs:
            message = f"No matching runs found for application {HETA_APPLICATION_ID} ({HETA_APPLICATION_VERSION}). "
            message += "This test requires the scheduled test test_application_runs_heta_version passing first."
            pytest.skip(message)
    
        run_id = runs[0].run_id
    
        # Explore run
        run = Service().application_run(run_id).details()
        print(
            f"Found existing run: {run.run_id}\n"
            f"application: {run.application_id} ({run.version_number})\n"
            f"status: {run.state}, output: {run.output}\n"
            f"submitted at: {run.submitted_at}, terminated at: {run.terminated_at}\n"
            f"statistics: {run.statistics!r}\n",
            f"custom_metadata: {run.custom_metadata!r}\n",
        )
    
        # Explore results
        results = list(Service().application_run(run_id).results())
        assert results, f"No results found for run {run_id}"
        for item in results:
            print(
                f"Found item: {item.item_id}, status: {item.state}, output: {item.output}, "
                f"external_id: {item.external_id}\n"
                f"custom_metadata: {item.custom_metadata!r}\n",
            )
    
        with patch(
            "aignostics.application._gui._page_application_run_describe.get_user_data_directory", return_value=tmp_path
        ):
            # Step 1: (Re)Install QuPath
            result = runner.invoke(cli, ["qupath", "install"])
            output = normalize_output(result.output, strip_ansi=True)
            assert f"QuPath v{QUPATH_VERSION} installed successfully" in output, (
                f"Expected 'QuPath v{QUPATH_VERSION} installed successfully' in output.\nOutput: {output}"
            )
            assert result.exit_code == 0
    
            # Step 2: Go to latest completed run via GUI
            await user.open(f"/application/run/{run.run_id}")
            await user.should_see(f"Run {run.run_id}")
            await user.should_see(f"Run of {HETA_APPLICATION_ID} ({HETA_APPLICATION_VERSION})")
    
            # Step 3: Open Result Download dialog
            await user.should_see(marker="BUTTON_OPEN_QUPATH", retries=100)
            user.find(marker="BUTTON_OPEN_QUPATH").click()
    
            # Step 4: Select Data destination
            await user.should_see(marker="BUTTON_DOWNLOAD_DESTINATION_DATA")
            download_destination_data_button: ui.button = user.find(
                marker="BUTTON_DOWNLOAD_DESTINATION_DATA"
            ).elements.pop()
            assert download_destination_data_button.enabled, "Download destination button should be enabled"
            user.find(marker="BUTTON_DOWNLOAD_DESTINATION_DATA").click()
            await assert_notified(user, "Using Launchpad results directory", 30)
    
            # Step 5: Trigger Download
            await user.should_see(marker="DIALOG_BUTTON_DOWNLOAD_RUN")
            download_run_button: ui.button = user.find(marker="DIALOG_BUTTON_DOWNLOAD_RUN").elements.pop()
            assert download_run_button.enabled, "Download button should be enabled before downloading"
            user.find(marker="DIALOG_BUTTON_DOWNLOAD_RUN").click()
            await assert_notified(user, "Downloading ...", 30)
    
            # Step 6: Check download completes, QuPath project created, and QuPath launched
>           await assert_notified(user, "Download and QuPath project creation completed.", 60 * 5)

.../aignostics/qupath/gui_test.py:232: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

user = <nicegui.testing.user.User object at 0x7f4d9aa77460>
expected_notification = 'Download and QuPath project creation completed.'
wait_seconds = 300

    async def assert_notified(user: User, expected_notification: str, wait_seconds: int = 5) -> str:
        """Check if the user receives a notification within the specified time.
    
        This utility function helps test GUI notifications by waiting for a specific
        notification message to appear in the user's notification messages.
    
        Args:
            user: The nicegui User instance for testing.
            expected_notification: The notification text to look for (partial match).
            wait_seconds: Maximum time to wait for the notification (default: 5).
    
        Returns:
            str: The oldest matching notification message found.
    
        Raises:
            pytest.fail: If no matching notification is found within the wait time.
        """
        for _ in range(wait_seconds):
            matching_messages = [msg for msg in user.notify.messages if expected_notification in msg]
            if matching_messages:
                return matching_messages[0]
            await sleep(1)
    
        recent_messages = (user.notify.messages[-10:] if len(user.notify.messages) > 10 else user.notify.messages)[::-1]
        total_count = len(user.notify.messages)
>       pytest.fail(
            f"No notification containing '{expected_notification}' was found within {wait_seconds} seconds. "
            f"Total messages: {total_count}. Recent messages: {recent_messages}"
        )
E       Failed: No notification containing 'Download and QuPath project creation completed.' was found within 300 seconds. Total messages: 5. Recent messages: ['#x1F389 Run he-tme completed!', '#x1F389 Run he-tme completed!', '#x1F389 Run he-tme completed!', 'Downloading ...', 'Using Launchpad results directory']

tests/conftest.py:131: Failed
tests.aignostics.application.gui_test::test_gui_run_download
Stack Traces | 487s run time
user = <nicegui.testing.user.User object at 0x7f4dac8950f0>
runner = <typer.testing.CliRunner object at 0x7f4dac894c30>
tmp_path = PosixPath('.../pytest-of-runner/pytest-21/test_gui_run_download1')
silent_logging = None
record_property = <function record_property.<locals>.append_property at 0x7f4db6c096f0>

    @pytest.mark.e2e
    @pytest.mark.long_running
    @pytest.mark.flaky(retries=1, delay=5)
    @pytest.mark.timeout(timeout=60 * 10)
    @pytest.mark.sequential  # Helps on Linux with image analysis step otherwise timing out
    async def test_gui_run_download(  # noqa: PLR0915
        user: User, runner: CliRunner, tmp_path: Path, silent_logging: None, record_property
    ) -> None:
        """Test that the user can download a run result via the GUI."""
        record_property("tested-item-id", "SPEC-APPLICATION-SERVICE, SPEC-GUI-SERVICE")
        with patch(
            "aignostics.application._gui._page_application_run_describe.get_user_data_directory",
            return_value=tmp_path,
        ):
            # Find run
            runs = Service().application_runs(
                application_id=HETA_APPLICATION_ID,
                application_version=HETA_APPLICATION_VERSION,
                external_id=SPOT_0_GS_URL,
                tags=["scheduled"],
                has_output=True,
                limit=1,
            )
            if not runs:
                message = f"No matching runs found for application {HETA_APPLICATION_ID} ({HETA_APPLICATION_VERSION}). "
                message += "This test requires the scheduled test test_application_runs_heta_version passing first."
                pytest.skip(message)
    
            run_id = runs[0].run_id
    
            # Explore run
            run = Service().application_run(run_id).details()
            print(
                f"Found existing run: {run.run_id}\n"
                f"application: {run.application_id} ({run.version_number})\n"
                f"status: {run.state}, output: {run.output}\n"
                f"submitted at: {run.submitted_at}, terminated at: {run.terminated_at}\n"
                f"statistics: {run.statistics!r}\n",
                f"custom_metadata: {run.custom_metadata!r}\n",
            )
            # Step 1: Go to latest completed run
            await user.open(f"/application/run/{run.run_id}")
            await user.should_see(f"Run {run.run_id}", retries=100)
            await user.should_see(
                f"Run of {run.application_id} ({run.version_number})",
                retries=100,
            )
    
            # Step 2: Open Result Download dialog
            await user.should_see(marker="BUTTON_DOWNLOAD_RUN", retries=100)
            user.find(marker="BUTTON_DOWNLOAD_RUN").click()
    
            # Step 3: Check download button is initially disabled, then select Data folder
            download_run_button: ui.button = user.find(marker="DIALOG_BUTTON_DOWNLOAD_RUN").elements.pop()
            assert not download_run_button.enabled, "Download button should be disabled before selecting target"
            await user.should_see(marker="BUTTON_DOWNLOAD_DESTINATION_DATA", retries=100)
            user.find(marker="BUTTON_DOWNLOAD_DESTINATION_DATA").click()
            await assert_notified(user, "Using Launchpad results directory")
    
            # Step 4: Trigger Download - wait for button to be enabled
            download_run_button = user.find(marker="DIALOG_BUTTON_DOWNLOAD_RUN").elements.pop()
            assert download_run_button.enabled, "Download button should be enabled after selecting target"
            user.find(marker="DIALOG_BUTTON_DOWNLOAD_RUN").click()
            await assert_notified(user, "Downloading ...")
    
            # Check: Download completed
>           await assert_notified(user, "Download completed.", 60 * 4)

.../aignostics/application/gui_test.py:414: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

user = <nicegui.testing.user.User object at 0x7f4dac8950f0>
expected_notification = 'Download completed.', wait_seconds = 240

    async def assert_notified(user: User, expected_notification: str, wait_seconds: int = 5) -> str:
        """Check if the user receives a notification within the specified time.
    
        This utility function helps test GUI notifications by waiting for a specific
        notification message to appear in the user's notification messages.
    
        Args:
            user: The nicegui User instance for testing.
            expected_notification: The notification text to look for (partial match).
            wait_seconds: Maximum time to wait for the notification (default: 5).
    
        Returns:
            str: The oldest matching notification message found.
    
        Raises:
            pytest.fail: If no matching notification is found within the wait time.
        """
        for _ in range(wait_seconds):
            matching_messages = [msg for msg in user.notify.messages if expected_notification in msg]
            if matching_messages:
                return matching_messages[0]
            await sleep(1)
    
        recent_messages = (user.notify.messages[-10:] if len(user.notify.messages) > 10 else user.notify.messages)[::-1]
        total_count = len(user.notify.messages)
>       pytest.fail(
            f"No notification containing '{expected_notification}' was found within {wait_seconds} seconds. "
            f"Total messages: {total_count}. Recent messages: {recent_messages}"
        )
E       Failed: No notification containing 'Download completed.' was found within 240 seconds. Total messages: 5. Recent messages: ['#x1F389 Run he-tme completed!', '#x1F389 Run he-tme completed!', '#x1F389 Run test-app completed!', 'Downloading ...', 'Using Launchpad results directory']

tests/conftest.py:131: Failed
View the full list of 1 ❄️ flaky test(s)
tests.aignostics.notebook.service_test::test_serve_notebook

Flake rate in main: 8.82% (Passed 31 times, Failed 3 times)

Stack Traces | 0.874s run time
user = <nicegui.testing.user.User object at 0x7f84784f8690>
caplog = <_pytest.logging.LogCaptureFixture object at 0x7f8498908b00>

    @pytest.mark.integration
    @pytest.mark.flaky(retries=1, delay=5, only_on=[AssertionError])
    @pytest.mark.sequential
    @pytest.mark.timeout(timeout=60 * 2)
    def test_serve_notebook(user: User, caplog: pytest.LogCaptureFixture) -> None:
        """Test notebook serving.
    
        Args:
            user: The test user fixture.
            caplog: Fixture to capture log messages.
    
        Raises:
            AssertionError: If the test assertions fail.
        """
        # Set up logging to capture DEBUG level and above
        caplog.set_level(logging.DEBUG)
    
        client = TestClient(app)
    
        try:
>           response = client.get("/notebook/4711?results_folder=/tmp", timeout=60)
                       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

.../aignostics/notebook/service_test.py:118: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
..../test-3-14-3/lib/python3.14............/site-packages/starlette/testclient.py:473: in get
    return super().get(
..../test-3-14-3/lib/python3.14................../site-packages/httpx/_client.py:1053: in get
    return self.request(
..../test-3-14-3/lib/python3.14............/site-packages/starlette/testclient.py:445: in request
    return super().request(
..../test-3-14-3/lib/python3.14................../site-packages/httpx/_client.py:825: in request
    return self.send(request, auth=auth, follow_redirects=follow_redirects)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
..../test-3-14-3/lib/python3.14.../site-packages/sentry_sdk/utils.py:1872: in runner
    return sentry_patched_function(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
..../test-3-14-3/lib/python3.14.../sentry_sdk/integrations/httpx.py:91: in send
    rv = real_send(self, request, **kwargs)
         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
..../test-3-14-3/lib/python3.14................../site-packages/httpx/_client.py:914: in send
    response = self._send_handling_auth(
..../test-3-14-3/lib/python3.14................../site-packages/httpx/_client.py:942: in _send_handling_auth
    response = self._send_handling_redirects(
..../test-3-14-3/lib/python3.14................../site-packages/httpx/_client.py:979: in _send_handling_redirects
    response = self._send_single_request(request)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
..../test-3-14-3/lib/python3.14................../site-packages/httpx/_client.py:1014: in _send_single_request
    response = transport.handle_request(request)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
..../test-3-14-3/lib/python3.14............/site-packages/starlette/testclient.py:348: in handle_request
    raise exc
..../test-3-14-3/lib/python3.14............/site-packages/starlette/testclient.py:345: in handle_request
    portal.call(self.app, scope, receive, send)
..../test-3-14-3/lib/python3.14....../site-packages/anyio/from_thread.py:326: in call
    return cast(T_Retval, self.start_task_soon(func, *args).result())
                          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../............../_temp/uv-python-dir/cpython-3.14.3-linux-x86_64-gnu/lib/python3.14....../concurrent/futures/_base.py:450: in result
    return self.__get_result()
           ^^^^^^^^^^^^^^^^^^^
../............../_temp/uv-python-dir/cpython-3.14.3-linux-x86_64-gnu/lib/python3.14....../concurrent/futures/_base.py:395: in __get_result
    raise self._exception
..../test-3-14-3/lib/python3.14....../site-packages/anyio/from_thread.py:257: in _call_func
    retval = await retval_or_awaitable
             ^^^^^^^^^^^^^^^^^^^^^^^^^
..../test-3-14-3/lib/python3.14.../site-packages/fastapi/applications.py:1139: in __call__
    await super().__call__(scope, receive, send)
..../test-3-14-3/lib/python3.14.../sentry_sdk/integrations/starlette.py:406: in _sentry_patched_asgi_app
    return await middleware(scope, receive, send)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
..../test-3-14-3/lib/python3.14.../sentry_sdk/integrations/asgi.py:179: in _run_asgi3
    return await self._run_app(scope, receive, send, asgi_version=3)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
..../test-3-14-3/lib/python3.14.../sentry_sdk/integrations/asgi.py:269: in _run_app
    raise exc from None
..../test-3-14-3/lib/python3.14.../sentry_sdk/integrations/asgi.py:264: in _run_app
    return await self.app(
..../test-3-14-3/lib/python3.14.../site-packages/starlette/applications.py:107: in __call__
    await self.middleware_stack(scope, receive, send)
..../test-3-14-3/lib/python3.14.../sentry_sdk/integrations/starlette.py:197: in _create_span_call
    return await old_call(app, scope, new_receive, new_send, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
..../test-3-14-3/lib/python3.14.../starlette/middleware/errors.py:176: in __call__
    response = await self.handler(request, exc)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
..../test-3-14-3/lib/python3.14.../site-packages/nicegui/nicegui.py:190: in _exception_handler_500
    raise exception  # Simply return "Internal Server Error", just like FastAPI would do
    ^^^^^^^^^^^^^^^
..../test-3-14-3/lib/python3.14.../starlette/middleware/errors.py:164: in __call__
    await self.app(scope, receive, _send)
..../test-3-14-3/lib/python3.14.../sentry_sdk/integrations/starlette.py:197: in _create_span_call
    return await old_call(app, scope, new_receive, new_send, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
..../test-3-14-3/lib/python3.14.../starlette/middleware/base.py:191: in __call__
    with recv_stream, send_stream, collapse_excgroups():
                                   ^^^^^^^^^^^^^^^^^^^^
../............../_temp/uv-python-dir/cpython-3.14.3-linux-x86_64-gnu/lib/python3.14/contextlib.py:162: in __exit__
    self.gen.throw(value)
..../test-3-14-3/lib/python3.14....../site-packages/starlette/_utils.py:85: in collapse_excgroups
    raise exc
..../test-3-14-3/lib/python3.14.../starlette/middleware/base.py:193: in __call__
    response = await self.dispatch_func(request, call_next)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
..../test-3-14-3/lib/python3.14....../site-packages/nicegui/middlewares.py:23: in dispatch
    response = await call_next(request)
               ^^^^^^^^^^^^^^^^^^^^^^^^
..../test-3-14-3/lib/python3.14.../starlette/middleware/base.py:168: in call_next
    raise app_exc from app_exc.__cause__ or app_exc.__context__
..../test-3-14-3/lib/python3.14.../starlette/middleware/base.py:144: in coro
    await self.app(scope, receive_or_disconnect, send_no_error)
..../test-3-14-3/lib/python3.14.../sentry_sdk/integrations/starlette.py:197: in _create_span_call
    return await old_call(app, scope, new_receive, new_send, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
..../test-3-14-3/lib/python3.14.../starlette/middleware/base.py:191: in __call__
    with recv_stream, send_stream, collapse_excgroups():
                                   ^^^^^^^^^^^^^^^^^^^^
../............../_temp/uv-python-dir/cpython-3.14.3-linux-x86_64-gnu/lib/python3.14/contextlib.py:162: in __exit__
    self.gen.throw(value)
..../test-3-14-3/lib/python3.14....../site-packages/starlette/_utils.py:85: in collapse_excgroups
    raise exc
..../test-3-14-3/lib/python3.14.../starlette/middleware/base.py:193: in __call__
    response = await self.dispatch_func(request, call_next)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
..../test-3-14-3/lib/python3.14....../site-packages/nicegui/middlewares.py:13: in dispatch
    response = await call_next(request)
               ^^^^^^^^^^^^^^^^^^^^^^^^
..../test-3-14-3/lib/python3.14.../starlette/middleware/base.py:168: in call_next
    raise app_exc from app_exc.__cause__ or app_exc.__context__
..../test-3-14-3/lib/python3.14.../starlette/middleware/base.py:144: in coro
    await self.app(scope, receive_or_disconnect, send_no_error)
..../test-3-14-3/lib/python3.14.../sentry_sdk/integrations/starlette.py:295: in _sentry_exceptionmiddleware_call
    await old_call(self, scope, receive, send)
..../test-3-14-3/lib/python3.14.../sentry_sdk/integrations/starlette.py:197: in _create_span_call
    return await old_call(app, scope, new_receive, new_send, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
..../test-3-14-3/lib/python3.14.../starlette/middleware/exceptions.py:63: in __call__
    await wrap_app_handling_exceptions(self.app, conn)(scope, receive, send)
..../test-3-14-3/lib/python3.14............/site-packages/starlette/_exception_handler.py:53: in wrapped_app
    raise exc
..../test-3-14-3/lib/python3.14............/site-packages/starlette/_exception_handler.py:42: in wrapped_app
    await app(scope, receive, sender)
..../test-3-14-3/lib/python3.14.../sentry_sdk/integrations/starlette.py:197: in _create_span_call
    return await old_call(app, scope, new_receive, new_send, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
..../test-3-14-3/lib/python3.14.../fastapi/middleware/asyncexitstack.py:18: in __call__
    await self.app(scope, receive, send)
..../test-3-14-3/lib/python3.14........./site-packages/starlette/routing.py:716: in __call__
    await self.middleware_stack(scope, receive, send)
..../test-3-14-3/lib/python3.14........./site-packages/starlette/routing.py:736: in app
    await route.handle(scope, receive, send)
..../test-3-14-3/lib/python3.14........./site-packages/starlette/routing.py:290: in handle
    await self.app(scope, receive, send)
..../test-3-14-3/lib/python3.14............/site-packages/fastapi/routing.py:120: in app
    await wrap_app_handling_exceptions(app, request)(scope, receive, send)
..../test-3-14-3/lib/python3.14............/site-packages/starlette/_exception_handler.py:53: in wrapped_app
    raise exc
..../test-3-14-3/lib/python3.14............/site-packages/starlette/_exception_handler.py:42: in wrapped_app
    await app(scope, receive, sender)
..../test-3-14-3/lib/python3.14............/site-packages/fastapi/routing.py:106: in app
    response = await f(request)
               ^^^^^^^^^^^^^^^^
..../test-3-14-3/lib/python3.14.../sentry_sdk/integrations/fastapi.py:137: in _sentry_app
    return await old_app(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
..../test-3-14-3/lib/python3.14............/site-packages/fastapi/routing.py:430: in app
    raw_response = await run_endpoint_function(
..../test-3-14-3/lib/python3.14............/site-packages/fastapi/routing.py:316: in run_endpoint_function
    return await dependant.call(**values)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
..../test-3-14-3/lib/python3.14.../site-packages/nicegui/page.py:166: in decorated
    with Client(self, request=request) as client:
         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
..../test-3-14-3/lib/python3.14.../site-packages/nicegui/client.py:88: in __init__
    self.outbox = Outbox(self)
                  ^^^^^^^^^^^^
..../test-3-14-3/lib/python3.14.../site-packages/nicegui/outbox.py:48: in __init__
    background_tasks.create_or_defer(self.loop(), name=f'outbox loop {client.id}')
..../test-3-14-3/lib/python3.14.../site-packages/nicegui/background_tasks.py:62: in create_or_defer
    core.app.on_startup(lambda: create(awaitable, name=name))
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <nicegui.app.app.App object at 0x7f84babb5010>
handler = <function create_or_defer.<locals>.<lambda> at 0x7f84784ef3d0>

    def on_startup(self, handler: Callable) -> None:
        """Called when NiceGUI is started or restarted.
    
        The callback can be synchronous or asynchronous.
    
        Needs to be called before `ui.run()`.
        """
        if self.is_started:
            if core.script_mode:
                raise RuntimeError('Unable to register a startup in script mode. Use a `@ui.page` function instead.')
>           raise RuntimeError('Unable to register another startup handler. NiceGUI has already been started.')
E           RuntimeError: Unable to register another startup handler. NiceGUI has already been started.

..../test-3-14-3/lib/python3.14.../nicegui/app/app.py:137: RuntimeError

The above exception was the direct cause of the following exception:

user = <nicegui.testing.user.User object at 0x7f84784f8690>
caplog = <_pytest.logging.LogCaptureFixture object at 0x7f8498908b00>

    @pytest.mark.integration
    @pytest.mark.flaky(retries=1, delay=5, only_on=[AssertionError])
    @pytest.mark.sequential
    @pytest.mark.timeout(timeout=60 * 2)
    def test_serve_notebook(user: User, caplog: pytest.LogCaptureFixture) -> None:
        """Test notebook serving.
    
        Args:
            user: The test user fixture.
            caplog: Fixture to capture log messages.
    
        Raises:
            AssertionError: If the test assertions fail.
        """
        # Set up logging to capture DEBUG level and above
        caplog.set_level(logging.DEBUG)
    
        client = TestClient(app)
    
        try:
            response = client.get("/notebook/4711?results_folder=/tmp", timeout=60)
            assert response.status_code == 200
            content = response.content.decode("utf-8")
            assert "iframe" in content
            assert "iframe src" in content
    
            # Look for the encoded iframe in the innerHTML property
            iframe_html = re.search(r'innerHTML":"&lt;iframe src=\\"([^"]+)\\"', content)
    
            # Enhanced error message with logs if assertion fails
            if iframe_html is None:
                log_messages = "\n".join([f"{record.levelname}: {record.message}" for record in caplog.records])
                error_msg = (
                    f"iframe src not found in response.\n"
                    f"Response content: {content[:1000]}{'...' if len(content) > 1000 else ''}\n"
                    f"Captured logs:\n{log_messages}"
                )
                pytest.fail(error_msg)
    
            # Extract the URL from the iframe src attribute
            notebook_url = iframe_html.group(1)
    
            # Enhanced error messages with logs for remaining assertions
            if not any(host in notebook_url for host in ("localhost", "127.0.0.1")):
                log_messages = "\n".join([f"{record.levelname}: {record.message}" for record in caplog.records])
                error_msg = (
                    f"localhost and 127.0.0.1 not found in iframe src: {notebook_url}\n"
                    f"Full response content: {content[:1000]}{'...' if len(content) > 1000 else ''}\n"
                    f"Captured logs:\n{log_messages}"
                )
                pytest.fail(error_msg)
    
            if "run_id=4711" not in notebook_url:
                log_messages = "\n".join([f"{record.levelname}: {record.message}" for record in caplog.records])
                error_msg = (
                    f"run_id not found in iframe src: {notebook_url}\n"
                    f"Full response content: {content[:1000]}{'...' if len(content) > 1000 else ''}\n"
                    f"Captured logs:\n{log_messages}"
                )
                pytest.fail(error_msg)
    
        except Exception as e:
            # If any unexpected exception occurs, capture and include logs
            log_messages = "\n".join([f"{record.levelname}: {record.message}" for record in caplog.records])
            error_msg = f"Test failed with exception: {e}\nCaptured logs:\n{log_messages}"
>           raise AssertionError(error_msg) from e
E           AssertionError: Test failed with exception: Unable to register another startup handler. NiceGUI has already been started.
E           Captured logs:
E           DEBUG: Using selector: EpollSelector

.../aignostics/notebook/service_test.py:163: AssertionError

To view more test analytics, go to the Test Analytics Dashboard
📋 Got 3 mins? Take this short survey to help us improve Test Analytics.

@helmut-hoffer-von-ankershoffen
Copy link
Copy Markdown
Contributor Author

testing automation via routine, closing this obsolete try

auto-merge was automatically disabled April 25, 2026 08:24

Pull request was closed

@helmut-hoffer-von-ankershoffen helmut-hoffer-von-ankershoffen deleted the claude/affectionate-pascal-iHHnc branch April 25, 2026 08:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants