使用 axios 提交退款表单时出现 server error
明确结论
问题出在前端提交的数据与请求头不匹配:
- 图片保存在
refusjonBilde中,提交时使用的却是不存在的refusjonData.image。 - 使用
FormData上传文件时,不能将Content-Type设置为application/json。 - “Kontonummer” 输入框的
name被误写成amount,所以kontonm始终为空。 - 后端捕获异常后仍返回 HTTP 200,Axios 因此会进入
.then(),前端也就显示了成功提示。 - 如果后端的 APIView 和数据模型都叫
Refusjon,那么Refusjon(image=...)可能实例化了当前 APIView,而不是数据模型。
Postman 能正常调用,不代表浏览器提交了完全相同的数据。需要对比两边的请求头、上传字段名和图片字段的实际内容。
修正前端图片处理
当前代码:
if ([e.target.name] == "image") {
setRefusjonBilde({
image: e.target.files,
});
}
这段代码保存的是一个包含 FileList 的对象,但提交时使用的是:
formData.append("image", refusjonData.image);
initialFormData 中没有 image,所以这里提交的通常是 undefined,不是用户选择的文件。
可以直接保存第一个文件:
const handleChange = (e) => {
const { name, value, files } = e.target;
if (name === "image") {
setRefusjonBilde(files?.[0] ?? null);
return;
}
updateFormData((current) => ({
...current,
[name]: value,
}));
};
提交时使用 refusjonBilde:
if (refusjonBilde) {
formData.append("image", refusjonBilde);
}
e.target.files 是 FileList。上传单个文件时,应使用 e.target.files[0]。
不要为 FormData 设置 application/json
当前配置将请求正文声明为 JSON:
const config = {
headers: {
Authorization: `Bearer ${localStorage.getItem("token")}`,
"Content-Type": "application/json",
},
};
实际发送的却是 FormData。浏览器需要自动生成类似下面的请求头,并附上必要的 boundary:
Content-Type: multipart/form-data; boundary=...
因此,不要手动设置 Content-Type:
const config = {
headers: {
Authorization: `Bearer ${localStorage.getItem("token")}`,
},
};
Axios 和浏览器会根据 FormData 自动设置正确的 multipart 请求头。
修正 kontonm 字段名称
“Kontonummer” 输入框目前是:
<input
type="text"
onChange={handleChange}
name="amount"
maxLength="40"
required
/>
这个输入框会覆盖 amount,不会更新 kontonm。应改为:
<input
type="text"
className="form-control col-10 m-2"
onChange={handleChange}
name="kontonm"
maxLength="40"
required
/>
否则,后端执行下面的代码时,得到的可能是空字符串:
data["kontonm"]
推荐的前端提交代码
提交事件更适合放在 <form> 的 onSubmit 上,而不是按钮的 onClick 上:
<form onSubmit={onSubmit}>
{/* 表单字段 */}
<button
className="btn btn-primary m-2"
type="submit"
disabled={loading}
>
Send
</button>
</form>
完整的提交逻辑可以写成:
const onSubmit = async (e) => {
e.preventDefault();
if (!refusjonBilde) {
setAlert("Velg et bilde", "danger");
return;
}
setLoading(true);
const formData = new FormData();
formData.append("name", refusjonData.name);
formData.append("email", refusjonData.email);
formData.append("address", refusjonData.address);
formData.append("subject", "Refusjon");
formData.append("amount", refusjonData.amount);
formData.append("date", refusjonData.date);
formData.append("image", refusjonBilde);
formData.append("message", refusjonData.message);
formData.append("kontonm", refusjonData.kontonm);
try {
const response = await axios.post(
`${process.env.REACT_APP_API_URL}/api/skjema/`,
formData,
{
headers: {
Authorization: `Bearer ${localStorage.getItem("token")}`,
},
}
);
console.log(response.data);
setAlert("Skjemaet ble sendt", "success");
window.scrollTo(0, 0);
} catch (error) {
console.error(
"Refund request failed:",
error.response?.status,
error.response?.data ?? error.message
);
setAlert("Skjema ble ikke sendt! Prøv igjen", "danger");
window.scrollTo(0, 0);
} finally {
setLoading(false);
}
};
调试时,可以检查实际提交的内容:
for (const [key, value] of formData.entries()) {
console.log(key, value);
}
此时,image 应显示为 File 对象,而不是 undefined。
为什么后端报错却触发 .then()
后端无论成功还是失败,都使用默认的 HTTP 200:
except:
return Response({
"error": "Skjemaet kunne ikke sendes! Prøv igjen."
})
Axios 根据 HTTP 状态码决定进入 .then() 还是 .catch()。即使响应正文中包含 "error",Axios 也不会自动将其视为失败。
后端应返回正确的错误状态码,例如:
from rest_framework import status
from rest_framework.response import Response
return Response(
{"error": "Skjemaet kunne ikke sendes! Prøv igjen."},
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
)
如果是输入数据缺失或格式错误,返回 400 Bad Request 更合适。
修正后端文件读取和错误处理
如果当前 APIView 的类名也是 Refusjon,下面的代码有明显风险:
class Refusjon(APIView):
# ...
ri = Refusjon(image=data["image"])
这里的 Refusjon 会指向当前 APIView,而不是同名的数据模型。实例化 APIView 时传入 image,很可能直接抛出 TypeError。
这里只需要读取上传的文件,不必创建临时模型对象:
uploaded_image = request.FILES.get("image")
视图可以改成下面的结构:
import logging
from django.core.mail import EmailMessage
from email.mime.image import MIMEImage
from rest_framework import status
from rest_framework.parsers import MultiPartParser, FormParser
from rest_framework.response import Response
from rest_framework.views import APIView
logger = logging.getLogger(__name__)
class RefusjonView(APIView):
parser_classes = [MultiPartParser, FormParser]
def post(self, request, format=None):
data = request.data
uploaded_image = request.FILES.get("image")
required_fields = [
"name",
"email",
"address",
"amount",
"date",
"kontonm",
"message",
]
missing_fields = [
field for field in required_fields if not data.get(field)
]
if uploaded_image is None:
missing_fields.append("image")
if missing_fields:
return Response(
{
"error": "Missing required fields",
"fields": missing_fields,
},
status=status.HTTP_400_BAD_REQUEST,
)
try:
body = "\n".join([
f"navn: {data['name']}",
f"epost: {data['email']}",
f"adresse: {data['address']}",
f"Sum: {data['amount']}",
f"dato: {data['date']}",
f"konto nummer: {data['kontonm']}",
f"melding: {data['message']}",
])
msg = EmailMessage(
subject="Refusjon",
body=body,
from_email="info@gmail.com",
to=["admin@gmail.com"],
)
image_subtype = uploaded_image.name.rsplit(".", 1)[-1].lower()
mime_image = MIMEImage(
uploaded_image.read(),
_subtype=image_subtype,
)
mime_image.add_header("Content-ID", "<image>")
msg.attach(mime_image)
sent_count = msg.send(fail_silently=False)
if sent_count != 1:
raise RuntimeError("Email backend did not send the message")
return Response(
{"success": "Skjemaet ble sendt"},
status=status.HTTP_200_OK,
)
except Exception:
logger.exception("Failed to send refund email")
return Response(
{"error": "Skjemaet kunne ikke sendes! Prøv igjen."},
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
)
其他注意事项
不要使用空的 except:。它会隐藏真正的错误,让图片缺失、字段拼写错误和邮件服务器故障看起来毫无区别。至少要记录异常堆栈:
except Exception:
logger.exception("Failed to send refund email")
另外,代码虽然创建了 serializer,却没有验证或使用它:
serializer = RefusjonSerializer(data=request.data)
如果已经定义了 serializer,应调用:
serializer.is_valid(raise_exception=True)
data = serializer.validated_data
这样一来,缺少 image、kontonm 或其他字段时,Django REST Framework 会直接返回清晰的 400 响应,不必等到构造邮件时才触发异常。
需要优先修复四处问题:上传 refusjonBilde 中的 File,移除错误的 JSON 请求头,将账户输入框改为 name="kontonm",并让后端在发生异常时返回非 2xx 状态码。
备注:内容仅供参考。
