GBFS: a feed that is not JSON is logged and skipped, not a KeyError

b0284cc · Gabe Morell · 2026-11-04 21:40

8 files +43 -7
Message
{commit_body(@commit)}

Files changed

modified apps/room_gbfs/lib/room_gbfs/gbfs_worker.ex
+18 −7
@@ -123,13 +123,9 @@ defmodule RoomGbfs.Worker do
123 123 # the body is an empty redirect page.
124 124 case HTTPoison.get(url, [], follow_redirect: true) do
125 125 {:ok, result} ->
126 − case result.body
127 − |> Jason.decode(keys: :atoms) do
128 − {:ok, json} ->
129 − write_feed(type, json, id, dt, lang)
130 −
131 − {:error, info} ->
132 − Logger.info(info.reason)
126 + case decode_feed(result.body, id, url) do
127 + {:ok, json} -> write_feed(type, json, id, dt, lang)
128 + :error -> :error
133 129 end
134 130
135 131 {:error, info} ->
@@ -137,6 +133,21 @@ defmodule RoomGbfs.Worker do
137 133 end
138 134 end
139 135
136 + # A %Jason.DecodeError{} has no :reason -- reading one raised a KeyError of
137 + # its own, so a feed that came back as an HTML error page took the worker
138 + # down instead of being skipped until the next pass.
139 + @doc false
140 + def decode_feed(body, id, url) do
141 + case Jason.decode(body, keys: :atoms) do
142 + {:ok, json} ->
143 + {:ok, json}
144 +
145 + {:error, error} ->
146 + Logger.error("GBFS::#{id} #{url} returned non-JSON: #{Exception.message(error)}")
147 + :error
148 + end
149 + end
150 +
140 151 defp write_feed(:sys_info, json, id, _dt, lang) do
141 152 Repo.insert(
142 153 RoomSanctum.Storage.change_sys_info(
added apps/room_gbfs/test/room_gbfs/worker_decode_test.exs
+25 −0
@@ -0,0 +1,25 @@
1 +defmodule RoomGbfs.WorkerDecodeTest do
2 + use ExUnit.Case, async: true
3 +
4 + import ExUnit.CaptureLog
5 +
6 + alias RoomGbfs.Worker
7 +
8 + test "a feed body that is JSON decodes with atom keys" do
9 + assert {:ok, %{data: %{stations: []}}} =
10 + Worker.decode_feed(~s({"data": {"stations": []}}), 7, "http://x/si.json")
11 + end
12 +
13 + # %Jason.DecodeError{} has no :reason, so the error branch used to raise a
14 + # KeyError of its own and take the worker down with it -- over exactly the
15 + # body (an HTML error page, a truncated download) it was there to survive.
16 + test "a body that is not JSON is logged and skipped rather than raising" do
17 + log =
18 + capture_log(fn ->
19 + assert :error = Worker.decode_feed("<html>gateway timeout</html>", 7, "http://x/ss.json")
20 + end)
21 +
22 + assert log =~ "GBFS::7"
23 + assert log =~ "http://x/ss.json"
24 + end
25 +end

Parents: 1af73d2