KeenPhoenixSvelte.Apps.Proxy (KeenPhoenixSvelte v1.0.0-rc.9)

Copy Markdown View Source

Serves a registered app's bundle same-origin by fetching it upstream on the server. This is the :proxy mode from KeenPhoenixSvelte.Apps — it turns a cross-origin CDN bundle into a first-party asset so it isn't subject to CORS or a strict CSP script-src, and lets you gate or cache it.

Mounting

Forward the :proxy_path (defaults to /apps) to this plug in your router:

forward "/apps", KeenPhoenixSvelte.Apps.Proxy

A request resolves via KeenPhoenixSvelte.Apps.resolve/1, serves the cached bundle, and revalidates it upstream when it goes stale. Both shapes are handled:

  • /apps/<name> — a single-file app (url:), served as text/javascript.
  • /apps/<name>/<sub-path> — a base-path app (base:); the sub-path is appended to the upstream directory, so a whole JS + CSS + assets bundle proxies through one registration. Each file's Content-Type is derived from its extension (.mjs/.jstext/javascript, .csstext/css, else MIME), while JS is always forced to a module-friendly type.

The default prefix is the same /apps that local bundles load from. That's intentional and safe: Plug.Static runs before the router, so local files at /apps/<name>/main.mjs are served directly, and only unmatched paths (/apps/<name>, the proxied bundles) fall through to this plug.

Caching & freshness

The bytes are cached and revalidated by KeenPhoenixSvelte.Apps.ProxyCache (ETS + single-flight conditional GET). Freshness is driven by the upstream's own Cache-Control / ETag / Last-Modified when present, and falls back to a :ttl (default 5 min) otherwise — so unversioned upstreams (cdn/app.js) are re-checked on a cadence instead of being pinned forever. See KeenPhoenixSvelte.Apps for the :proxy_cache config and per-app ttl / immutable / client_cache_control overrides.

On the way out this plug forwards an ETag and a revalidate-friendly Cache-Control, and answers the browser's own If-None-Match with a 304 — completing a browser → Phoenix → origin conditional-request chain. The Cache-Control value is resolved most-specific-first: a per-app client_cache_control: string, else a per-app immutable: true (whose value is the global immutable_cache_control, defaulting to a 1-year immutable string), else the global client_cache_control, else the built-in default.

Fetching

The upstream fetch uses Erlang's built-in :httpc by default. Override it for tests or a different client with an :app_provider — either a 1-arity fn url -> {:ok, body_binary} end (legacy; always treated as a fresh 200) or a 2-arity fn url, validators -> {:ok, resp} | :not_modified | {:error, reason} end that can honor validators.etag / validators.last_modified for conditional revalidation (resp is a map with :body and optional :etag, :last_modified, :cache_control):

config :keen_phoenix_svelte,
  app_provider: fn _url, _validators -> {:ok, %{body: "export default 1;"}} end