Turn a pasted URL into a preview card image.
Four steps, each of which can fail without taking the paste with it — the caller falls back to leaving the link on the canvas as text:
fetch/1— GET the page, under SSRF, size and time limits.metadata/2— read its OpenGraph tags, falling back to<title>.scene/1— lay the card out as anOpenFresco.Scene.svg/1— emit it as a self-contained SVG.
Why SVG and not PNG
open_fresco can rasterise, but only through the optional :resvg NIF or a
resvg / rsvg-convert / magick binary on PATH — and "magick is installed" is
not the same as "magick can draw an SVG": without its rsvg delegate it
falls back to an internal renderer that can't load fonts and emits zero
bytes, while still reporting a rasterizer as available.
The browser already has a correct SVG renderer with the right fonts, so it does that half: the client draws this SVG onto a canvas, exports a PNG, and sends it through the ordinary image-upload path. No native dependency, and the card ends up in storage like any other pasted picture.
The hero image is inlined as a data: URL for the same reason it has to
be: an SVG referencing a remote image would make the renderer fetch it,
which is the hole the SSRF guard here exists to close.
Why the fetch is the careful part
The URL comes from whatever a user pasted, and the request is made by the
server, from inside the network the server lives in. Unguarded that is a
textbook SSRF: paste http://169.254.169.254/… and the reply is the cloud
instance's credentials. So the host is resolved and every address it maps
to is checked against the private, loopback, link-local and CGNAT ranges
before a byte is sent — and again on each redirect, because a public host
is free to redirect to a private one.
The hero image is fetched the same way, by the same walk — an og:image
is as attacker-controlled as the page, and a hero that redirects into the
private range would reach it just as surely.
The rest is proportion: a 12s budget for the whole unfurl, a 2MB cap enforced as the body streams rather than after it has all arrived, and HTML only. A link preview is a nicety, and none of it is worth a hung request or a page of arbitrary size buffered into memory.
Known limit
The guard resolves the host and then asks Req for the same host by name,
so a DNS answer that changes between the two — rebinding — is not caught.
Closing that means pinning the checked address and carrying the original
host through as a header and SNI name, which is a lot of machinery for a
surface only an admin can reach. It is a deliberate omission, not an
oversight.
Summary
Functions
Unfurl url into {:ok, %{svg: binary, width: integer, height: integer}}.