Merge remote-tracking branch 'upstream/develop' into registration-workflow
[akkoma] / lib / pleroma / web / plugs / cache.ex
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
4
5 defmodule Pleroma.Web.Plugs.Cache do
6 @moduledoc """
7 Caches successful GET responses.
8
9 To enable the cache add the plug to a router pipeline or controller:
10
11 plug(Pleroma.Web.Plugs.Cache)
12
13 ## Configuration
14
15 To configure the plug you need to pass settings as the second argument to the `plug/2` macro:
16
17 plug(Pleroma.Web.Plugs.Cache, [ttl: nil, query_params: true])
18
19 Available options:
20
21 - `ttl`: An expiration time (time-to-live). This value should be in milliseconds or `nil` to disable expiration. Defaults to `nil`.
22 - `query_params`: Take URL query string into account (`true`), ignore it (`false`) or limit to specific params only (list). Defaults to `true`.
23 - `tracking_fun`: A function that is called on successfull responses, no matter if the request is cached or not. It should accept a conn as the first argument and the value assigned to `tracking_fun_data` as the second.
24
25 Additionally, you can overwrite the TTL inside a controller action by assigning `cache_ttl` to the connection struct:
26
27 def index(conn, _params) do
28 ttl = 60_000 # one minute
29
30 conn
31 |> assign(:cache_ttl, ttl)
32 |> render("index.html")
33 end
34
35 """
36
37 import Phoenix.Controller, only: [current_path: 1, json: 2]
38 import Plug.Conn
39
40 @behaviour Plug
41
42 @defaults %{ttl: nil, query_params: true}
43
44 @impl true
45 def init([]), do: @defaults
46
47 def init(opts) do
48 opts = Map.new(opts)
49 Map.merge(@defaults, opts)
50 end
51
52 @impl true
53 def call(%{method: "GET"} = conn, opts) do
54 key = cache_key(conn, opts)
55
56 case Cachex.get(:web_resp_cache, key) do
57 {:ok, nil} ->
58 cache_resp(conn, opts)
59
60 {:ok, {content_type, body, tracking_fun_data}} ->
61 conn = opts.tracking_fun.(conn, tracking_fun_data)
62
63 send_cached(conn, {content_type, body})
64
65 {:ok, record} ->
66 send_cached(conn, record)
67
68 {atom, message} when atom in [:ignore, :error] ->
69 render_error(conn, message)
70 end
71 end
72
73 def call(conn, _), do: conn
74
75 # full path including query params
76 defp cache_key(conn, %{query_params: true}), do: current_path(conn)
77
78 # request path without query params
79 defp cache_key(conn, %{query_params: false}), do: conn.request_path
80
81 # request path with specific query params
82 defp cache_key(conn, %{query_params: query_params}) when is_list(query_params) do
83 query_string =
84 conn.params
85 |> Map.take(query_params)
86 |> URI.encode_query()
87
88 conn.request_path <> "?" <> query_string
89 end
90
91 defp cache_resp(conn, opts) do
92 register_before_send(conn, fn
93 %{status: 200, resp_body: body} = conn ->
94 ttl = Map.get(conn.assigns, :cache_ttl, opts.ttl)
95 key = cache_key(conn, opts)
96 content_type = content_type(conn)
97
98 conn =
99 unless opts[:tracking_fun] do
100 Cachex.put(:web_resp_cache, key, {content_type, body}, ttl: ttl)
101 conn
102 else
103 tracking_fun_data = Map.get(conn.assigns, :tracking_fun_data, nil)
104 Cachex.put(:web_resp_cache, key, {content_type, body, tracking_fun_data}, ttl: ttl)
105
106 opts.tracking_fun.(conn, tracking_fun_data)
107 end
108
109 put_resp_header(conn, "x-cache", "MISS from Pleroma")
110
111 conn ->
112 conn
113 end)
114 end
115
116 defp content_type(conn) do
117 conn
118 |> Plug.Conn.get_resp_header("content-type")
119 |> hd()
120 end
121
122 defp send_cached(conn, {content_type, body}) do
123 conn
124 |> put_resp_content_type(content_type, nil)
125 |> put_resp_header("x-cache", "HIT from Pleroma")
126 |> send_resp(:ok, body)
127 |> halt()
128 end
129
130 defp render_error(conn, message) do
131 conn
132 |> put_status(:internal_server_error)
133 |> json(%{error: message})
134 |> halt()
135 end
136 end