Skip to content

[OMEGA-419] Route Telegram file downloads through the gateway proxy - #353

Open
surafelfikru wants to merge 4 commits into
singnet:mainfrom
iCog-Labs-Dev:feat/telegram-file-proxy-route
Open

surafelfikru wants to merge 4 commits into
singnet:mainfrom
iCog-Labs-Dev:feat/telegram-file-proxy-route

Conversation

@surafelfikru

Copy link
Copy Markdown
Collaborator

Telegram serves API methods from /bot<token>/ but uploaded files from /file/bot<token>/, and the proxy only routed the first, so every media download 404'd behind the gateway while text messages kept working — which reads as broken media handling rather than a missing route. This adds the file route next to the existing one, so photos, documents and voice notes resolve:

location /telegram-file/ {
    rewrite ^/telegram-file/(.*)$ /file/bot${TG_BOT_TOKEN}/$1 break;
    proxy_pass https://api.telegram.org;
}

nginx.sh needs no change; it builds its envsubst list by grepping ${VAR} out of the template, so TG_BOT_TOKEN is already picked up from the existing /telegram/ route. Verified by rendering the template through the same grep-and-envsubst that nginx.sh runs: the token expands and nginx's own $1 capture group is left alone.

One note for later. This route belongs to a plugin, but it sits in core, like the Slack and Mattermost ones. A plugin cannot add a route on its own today. The template has no include line, and the Dockerfile copies proxy/* flat, so a file shipped by a plugin is copied but never read. We could change this with an include inside the server block and a Dockerfile line that collects plugins/*/proxy/*.conf. Two rules would make it safe. First, collect the files at build time, not with a glob at runtime. Then the list of routes is fixed inside the image, and the agent cannot add one while the container is running. Second, run envsubst on each plugin file with only the variables that the file itself declares. Right now nginx.sh greps the variable names out of the template and gives them to envsubst. If we did the same for plugin files, a plugin could write ${ANTHROPIC_API_KEY} inside its own route and read the real key. With a per-file list, a name that is not declared stays as plain text, nginx does not know it, and the container fails to start instead of leaking the key:

# plugins/telegram/proxy/telegram-file.conf
# vars: TG_BOT_TOKEN
location /telegram-file/ { ... }

Telegram serves API methods from /bot<token>/ but uploaded files from
/file/bot<token>/, and the proxy only routed the first, so every media download
404'd behind the gateway while text messages kept working, which reads as broken
media handling rather than a missing route. This adds the file route next to the
existing one, so photos, documents and voice notes resolve:

    location /telegram-file/ {
        rewrite ^/telegram-file/(.*)$ /file/bot${TG_BOT_TOKEN}/$1 break;
        proxy_pass https://api.telegram.org;
    }

nginx.sh needs no change; it builds its envsubst list by grepping ${VAR} out of
the template, so TG_BOT_TOKEN is already picked up from the existing route.
@surafelfikru

Copy link
Copy Markdown
Collaborator Author

Let me know if the plugin extensions for the nginx configuration is the correct approach and i will start working on it.

@vsbogd vsbogd left a comment •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me, thanks!

Let me know if the plugin extensions for the nginx configuration is the correct approach and i will start working on it.

There is a suggestion to migrate to use LiteLLM as a proxy for keeping authentication secrets. From this perspective I think it is better to implement it for the LiteLLM after it is introduced.

@vsbogd

vsbogd commented Sep 17, 2026

Copy link
Copy Markdown
Member

From this perspective I think it is better to implement it for the LiteLLM after it is introduced.

Oh, I forgot that this is about communication plugins and they cannot be proxied by LiteLLM so Nginx or other separate solution will still be needed. Thus make sense to introduce it.

Getting the Nginx configuration from the plugins on agent start looks like a good start. It would be cool if we could change configuration dynamically when plugin is loaded or unloaded instead of doing this once on the start. But it seems it make things less safe and more complex. I would think about it for a day and return to you if you don't mind.

@surafelfikru

Copy link
Copy Markdown
Collaborator Author

Sure, take your time. dynamic loading seems interesting so i will keep it on the back of my head.

@surafelfikru surafelfikru changed the title feat: route Telegram file downloads through the gateway proxy [OMEGA-419] Route Telegram file downloads through the gateway proxy" Sep 21, 2026
@surafelfikru surafelfikru changed the title [OMEGA-419] Route Telegram file downloads through the gateway proxy" [OMEGA-419] Route Telegram file downloads through the gateway proxy Sep 21, 2026
@TossSky

TossSky commented Sep 25, 2026

Copy link
Copy Markdown
Collaborator

Tested: main 6657798 image with proxy/nginx.conf.template from 0463f32 in place of the built-in one, started with scripts/omega, a test bot's token in TG_BOT_TOKEN. Downloads went through the proxy with plain HTTP and with aiogram 3.31.0 configured like the Telegram plugin, TelegramAPIServer(file=".../telegram-file/{path}").

What I checked

  • Without the change, nginx answers every /telegram-file/ download with its own 404, and aiogram fails on all 8 files with ClientResponseError: 404.
  • With the change, the same 8 files come back with status 200 and the exact bytes: documents of 10 KB, 300 KB, 1 MB, 5 MB, 15 MB and 19 MB, a photo from photos/ and a voice note from voice/. aiogram gets the same bytes. Inside the container, curl running as nobody gets the 1 MB file with status 200 and its full length.
  • With %2e%2e or ..%2F in the path, the answer is nginx's 400 or Telegram's 404 for a missing file, and no such request gets a Bot API reply. A plain /telegram-file/../telegram/getMe is normalized to /telegram/getMe before nginx picks a location, so the existing /telegram/ route handles it like a direct call.
  • The agent's processes have no TG_BOT_TOKEN, and nobody cannot read /opt/nginx/nginx.conf or /tmp/proxy.

Large downloads put the bot token into docker logs

When a response outgrows nginx's in-memory buffers, nginx moves it to a temporary file in /tmp/proxy and logs a warning with the upstream URL. Because of the rewrite in this block, that URL contains the token. <TOKEN> below stands in for the real token, which the log has in full:

[warn] 26#26: *37 an upstream response is buffered to a temporary file /tmp/proxy/0000000001 while reading upstream, client: 172.17.0.1, server: , request: "GET /telegram-file/documents/file_151.bin HTTP/1.1", upstream: "https://149.154.166.110:443/file/bot<TOKEN>/documents/file_151.bin", host: "172.17.0.3:8080"

I got 5 such lines: one for the 19 MB file read at full speed, one for the 5 MB file read at 100 KB/s, and one for each of three parallel 19 MB downloads at 256 KB/s. The three parallel downloads also filled the container's 64 MB /tmp to 87%. The access log has no token because it records the original request line.

Adding proxy_max_temp_file_size 0; to the /telegram-file/ block fixes this. With it, the same runs leave no warnings and no token in the log, /tmp stays at 4 KB, and every file arrives complete.

Verdict: FAIL
@vsbogd @surafelfikru

When a download was larger than nginx's memory buffers, nginx wrote it
to /tmp/proxy and logged a warning with the upstream URL. The rewrite
puts the bot token in that URL, so the token ended up in docker logs.
Parallel large downloads also filled the container's 64 MB /tmp.

With proxy_max_temp_file_size 0, nginx passes the response straight to
the client, so nothing is written to /tmp and nothing is logged.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants