Network middlewares v0.16.0+
In some cases it may make sense to intercept all outgoing requests and control the request flow.
Default middlewares
By default, mtcute uses two middlewares: flood-waiter and internal errors handler. The combined default middleware is exported in networkMiddlewares.basic
, and can be configured as follows:
const tg = new TelegramClient({
...,
network: {
middlewares: networkMiddlewares.basic({
floodWaiter: { maxWait: 5000, maxRetries: 5 },
internalErrors: { maxRetries: 5 }
})
}
})
const tg = new TelegramClient({
...,
network: {
middlewares: networkMiddlewares.basic({
floodWaiter: { maxWait: 5000, maxRetries: 5 },
internalErrors: { maxRetries: 5 }
})
}
})
Flood-waiter and internal errors handler middlewares themselves are exported under networkMiddlewares.floodWaiter
and networkMiddlewares.internalErrorsHandler
respectively.
Writing middlewares
Middleware is simply an async function that takes ctx
and next
as arguments.
The ctx
object contains information about the RPC call, including the request itself and any additional parameters that were passed along, and next
function can be used to call the next middleware in the chain, returning the call result (or an error):
const myMiddleware: RpcCallMiddleware = async (ctx, next) => {
if (ctx.request._ === 'help.getConfig') {
return myConfig
}
return next(ctx)
}
const myMiddleware: RpcCallMiddleware = async (ctx, next) => {
if (ctx.request._ === 'help.getConfig') {
return myConfig
}
return next(ctx)
}
INFO
If you are familiar with grammY/telegraf or koa middlewares, you might find the ctx, next
syntax familiar. Indeed, these middlewares were heavily inspired by them.
However, they work slightly different here, as the task is slightly different too.
Unlike grammY-style middlewares, next
can be called multiple times, and the last pseudo-"middleware" in the chain will actually execute the request contained in the ctx
(instead of being a no-op).
And because of that, ctx
is always passed explicitly, allowing to execute multiple different requests from a single middleware.
Errors in middlewares
To improve performance, RPC errors in middlewares are monadic, meaning that an RPC error is considered a valid result.
To check if the call resulted in an error, you can use isTlRpcError
handler:
const myMiddleware: RpcCallMiddleware = async (ctx, next) => {
const res = await next(ctx)
if (isTlRpcError(res) && res.errorMessage === 'PEER_ID_INVALID') {
logPeerIdInvalid(ctx.request)
}
return res
}
const myMiddleware: RpcCallMiddleware = async (ctx, next) => {
const res = await next(ctx)
if (isTlRpcError(res) && res.errorMessage === 'PEER_ID_INVALID') {
logPeerIdInvalid(ctx.request)
}
return res
}
You can also use networkMiddlewares.onRpcError
helper to create a middleware that only handles RPC errors:
const client = new TelegramClient({
...,
network: {
middlewares: [
networkMiddlewares.onRpcError(async (ctx, error) => {
if (error.errorMessage === 'PEER_ID_INVALID') {
logPeerIdInvalid(ctx.request)
}
}),
networkMiddlewares.basic()
]
}
})
const client = new TelegramClient({
...,
network: {
middlewares: [
networkMiddlewares.onRpcError(async (ctx, error) => {
if (error.errorMessage === 'PEER_ID_INVALID') {
logPeerIdInvalid(ctx.request)
}
}),
networkMiddlewares.basic()
]
}
})
Modifying request
In some cases, it might make sense to modify the request before sending.
One way to do so is to overwrite the ctx
fields:
const myMiddleware: RpcCallMiddleware = async (ctx, next) => {
if (ctx.request._ === 'users.getFullUser') {
ctx.request.id = { _: 'inputUserSelf' }
}
return next(ctx)
}
const myMiddleware: RpcCallMiddleware = async (ctx, next) => {
if (ctx.request._ === 'users.getFullUser') {
ctx.request.id = { _: 'inputUserSelf' }
}
return next(ctx)
}
Alternatively, you can construct your own context:
const myMiddleware: RpcCallMiddleware = async (ctx, next) => {
if (ctx.request._ === 'users.getFullUser') {
return next({
manager: ctx.manager,
params: ctx.params,
request: {
_: 'users.getFullUser',
id: { _: 'inputUserSelf' }
}
})
}
return next(ctx)
}
const myMiddleware: RpcCallMiddleware = async (ctx, next) => {
if (ctx.request._ === 'users.getFullUser') {
return next({
manager: ctx.manager,
params: ctx.params,
request: {
_: 'users.getFullUser',
id: { _: 'inputUserSelf' }
}
})
}
return next(ctx)
}
Applying middlewares
Once you're done writing your middleware, you need to connect it to the client. That's done by passing an array to the middlewares
option, like this:
const tg = new TelegramClient({
...,
network: {
middlewares: [
myMiddleware,
myOtherMiddleware,
// You'll probably also want to include all the default
// middlewares, as passing this option overrides them.
...networkMiddlewares.basic()
]
}
})
const tg = new TelegramClient({
...,
network: {
middlewares: [
myMiddleware,
myOtherMiddleware,
// You'll probably also want to include all the default
// middlewares, as passing this option overrides them.
...networkMiddlewares.basic()
]
}
})
INFO
Middleware order matters, which is why we include the basic middlewares last — we want myMiddleware
and myOtherMiddleware
to also benefit from them (i.e. have flood waits and internal errors handled)