Customizing Requests with prepare_req

Copy Markdown View Source

Every client module generated by use Grephql includes an overridable prepare_req/1 callback. It receives the fully built %Req.Request{} just before it is sent, giving you a hook to attach Req response steps, add headers, or apply any other request-level customization.

How It Works

  1. Override prepare_req/1 in your client module
  2. In a Req response step, use Grephql.Result.put_resp_assign/3 to stash values from the response
  3. After decoding, those values appear in result.assigns
defmodule MyApp.API do
  use Grephql,
    otp_app: :my_app,
    source: "priv/schemas/api.json",
    endpoint: "https://api.example.com/graphql"

  def prepare_req(req) do
    Req.Request.append_response_steps(req,
      capture_request_id: fn {req, resp} ->
        request_id = Req.Response.get_header(resp, "x-request-id")
        {req, Grephql.Result.put_resp_assign(resp, :request_id, request_id)}
      end
    )
  end

  defgql :get_user, ~GQL"""
    query GetUser($id: ID!) {
      user(id: $id) { name }
    }
  """
end

{:ok, result} = MyApp.API.get_user(%{id: "1"})
result.assigns[:request_id]  #=> ["req-abc-123"]

Storing Multiple Values

Call put_resp_assign/3 multiple times to store different pieces of metadata:

def prepare_req(req) do
  Req.Request.append_response_steps(req,
    capture_metadata: fn {req, resp} ->
      resp =
        resp
        |> Grephql.Result.put_resp_assign(:extensions, resp.body["extensions"])
        |> Grephql.Result.put_resp_assign(:request_id, Req.Response.get_header(resp, "x-request-id"))

      {req, resp}
    end
  )
end

Testing

Use Req.Test as usual. Include the metadata you want to capture in the stubbed response:

test "captures request metadata" do
  Req.Test.stub(MyApp.API, fn conn ->
    conn
    |> Plug.Conn.put_resp_header("x-request-id", "test-123")
    |> Req.Test.json(%{
      "data" => %{"user" => %{"name" => "Alice"}}
    })
  end)

  assert {:ok, result} = MyApp.API.get_user(%{id: "1"})
  assert result.assigns[:request_id] == ["test-123"]
end

Example: Shopify Rate Limiting via Extensions

Shopify's GraphQL API returns rate-limit and cost information in the extensions field:

{
  "data": { "products": { ... } },
  "extensions": {
    "cost": {
      "requestedQueryCost": 12,
      "actualQueryCost": 10,
      "throttleStatus": {
        "currentlyAvailable": 980,
        "maximumAvailable": 1000.0,
        "restoreRate": 50.0
      }
    }
  }
}

Capture it with prepare_req/1:

defmodule MyApp.Shopify do
  use Grephql,
    otp_app: :my_app,
    source: "priv/schemas/shopify.json",
    endpoint: "https://myshop.myshopify.com/admin/api/graphql.json"

  def prepare_req(req) do
    Req.Request.append_response_steps(req,
      capture_extensions: fn {req, resp} ->
        {req, Grephql.Result.put_resp_assign(resp, :extensions, resp.body["extensions"])}
      end
    )
  end

  defgql :get_products, ~GQL"""
    query GetProducts($first: Int!) {
      products(first: $first) {
        edges {
          node {
            title
          }
        }
      }
    }
  """
end

Then use the captured cost data to throttle API calls:

{:ok, result} = MyApp.Shopify.get_products(%{first: 10})

throttle_status = result.assigns[:extensions]["cost"]["throttleStatus"]
throttle_status["currentlyAvailable"]  #=> 980
throttle_status["restoreRate"]         #=> 50.0
defmodule MyApp.Shopify.RateLimiter do
  def maybe_throttle(%Grephql.Result{assigns: assigns}) do
    case assigns[:extensions] do
      %{"cost" => %{"throttleStatus" => %{"currentlyAvailable" => available}}}
      when available < 100 ->
        Process.sleep(1_000)

      _other ->
        :ok
    end
  end
end

API Reference

  • prepare_req/1 — overridable callback on client modules, receives the %Req.Request{} before it is sent
  • Grephql.Result.put_resp_assign/3 — stores a key-value pair on the Req response for later transfer to Result.assigns
  • Grephql.Result — the :assigns field (default %{}) holds all values stored via put_resp_assign/3