Skip to content

Commit 2c91ec6

Browse files
authored
fix: intermittent webhook conn malfunctions (#53)
1 parent c96a284 commit 2c91ec6

File tree

5 files changed

+66
-63
lines changed

5 files changed

+66
-63
lines changed

lib/algora/integrations/github/webhook.ex

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,16 +22,20 @@ defmodule Algora.Github.Webhook do
2222

2323
defstruct @enforce_keys
2424

25-
def new(conn, payload) do
25+
def new(conn) do
2626
secret = Algora.Github.webhook_secret()
2727

2828
with {:ok, headers} <- parse_headers(conn),
29-
{:ok, _} <- verify_signature(headers.signature_256, conn.assigns[:raw_body], secret) do
30-
build_webhook(headers, payload)
29+
{:ok, payload, conn} = Plug.Conn.read_body(conn),
30+
{:ok, _} <- verify_signature(headers.signature_256, payload, secret),
31+
{:ok, webhook} <- build_webhook(headers, payload) do
32+
{:ok, webhook, conn}
3133
end
3234
end
3335

3436
defp build_webhook(headers, payload) do
37+
payload = Jason.decode!(payload)
38+
3539
params =
3640
headers
3741
|> Map.put(:payload, payload)

lib/algora_web/controllers/webhooks/github_controller.ex

Lines changed: 0 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -19,34 +19,6 @@ defmodule AlgoraWeb.Webhooks.GithubController do
1919
# TODO: persist & alert about failed deliveries
2020
# TODO: auto-retry failed deliveries with exponential backoff
2121

22-
def new(conn, payload) do
23-
with {:ok, webhook} <- Webhook.new(conn, payload),
24-
:ok <- process_delivery(webhook) do
25-
conn |> put_status(:accepted) |> json(%{status: "ok"})
26-
else
27-
{:error, :bot_event} ->
28-
conn |> put_status(:ok) |> json(%{status: "ok"})
29-
30-
{:error, :missing_header} ->
31-
conn |> put_status(:bad_request) |> json(%{error: "Missing header"})
32-
33-
{:error, :signature_mismatch} ->
34-
conn |> put_status(:unauthorized) |> json(%{error: "Signature mismatch"})
35-
36-
{:error, reason} ->
37-
Logger.error("Error processing webhook: #{inspect(reason)}")
38-
conn |> put_status(:internal_server_error) |> json(%{error: "Internal server error"})
39-
40-
error ->
41-
Logger.error("Error processing webhook: #{inspect(error)}")
42-
conn |> put_status(:internal_server_error) |> json(%{error: "Internal server error"})
43-
end
44-
rescue
45-
e ->
46-
Logger.error(Exception.format(:error, e, __STACKTRACE__))
47-
conn |> put_status(:internal_server_error) |> json(%{error: "Internal server error"})
48-
end
49-
5022
def process_delivery(webhook) do
5123
with :ok <- ensure_human_author(webhook),
5224
{:ok, commands} <- process_commands(webhook),

lib/algora_web/endpoint.ex

Lines changed: 3 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -46,32 +46,9 @@ defmodule AlgoraWeb.Endpoint do
4646
handler: AlgoraWeb.Webhooks.StripeController,
4747
secret: {Algora, :config, [[:stripe, :webhook_secret]]}
4848

49-
plug :parse_body
50-
51-
opts = [
52-
parsers: [:urlencoded, :multipart, :json],
53-
pass: ["*/*"],
54-
json_decoder: Phoenix.json_library()
55-
]
56-
57-
defmodule BodyReader do
58-
@moduledoc false
59-
def cache_raw_body(conn, opts) do
60-
with {:ok, body, conn} <- Plug.Conn.read_body(conn, opts) do
61-
conn = update_in(conn.assigns[:raw_body], &[body | &1 || []])
62-
63-
{:ok, body, conn}
64-
end
65-
end
66-
end
67-
68-
@parser_without_cache Plug.Parsers.init(opts)
69-
@parser_with_cache Plug.Parsers.init([body_reader: {BodyReader, :cache_raw_body, []}] ++ opts)
70-
71-
# All endpoints that start with "webhooks" have their body cached.
72-
defp parse_body(%{path_info: ["webhooks" | _]} = conn, _), do: Plug.Parsers.call(conn, @parser_with_cache)
73-
74-
defp parse_body(conn, _), do: Plug.Parsers.call(conn, @parser_without_cache)
49+
plug AlgoraWeb.Plugs.GithubWebhooks,
50+
at: "/webhooks/github",
51+
handler: AlgoraWeb.Webhooks.GithubController
7552

7653
plug Plug.MethodOverride
7754
plug Plug.Head
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
defmodule AlgoraWeb.Plugs.GithubWebhooks do
2+
@moduledoc false
3+
@behaviour Plug
4+
5+
import Plug.Conn
6+
7+
alias Algora.Github.Webhook
8+
alias AlgoraWeb.Webhooks.GithubController
9+
alias Plug.Conn
10+
11+
require Logger
12+
13+
@impl true
14+
def init(opts) do
15+
path_info = String.split(opts[:at], "/", trim: true)
16+
17+
opts
18+
|> Map.new()
19+
|> Map.put_new(:path_info, path_info)
20+
end
21+
22+
@impl true
23+
def call(%Conn{method: "POST", path_info: path_info} = conn, %{path_info: path_info} = _opts) do
24+
with {:ok, webhook, conn} <- Webhook.new(conn),
25+
:ok <- GithubController.process_delivery(webhook) do
26+
conn |> send_resp(200, "Webhook received.") |> halt()
27+
else
28+
{:error, :bot_event} ->
29+
conn |> send_resp(200, "Webhook received.") |> halt()
30+
31+
{:error, :missing_header} ->
32+
Logger.error("Missing header")
33+
conn |> send_resp(400, "Bad request.") |> halt()
34+
35+
{:error, :signature_mismatch} ->
36+
Logger.error("Signature mismatch")
37+
conn |> send_resp(400, "Bad request.") |> halt()
38+
39+
error ->
40+
Logger.error("Bad request: #{inspect(error)}")
41+
conn |> send_resp(400, "Bad request.") |> halt()
42+
end
43+
rescue
44+
e ->
45+
Logger.error(Exception.format(:error, e, __STACKTRACE__))
46+
conn |> send_resp(400, "Bad request.") |> halt()
47+
end
48+
49+
@impl true
50+
def call(%Conn{path_info: path_info} = conn, %{path_info: path_info}) do
51+
conn |> send_resp(400, "Bad request.") |> halt()
52+
end
53+
54+
@impl true
55+
def call(conn, _), do: conn
56+
end

lib/algora_web/router.ex

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,6 @@ defmodule AlgoraWeb.Router do
2020
plug :accepts, ["json"]
2121
end
2222

23-
scope "/webhooks", AlgoraWeb do
24-
pipe_through :api
25-
26-
post "/github", Webhooks.GithubController, :new
27-
end
28-
2923
scope "/", AlgoraWeb do
3024
pipe_through [:browser]
3125

0 commit comments

Comments
 (0)