Public: anyone can read this; the crew can change it.

Files render in the browser, part 1: a safe media route and one registry of file kinds #7330

shipped production is serving it opened this has it
opened this

This is the foundation for #7329. Every renderer is blocked by it.

1. A media route: GET /:owner/:name/media/:ref/*path. It serves a file's bytes with a real, allowlisted content type, so the browser can show them in <img>, <video>, <audio>, <object> and fetch():

  • Allowed types: png, jpeg, gif, webp, avif, bmp, ico, svg (image/svg+xml), pdf, mp3, ogg/oga, wav, flac, m4a/aac, mp4/m4v, webm, mov, plus model/stl and model/gltf-binary. These are decided by extension and confirmed by magic bytes where a format has them. A mismatch falls back to application/octet-stream.
  • Never text/html or JavaScript. Send X-Content-Type-Options: nosniff and Content-Security-Policy: sandbox; default-src 'none'; img-src 'self' data:; media-src 'self'; style-src 'unsafe-inline', so an SVG with a <script> renders as an image and never runs.
  • HTTP Range: answer 206 with Accept-Ranges: bytes, so video and audio can seek and a PDF can stream. Test a single range, a suffix range and an unsatisfiable range (416).
  • Caching: an ETag of the blob id, and Cache-Control: private, max-age=31536000, immutable when :ref is a full commit sha, otherwise short.
  • Access is the same as /raw. A private repo stays private, and the route is tested signed out.
  • Big files stream from git or the bucket, and are never buffered whole. Cap per kind, and over the cap the page offers Download.

2. One registry of file kinds: src/web/render/kinds.ts. fileKind(path, firstBytes) returns image | svg | pdf | audio | video | csv | model3d | config | markdown | text | binary. Each kind maps to a renderer module that a later issue fills in.

  • renderBlob asks the registry. A kind with no renderer yet keeps today's view, so nothing regresses.
  • The blob page header gets the switch Rendered | Source | Raw | Download, generalising Markdown's ?view=source.

Tests:

  • Content type per extension, and a magic-byte mismatch falls back.
  • An SVG with a script is served under the sandbox CSP.
  • 206 ranges and the 416 case.
  • A private repo returns 404 signed out.
  • A 304 on the ETag.
  • The registry maps every listed extension.

Shared rules for every issue in this epic (#7329):

  • Each renderer lives in its own file under src/web/render/<kind>.ts and registers itself in the kind registry from #7330. Don't grow src/web/pages/repo.ts. It's a hot file, and five open pulls on it stall the crew.
  • No CDN and no remote scripts (the forge CSP). Any library is vendored the way src/web/og/vendor is, with its licence, and loaded only on the page that needs it.
  • Repo content never runs as script on the forge origin. Bytes come from the media route in #7330, which has its own sandbox CSP and nosniff.
  • Every view keeps the Rendered | Source | Raw | Download switch, like Markdown's ?view=source, so nothing is lost.
  • It works at phone width (390 px) and in both themes. Tests cover the HTML shape and the bytes route, not pixels.
  • It stays fast: the blob page's existing budget holds. Heavy viewers load their script lazily.
Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs