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可以让客户端更直观地识别输入验证失败的情况。通过上述步骤,我们:
- 保留了GraphQL错误的结构化信息(客户端仍然能在
errors数组里看到具体的验证失败原因) - 同时让HTTP状态码正确反映请求的处理结果
- 不需要在Context层直接操作
conn,保持了分层架构的清晰
内容的提问来源于stack exchange,提问作者Bitwise
相关产品推荐
相关产品推荐

