Packages

Custom Credo checks for common Elixir/Phoenix anti-patterns and CWE Top 25 security vulnerabilities. Provides 40 comprehensive checks covering code quality, error handling, performance, change risk, telemetry, and security (SQL injection, XSS, path traversal, CSRF, SSRF, hardcoded credentials, etc).

Current section

Files

Jump to
oeditus_credo lib credo check warning blocking_in_plug.ex
Raw

lib/credo/check/warning/blocking_in_plug.ex

defmodule OeditusCredo.Check.Warning.BlockingInPlug do
@moduledoc """
Detects expensive blocking operations (Repo, HTTP, File) inside Plug
functions that accept `conn` as the first argument.
Blocking in a plug serialises the entire request pipeline; move heavy
work to the controller action, a background job, or an async task.
"""
use Credo.Check,
base_priority: :normal,
category: :warning,
explanations: [
check: """
Expensive blocking operations in Plug functions slow down request processing.
Move expensive operations to background jobs or async tasks.
Bad:
plug :load_user_data
def load_user_data(conn, _opts) do
user = Repo.get!(User, conn.assigns.user_id)
assign(conn, :user, user)
end
Good:
# Load user data in the controller action instead
def show(conn, params) do
user = Repo.get!(User, params["id"])
render(conn, "show.html", user: user)
end
""",
params: [
exclude_test_files: "Set to true to skip test files (default: false)",
extra_blocking_modules: "Additional module atoms to treat as blocking (default: [])"
]
]
@default_blocking_modules [:Repo, :HTTPoison, :Req, :File]
import OeditusCredo.Helpers, only: [test_file?: 1]
@doc false
@impl true
def run(%SourceFile{}, false), do: []
def run(%SourceFile{} = source_file, params) do
issue_meta = IssueMeta.for(source_file, params)
if Params.get(params, :exclude_test_files, __MODULE__) and
test_file?(source_file.filename) do
[]
else
extra = Params.get(params, :extra_blocking_modules, __MODULE__)
blocking = @default_blocking_modules ++ extra
source_file
|> Credo.Code.prewalk(&traverse(&1, &2, {issue_meta, blocking}))
end
end
@doc false
@impl true
def param_defaults, do: [exclude_test_files: false, extra_blocking_modules: []]
# Check functions that might be used as plugs (accept conn as first arg)
defp traverse(
{:def, meta, [{func_name, _, [{:conn, _, _} | _rest]}, [do: body]]} = ast,
issues,
{issue_meta, blocking}
) do
issues =
if has_blocking_calls?(body, blocking) do
[issue_for(issue_meta, meta[:line], func_name) | issues]
else
issues
end
{ast, issues}
end
defp traverse(ast, issues, _ctx) do
{ast, issues}
end
defp has_blocking_calls?({:__block__, _, statements}, blocking) when is_list(statements) do
Enum.any?(statements, &has_blocking_calls?(&1, blocking))
end
defp has_blocking_calls?({{:., _, [{:__aliases__, _, aliases}, _func]}, _, _}, blocking) do
List.last(aliases) in blocking
end
defp has_blocking_calls?({_form, _, args}, blocking) when is_list(args) do
Enum.any?(args, &has_blocking_calls?(&1, blocking))
end
defp has_blocking_calls?(_, _blocking), do: false
defp issue_for(issue_meta, line_no, func_name) do
format_issue(
issue_meta,
message: "Blocking operation in plug function #{func_name} - consider moving to controller",
trigger: "#{func_name}",
line_no: line_no
)
end
end