diff --git a/lib/inngest/headers.ex b/lib/inngest/headers.ex index 744c41e..df86ad3 100644 --- a/lib/inngest/headers.ex +++ b/lib/inngest/headers.ex @@ -3,17 +3,16 @@ defmodule Inngest.Headers do Header values used by the SDK """ - def env, do: "X-Inngest-Env" - def forwarded_for, do: "X-Forwarded-For" - def framework, do: "X-Inngest-Framework" - def platform, do: "X-Inngest-Platform" - def sdk, do: "X-Inngest-SDK" - def signature, do: "X-Inngest-Signature" - def server_kind, do: "X-Inngest-Server-Kind" - def req_version, do: "X-Inngest-Req-Version" - def server_timing, do: "Server-Timing" + def env, do: "x-inngest-env" + def forwarded_for, do: "x-forwarded-for" + def framework, do: "x-inngest-framework" + def platform, do: "x-inngest-platform" + def sdk_version, do: "x-inngest-sdk" + def signature, do: "x-inngest-signature" + def server_kind, do: "x-inngest-server-kind" + def server_timing, do: "server-timing" - # Retries - def no_retry, do: "X-Inngest-No-Retry" - def retry_after, do: "Retry-After" + # retries + def no_retry, do: "x-inngest-no-retry" + def retry_after, do: "retry-after" end diff --git a/lib/inngest/response.ex b/lib/inngest/response.ex index ccad2b7..b79def3 100644 --- a/lib/inngest/response.ex +++ b/lib/inngest/response.ex @@ -1,3 +1,97 @@ defmodule Inngest.NonRetriableError do defexception message: "Not retrying error. Exiting." end + +defmodule Inngest.SdkResponse do + @moduledoc """ + Represents an SDK response to the executor when ran + """ + + defstruct [ + :status, + :body, + :retry + ] + + @type t() :: %__MODULE__{ + status: number(), + body: binary(), + retry: nil | :noretry | binary() | boolean() + } + + alias Inngest.Headers + + # NOTES: + # ********* RESPONSE *********** + # Each results has a specific meaning to it. + # status, data + # 206, generatorcode -> store result and continue execution + # 200, resp -> execution completed (including steps) of function + # 400, error -> non retriable error + # 500, error -> retriable error + def from_result({:ok, value}) do + case Jason.encode(value) do + {:ok, encoded} -> + %__MODULE__{ + status: 200, + body: encoded + } + + {:error, _error} -> + %__MODULE__{ + status: 500, + body: "Failed to encode result into JSON: #{value}" + } + end + end + + def from_result({:ok, opcodes, :continue}) do + %__MODULE__{ + status: 206, + body: Jason.encode!(opcodes) + } + end + + # No retry error response + def from_result({:error, error, :noretry}) do + encoded = + case Jason.encode(error) do + {:ok, encoded} -> encoded + {:error, _} -> "Failed to encode error: #{error}" + end + + %__MODULE__{ + status: 400, + body: encoded, + retry: :noretry + } + end + + def from_result({:error, error, _}) do + encoded = + case Jason.encode(error) do + {:ok, encoded} -> encoded + {:error, _} -> "Failed to encode error: #{error}" + end + + %__MODULE__{ + status: 500, + body: encoded, + retry: true + } + end + + @doc """ + Set the retry header depending on response + """ + @spec maybe_retry_header(Plug.Conn.t(), t()) :: Plug.Conn.t() + def maybe_retry_header(conn, %{retry: :noretry} = _resp) do + Plug.Conn.put_resp_header(conn, Headers.no_retry(), "true") + end + + def maybe_retry_header(conn, %{retry: dur} = _resp) when is_binary(dur) do + Plug.Conn.put_resp_header(conn, Headers.retry_after(), dur) + end + + def maybe_retry_header(conn, _resp), do: conn +end diff --git a/lib/inngest/router/invoke.ex b/lib/inngest/router/invoke.ex index ee02761..056802b 100644 --- a/lib/inngest/router/invoke.ex +++ b/lib/inngest/router/invoke.ex @@ -3,7 +3,7 @@ defmodule Inngest.Router.Invoke do import Plug.Conn import Inngest.Router.Helper - alias Inngest.{Config, Signature} + alias Inngest.{Config, Headers, Signature, SdkResponse} alias Inngest.Function.GeneratorOpCode @content_type "application/json" @@ -58,73 +58,45 @@ defmodule Inngest.Router.Invoke do step: Inngest.StepTool } - {status, payload} = + resp = case Config.is_dev() do true -> - {status, resp} = invoke(func, ctx, input) - - payload = - case Jason.encode(resp) do - {:ok, val} -> val - {:error, err} -> Jason.encode!(err.message) - end - - {status, payload} + invoke(func, ctx, input) false -> with sig <- conn |> Plug.Conn.get_req_header("x-inngest-signature") |> List.first(), true <- Signature.signing_key_valid?(sig, Config.signing_key(), body) do - {status, resp} = invoke(func, ctx, input) - - payload = - case Jason.encode(resp) do - {:ok, val} -> val - {:error, err} -> Jason.encode!(err.message) - end - - {status, payload} + invoke(func, ctx, input) else - _ -> {400, Jason.encode!(%{error: "unable to verify signature"})} + _ -> + SdkResponse.from_result({:error, "unable to verify signature", :noretry}) end end conn |> put_resp_content_type(@content_type) - |> put_req_header("x-inngest-sdk", "elixir:v1") - |> send_resp(status, payload) + |> put_resp_header(Headers.sdk_version(), Config.sdk_version()) + |> SdkResponse.maybe_retry_header(resp) + |> send_resp(resp.status, resp.body) |> halt() end - # NOTES: - # ********* RESPONSE *********** - # Each results has a specific meaning to it. - # status, data - # 206, generatorcode -> store result and continue execution - # 200, resp -> execution completed (including steps) of function - # 400, error -> non retriable error - # 500, error -> retriable error defp invoke(func, ctx, input) do try do - case func.mod.exec(ctx, input) do - {:ok, val} -> - {200, val} - - {:error, error} -> - {400, error} - end + func.mod.exec(ctx, input) |> SdkResponse.from_result() rescue non_retry in Inngest.NonRetriableError -> - {400, non_retry.message} + SdkResponse.from_result({:error, non_retry.message, :noretry}) - err -> - {400, err.message} + error -> + SdkResponse.from_result({:error, error.message, :retry}) catch # Finished step, report back to executor %GeneratorOpCode{} = opcode -> - {206, [opcode]} + SdkResponse.from_result({:ok, [opcode], :continue}) - _ -> - {400, "error"} + error -> + SdkResponse.from_result({:error, "unknown error: #{error}", []}) end end diff --git a/lib/inngest/trigger.ex b/lib/inngest/trigger.ex index 8c02813..c1796aa 100644 --- a/lib/inngest/trigger.ex +++ b/lib/inngest/trigger.ex @@ -31,10 +31,14 @@ end defimpl Jason.Encoder, for: Inngest.Trigger do def encode(%{cron: cron} = value, opts) when is_binary(cron) do - Jason.Encode.map(Map.take(value, [:cron]), opts) + value + |> Map.take([:cron]) + |> Jason.Encode.map(opts) end def encode(value, opts) do - Jason.Encode.map(Map.take(value, [:event, :expression]), opts) + value + |> Map.take([:event, :expression]) + |> Jason.Encode.map(opts) end end