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

Discord Bot+FastAPI集成发送消息遇RuntimeError及后续问题求助

问题描述

我正在开发一个与Discord Bot集成的API,尝试通过Bot向频道发送消息时触发了RuntimeError。改用Discord Webhook发送消息可成功,但Webhook不返回消息ID,无法通过Bot为消息添加反应;同时我不确定调用反应相关函数是否也会触发相同的RuntimeError。曾尝试直接调用Discord API但未成功,担心仍会出现相同报错,寻求解决方案。


完整报错栈
ERROR:    Exception in ASGI application
Traceback (most recent call last):
  File "/home/container/.local/lib/python3.10/site-packages/uvicorn/protocols/http/h11_impl.py", line 407, in run_asgi
    result = await app(  # type: ignore[func-returns-value]
  File "/home/container/.local/lib/python3.10/site-packages/uvicorn/middleware/proxy_headers.py", line 78, in __call__
    return await self.app(scope, receive, send)
  File "/home/container/.local/lib/python3.10/site-packages/fastapi/applications.py", line 271, in __call__
    await super().__call__(scope, receive, send)
  File "/home/container/.local/lib/python3.10/site-packages/starlette/applications.py", line 118, in __call__
    await self.middleware_stack(scope, receive, send)
  File "/home/container/.local/lib/python3.10/site-packages/starlette/middleware/errors.py", line 184, in __call__
    raise exc
  File "/home/container/.local/lib/python3.10/site-packages/starlette/middleware/errors.py", line 162, in __call__
    await self.app(scope, receive, _send)
  File "/home/container/.local/lib/python3.10/site-packages/starlette/middleware/exceptions.py", line 79, in __call__
    raise exc
  File "/home/container/.local/lib/python3.10/site-packages/starlette/middleware/exceptions.py", line 68, in __call__
    await self.app(scope, receive, sender)
  File "/home/container/.local/lib/python3.10/site-packages/fastapi/middleware/asyncexitstack.py", line 21, in __call__
    raise e
  File "/home/container/.local/lib/python3.10/site-packages/fastapi/middleware/asyncexitstack.py", line 18, in __call__
    await self.app(scope, receive, send)
  File "/home/container/.local/lib/python3.10/site-packages/starlette/routing.py", line 706, in __call__
    await route.handle(scope, receive, send)
  File "/home/container/.local/lib/python3.10/site-packages/starlette/routing.py", line 276, in handle
    await self.app(scope, receive, send)
  File "/home/container/.local/lib/python3.10/site-packages/starlette/routing.py", line 66, in app
    response = await func(request)
  File "/home/container/.local/lib/python3.10/site-packages/fastapi/routing.py", line 237, in app
    raw_response = await run_endpoint_function(
  File "/home/container/.local/lib/python3.10/site-packages/fastapi/routing.py", line 163, in run_endpoint_function
    return await dependant.call(**values)
  File "/home/container/bot.py", line 345, in test
    m = await channel.send(embed=embed)
  File "/home/container/.local/lib/python3.10/site-packages/discord/abc.py", line 1561, in send
    data = await state.http.send_message(channel.id, params=params)
  File "/home/container/.local/lib/python3.10/site-packages/discord/http.py", line 624, in request
    async with self.__session.request(method, url, **kwargs) as response:
  File "/home/container/.local/lib/python3.10/site-packages/aiohttp/client.py", line 1141, in __aenter__
    self._resp = await self._coro
  File "/home/container/.local/lib/python3.10/site-packages/aiohttp/client.py", line 467, in _request
    with timer:
  File "/home/container/.local/lib/python3.10/site-packages/aiohttp/helpers.py", line 701, in __enter__
    raise RuntimeError(
RuntimeError: Timeout context manager should be used inside a task

API调用代码
class ClanCreateBody(BaseModel):
    clan_tag: str
    user_id: int
    username: str

        
@app.get("/createclan/")
async def test(body: ClanCreateBody, AuthToken: str | None = Header(default=None)):
    if AuthToken == "testtoken":
        pass
    else:
        return {"Invalid": "Token"}
    
    check_emoji = bool(emoji.get_emoji_regexp().search(body.clan_tag))
    if check_emoji:
        return {"Invalid": "Emojis are not allowed in clan tags"}

    if not body.clan_tag:
        return {"Invalid": "There is no clan tag"}

    # Check they don't already have a clan
    has_clan, clan = await bot.id_has_clan_both(body.user_id)
    if has_clan:
        if clan.role_id == -1:
            return {"Invalid": "User is already in a clan"}
        else:
            return {"Invalid": "User is already in a clan"}

    # Check this clan name isn't already taken
    name_is_taken = await bot.clan_name_is_taken(body.clan_tag)
    if name_is_taken:
        return {"Invalid": "This clan name is already taken!"}

    # It's good to create
    channel = bot.get_channel(secret_file["staff_channel_id"])
    embed = discord.Embed(
        title="Clan creation request",
        description=f"Clan name: `{body.clan_tag}`\nRequester: {body.username}\n\n"
        f"To accept this please react with a tick.\n"
        f"To reject this please react with a cross.",
        color=embed_color,
        timestamp=discord.utils.utcnow(),
    )
    embed.set_footer(text=f"{body.user_id} : Sent from web")
    m = await channel.send(embed=embed)
    await m.add_reaction("✅")
    await m.add_reaction("❎")

    await bot.db.pending_clans.insert(
        {
            "_id": body.clan_tag.lower(),
            "name": body.clan_tag,
            "role_id": -1,
            "clan_points": 0,
            "total_koth_wins":0,
            "total_roam_wins":0,
            "total_maze_wins":0,       
            "leader_id": body.id,
            "members": [],
            "co_leaders": [],
            "pending_invites": [],
            "pending_message_id": m.id,
            "created_at": datetime.datetime.now().strftime("%-I:%M %p %m/%d/%Y"),
            "discord_invite": "Set with !clansetdiscord",
            "video_url": "!Set with !clannew",
            "channel_id": None,
        }
    )


    for member in staff:
        try:
            sname = secret_file["server_name"]
            await member.send(embed = discord.Embed(title= f"New Clan Application on {sname}",description=f"{body.user_id} wants to make **{body.clan_tag}** [Click Here]({m.jump_url}) to accept/decline that.",color=embed_color))
        
        except (discord.HTTPException, discord.Forbidden):
            pass
    
    return {"Status": "Request sent"}

解决方案

这个报错的核心原因是:aiohttp的超时上下文管理器没有在合法的asyncio任务环境中运行,本质是FastAPI的ASGI请求上下文与discord.py的事件循环不兼容导致的。以下是三种可行的解决方式:

方式1:用discord.py的事件循环创建独立任务执行Discord操作

将发送消息、添加反应、数据库插入等Discord相关逻辑,放到独立的异步函数中,通过discord.py的事件循环创建任务执行,脱离FastAPI的请求上下文:

# 定义独立的异步处理函数
async def handle_clan_request(channel, embed, bot, body, staff, secret_file):
    m = await channel.send(embed=embed)
    await m.add_reaction("✅")
    await m.add_reaction("❎")
    
    # 修复原代码bug:leader_id应该取body.user_id而非body.id
    await bot.db.pending_clans.insert(
        {
            "_id": body.clan_tag.lower(),
            "name": body.clan_tag,
            "role_id": -1,
            "clan_points": 0,
            "total_koth_wins":0,
            "total_roam_wins":0,
            "total_maze_wins":0,       
            "leader_id": body.user_id,
            "members": [],
            "co_leaders": [],
            "pending_invites": [],
            "pending_message_id": m.id,
            "created_at": datetime.datetime.now().strftime("%-I:%M %p %m/%d/%Y"),
            "discord_invite": "Set with !clansetdiscord",
            "video_url": "!Set with !clannew",
            "channel_id": None,
        }
    )

    for member in staff:
        try:
            sname = secret_file["server_name"]
            await member.send(embed = discord.Embed(title= f"New Clan Application on {sname}",description=f"{body.user_id} wants to make **{body.clan_tag}** [Click Here]({m.jump_url}) to accept/decline that.",color=embed_color))
        except (discord.HTTPException, discord.Forbidden):
            pass

# 在原test函数中,替换原发送逻辑为创建任务
bot.loop.create_task(handle_clan_request(channel, embed, bot, body, staff, secret_file))

方式2:让FastAPI与discord.py共享同一个事件循环

配置uvicorn使用discord.py的事件循环启动FastAPI,确保上下文兼容:

from uvicorn import Config, Server

# 初始化bot后获取它的事件循环
loop = bot.loop

# 配置FastAPI使用该循环
config = Config(app=app, loop=loop)
server = Server(config)

# 启动服务
loop.run_until_complete(server.serve())

方式3:直接调用Discord REST API(可获取消息ID)

绕过discord.py的会话,用aiohttp直接调用Discord官方API,发送消息后拿到ID再调用添加反应的接口:

import aiohttp

async def send_and_react(channel_id, embed, bot_token):
    headers = {"Authorization": f"Bot {bot_token}"}
    async with aiohttp.ClientSession() as session:
        # 发送消息
        send_url = f"https://discord.com/api/v9/channels/{channel_id}/messages"
        payload = {"embeds": [embed.to_dict()]}
        async with session.post(send_url, json=payload, headers=headers) as resp:
            if resp.status != 200:
                raise Exception(f"发送消息失败: {await resp.text()}")
            message_data = await resp.json()
            message_id = message_data["id"]
            
            # 添加✅反应
            react_url = f"https://discord.com/api/v9/channels/{channel_id}/messages/{message_id}/reactions/%E2%9C%85/@me"
            await session.put(react_url, headers=headers)
            # 添加❎反应
            react_url = f"https://discord.com/api/v9/channels/{channel_id}/messages/{message_id}/reactions/%E2%9D%8E/@me"
            await session.put(react_url, headers=headers)
            
            return message_id

内容的提问来源于stack exchange,提问作者Murder

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.28 18:07:18