Fix protobuf body parsing causing 400 errors on agent heartbeat

Plug.Parsers was trying to parse protobuf bodies as JSON, failing with
400 errors before the request reached the controller.

Changes:
- Added custom BodyReader to endpoint that skips parsing for protobuf
- When Content-Type is application/x-protobuf, return empty body to parser
- Controller reads the raw body directly for protobuf requests
- Added error handling for protobuf decode failures in heartbeat endpoint

This fixes the 400 errors agents were seeing on heartbeat requests.
This commit is contained in:
Graham McIntire 2026-01-14 18:35:20 -06:00
parent 31b906b0fa
commit ae521d2108
No known key found for this signature in database
2 changed files with 41 additions and 7 deletions

View file

@ -98,14 +98,31 @@ defmodule ToweropsWeb.Api.AgentController do
case get_req_header(conn, "content-type") do
["application/x-protobuf" | _] ->
# Decode protobuf
{:ok, body, _conn} = Plug.Conn.read_body(conn)
proto_metadata = HeartbeatMetadata.decode(body)
case Plug.Conn.read_body(conn) do
{:ok, body, _conn} ->
try do
proto_metadata = HeartbeatMetadata.decode(body)
%{
"version" => proto_metadata.version,
"hostname" => proto_metadata.hostname,
"uptime_seconds" => proto_metadata.uptime_seconds
}
%{
"version" => proto_metadata.version,
"hostname" => proto_metadata.hostname,
"uptime_seconds" => proto_metadata.uptime_seconds
}
rescue
e ->
require Logger
Logger.error("Failed to decode protobuf heartbeat: #{inspect(e)}")
# Return empty metadata on decode error
%{}
end
{:error, reason} ->
require Logger
Logger.error("Failed to read heartbeat body: #{inspect(reason)}")
%{}
end
_ ->
# JSON format (fallback)

View file

@ -49,8 +49,25 @@ defmodule ToweropsWeb.Endpoint do
plug Plug.Parsers,
parsers: [:urlencoded, :multipart, :json],
pass: ["*/*"],
body_reader: {ToweropsWeb.Endpoint.BodyReader, :read_body, []},
json_decoder: Phoenix.json_library()
# Custom body reader that skips parsing for protobuf content type
defmodule BodyReader do
@moduledoc false
def read_body(conn, opts) do
case Plug.Conn.get_req_header(conn, "content-type") do
["application/x-protobuf" | _] ->
# Don't parse protobuf, let the controller handle it
{:ok, "", conn}
_ ->
# Use default body reader for other content types
Plug.Conn.read_body(conn, opts)
end
end
end
plug Plug.MethodOverride
plug Plug.Head
plug Plug.Session, @session_options