Packages

Plug for generating Early Hints response

Current section

Files

Jump to
plug_early_hints lib plug_early_hints.ex
Raw

lib/plug_early_hints.ex

defmodule PlugEarlyHints do
@moduledoc """
Convenience plug for sending [HTTP 103 Early Hints][mdn-103].
This is useful for static resources that will be **for sure** required by
the resulting page. For example you can use it for informing the client
that you will need CSS later, so it can start fetching it right now.
## Usage
plug #{inspect(__MODULE__)},
# List all resources that will be needed later when rendering page
paths: [
# External resources that will be connected to as we will use
# different resources from it. It will speedup as the TLS handshake
# will be already ended, so we will be able to fetch resources
# right away
"https://gravatar.com/": [rel: "dns-prefetch"],
"https://gravatar.com/": [rel: "preconnect"],
# "Regular" resources. We need to set `:as` to inform the client
# (browser) what kinf of resource it is, so it will be able to
# properly connect them
"/css/app.css": [rel: "preload", as: "style"],
"/js/app.js": [rel: "preload", as: "script"],
# Preloading fonts will require additional `:type` and `:crossorgin`
# to allow CSS engine to properly detect when apply the resource as
# well as to prevent double load.
"/fonts/recursive.woff2": [
rel: "preload",
as: "font",
crossorgin: :anonymous,
type: "font/woff2"
]
]
For more information about available options check out [MDN `Link`][mdn-link].
## Options
- `:paths` - enumerable containing pairs in form of `{path, options}`.
- `:callback` - 2-ary function used for expanding `path` value from `:paths`.
It is useful for example to expand static assets in Phoenix applications.
Due to nature of the `Plug` it must be in form of `&Module.function/2`
(it cannot be `&function/2` nor `fn conn, path -> … end`).
1st argument will be `conn` passed to the plug and 2nd argument will be
current path. By default it return `path` unmodified.
- `:enable` - 1-ary function that will receive `conn` and should return boolean
whether the early hints should be sent or not. You mostly want to do it only
for requests returning HTML. The same rules as in `:callback` apply. By default
uses function that alwayst return `true`.
[mdn-103]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/103 "103 Early Hints"
[mdn-link]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Link "Link"
"""
@behaviour Plug
@impl true
def init(opts) do
enable = Keyword.get(opts, :enable, &__MODULE__.__true__/1)
paths = Keyword.fetch!(opts, :paths)
cb = Keyword.get(opts, :callback, &__MODULE__.__id__/2)
%{
paths: paths,
callback: cb,
enable: enable
}
end
@impl true
def call(conn, %{paths: paths, callback: cb, enable: enable}) do
if enable.(conn) and :"HTTP/2" == Plug.Conn.get_http_protocol(conn) do
headers =
for {path, args} <- paths,
path = cb.(conn, to_string(path)),
not is_nil(path),
do: {"link", encode(path, args)}
Plug.Conn.inform(conn, :early_hints, headers)
else
conn
end
end
defp encode(path, args) do
encoded_args =
args
|> Enum.map(fn {name, value} -> ~s[#{name}=#{value}] end)
|> Enum.join("; ")
"<#{path}>; " <> encoded_args
end
@doc false
def __true__(_conn), do: true
@doc false
def __id__(_conn, path), do: path
end