telega/webhook_reply
Webhook reply optimization: answer an update by embedding a bot API call into the webhook HTTP response body instead of making a separate HTTP request to Telegram.
Telegram allows the webhook HTTP response to carry one API call as JSON
({"method": "sendMessage", ...}) — it is executed as if it were a normal
request, saving one HTTP round-trip and not counting against the request
quota. This module implements the claim protocol between a handler’s API
call and the webhook HTTP process that is still holding the connection.
⚠️ Telegram does not return the result of an embedded call. A claimed call resolves to a synthetic stub:
Truefor boolean methods, and forsendMessagea fakeMessagewithmessage_id: -1anddate: 0. If your handler needs the realmessage_id, do not use webhook reply for that call.
This is opt-in: adapters expose it as handle_bot_with_reply, built on
telega.handle_update_webhook. Regular handle_bot behavior is unchanged.
Usage
import telega_wisp
fn handle_request(bot, req) {
use <- telega_wisp.handle_bot_with_reply(bot, req, timeout: 5000)
// other routes...
}
telega_mist.handle_bot_with_reply works the same way. For a custom
adapter, call telega.handle_update_webhook(telega:, update:, timeout:)
and map the result yourself:
case telega.handle_update_webhook(telega:, update:, timeout: 5000) {
// Answer 200 with this JSON body, Content-Type: application/json.
telega.JsonResponse(body:) -> todo
// Answer an empty 200.
telega.EmptyResponse -> todo
}
With handle_bot_with_reply, a bot answering a callback button makes
zero outgoing HTTP calls for that update.
Eligible methods
Only the first eligible POST call per update is claimed:
answerCallbackQuery,deleteMessage,setMessageReaction,sendChatAction— boolean methods, the stub is honest;sendMessage— with the documented fakeMessageabove (the chat id is copied from your request when numeric,-1otherwise).
Everything else — GET requests included — always goes over HTTP. If a
handler needs the real message_id (to edit or reply to the message
later), that call must go over regular HTTP — do not use webhook reply
for it.
Latency semantics
Unlike handle_bot, the request process waits — up to timeout ms —
for the handler to either claim a call or finish. Telegram serializes
updates per webhook connection slot, so pick a timeout well below
Telegram’s own webhook timeout; 5000 ms is a sensible default. When the
timeout fires the adapter answers an empty 200 OK, the handler keeps
running in the background, and all of its API calls go over regular HTTP.
Claim protocol
telega.handle_update_webhookcreates a per-updateEnvelopeand dispatches the update, then waits on the envelope and on handler completion, whichever comes first.- The chat instance wraps that update’s API client with
transformer, which serializes the first eligible call into aClaimand waits for a grant for at most 100 ms. - First-wins, race-free: only an HTTP process that is still waiting grants a claim. If it already answered (or timed out), the grant wait expires and the call falls back to a regular HTTP request. At most one claim is attempted per update, so the 100 ms window is paid at most once, and only when the HTTP process is already gone.
- On a grant,
handle_update_webhooksplices"method"into the request params and returnsJsonResponse.
The claimed call resolves inside your handler immediately on grant — the handler does not wait for Telegram to execute it.
Testing
Use a routed mock and assert on the response body — see
test/webhook_reply_test.gleam for full examples:
let assert telega.JsonResponse(body:) =
telega.handle_update_webhook(telega: bot, update: raw, timeout: 5000)
string.contains(body, "\"method\":\"answerCallbackQuery\"")
Types
Per-update channel owned by the webhook HTTP process.
pub type Envelope =
process.Subject(EnvelopeMessage)
Message sent by a handler’s API call to the webhook HTTP process holding
the connection. The sender waits on granted; only a still-waiting HTTP
process replies True. If it already responded or timed out, the wait
expires and the call goes over regular HTTP.
pub type EnvelopeMessage {
Claim(reply: WebhookReply, granted: process.Subject(Bool))
}
Constructors
-
Claim(reply: WebhookReply, granted: process.Subject(Bool))
An API call claimed for delivery in the webhook HTTP response body.
pub type WebhookReply {
WebhookReply(method: String, params_json: String)
}
Constructors
-
WebhookReply(method: String, params_json: String)
Values
pub fn to_response_body(reply reply: WebhookReply) -> String
Render a granted claim as the webhook HTTP response body:
the request params with "method" spliced in front.
pub fn transformer(
envelope envelope: process.Subject(EnvelopeMessage),
) -> fn(
client.TelegramApiRequest,
fn(client.TelegramApiRequest) -> Result(
response.Response(String),
error.TelegaError,
),
) -> Result(response.Response(String), error.TelegaError)
Build the API-call transformer that implements the handler side of the claim protocol for one update. Installed automatically by the chat instance when the update was dispatched with an envelope — not meant to be added manually.
pub fn without_claim(work work: fn() -> a) -> a
Run work with webhook-reply claiming disabled for the calling process.
Use around API calls whose real result you need — a claimed call would
resolve to a synthetic stub instead (see the module doc).