Repository: aiogram/aiogram

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"


Discussion:

Patch:
diff --git a/CHANGES/1664.bugfix.rst b/CHANGES/1664.bugfix.rst
new file mode 100644
index 0000000000..9a4fb2a370
--- /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 7caa0e1586..a453c9d84b 100644
--- a/aiogram/webhook/aiohttp_server.py
+++ b/aiogram/webhook/aiohttp_server.py
@@ -4,7 +4,7 @@
 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 @@ async def _handle_request_background(self, bot: Bot, request: web.Request) -> we
 
     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 f29895ba2a..c185b8414e 100644
--- a/tests/test_webhook/test_aiohtt_server.py
+++ b/tests/test_webhook/test_aiohtt_server.py
@@ -150,13 +150,9 @@ def handle_message(msg: Message):
 
         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()