You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

GraphQL突变处理客户端非法输入:如何返回4xx状态码

如何在GraphQL突变中返回4xx状态码处理非法输入

我完全理解你的痛点:GraphQL默认会把错误包装在响应的errors数组里并返回200状态码,但你希望非法输入(比如验证失败)时返回4xx(比如422 Unprocessable Entity)。由于你的Context层没有直接访问conn的权限,我们可以分几步来实现需求:


第一步:传递详细的验证错误信息

首先,别再返回简单的{:error, "error"}了,把Ecto Changeset的错误信息完整传递到上层。修改你的Teams.create_team函数:

def create_team(attrs, info) do
  user_id = Auth.current_user_id(info)
  changeset = Team.changeset(%Team{user_id: user_id}, attrs)
  case Repo.insert(changeset) do
    {:ok, team} -> {:ok, team}
    {:error, changeset} -> {:error, changeset} # 直接返回完整的changeset
  end
end

接下来,我们需要把Changeset的错误转换成Absinthe能识别的结构化错误,方便后续判断错误类型。可以写一个辅助模块来处理:

defmodule YourAppWeb.GraphQL.ErrorHelpers do
  def format_changeset_errors(changeset) do
    # 把Changeset的错误转换成易读的键值对格式
    validation_errors = Ecto.Changeset.traverse_errors(changeset, fn {msg, opts} ->
      Enum.reduce(opts, msg, fn {key, value}, acc ->
        String.replace(acc, "%{#{key}}", to_string(value))
      end)
    end)

    # 返回带标记的Absinthe错误结构,方便后续识别这是验证错误
    {:error, [
      %Absinthe.Error{
        message: "输入验证失败",
        extra: %{type: :validation, details: validation_errors}
      }
    ]}
  end
end

然后在Context里调用这个辅助函数:

def create_team(attrs, info) do
  user_id = Auth.current_user_id(info)
  changeset = Team.changeset(%Team{user_id: user_id}, attrs)
  case Repo.insert(changeset) do
    {:ok, team} -> {:ok, team}
    {:error, changeset} -> YourAppWeb.GraphQL.ErrorHelpers.format_changeset_errors(changeset)
  end
end

第二步:根据错误类型设置HTTP状态码

现在错误已经带上了:validation类型标记,接下来需要让Phoenix根据这个标记设置对应的状态码。有两种常见实现方式:

方式一:自定义GraphQL控制器处理

如果你用的是Absinthe.Phoenix,可以修改你的GraphQL控制器,在返回响应前检查错误类型:

defmodule YourAppWeb.GraphQLController do
  use YourAppWeb, :controller

  def execute(conn, params) do
    context = build_context(conn) # 你的上下文构建逻辑
    result = Absinthe.run(params, YourAppWeb.Schema, context: context)

    case result do
      {:ok, %{errors: errors}} when length(errors) > 0 ->
        # 检查是否存在验证类型的错误
        has_validation_errors = Enum.any?(errors, fn error ->
          error.extra[:type] == :validation
        end)

        # 根据错误类型设置状态码:验证失败用422,其他错误用400
        status = if has_validation_errors, do: :unprocessable_entity, else: :bad_request
        conn |> put_status(status) |> json(Absinthe.Response.format(result))

      _ ->
        # 没有错误时正常返回200
        conn |> json(Absinthe.Response.format(result))
    end
  end

  # 其他必要的函数(比如build_context)...
end

方式二:使用Absinthe插件

如果你更倾向于在Absinthe的处理流程中处理,可以写一个自定义插件:

defmodule YourAppWeb.GraphQL.ValidationErrorPlug do
  @behaviour Absinthe.Plug

  @impl true
  def init(opts), do: opts

  @impl true
  def call(conn, %Absinthe.Plug.Conn{result: {:ok, %{errors: errors}}} = conn_state, _opts) do
    has_validation_errors = Enum.any?(errors, fn error ->
      error.extra[:type] == :validation
    end)

    if has_validation_errors do
      # 修改conn的状态码为422
      updated_conn = Plug.Conn.put_status(conn, :unprocessable_entity)
      %{conn_state | conn: updated_conn}
    else
      conn_state
    end
  end

  # 其他情况直接返回原状态
  def call(conn, conn_state, _opts), do: conn_state
end

然后在你的Schema里注册这个插件:

defmodule YourAppWeb.Schema do
  use Absinthe.Schema

  # 注册自定义插件
  plug YourAppWeb.GraphQL.ValidationErrorPlug

  # 你的Schema定义...
  mutation do
    field :create_team, :team do
      arg(:name, non_null(:string))
      arg(:league_id, non_null(:id))
      resolve(&TeamResolver.create_team/3)
    end
  end
end

为什么这样可行?

GraphQL规范本身并没有强制要求错误时返回非200状态码,但在实际业务场景中,返回422可以让客户端更直观地识别输入验证失败的情况。通过上述步骤,我们:

  1. 保留了GraphQL错误的结构化信息(客户端仍然能在errors数组里看到具体的验证失败原因)
  2. 同时让HTTP状态码正确反映请求的处理结果
  3. 不需要在Context层直接操作conn,保持了分层架构的清晰

内容的提问来源于stack exchange,提问作者Bitwise

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.14 08:20:22