bug: Webhook requests repeatedly register middleware on cached bots until the call stack overflows
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 26
- Forks
- 7
- PR merge metrics
- No merged PRs in 30d
Description
[!NOTE]
This issue was primarily written by GPT-5.6 Sol xhigh in ChatGPT Work.
Summary
In webhook mode, JsonBot registers the same application composer on the bot for every incoming POST request:
const bot = getBot(botMode, botToken);
if (bot) {
bot.use(composer);
return await webhookCallback(bot, "std/http")(req);
}
Source: https://github.com/SpEcHiDe/JsonBot/blob/3770d5cdcb40fe1ac8771d344eb5a1a04e5eca50/start.ts#L38-L46
However, getBot caches and returns the same Bot instance for subsequent requests using the same token:
const bots = new Map<string, Bot<MyContext>>();
export function getBot(mode: string, token: string) {
let bot = bots.get(token);
if (!bot) {
// Construct and configure the bot
// ...
bots.set(token, bot);
}
return bot;
}
Source: https://github.com/SpEcHiDe/JsonBot/blob/3770d5cdcb40fe1ac8771d344eb5a1a04e5eca50/src/bots.ts#L4-L8
Source: https://github.com/SpEcHiDe/JsonBot/blob/3770d5cdcb40fe1ac8771d344eb5a1a04e5eca50/src/bots.ts#L59-L65
Consequently, every webhook request permanently appends another copy of the application composer to the cached bot's middleware chain. The chain grows without bound until dispatching an update throws:
RangeError: Maximum call stack size exceeded
at .../src/composer.ts:147:12
at .../src/composer.ts:149:15
at .../src/composer.ts:149:15
...
The repeated composer.ts:149 frames in the reported stack trace are consistent with grammY recursively entering the increasingly deep chain of composed middleware.
Minimal reproduction
The failure still occurs when the registered application composer contains only a terminating leaf middleware. The leaf does not call next and therefore cannot itself recurse into downstream middleware:
import { Composer } from "grammy/mod.ts";
let leafCalls = 0;
const applicationComposer = new Composer();
// This is a terminating leaf. It does not accept or call `next`.
applicationComposer.use(() => {
leafCalls++;
});
// Represents the Bot instance retained in JsonBot's `bots` map.
// Bot extends Composer, so the relevant composition behavior is identical.
const cachedBot = new Composer();
// Represents repeatedly executing `bot.use(composer)` inside the
// webhook request handler.
for (let request = 0; request < 20_000; request++) {
cachedBot.use(applicationComposer);
}
try {
await cachedBot.middleware()(
{} as never,
() => Promise.resolve(),
);
} catch (error) {
console.log(error instanceof RangeError); // true
console.log(leafCalls); // 0
}
This produces RangeError: Maximum call stack size exceeded. The precise number of registrations required depends on the JavaScript engine and its available stack size.
Notably, leafCalls remains 0. The terminating application middleware is never reached before the stack overflows.
No Telegram API request or real context object is needed to reproduce the failure.
Registering applicationComposer once and then invoking the resulting middleware for 20,000 requests completes successfully.
Expected behavior
The application composer should be registered once when a bot is created. Processing additional webhook requests should not mutate or enlarge the bot's middleware chain.
A terminating application composer should then handle an update and stop the middleware flow normally.
Actual behavior
Every webhook request registers the application composer again on the cached bot.
The middleware chain and its retained closures therefore grow with the number of requests. After enough requests, middleware dispatch exceeds the maximum call-stack size.
Once this happens, later requests for the same cached bot continue to fail because the oversized middleware chain remains attached and continues growing.
The stack overflow occurs before execution reaches even the first terminating application middleware.
Suspected cause
Composer.use appends middleware by replacing the current handler with a new handler that wraps the complete previous handler:
use(...middleware: Array<Middleware<C>>) {
const composer = new Composer(...middleware);
this.handler = concat(this.handler, flatten(composer));
return composer;
}
Conceptually, repeated registration produces:
H₁ = concat(H₀, applicationComposer)
H₂ = concat(H₁, applicationComposer)
H₃ = concat(H₂, applicationComposer)
...
Hₙ = concat(Hₙ₋₁, applicationComposer)
When Hₙ handles an update, concat invokes its first handler before it can invoke andThen:
return async (ctx, next) => {
let nextCalled = false;
await first(ctx, async () => {
if (nextCalled) throw new Error("`next` already called before!");
else nextCalled = true;
await andThen(ctx, next);
});
};
Execution must therefore descend through the accumulated outer handlers:
Hₙ → Hₙ₋₁ → Hₙ₋₂ → ... → H₀
Only after that descent could the first registered copy of applicationComposer run and terminate the middleware flow. With enough registrations, the call stack is exhausted during the descent, before that leaf is reached.
The terminating nature of the application composer prevents later middleware from running, but it does not prevent the cached bot's repeatedly wrapped outer handler from overflowing the stack.
The bot.use(composer) call itself does not normally throw immediately. The overflow occurs when grammY subsequently dispatches an update through the accumulated middleware chain.
The newer grammY revision currently pinned by JsonBot retains the same relevant Composer.use and concat implementation, so updating the grammY revision alone does not resolve this application lifecycle problem.
Suggested fix
The application composer should be registered exactly once when each bot is created.
Webhook requests should retrieve the already-configured bot and dispatch the update without mutating its middleware chain.
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in start.ts at the webhook handler and then inspect src/bots.ts at getBot and the bot cache. Reproduce the repeated registration with the minimal middleware example from the issue, then verify that webhook requests reuse a configured bot without growing its middleware chain and that a terminating middleware completes normally.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- api, backend
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 78/100