Skip to content

fix(#673): make PDF preview work on real Chrome (desktop + mobile)#674

Merged
Kewton merged 8 commits intomainfrom
develop
Apr 22, 2026
Merged

fix(#673): make PDF preview work on real Chrome (desktop + mobile)#674
Kewton merged 8 commits intomainfrom
develop

Conversation

@Kewton
Copy link
Copy Markdown
Owner

@Kewton Kewton commented Apr 22, 2026

Summary

  • Fix Issue pdfビューワ #673 PDF preview which was blocked by Chrome in both desktop and mobile environments
  • Desktop: "このページは Chrome によってブロックされています" caused by CSP (frame-ancestors 'none', missing data: in connect-src), X-Frame-Options: DENY, and sandbox="allow-scripts" colliding with Chrome's built-in PDF viewer MIME-handler navigation
  • Mobile: "このコンテンツはブロックされました" because mobile Chrome lacks an in-page PDF viewer entirely — route PDFs to an open-in-new-tab / download UI instead of an iframe

Changes

CSP / framing (next.config.js)

  • Add data: to connect-src so PdfPreview's fetch(dataUri) passes
  • X-Frame-Options: DENYSAMEORIGIN (allow same-origin blob iframes)
  • CSP frame-ancestors 'none''self' (keep external clickjacking protection, allow our own blob iframe)

PdfPreview (src/components/worktree/PdfPreview.tsx + src/config/pdf-extensions.ts)

  • Drop the iframe sandbox attribute entirely — every value (even allow-scripts allow-same-origin) collides with Chrome's PDF MIME-handler navigation. Clickjacking protection is preserved at the host-page level (X-Frame-Options + frame-ancestors)
  • Add variant prop: iframe (default, desktop) vs download (mobile) with PDFを新しいタブで開く / ダウンロード buttons that point at the Blob URL

Mobile viewer (src/components/worktree/FileViewer.tsx)

  • Add content.isPdf branch that renders PdfPreview in download variant, so mobile taps hand the PDF off to the OS PDF handler instead of hitting the iframe block

Tests

  • tests/unit/config/pdf-extensions.test.ts: PDF_IFRAME_SANDBOX is now undefined
  • tests/unit/components/PdfPreview.test.tsx: iframe no longer has a sandbox attribute

Verification

  • Desktop Chrome on http://localhost:3000/worktrees/mywebdata-main: PDF renders with thumbnails, toolbar, zoom (verified via Playwright MCP after each fix)
  • Mobile Chrome screenshot from the user shows the block message was caused by mobile-specific viewer gap; the new download-variant UI side-steps that limitation

Test plan

  • Desktop Chrome: open a .pdf in the Files tab and confirm the iframe renders with thumbnail / toolbar
  • Desktop Firefox: confirm built-in pdf.js viewer still renders the iframe variant
  • Mobile Chrome (Android): confirm the download-variant UI appears, PDFを新しいタブで開く opens the PDF in the OS handler, and ダウンロード saves the file with the correct name
  • iOS Safari: confirm the same mobile UI works (opens via Quick Look / Safari PDF viewer)
  • Existing non-PDF file types (images, videos, HTML, MARP, markdown, code) still render unchanged
  • npm run test:unit passes (PdfPreview / pdf-extensions tests updated)

🤖 Generated with Claude Code

Kewton and others added 8 commits April 17, 2026 00:29
chore: merge release v0.5.5 to develop
`src/lib/ws-server.ts` の upgrade ハンドラに `/proxy/<prefix>/...` 分岐を追加し、
対応する external_apps レコードが enabled かつ websocket_enabled の場合は
upstream へ raw TCP で WebSocket をパススルーするようにした。これにより
Streamlit/Shiny 等 WebSocket を使う External App が CommandMate プロキシ越しでも
完全に起動できるようになる。

主な変更:
- src/lib/ws-server.ts: handleProxyUpgrade() を DI 構成で実装し、`/proxy/<prefix>` を
  upstream 解決→TCP 接続→双方向パイプ
- SSRF 防御: PROXY_ALLOWED_HOSTS = {'localhost', '127.0.0.1'} allow-list
- src/lib/proxy/handler.ts: 旧 proxyWebSocket() を @deprecated 化 (実際には
  Route Handler に upgrade は到達しないため dead code)
- src/app/proxy/[...path]/route.ts: HEAD メソッドを追加 (ヘルスチェック向け)
- next.config.js: skipTrailingSlashRedirect: true を設定し
  `/proxy/<prefix>/` の 308 を抑止
- tests/unit/ws-server-proxy-upgrade.test.ts (11 ケース) と
  tests/unit/proxy/route.test.ts (HEAD ケース) を追加

品質: lint / tsc / 6342 unit / 179 regression すべて pass。
新規コードパスのカバレッジ ~92%。

Closes #671

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
fix(#671): proxy WebSocket upgrades to External Apps
Node's fetch transparently decompresses the upstream body (Node 18+
undici default). Forwarding the upstream Content-Encoding: gzip along
with the already-decompressed body causes browsers to fail with
ERR_CONTENT_DECODING_FAILED, resulting in a blank page (observed on
Streamlit-based External Apps, e.g. Photon via /proxy/photon/).

Add content-encoding and content-length to HOP_BY_HOP_RESPONSE_HEADERS
so they are filtered out alongside the existing hop-by-hop entries.
The Node response layer re-adds correct framing (chunked or computed
length) for the decoded body, and the browser parses correctly.

Regressions via curl were hidden because curl omits Accept-Encoding
by default; the upstream then skips compression and the bug does not
manifest.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Apply minor refactoring to the Issue #673 PDF viewer implementation:

- src/config/pdf-extensions.ts: Reuse normalizeExtension from
  image-extensions.ts (same pattern as video-extensions.ts), removing
  7 lines of duplicated normalization logic.
- src/app/api/worktrees/[id]/files/[...path]/route.ts: Remove
  WHAT-level comments in the PDF branch (e.g. "Read file as binary",
  "magic bytes failure") whose intent is self-evident from the
  identifiers and control flow.
- src/components/worktree/PdfPreview.tsx: Remove one WHAT-level
  comment explaining state-reset lines that are already obvious.

Quality Metrics:
- Lint: pass
- TypeScript: pass
- Unit tests: 6381 passing (unchanged)

Refs #673

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Integrate the remaining PDF viewer pieces with the API/component/config
modules already landed in e7ea044. PDF files in the Files tab now
render via an iframe backed by a Blob URL, matching the image/video
preview pattern while honoring PDF-specific security constraints.

- src/types/models.ts: Add isPdf?: boolean to FileContent for the new
  routing signal from the file fetch API.
- src/components/worktree/FilePanelContent.tsx: Route isPdf responses
  to a dynamically-imported PdfPreview between the video and HTML
  branches.
- src/hooks/useFileContentPolling.ts: Disable auto-polling for PDF tabs
  to avoid refetching 20MB payloads on every tick (S3-003).
- next.config.js: Extend frame-src with blob: so Blob URL iframes can
  load (Issue #490 DR4-007 retracted for #673 PDF preview).
- tests/helpers/pdf-fixtures.ts: Shared helpers for minimal/broken PDF
  buffers used by the new pdf-extensions, API and PdfPreview tests.
- tests/unit/config/pdf-extensions.test.ts,
  tests/unit/api/files-pdf.test.ts,
  tests/unit/components/PdfPreview.test.tsx,
  tests/unit/components/FilePanelContent.test.tsx: Cover validation,
  API branches (PDF_SIZE_EXCEEDED / INVALID_MAGIC_BYTES / ENOENT),
  iframe attributes, Blob URL lifecycle and routing.
- CLAUDE.md: Index the new pdf-extensions.ts module.

Refs #673

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ptance)

Persist the PM automation artifacts produced during
/pm-auto-issue2dev 673 so that the review findings, design
decisions, and acceptance verification remain auditable.

- issue-review/: Stage 1 (normal) and Stage 3 (impact) review results
  with 15 + 14 findings applied to the Issue body (Stages 5-8 Codex
  delegation skipped per session policy).
- work-plan.md: Scoped work plan that defers PoC, method B streaming,
  i18n and mobile-specific work to follow-up issues.
- pm-auto-dev/iteration-1/: TDD context + result JSON, acceptance
  verification (15/15 passed), refactor notes.
- hypothesis-verification.md and summary-report.md for traceability.

Refs #673

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Issue #673 iframe-based PDF preview was blocked across the board:
- Desktop Chrome showed "このページは Chrome によってブロックされています"
  because CSP `frame-ancestors 'none'` + `X-Frame-Options: DENY` plus the
  `sandbox="allow-scripts"` iframe collided with Chrome's built-in PDF
  viewer MIME-handler navigation, and `connect-src` rejected the
  PdfPreview `fetch(dataUri)`.
- Mobile Chrome additionally lacks an in-page PDF viewer entirely and
  shows "このコンテンツはブロックされました" for any iframe-rendered PDF.

Adjust the stack end-to-end:
- next.config.js: allow `data:` in `connect-src`, downgrade
  `X-Frame-Options` to `SAMEORIGIN`, and loosen CSP `frame-ancestors`
  to `'self'` so same-origin blob iframes are permitted while external
  clickjacking remains blocked.
- PdfPreview: drop the `sandbox` attribute (any value conflicts with
  Chrome's PDF MIME-handler) and introduce a `variant` prop — `iframe`
  for desktop, `download` for mobile where tapping opens the PDF in the
  OS/browser PDF handler or downloads it.
- FileViewer (mobile): route PDFs through the new `download` variant.
- Tests updated for the new sandbox semantics.
@Kewton Kewton merged commit 8b21438 into main Apr 22, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant