Skip to content

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:

ts
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):

ts
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:

ts
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
}

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:

ts
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:

ts
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:

ts
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)

mtcute is not affiliated with Telegram.