From c9df0e39829330baf3a8312aa0a424e8d1c757b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A9di-R=C3=A9mi=20Hashim?= Date: Mon, 18 May 2026 13:59:58 +0100 Subject: [PATCH 1/2] Return the threaded req on JSON decode error in read_json_body The function dropped the updated req on Jason.decode failure, returning a bare {:error, _} from the `with`. Callers ended up replying with the stale pre-read_body req, leaving Cowboy in an inconsistent state. read_json_body now returns {:error, :invalid_json, req}, and its consumers are updated to match the 3-tuple shape: - metrics.ex and logs.ex previously matched the 2-tuple {:error, :invalid_json}, which would have crashed with CaseClauseError on bad JSON anyway (Jason returns {:error, %Jason.DecodeError{}}, not the :invalid_json atom). - read_arguments/3 used a strict {:ok, _, _} = match, which would have crashed with MatchError. Switched to `with`, letting the error tuple propagate; existing api.ex callers already handle {:error, _, req}. Co-Authored-By: Claude Opus 4.7 --- server/lib/coflux/handlers/logs.ex | 2 +- server/lib/coflux/handlers/metrics.ex | 2 +- server/lib/coflux/handlers/utils.ex | 81 ++++++++++++++------------- 3 files changed, 43 insertions(+), 42 deletions(-) diff --git a/server/lib/coflux/handlers/logs.ex b/server/lib/coflux/handlers/logs.ex index f0c04fa9..833d5a84 100644 --- a/server/lib/coflux/handlers/logs.ex +++ b/server/lib/coflux/handlers/logs.ex @@ -79,7 +79,7 @@ defmodule Coflux.Handlers.Logs do {:ok, req, opts} end - {:error, :invalid_json} -> + {:error, :invalid_json, req} -> req = json_error_response(req, "invalid_json") {:ok, req, opts} end diff --git a/server/lib/coflux/handlers/metrics.ex b/server/lib/coflux/handlers/metrics.ex index 619fc33a..2ddc4db0 100644 --- a/server/lib/coflux/handlers/metrics.ex +++ b/server/lib/coflux/handlers/metrics.ex @@ -78,7 +78,7 @@ defmodule Coflux.Handlers.Metrics do {:ok, req, opts} end - {:error, :invalid_json} -> + {:error, :invalid_json, req} -> req = json_error_response(req, "invalid_json") {:ok, req, opts} end diff --git a/server/lib/coflux/handlers/utils.ex b/server/lib/coflux/handlers/utils.ex index 440bd4bb..2df0fe50 100644 --- a/server/lib/coflux/handlers/utils.ex +++ b/server/lib/coflux/handlers/utils.ex @@ -191,8 +191,9 @@ defmodule Coflux.Handlers.Utils do def read_json_body(req) do case :cowboy_req.read_body(req) do {:ok, data, req} -> - with {:ok, result} <- Jason.decode(data) do - {:ok, result, req} + case Jason.decode(data) do + {:ok, result} -> {:ok, result, req} + {:error, _} -> {:error, :invalid_json, req} end end end @@ -206,48 +207,48 @@ defmodule Coflux.Handlers.Utils do end def read_arguments(req, required_specs, optional_specs \\ %{}) do - {:ok, body, req} = read_json_body(req) - - {values, errors} = - Enum.reduce( - %{true: required_specs, false: optional_specs}, - {%{}, %{}}, - fn {required, specs}, {values, errors} -> - Enum.reduce(specs, {values, errors}, fn {key, spec}, {values, errors} -> - {field, parser} = - case spec do - {field, parser} -> {field, parser} - field when is_binary(field) -> {field, &default_parser/1} - end - - case Map.fetch(body, field) do - {:ok, nil} when not required -> - {values, errors} - - {:ok, value} -> - case parser.(value) do - {:ok, value} -> - {Map.put(values, key, value), errors} - - {:error, error} -> - {values, merge_error(errors, key, error)} + with {:ok, body, req} <- read_json_body(req) do + {values, errors} = + Enum.reduce( + %{true: required_specs, false: optional_specs}, + {%{}, %{}}, + fn {required, specs}, {values, errors} -> + Enum.reduce(specs, {values, errors}, fn {key, spec}, {values, errors} -> + {field, parser} = + case spec do + {field, parser} -> {field, parser} + field when is_binary(field) -> {field, &default_parser/1} end - :error -> - if required do - {values, merge_error(errors, key, :required)} - else + case Map.fetch(body, field) do + {:ok, nil} when not required -> {values, errors} - end - end - end) - end - ) - if Enum.empty?(errors) do - {:ok, values, req} - else - {:error, errors, req} + {:ok, value} -> + case parser.(value) do + {:ok, value} -> + {Map.put(values, key, value), errors} + + {:error, error} -> + {values, merge_error(errors, key, error)} + end + + :error -> + if required do + {values, merge_error(errors, key, :required)} + else + {values, errors} + end + end + end) + end + ) + + if Enum.empty?(errors) do + {:ok, values, req} + else + {:error, errors, req} + end end end From 224e9806e3bb4b615d971d15b137fd7715e7f9d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A9di-R=C3=A9mi=20Hashim?= Date: Mon, 18 May 2026 14:00:23 +0100 Subject: [PATCH 2/2] Handle :more chunks when reading the request body in read_json_body MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit :cowboy_req.read_body/1 returns {:more, data, req} when the body is larger than its default 8MB chunk length, but the function only matched {:ok, _, _} — crashing the handler with CaseClauseError on any sufficiently large POST. Accumulate chunks until :ok. Co-Authored-By: Claude Opus 4.7 --- server/lib/coflux/handlers/utils.ex | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/server/lib/coflux/handlers/utils.ex b/server/lib/coflux/handlers/utils.ex index 2df0fe50..9925c9d1 100644 --- a/server/lib/coflux/handlers/utils.ex +++ b/server/lib/coflux/handlers/utils.ex @@ -189,12 +189,18 @@ defmodule Coflux.Handlers.Utils do end def read_json_body(req) do + {:ok, data, req} = read_full_body(req) + + case Jason.decode(data) do + {:ok, result} -> {:ok, result, req} + {:error, _} -> {:error, :invalid_json, req} + end + end + + defp read_full_body(req, acc \\ []) do case :cowboy_req.read_body(req) do - {:ok, data, req} -> - case Jason.decode(data) do - {:ok, result} -> {:ok, result, req} - {:error, _} -> {:error, :invalid_json, req} - end + {:ok, data, req} -> {:ok, IO.iodata_to_binary([acc | [data]]), req} + {:more, data, req} -> read_full_body(req, [acc | [data]]) end end