Skip to content

fix(ui): write cached web UI HTML atomically - #5490

Merged
dsfaccini merged 15 commits into
pydantic:mainfrom
pragnyanramtha:pragnyan/fix-ui-html-cache-atomic-5399
Jul 3, 2026
Merged

dsfaccini merged 15 commits into
pydantic:mainfrom
pragnyanramtha:pragnyan/fix-ui-html-cache-atomic-5399

Conversation

@pragnyanramtha

Copy link
Copy Markdown
Contributor

Summary

Addresses #5399, cache race item.

  • write cached web UI HTML through a same-directory temporary file and os.replace()
  • treat empty cache files as cache misses so zero-byte files are repaired on the next fetch
  • add regression coverage for empty cache recovery

Testing

  • uv run pytest tests/test_ui_web.py -k "get_ui_html_refetches_empty_cache_file or get_ui_html_cdn_fetch or get_ui_html_filesystem_cache_hit or get_ui_html_custom_url_caching"
  • uv run ruff check pydantic_ai_slim/pydantic_ai/ui/_web/app.py tests/test_ui_web.py
  • uv run pyright pydantic_ai_slim/pydantic_ai/ui/_web/app.py tests/test_ui_web.py
  • git diff --check

Checklist

  • Any AI generated code has been reviewed line-by-line by the human PR author, who stands by it.
  • No breaking changes in accordance with the version policy.
  • PR title is fit for the release changelog.

@github-actions github-actions Bot added size: S Small PR (≤100 weighted lines) bug Report that something isn't working, or PR implementing a fix labels May 16, 2026
@pragnyanramtha
pragnyanramtha force-pushed the pragnyan/fix-ui-html-cache-atomic-5399 branch from 165e622 to 018418a Compare May 17, 2026 08:44
@pragnyanramtha
pragnyanramtha marked this pull request as ready for review May 17, 2026 16:07
Copilot AI review requested due to automatic review settings May 17, 2026 16:07

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

…ests

Add `test_get_ui_html_cache_write_is_atomic` — interposes on `os.replace`
to assert deterministically that the cache file only materializes complete
via the atomic rename (the source already holds the full content), so a
reader can never catch it existing-but-empty.

Add an `isolated_ui_cache` fixture so `test_chat_app_index_endpoint` and
`test_chat_app_index_caching` point `_get_cache_dir` at a temp dir and stub
the CDN fetch, removing the live jsdelivr dependency and real `~/.cache`
pollution from the suite.
@github-actions github-actions Bot added size: M Medium PR (101-500 weighted lines) and removed size: S Small PR (≤100 weighted lines) labels Jun 26, 2026
The `isolated_ui_cache` fixture monkeypatches `_get_cache_dir`, which left its
real body uncovered now that the index-route tests no longer exercise it. Add
`test_get_cache_dir_uses_xdg_cache_home` to cover it via the public XDG path.
@dsfaccini dsfaccini added web-ui chore and removed bug Report that something isn't working, or PR implementing a fix labels Jun 26, 2026
@dsfaccini dsfaccini added the pydanty:review-branch Trigger pydanty review-branch auto-review on this PR label Jun 26, 2026
@pydanty pydanty Bot added the pydanty:is-working Pydanty is mid-flight reviewing/remediating this PR; do not edit until it clears label Jun 26, 2026
@pydanty

pydanty Bot commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

pydanty runtime failure

Run df_run_5e4061cb8cc24ff8a128 started from the GitHub webhook, but the Dogfooding runtime
failed before pydanty completed the workflow. The pydanty status comment above
(if any) is from a completed run and still stands.

Next step: maintainer attention needed; rerun after fixing the runtime failure.

Failure detail (tail)
imax/minimax-m3,z-ai/glm-5.2:exacto
19:12:41.845         running tool: shell
19:12:41.887         chat fallback:minimax/minimax-m3,z-ai/glm-5.2:exacto
19:13:24.504         running tool: list_repository_files
19:13:24.510         running tool: search_repository_text
19:13:24.541         chat fallback:minimax/minimax-m3,z-ai/glm-5.2:exacto
19:13:27.752         running tool: search_repository_text
19:13:27.774         chat fallback:minimax/minimax-m3,z-ai/glm-5.2:exacto
19:13:35.151         running tool: shell
19:13:35.192     chat fallback:z-ai/glm-5.2:exacto,moonshotai/kimi-k2.7-code
19:13:39.153     running tool: shell
19:13:39.168     chat fallback:z-ai/glm-5.2:exacto,moonshotai/kimi-k2.7-code
19:13:41.939     running tool: shell
19:13:41.963     chat fallback:z-ai/glm-5.2:exacto,moonshotai/kimi-k2.7-code
19:17:36.450     running tool: write_plan
19:17:36.460     chat fallback:z-ai/glm-5.2:exacto,moonshotai/kimi-k2.7-code
19:17:53.419     running tool: write_plan
19:17:53.429     chat fallback:z-ai/glm-5.2:exacto,moonshotai/kimi-k2.7-code
19:18:00.764     running tool: shell
19:18:00.785     chat fallback:z-ai/glm-5.2:exacto,moonshotai/kimi-k2.7-code
19:18:09.349     running tool: write_plan
19:18:09.360     chat fallback:z-ai/glm-5.2:exacto,moonshotai/kimi-k2.7-code
19:18:11.682     running tool: shell
19:18:11.699     chat fallback:z-ai/glm-5.2:exacto,moonshotai/kimi-k2.7-code
19:18:17.398     chat fallback:z-ai/glm-5.2:exacto,moonshotai/kimi-k2.7-code
AGENT_ERROR (relevant leg):
  File "/home/pydanty/dogfooding/.runner.py", line 206, in <module>
    asyncio.run(main())
  File "/home/pydanty/dogfooding/.runner.py", line 182, in main
    result = await result
             ^^^^^^^^^^^^
UnexpectedModelBehavior: Tool 'post_review_outcome' exceeded max retries count of 1

@dsfaccini dsfaccini removed pydanty:review-branch Trigger pydanty review-branch auto-review on this PR pydanty:is-working Pydanty is mid-flight reviewing/remediating this PR; do not edit until it clears labels Jun 30, 2026
@dsfaccini

dsfaccini commented Jun 30, 2026 •

Copy link
Copy Markdown
Contributor

David's AICA here: 🔍 Local review suite (automated, pre-merge)

Wave Status Blocking Required (do) Warnings (defer/skip)
1 · code-correctness ✅ closed 0 0 0
2 · tests/cassettes/docs ✅ closed 0 0 0
3 · cross-cutting/siblings ✅ closed 0 0 0

Verdict: ✅ all 3 waves passed, 0 blocking, 0 required
Reviewed at 2026-07-02T19:17:15Z · diff 794ac1aed048

pragnyanramtha and others added 3 commits June 30, 2026 13:07
- _write_cached_file: wrap both the write and os.replace in try/except so
  the delete=False temp file is unlinked on any failure (incl. write error),
  not just os.replace failure; flush before the rename so the temp holds
  complete content at replace time.
- Add docstrings to _read_cached_file / _write_cached_file.
- Dedupe the MockAsyncClient/MockResponse doubles behind a shared
  _stub_cdn_fetch helper used by the cache tests.
@dsfaccini dsfaccini added the douwebot reviews the PR that gets labeled and removes the label at the end label Jul 1, 2026
Comment thread tests/test_ui_web.py Resolved
@github-actions github-actions Bot removed the douwebot reviews the PR that gets labeled and removes the label at the end label Jul 1, 2026
@coderabbitai

coderabbitai Bot commented Jul 2, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This change updates UI HTML caching to use atomic file writes. New helpers treat missing or empty cache files as misses and write via a same-directory temp file plus os.replace, with cleanup on failure. _get_ui_html() now uses those helpers for both the default CDN path and custom URL path. Tests add cache isolation, a shared CDN fetch stub, and coverage for empty-cache refetching, rename failure cleanup, and atomic write behavior.

Related PRs: None specified.

Suggested labels: area: ui, testing

Suggested reviewers: None specified.

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main change: atomic writes for cached web UI HTML.
Description check ✅ Passed The description covers the summary, testing, issue reference, and checklist; it is mostly complete for the template.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
pydantic_ai_slim/pydantic_ai/ui/_web/app.py (1)

100-108: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicated fetch-and-cache flow between the CDN and custom-URL branches.

Both branches now execute the same _read_cached_file → httpx fetch → raise_for_status → _write_cached_file sequence. Since both paths were touched in this PR, consider extracting a small _fetch_and_cache(url, cache_file) helper to avoid future drift between the two copies.

♻️ Proposed consolidation
+async def _fetch_and_cache(url: str, cache_file: Path) -> bytes:
+    if content := _read_cached_file(cache_file):
+        return content
+
+    async with httpx.AsyncClient() as client:
+        response = await client.get(url)
+        response.raise_for_status()
+        content = response.content
+
+    _write_cached_file(cache_file, content)
+    return content

Then call await _fetch_and_cache(DEFAULT_HTML_URL, cache_file) and await _fetch_and_cache(html_source, cache_file) from the two branches respectively.

Also applies to: 124-132

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pydantic_ai_slim/pydantic_ai/ui/_web/app.py` around lines 100 - 108, Both the
default HTML and custom HTML source branches in app.py duplicate the same
cache-read, HTTP fetch, status check, and cache-write flow. Extract that logic
into a shared helper such as _fetch_and_cache(url, cache_file) and update the
existing branches in the HTML-loading function to call it with DEFAULT_HTML_URL
or html_source so the behavior stays in one place and does not drift.
tests/test_ui_web.py (1)

194-223: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Migration to _stub_cdn_fetch is incomplete.

Good shared-helper extraction, but per the PR discussion, older inline MockResponse/MockAsyncClient setups elsewhere in this file (still present per current graph evidence) haven't been migrated to this helper yet, leaving duplicated mocking logic. Worth finishing the migration (or explicitly deferring) to avoid the two patterns diverging over time.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_ui_web.py` around lines 194 - 223, The migration to the shared CDN
fetch stub is only partially complete, and this file still has duplicated inline
`MockResponse`/`MockAsyncClient` mocking logic alongside `_stub_cdn_fetch`.
Update the remaining tests to use `_stub_cdn_fetch` instead of their local
`httpx.AsyncClient` stubs, or clearly defer those cases, so `_get_ui_html`
cache/CDN branch tests all share the same helper and stay consistent.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@pydantic_ai_slim/pydantic_ai/ui/_web/app.py`:
- Around line 64-79: Move the atomic rename in _write_cached_file outside the
NamedTemporaryFile context so the temp file is closed before os.replace runs;
the current flow keeps tmp_file open during the replace, which can fail on
Windows. Keep the existing cleanup behavior by still unlinking tmp_path on any
exception, but ensure the write/flush happens inside the with block and the
replace happens after the file handle is closed.

---

Nitpick comments:
In `@pydantic_ai_slim/pydantic_ai/ui/_web/app.py`:
- Around line 100-108: Both the default HTML and custom HTML source branches in
app.py duplicate the same cache-read, HTTP fetch, status check, and cache-write
flow. Extract that logic into a shared helper such as _fetch_and_cache(url,
cache_file) and update the existing branches in the HTML-loading function to
call it with DEFAULT_HTML_URL or html_source so the behavior stays in one place
and does not drift.

In `@tests/test_ui_web.py`:
- Around line 194-223: The migration to the shared CDN fetch stub is only
partially complete, and this file still has duplicated inline
`MockResponse`/`MockAsyncClient` mocking logic alongside `_stub_cdn_fetch`.
Update the remaining tests to use `_stub_cdn_fetch` instead of their local
`httpx.AsyncClient` stubs, or clearly defer those cases, so `_get_ui_html`
cache/CDN branch tests all share the same helper and stay consistent.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: d32b211b-b4db-4e08-a83d-6d41aa01e1cc

📥 Commits

Reviewing files that changed from the base of the PR and between 1a44ec2 and 370ddad.

📒 Files selected for processing (2)
  • pydantic_ai_slim/pydantic_ai/ui/_web/app.py
  • tests/test_ui_web.py
🔗 Linked repositories identified

CodeRabbit considers these linked repositories for cross-repo context during reviews:

  • pydantic/logfire (auto-detected)
  • pydantic/pydantic (auto-detected)

dsfaccini added 2 commits July 2, 2026 12:34
…cache write

On Windows, `os.replace` on a `NamedTemporaryFile` whose handle is still open raises `PermissionError` (WinError 32), so the atomic web UI HTML cache write in `_write_cached_file` failed there. Close the handle before the rename, and add a regression test pinning the close-before-replace ordering.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/test_ui_web.py`:
- Around line 337-370: The `capturing_named_temporary_file` helper uses a `dir`
keyword parameter to mirror the `tempfile.NamedTemporaryFile(dir=...)` call in
`_write_cached_file`, but that shadows the builtin and triggers Ruff A002. Keep
the parameter name unchanged so the monkeypatch still matches the production
call, and suppress the lint on that parameter or function with an explicit `#
noqa` annotation.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: dcaf2f98-ee7a-4488-afb2-657093ca97b0

📥 Commits

Reviewing files that changed from the base of the PR and between 370ddad and b86cbc2.

📒 Files selected for processing (2)
  • pydantic_ai_slim/pydantic_ai/ui/_web/app.py
  • tests/test_ui_web.py
🔗 Linked repositories identified

CodeRabbit considers these linked repositories for cross-repo context during reviews:

  • pydantic/logfire (auto-detected)
  • pydantic/pydantic (auto-detected)
🚧 Files skipped from review as they are similar to previous changes (1)
  • pydantic_ai_slim/pydantic_ai/ui/_web/app.py

Comment thread tests/test_ui_web.py Resolved
@adtyavrdhn adtyavrdhn mentioned this pull request Jul 2, 2026
3 tasks done
@dsfaccini
dsfaccini merged commit 80017c7 into pydantic:main Jul 3, 2026
94 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

chore size: M Medium PR (101-500 weighted lines) web-ui

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants