You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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方法也未处理异步操作的等待逻辑。

修复步骤

  1. 修正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)
    
  2. 改造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"])
    
  3. 可选优化建议

    • 移除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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.24 03:07:39