grammY: Timeouts on Vercel deployments

I’m using the following configuration of handling webhooks. While it’s working in development (API route build time is under 1.5s), Vercel constantly reports function timeout without response. Some of the commands are using image send from static host with ctx.replyWithPhoto. The std/http method is the only one working for me, neither http/https, not next-js are not working because of different issues

import { Context as GrammyContext, SessionFlavor } from "grammy";
import { Conversation, ConversationFlavor } from "@grammyjs/conversations";

export interface SessionData {
  // session fields types
}

export type Context = GrammyContext &
  ConversationFlavor &
  SessionFlavor<SessionData>;
export type ConversationContext = Conversation<Context>;
export const POST = async (req: NextRequest, ...args: any[]) => {
  const { data, error } = await supabase
    .from("companies")
    .select("bot_token")
    .eq("slug", req.headers.get("host")!.split(".")[0])
    .single();

  if (error) {
    return NextResponse.json({ ...error }, { status: 500 });
  }

  const token = data.bot_token;

  const middleware = new Composer<Context>();

  middleware.command("start", start);
  middleware.command("request", request);

  const bot = new Bot<Context>(token);

  bot.use(
    session({
      initial: () => ({
        ...
      }),
      storage: enhanceStorage({
        storage: freeStorage(token),
        millisecondsToLive: 10 * 60 * 1000,
      }),
    })
  );

  // conversations, commands and handlers used here

  bot.catch((err) => {
    console.error("Error:", err);
  });

  const handleUpdate = webhookCallback(bot, "std/http", "throw", 15_000);

  return handleUpdate(req, ...args);
};

Any suggestions or recommendations?

Thanks in advance!

About this issue

  • Original URL
  • State: closed
  • Created 7 months ago
  • Comments: 36 (17 by maintainers)

Most upvoted comments

I think I have found a solution to the issue.

The key lies in how Next.js handles server dependencies during build time. Just add the grammy dependency to serverComponentsExternalPackages in the next.config.js config and it should work. Find more information about serverComponentsExternalPackages"here: https://nextjs.org/docs/app/api-reference/next-config-js/serverComponentsExternalPackages

My code:

Route

// src/app/api/bot/route.ts

import { NextRequest } from 'next/server';
import { Bot, webhookCallback } from 'grammy';

export const POST = async (req: NextRequest, ...args: any[]) => {
	const token = process.env.TELEGRAM_TOKEN;
	if (!token) throw new Error('TELEGRAM_TOKEN is unset');

	const bot = new Bot(token);

	bot.command('start', ctx => ctx.reply('Ласкаво просимо! Бот запущений.'));

	bot.on('message', ctx => ctx.reply('Отримав ще одне повідомлення!'));

	const handleUpdate = webhookCallback(bot, 'std/http', 'throw', 10000);

	return handleUpdate(req, ...args);
};

Next.js config

// next.config.mjs

/** @type {import('next').NextConfig} */
const nextConfig = {
	experimental: {
		serverComponentsExternalPackages: ['grammy']
	}
};

export default nextConfig;

I hope this will be useful to someone who also decides to create a telegram bot w/ grammY and Next.js.

At some point I decided to migrate my bot to Deno, so it will take me some time to replicate the existing bot back on Next.js. I will post an update as soon as I will do some testings

Hey @KnorpelSenf! Sorry for long reply, wasn’t able to test out the suggestion you made during weekends. I tried to implement it now, but it still failing with timeout. Including the source code of the endpoint and the screenshot of log for triggering the endpoint

app/api/bots/requests/route.ts

import { type NextRequest, NextResponse } from "next/server";
import { Bot } from "grammy";

import { supabase } from "@/utils/supabase";

const bot = new Bot(process.env.REQUESTS_BOT_TOKEN!);

bot.on("message::bot_command", async (ctx) => {
  // Match command pattern /process_<id>
  const match = ctx.message!.text!.match(/^\/process_(\d+)$/);

  if (!match) return;

  const id = match[1];

  const { data, error } = await supabase
    .from("requests")
    .select()
    .eq("id", id)
    .single();

  if (error) {
    await ctx.reply("Виникла помилка при завантаженні заявки.");
    await ctx.reply(error.message);

    return;
  }

  await ctx.reply(
    `
<pre><code>
company_name: ${data.company_name}
company_slug: ${data.company_slug}
phone_number: ${data.phone_number}
user_id: ${data.user_id}
user_username: ${data.user_username}
</code></pre>
    `
  );
});

bot.on("message", async (ctx) => {
  await ctx.reply("Ping");
});

export const POST = async (req: NextRequest) => {
  let initialized = false;

  if (!initialized) {
    await bot.init();

    initialized = true;
  }

  let usedWebhookReply = false;

  const webhookReplyEnvelope = {
    send: async (json: any) => {
      usedWebhookReply = true;
      await new Promise((resolve) => resolve(NextResponse.json(json)));
    },
  };

  await bot.handleUpdate(await req.json(), webhookReplyEnvelope);

  if (!usedWebhookReply) {
    return NextResponse.json(null, { status: 200 });
  }
};

image