跳到内容

422 Unprocessable Entity 错误的原因和解决方案|验证错误的正确返回方式

分类:HTTP·API 设计
本文目前仅提供日文版本。我们正在进行翻译工作。

开发 API 时,经常会遇到从客户端发送的数据在语法上是正确的,但不满足业务逻辑要求的情况。此时应返回的 HTTP 状态码是 <strong>422 Unprocessable Entity</strong>。本文解释了 422 和 400 之间的区别、主要框架中的实现模式、如何在文件上传中使用 422,以及如何设计符合 RFC 7807 的错误响应。

422 和 400 的区别:句法错误 vs 语义错误

HTTP 400 (Bad Request) 和 422 (Unprocessable Entity) 经常被混淆,但有明确的区分标准。

状态 名称 含义 具体例子
400 Bad Request 请求语法无效,服务器无法解析 JSON 无效、缺少必需的标头、Content-Type 不匹配
422 Unprocessable Entity 语法正确但包含的数据含义无效 无效的邮箱格式、超出范围的数值或无效的文件格式

简言之,<strong>400 是解析错误</strong>(JSON 格式错误等),<strong>422 是验证错误</strong>(JSON 正确但内容不符合要求)。422 最初在 WebDAV 扩展(RFC 4918)中定义,但现在已广泛应用于 REST API。

// 400 Bad Request の例: JSONの構文が壊れている
// リクエストボディ: {"name": "太郎", "email": }  ← JSONパースエラー

// 422 Unprocessable Entity の例: JSONは正しいがバリデーション失敗
// リクエストボディ: {"name": "", "email": "not-an-email"}
// レスポンス:
{
    "message": "The given data was invalid.",
    "errors": {
        "name": ["名前は必須です。"],
        "email": ["有効なメールアドレスを入力してください。"]
    }
}

在 Laravel 中使用 422 的方法

Laravel 在验证失败时自动返回 422 响应。这是使用 <code>FormRequest</code> 或 <code>$request->validate()</code> 时的默认行为。

// Laravel: バリデーション失敗時に自動で422を返す
class StoreUserRequest extends FormRequest
{
    public function rules(): array
    {
        return [
            'name'  => ['required', 'string', 'max:255'],
            'email' => ['required', 'email', 'unique:users,email'],
            'age'   => ['required', 'integer', 'min:0', 'max:150'],
            'avatar' => ['nullable', 'image', 'mimes:jpg,png,webp', 'max:5120'],
        ];
    }

    public function messages(): array
    {
        return [
            'name.required'  => '名前は必須です。',
            'email.required' => 'メールアドレスは必須です。',
            'email.email'    => '有効なメールアドレスを入力してください。',
            'email.unique'   => 'このメールアドレスは既に登録されています。',
            'avatar.max'     => 'アバター画像は5MB以下にしてください。',
        ];
    }
}

// コントローラー
class UserController extends Controller
{
    public function store(StoreUserRequest $request)
    {
        // バリデーション通過済み - ここに到達した時点で422は返らない
        $user = User::create($request->validated());
        return response()->json($user, 201);
    }
}

Laravel 在验证失败时返回的响应取决于请求是否期望 JSON (<code>Accept: application/json</code>)。对于 JSON 请求,返回带有 422 状态的错误 JSON,而对于普通表单请求,错误被存储在会话中并重定向到原始页面。

如何在 Django 中使用 422

Django REST Framework (DRF) 在验证失败时默认返回 400,但可以自定义为使用 422。

# Django REST Framework: カスタム例外ハンドラーで422を返す
from rest_framework.views import exception_handler
from rest_framework.exceptions import ValidationError
from rest_framework import status

def custom_exception_handler(exc, context):
    response = exception_handler(exc, context)

    if isinstance(exc, ValidationError) and response is not None:
        # バリデーションエラーのステータスを422に変更
        response.status_code = status.HTTP_422_UNPROCESSABLE_ENTITY

    return response

# settings.py に設定
# REST_FRAMEWORK = {
#     'EXCEPTION_HANDLER': 'myapp.utils.custom_exception_handler'
# }

# シリアライザーでのバリデーション
from rest_framework import serializers

class UserSerializer(serializers.Serializer):
    name = serializers.CharField(max_length=255)
    email = serializers.EmailField()
    avatar = serializers.ImageField(required=False)

    def validate_email(self, value):
        if User.objects.filter(email=value).exists():
            raise serializers.ValidationError(
                "このメールアドレスは既に登録されています。"
            )
        return value

Rails 中 422 的使用方法

在 Ruby on Rails 中,模型验证失败时通常使用 <code>:unprocessable_entity</code> 符号返回 422。

# Rails: バリデーションエラー時に422を返す
class UsersController < ApplicationController
  def create
    user = User.new(user_params)

    if user.save
      render json: user, status: :created
    else
      render json: {
        message: "バリデーションエラー",
        errors: user.errors.full_messages
      }, status: :unprocessable_entity  # 422
    end
  end

  private

  def user_params
    params.require(:user).permit(:name, :email, :avatar)
  end
end

# モデルのバリデーション
class User < ApplicationRecord
  validates :name, presence: true, length: { maximum: 255 }
  validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
  validates :email, uniqueness: true

  has_one_attached :avatar
  validates :avatar, content_type: ['image/jpeg', 'image/png', 'image/webp'],
                     size: { less_than: 5.megabytes }
end

文件上传中的 422

文件上传功能中,各种验证可能会返回 422。与 413(大小超过)不同,422 在服务器成功接收请求但文件内容不符合要求时返回。

验证项目 说明 状态码
无效的 MIME 类型 扩展名为 <code>.jpg</code> 但 MIME 类型为 <code>text/plain</code> 的情况 422
无效扩展名 不允许的文件格式(如 .exe) 422
图像尺寸超出范围 小于最小大小或超过最大大小 422
文件损坏 无法作为图像加载的损坏文件 422
病毒检测 当ClamAV等工具检测到恶意软件时 422
大小超过限制 应用程序端限制超出 422 or 413
// ファイルアップロードの詳細バリデーション例(Laravel)
public function uploadAvatar(Request $request)
{
    $request->validate([
        'avatar' => ['required', 'file'],
    ]);

    $file = $request->file('avatar');

    // MIMEタイプの二重チェック(拡張子偽装対策)
    $allowedMimes = ['image/jpeg', 'image/png', 'image/webp'];
    $detectedMime = $file->getMimeType(); // finfo による判定

    if (!in_array($detectedMime, $allowedMimes)) {
        return response()->json([
            'message' => 'バリデーションエラー',
            'errors' => [
                'avatar' => [
                    "許可されていないファイル形式です。検出されたMIMEタイプ: {$detectedMime}"
                ]
            ]
        ], 422);
    }

    // 画像として正常に読み込めるか確認
    $imageInfo = @getimagesize($file->getRealPath());
    if ($imageInfo === false) {
        return response()->json([
            'message' => 'バリデーションエラー',
            'errors' => [
                'avatar' => ['ファイルが破損しているか、有効な画像ファイルではありません。']
            ]
        ], 422);
    }

    // 画像寸法チェック
    [$width, $height] = $imageInfo;
    if ($width < 100 || $height < 100) {
        return response()->json([
            'message' => 'バリデーションエラー',
            'errors' => [
                'avatar' => ["画像は100x100px以上である必要があります。現在: {$width}x{$height}px"]
            ]
        ], 422);
    }

    // バリデーション通過 - 保存処理
    $path = $file->store('avatars', 'public');
    return response()->json(['path' => $path], 201);
}

API 的 422 响应格式(RFC 7807 Problem Details)

RFC 7807(HTTP API 的问题详情)已被制定以标准化 API 中的错误响应。通过遵守此规范,客户端侧的错误处理可以一致地执行。

// RFC 7807 準拠の 422 レスポンス例
// Content-Type: application/problem+json

{
    "type": "https://example.com/problems/validation-error",
    "title": "バリデーションエラー",
    "status": 422,
    "detail": "送信されたデータに2件のエラーがあります。",
    "instance": "/api/users",
    "errors": [
        {
            "field": "email",
            "message": "有効なメールアドレスを入力してください。",
            "code": "invalid_format"
        },
        {
            "field": "avatar",
            "message": "ファイル形式はJPEG・PNG・WebPのみ対応しています。",
            "code": "invalid_mime_type"
        }
    ]
}
// Laravel で RFC 7807 準拠のレスポンスを返す
use Symfony\Component\HttpFoundation\Response;

class ApiController extends Controller
{
    protected function validationProblem(
        array $errors,
        string $detail = 'バリデーションエラーが発生しました。'
    ): Response {
        $formattedErrors = [];
        foreach ($errors as $field => $messages) {
            foreach ($messages as $message) {
                $formattedErrors[] = [
                    'field'   => $field,
                    'message' => $message,
                ];
            }
        }

        return response()->json([
            'type'   => 'https://example.com/problems/validation-error',
            'title'  => 'Unprocessable Entity',
            'status' => 422,
            'detail' => $detail,
            'errors' => $formattedErrors,
        ], 422, [
            'Content-Type' => 'application/problem+json',
        ]);
    }
}

前端错误显示模式

前端收到 422 响应时,必须以清晰易懂的方式为每个字段显示错误消息。

// fetch API での 422 エラーハンドリング
async function submitForm(formData) {
    try {
        const response = await fetch('/api/users', {
            method: 'POST',
            headers: {
                'Accept': 'application/json',
                'Content-Type': 'application/json',
            },
            body: JSON.stringify(formData),
        });

        if (response.status === 422) {
            const data = await response.json();

            // フィールドごとにエラーを表示
            clearErrors();
            for (const [field, messages] of Object.entries(data.errors)) {
                const input = document.querySelector(`[name="${field}"]`);
                if (input) {
                    input.classList.add('border-red-500');
                    const errorDiv = document.createElement('div');
                    errorDiv.className = 'text-red-500 text-sm mt-1';
                    errorDiv.textContent = messages[0];
                    input.parentNode.appendChild(errorDiv);
                }
            }
            return;
        }

        if (!response.ok) {
            throw new Error('サーバーエラーが発生しました。');
        }

        const result = await response.json();
        showSuccess('登録が完了しました。');
    } catch (error) {
        showError(error.message);
    }
}

function clearErrors() {
    document.querySelectorAll('.border-red-500').forEach(el => {
        el.classList.remove('border-red-500');
    });
    document.querySelectorAll('.text-red-500').forEach(el => {
        el.remove();
    });
}
// axios での 422 エラーハンドリング(Vue.js / React 等で利用)
import axios from 'axios';

// グローバルインターセプターで422を処理
axios.interceptors.response.use(
    response => response,
    error => {
        if (error.response && error.response.status === 422) {
            // バリデーションエラーをストアに保存
            const errors = error.response.data.errors;
            store.commit('setValidationErrors', errors);
        }
        return Promise.reject(error);
    }
);

// コンポーネントでの使用例
async function handleSubmit() {
    try {
        store.commit('clearValidationErrors');
        const response = await axios.post('/api/users', formData);
        // 成功処理
    } catch (error) {
        if (error.response?.status !== 422) {
            // 422以外のエラーはグローバルで処理
            alert('予期しないエラーが発生しました。');
        }
        // 422はインターセプターで処理済み
    }
}

本文中可用的测试文件(免费)

  • → <a href="/ja/files/images/" class="text-primary-600 dark:text-primary-400 hover:underline">测试图像列表</a> — 用于测试 MIME 类型和文件扩展名验证
  • → <a href="/ja/files/threshold/" class="text-primary-600 dark:text-primary-400 hover:underline">边界值测试文件列表</a> — 用于文件大小上限验证的边界值测试
  • → <a href="/ja/files/images/png/1mb/" class="text-primary-600 dark:text-primary-400 hover:underline">1MB 测试 PNG 图像</a> — 用于基本图像上传测试
  • → <a href="/ja/files/pdf/" class="text-primary-600 dark:text-primary-400 hover:underline">PDF 测试文件列表</a> — 用于文件格式验证测试

相关文章

  • → <a href="/ja/blog/http-413-error/" class="text-primary-600 dark:text-primary-400 hover:underline">413 Request Entity Too Large 错误:原因和解决方案</a>
  • → <a href="/ja/blog/file-validation-checklist/" class="text-primary-600 dark:text-primary-400 hover:underline">Web 表单文件验证实现检查清单</a>
  • → <a href="/ja/blog/laravel-file-upload/" class="text-primary-600 dark:text-primary-400 hover:underline">Laravel 文件上传实现指南|验证、Storage 和 S3 支持</a>
  • → <a href="/ja/blog/http-507-error/" class="text-primary-600 dark:text-primary-400 hover:underline">507 Insufficient Storage 错误:原因和解决方法</a>