Repository: aiogram/aiogram (python)

Issue:
Webhook empty formdata response has unexpected format for TG
### Checklist

- [x] I am sure the error is coming from aiogram code
- [x] I have searched in the issue tracker for similar bug reports, including closed ones

### Operating system

Lambda (Linux), macOS

### Python version

3.12, 3.13

### aiogram version

3.19.0

### Expected behavior

Response into webhook should work even for empty response - for unhandled events.

### Current behavior

Response into webhook is not recognized by Telegram server, it sends the same update again and again, then stops sending webhooks at all.

### Steps to reproduce

run an echo bot only for text, use response into webhook:
```python
webhook_requests_handler = SimpleRequestHandler(
    dispatcher=dp,
    bot=bot,
    handle_in_background=False,
)
```

send a sticker / a photo / anything but text

### Code example

```python3

```

### Logs

```sh

```

### Additional information

Hi
Webhooks issue, answer into webhook doesn't work when sending an empty response, for example on an unhandled event.

Case:
- made an echo bot - responding only to text
- Sent a sticker - bot responds with an empty form date:
 + headers:
```
Content-Type multipart/form-data; boundary=webhookBoundaryvsF_aMHhspPjfOq7O0JNRg
```
 + body:
```
--webhookBoundaryvsF_aMHhspPjfOq7O0JNRg--
```
- sent one sticker.
- Telegram sent 10 updates - each time application responds with an empty, and Telegram sends an update again.

how to run: example with examples - run local webhook

debugging: run ngrok, I can see both incoming requests and responses.


Probable reason:

An empty formdata should look smth like this:
```
--webhookBoundaryvsF_aMHhspPjfOq7O0JNRg
--webhookBoundaryvsF_aMHhspPjfOq7O0JNRg--
```
it has:
- start
- end

but aiohttp builds response w/o "start"

Pull request description:
fix empty response into webhook
# Description

Fixes #1664

## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)

# How Has This Been Tested?

- [x] Locally with ngrok

# Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my own code
- [x] I have made corresponding changes to the documentation
- [x] I have added tests that prove my fix is effective or that my feature works
- [x] New and existing unit tests pass locally with my changes


Patch:
diff --git a/CHANGES/1664.bugfix.rst b/CHANGES/1664.bugfix.rst
new file mode 100644
index 00000000..9a4fb2a3
--- /dev/null
+++ b/CHANGES/1664.bugfix.rst
@@ -0,0 +1,25 @@
+Fix empty response into webhook.
+
+We need to return something “empty”, and “empty” form doesn’t work since
+it’s sending only “end” boundary w/o “start”.
+
+An empty formdata should look smth like this for Telegram to understand:
+
+::
+
+   --webhookBoundaryvsF_aMHhspPjfOq7O0JNRg
+   --webhookBoundaryvsF_aMHhspPjfOq7O0JNRg--
+
+But aiohttp sends only the ending boundary:
+
+::
+
+   --webhookBoundaryvsF_aMHhspPjfOq7O0JNRg--
+
+Such response doesn't suit Telegram servers.
+
+The fix replaces empty response with empty JSON response:
+
+::
+
+   {}
diff --git a/aiogram/webhook/aiohttp_server.py b/aiogram/webhook/aiohttp_server.py
index 7caa0e15..a453c9d8 100644
--- a/aiogram/webhook/aiohttp_server.py
+++ b/aiogram/webhook/aiohttp_server.py
@@ -4,7 +4,7 @@ from abc import ABC, abstractmethod
 from asyncio import Transport
 from typing import Any, Awaitable, Callable, Dict, Optional, Set, Tuple, cast
 
-from aiohttp import MultipartWriter, web
+from aiohttp import JsonPayload, MultipartWriter, Payload, web
 from aiohttp.abc import Application
 from aiohttp.typedefs import Handler
 from aiohttp.web_middlewares import middleware
@@ -151,13 +151,17 @@ class BaseRequestHandler(ABC):
 
     def _build_response_writer(
         self, bot: Bot, result: Optional[TelegramMethod[TelegramType]]
-    ) -> MultipartWriter:
+    ) -> Payload:
+        if not result:
+            # we need to return something "empty"
+            # and "empty" form doesn't work
+            # since it's sending only "end" boundary w/o "start"
+            return JsonPayload({})
+
         writer = MultipartWriter(
             "form-data",
             boundary=f"webhookBoundary{secrets.token_urlsafe(16)}",
         )
-        if not result:
-            return writer
 
         payload = writer.append(result.__api_method__)
         payload.set_content_disposition("form-data", name="method")


Test patch:
diff --git a/tests/test_webhook/test_aiohtt_server.py b/tests/test_webhook/test_aiohtt_server.py
index f29895ba..c185b841 100644
--- a/tests/test_webhook/test_aiohtt_server.py
+++ b/tests/test_webhook/test_aiohtt_server.py
@@ -150,13 +150,9 @@ class TestSimpleRequestHandler:
 
         resp = await self.make_reqest(client=client, text="spam")
         assert resp.status == 200
-        assert resp.content_type == "multipart/form-data"
-        result = {}
-        reader = MultipartReader.from_response(resp)
-        while part := await reader.next():
-            value = await part.read()
-            result[part.name] = value.decode()
-        assert not result
+        assert resp.content_type == "application/json"
+        expected_result = {}
+        assert await resp.json() == expected_result
 
     async def test_reply_into_webhook_background(self, bot: MockedBot, aiohttp_client):
         app = Application()