Django异步项目(Uvicorn)POST构建触发sync_to_async类型错误
依赖版本
- python==3.11.3
- asgiref==3.6.0
- Django==4.1.8
问题描述
我正在开发一个基于Uvicorn的Django异步项目,书籍构建功能本身逻辑正常,但触发构建的POST请求执行时,虽然书籍已成功创建,却抛出如下错误:
TypeError at /builds/ sync_to_async can only be applied to sync functions.
刷新页面后显示正常,相关代码如下:
import re from subprocess import check_output from asgiref.sync import sync_to_async from django.contrib.auth import get_user_model from django.contrib.auth.mixins import LoginRequiredMixin from django.db.utils import IntegrityError from django.shortcuts import get_object_or_404, render, redirect from django.views.generic import View from crayon.books.models import Book from crayon.builds.models import Build from crayon.core.mixin import AsyncViewMixin class AsyncViewMixin: async def __call__(self): return super().__call__(self) def build_book(book_id: int, user_id: int) -> str: global document, build # step 1: Get the book book = Book.objects.get(id=book_id, creator_id=user_id) # step 2: compile the book's contents build = Build.objects.create(builder_id=user_id, book_id=book_id) build.status = Build.STATUS.IN_PROGRESS build.save() document_list = [] for chapter in book.chapter_set.all(): document_list.append(f"# {chapter.title}") # If a "reges" finds an markdown H1 if re.search(r"\n\#\s", chapter.body) or re.match("\#\s", chapter.body): chapter.body = add_pound_to_headers(chapter.body) document_list.append(chapter.body) document = "\n\n".join(document_list) # step 3: render as something using pandoc # Pandoc -o book.html input.md with open("input.md", "w") as f: f.write(document) commands = ["pandoc", "-o", "book.html", "input.md"] output = check_output(commands) return output class BuildView(LoginRequiredMixin, AsyncViewMixin, View): def get(self, request, *args, **kwargs): # If without ID then shows list of builds for user # If with ID then show details return render(request, "builds/build.html", {}) def post(self, request, *args, **kwargs): # Step 1: get the book book = get_object_or_404(Book, id=self.request.POST["book_id"], creator=request.user.id) # accept a form request asking for a build sync_to_async(build_book(book_id=book.id, user_id=request.user.id)) return redirect("books:update", pk=request.POST["book_id"])
问题原因与修复
核心错误
你调用sync_to_async时犯了关键错误:直接执行build_book(...)并把函数返回值传给了sync_to_async,但sync_to_async要求传入的是同步函数对象,而不是函数执行后的结果(这里返回的是字符串),因此触发类型错误。同时你的AsyncViewMixin实现不正确,无法支撑异步视图的执行逻辑,post方法也未处理异步操作的等待逻辑。
修复步骤
修正
AsyncViewMixin实现
让Mixin正确处理异步视图的调用流程:import asyncio class AsyncViewMixin: async def __call__(self, request, *args, **kwargs): # 处理异步GET/POST方法 if hasattr(self, 'get') and asyncio.iscoroutinefunction(self.get): return await self.get(request, *args, **kwargs) elif hasattr(self, 'post') and asyncio.iscoroutinefunction(self.post): return await self.post(request, *args, **kwargs) # 兼容同步方法 return super().__call__(request, *args, **kwargs)改造
post方法为异步并正确使用sync_to_async
将post改为异步方法,用sync_to_async包裹函数对象并传入参数,最后await执行:class BuildView(LoginRequiredMixin, AsyncViewMixin, View): # 保留原有get方法不变 def get(self, request, *args, **kwargs): return render(request, "builds/build.html", {}) async def post(self, request, *args, **kwargs): book = get_object_or_404(Book, id=request.POST["book_id"], creator=request.user) # 正确用法:先包裹函数对象,再传入参数并await await sync_to_async(build_book)(book_id=book.id, user_id=request.user.id) return redirect("books:update", pk=request.POST["book_id"])可选优化建议
- 移除
build_book中的全局变量document和build,改用局部变量,避免并发场景下的变量污染 - 不要直接使用当前目录的
input.md,建议用临时文件或用户专属目录,防止多用户并发时文件被覆盖 - 为
check_output添加异常捕获,处理pandoc未安装或执行失败的情况
- 移除
后台执行构建(可选)
如果不需要等待构建完成就返回响应,可以用asyncio.create_task将任务丢到后台执行:
import asyncio async def post(self, request, *args, **kwargs): book = get_object_or_404(Book, id=request.POST["book_id"], creator=request.user) asyncio.create_task(sync_to_async(build_book)(book_id=book.id, user_id=request.user.id)) return redirect("books:update", pk=request.POST["book_id"])
内容的提问来源于stack exchange,提问作者Houdaeyfha Alsalih
相关产品推荐
相关产品推荐

