Skip to content

Commit

Permalink
fix: [ENG-1383] Validate & log invalid JSON response (#69)
Browse files Browse the repository at this point in the history
* fix: don't throw exception on invalid JSON response, return error tuple
  • Loading branch information
jcartwright authored Nov 12, 2024
1 parent 58c59f5 commit bb5aa39
Show file tree
Hide file tree
Showing 3 changed files with 67 additions and 57 deletions.
33 changes: 17 additions & 16 deletions lib/postscript/request.ex
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
defmodule Postscript.Request do
alias Postscript.{ Config, Helpers, Operation, Response }
alias Postscript.{Config, Helpers, Operation, Response}

@type t ::
%__MODULE__{
Expand All @@ -10,20 +10,18 @@ defmodule Postscript.Request do
url: String.t()
}

defstruct [
body: nil,
headers: [],
method: nil,
private: %{},
url: nil
]
defstruct body: nil,
headers: [],
method: nil,
private: %{},
url: nil

@spec new(Operation.t(), Config.t()) :: t
def new(operation, config) do
body = Helpers.Body.encode!(operation, config)

headers = []
headers = headers ++ [{ "content-type", "application/json" }]
headers = headers ++ [{"content-type", "application/json"}]
headers = headers ++ config.http_headers

url = Helpers.Url.to_string(operation, config)
Expand All @@ -49,15 +47,16 @@ defmodule Postscript.Request do
|> finish(config)
end

defp retry(response, _request, %_{ retry: retry }) when is_nil(retry) or retry == false do
defp retry(response, _request, %_{retry: retry}) when is_nil(retry) or retry == false do
response
end

defp retry({ :ok, %{ status_code: status_code } } = response, request, config) when status_code >= 500 do
defp retry({:ok, %{status_code: status_code}} = response, request, config)
when status_code >= 500 do
do_retry(response, request, config)
end

defp retry({ :error, _ } = response, request, config) do
defp retry({:error, _} = response, request, config) do
do_retry(response, request, config)
end

Expand Down Expand Up @@ -89,10 +88,12 @@ defmodule Postscript.Request do

defp finish(response, config) do
case response do
{ :ok, %{ status_code: status_code } = response } when status_code >= 400 ->
{ :error, Response.new(response, config) }
{ :ok, %{ status_code: status_code } = response } when status_code >= 200 ->
{ :ok, Response.new(response, config) }
{:ok, %{status_code: status_code} = response} when status_code >= 400 ->
{:error, Response.new(response, config)}

{:ok, %{status_code: status_code} = response} when status_code >= 200 ->
{:ok, Response.new(response, config)}

otherwise ->
otherwise
end
Expand Down
8 changes: 6 additions & 2 deletions lib/postscript/response.ex
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
defmodule Postscript.Response do
alias Postscript.{ Config, Http }
alias Postscript.{Config, Http}

@type t ::
%__MODULE__{
Expand All @@ -15,7 +15,11 @@ defmodule Postscript.Response do
body =
response
|> Map.get(:body)
|> config.json_codec.decode!()
|> config.json_codec.decode()
|> case do
{:ok, json} -> json
{:error, _decode_error} -> Map.get(response, :body)
end

%__MODULE__{}
|> Map.put(:body, body)
Expand Down
83 changes: 44 additions & 39 deletions test/postscript_test.exs
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
defmodule PostscriptTest do
use ExUnit.Case, async: true

alias Postscript.{ Http, Operation, Response }
alias Postscript.{Http, Operation, Response}

@ok_resp %{ body: "{\"ok\":true}", headers: [], status_code: 200 }
@ok_resp %{body: "{\"ok\":true}", headers: [], status_code: 200}

@not_ok_resp %{ body: "{\"ok\":false}", headers: [], status_code: 400 }
@not_ok_resp %{body: "{\"ok\":false}", headers: [], status_code: 400}

test "sends the proper HTTP method" do
Http.Mock.start_link()

response = { :ok, @ok_resp }
response = {:ok, @ok_resp}

Http.Mock.put_response(response)

operation = %Operation{ method: :get, params: [hello: "world"], path: "/fake" }
operation = %Operation{method: :get, params: [hello: "world"], path: "/fake"}

Postscript.request(operation, http_client: Http.Mock)

Expand All @@ -24,11 +24,11 @@ defmodule PostscriptTest do
test "uses the proper URL for GET requests" do
Http.Mock.start_link()

response = { :ok, @ok_resp }
response = {:ok, @ok_resp}

Http.Mock.put_response(response)

operation = %Operation{ method: :get, params: [hello: "world"], path: "/fake" }
operation = %Operation{method: :get, params: [hello: "world"], path: "/fake"}

Postscript.request(operation, http_client: Http.Mock)

Expand All @@ -38,11 +38,11 @@ defmodule PostscriptTest do
test "uses the proper URL for DELETE requests" do
Http.Mock.start_link()

response = { :ok, @ok_resp }
response = {:ok, @ok_resp}

Http.Mock.put_response(response)

operation = %Operation{ method: :delete, params: [hello: "world"], path: "/fake" }
operation = %Operation{method: :delete, params: [hello: "world"], path: "/fake"}

Postscript.request(operation, http_client: Http.Mock)

Expand All @@ -52,11 +52,11 @@ defmodule PostscriptTest do
test "uses the proper URL for non-GET requests" do
Http.Mock.start_link()

response = { :ok, @ok_resp }
response = {:ok, @ok_resp}

Http.Mock.put_response(response)

operation = %Operation{ method: :post, params: [hello: "world"], path: "/fake" }
operation = %Operation{method: :post, params: [hello: "world"], path: "/fake"}

Postscript.request(operation, http_client: Http.Mock)

Expand All @@ -66,31 +66,36 @@ defmodule PostscriptTest do
test "sends the proper HTTP headers" do
Http.Mock.start_link()

response = { :ok, @ok_resp }
response = {:ok, @ok_resp}

Http.Mock.put_response(response)

operation = %Operation{}
operation = Map.put(operation, :method, :get)
operation = Map.put(operation, :params, [hello: "world"])
operation = Map.put(operation, :params, hello: "world")
operation = Map.put(operation, :path, "/fake")

Postscript.request(operation, api_key: "thisisfake", http_client: Http.Mock, http_headers: [{ "x-custom-header", "true" }], shop_token: "thisisfake")

assert { "content-type", "application/json" } in Http.Mock.get_request_headers()
assert { "authorization", "Bearer thisisfake" } in Http.Mock.get_request_headers()
assert { "x-custom-header", "true" } in Http.Mock.get_request_headers()
assert { "x-postscript-shop-token", "thisisfake" } in Http.Mock.get_request_headers()
Postscript.request(operation,
api_key: "thisisfake",
http_client: Http.Mock,
http_headers: [{"x-custom-header", "true"}],
shop_token: "thisisfake"
)

assert {"content-type", "application/json"} in Http.Mock.get_request_headers()
assert {"authorization", "Bearer thisisfake"} in Http.Mock.get_request_headers()
assert {"x-custom-header", "true"} in Http.Mock.get_request_headers()
assert {"x-postscript-shop-token", "thisisfake"} in Http.Mock.get_request_headers()
end

test "sends the proper body for GET requests" do
Http.Mock.start_link()

response = { :ok, @ok_resp }
response = {:ok, @ok_resp}

Http.Mock.put_response(response)

operation = %Operation{ method: :get, params: [hello: "world"], path: "/fake" }
operation = %Operation{method: :get, params: [hello: "world"], path: "/fake"}

Postscript.request(operation, http_client: Http.Mock)

Expand All @@ -100,11 +105,11 @@ defmodule PostscriptTest do
test "sends the proper body for DELETE requests" do
Http.Mock.start_link()

response = { :ok, @ok_resp }
response = {:ok, @ok_resp}

Http.Mock.put_response(response)

operation = %Operation{ method: :delete, params: [hello: "world"], path: "/fake" }
operation = %Operation{method: :delete, params: [hello: "world"], path: "/fake"}

Postscript.request(operation, http_client: Http.Mock)

Expand All @@ -114,11 +119,11 @@ defmodule PostscriptTest do
test "sends the proper body for non-GET requests" do
Http.Mock.start_link()

response = { :ok, @ok_resp }
response = {:ok, @ok_resp}

Http.Mock.put_response(response)

operation = %Operation{ method: :post, params: [hello: "world"], path: "/fake" }
operation = %Operation{method: :post, params: [hello: "world"], path: "/fake"}

Postscript.request(operation, http_client: Http.Mock)

Expand All @@ -128,39 +133,39 @@ defmodule PostscriptTest do
test "returns :ok when the request is successful" do
Http.Mock.start_link()

response = { :ok, @ok_resp }
response = {:ok, @ok_resp}

Http.Mock.put_response(response)

operation = %Operation{ method: :post, params: [hello: "world"], path: "/fake" }
operation = %Operation{method: :post, params: [hello: "world"], path: "/fake"}

result = Postscript.request(operation, http_client: Http.Mock)

assert { :ok, %Response{} } = result
assert {:ok, %Response{}} = result
end

test "returns :error when the request is not successful" do
Http.Mock.start_link()

response = { :ok, @not_ok_resp }
response = {:ok, @not_ok_resp}

Http.Mock.put_response(response)

operation = %Operation{ method: :post, params: [hello: "world"], path: "/fake" }
operation = %Operation{method: :post, params: [hello: "world"], path: "/fake"}

result = Postscript.request(operation, http_client: Http.Mock)

assert { :error, %Response{} } = result
assert {:error, %Response{}} = result
end

test "passes the response through when unrecognized" do
Http.Mock.start_link()

response = { :error, :timeout }
response = {:error, :timeout}

Http.Mock.put_response(response)

operation = %Operation{ method: :post, params: [hello: "world"], path: "/fake" }
operation = %Operation{method: :post, params: [hello: "world"], path: "/fake"}

result = Postscript.request(operation, http_client: Http.Mock)

Expand All @@ -170,30 +175,30 @@ defmodule PostscriptTest do
test "retries failed requests" do
Http.Mock.start_link()

response_1 = { :error, :timeout }
response_2 = { :ok, @ok_resp }
response_1 = {:error, :timeout}
response_2 = {:ok, @ok_resp}

Http.Mock.put_response(response_1)
Http.Mock.put_response(response_2)

operation = %Operation{ method: :post, params: [hello: "world"], path: "/fake" }
operation = %Operation{method: :post, params: [hello: "world"], path: "/fake"}

result = Postscript.request(operation, http_client: Http.Mock, retry: Postscript.Retry.Linear)

assert { :ok, %Response{} } = result
assert {:ok, %Response{}} = result
end

test "retries up to max attempts" do
Http.Mock.start_link()

response = { :error, :timeout }
response = {:error, :timeout}

Http.Mock.put_response(response)
Http.Mock.put_response(response)
Http.Mock.put_response(response)
Http.Mock.put_response(response)

operation = %Operation{ method: :post, params: [hello: "world"], path: "/fake" }
operation = %Operation{method: :post, params: [hello: "world"], path: "/fake"}

Postscript.request(operation, http_client: Http.Mock, retry: Postscript.Retry.Linear)

Expand Down

0 comments on commit bb5aa39

Please sign in to comment.