422 Unprocessable Entity 错误的原因和解决方案|验证错误的正确返回方式
开发 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>