Skip to content

Commit cf86b41

Browse files
committed
Fixed multipart for aiohttp and binary response for falcon
1 parent 2268f04 commit cf86b41

2 files changed

Lines changed: 50 additions & 38 deletions

File tree

fastopenapi/routers/aiohttp/extractors.py

Lines changed: 48 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
from fastopenapi.core.types import FileUpload
66
from fastopenapi.routers.extractors import BaseAsyncRequestDataExtractor
77

8+
_MULTIPART_CACHE_ATTR = "_fastopenapi_multipart_cache"
9+
810

911
class AioHttpRequestDataExtractor(BaseAsyncRequestDataExtractor):
1012
@classmethod
@@ -43,57 +45,73 @@ async def _get_body(cls, request: Any) -> dict | list | None:
4345
except Exception:
4446
return {}
4547

48+
@classmethod
49+
async def _parse_multipart(
50+
cls, request: Any
51+
) -> tuple[dict, dict[str, FileUpload | list[FileUpload]]]:
52+
"""Parse multipart stream once and cache the result on the request."""
53+
cached = getattr(request, _MULTIPART_CACHE_ATTR, None)
54+
if isinstance(cached, tuple):
55+
return cached
56+
57+
form_data = {}
58+
files: dict[str, FileUpload | list[FileUpload]] = {}
59+
60+
reader = await request.multipart()
61+
async for part in reader:
62+
if part.filename:
63+
file_data = await part.read()
64+
file_upload = FileUpload(
65+
filename=part.filename,
66+
content_type=part.headers.get("Content-Type"),
67+
size=len(file_data),
68+
file=file_data,
69+
)
70+
if part.name in files:
71+
existing = files[part.name]
72+
if isinstance(existing, list):
73+
existing.append(file_upload)
74+
else:
75+
files[part.name] = [existing, file_upload]
76+
else:
77+
files[part.name] = file_upload
78+
else:
79+
form_data[part.name] = await part.text()
80+
81+
result = (form_data, files)
82+
setattr(request, _MULTIPART_CACHE_ATTR, result)
83+
return result
84+
4685
@classmethod
4786
async def _get_form_data(cls, request: Any) -> dict:
4887
"""Extract form data"""
49-
form_data = {}
5088
content_type = request.content_type or ""
5189

5290
if content_type == "application/x-www-form-urlencoded":
5391
post_data = await request.post()
92+
form_data = {}
5493
for key in post_data:
5594
values = (
5695
post_data.getall(key)
5796
if hasattr(post_data, "getall")
5897
else [post_data[key]]
5998
)
6099
form_data[key] = values[0] if len(values) == 1 else values
100+
return form_data
61101

62-
elif content_type == "multipart/form-data":
63-
reader = await request.multipart()
64-
async for part in reader:
65-
if part.filename:
66-
continue # Files handled separately
67-
form_data[part.name] = await part.text()
102+
if content_type == "multipart/form-data":
103+
form_data, _ = await cls._parse_multipart(request)
104+
return form_data
68105

69-
return form_data
106+
return {}
70107

71108
@classmethod
72109
async def _get_files(cls, request: Any) -> dict[str, FileUpload | list[FileUpload]]:
73110
"""Extract files from AioHTTP request"""
74-
files = {}
75111
content_type = request.content_type or ""
76112

77113
if content_type == "multipart/form-data":
78-
reader = await request.multipart()
79-
async for part in reader:
80-
if not part.filename:
81-
continue
82-
83-
file_data = await part.read()
84-
file_upload = FileUpload(
85-
filename=part.filename,
86-
content_type=part.headers.get("Content-Type"),
87-
size=len(file_data),
88-
file=file_data,
89-
)
90-
91-
if part.name in files:
92-
if isinstance(files[part.name], list):
93-
files[part.name].append(file_upload)
94-
else:
95-
files[part.name] = [files[part.name], file_upload]
96-
else:
97-
files[part.name] = file_upload
114+
_, files = await cls._parse_multipart(request)
115+
return files
98116

99-
return files
117+
return {}

fastopenapi/routers/falcon/async_router.py

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,15 +26,9 @@ async def handle(request, response, **path_params):
2626

2727
# Falcon needs special handling
2828
if isinstance(result_response, Response):
29-
response.status = result_response.status_code
30-
response.media = result_response.content
31-
for key, value in result_response.headers.items():
32-
response.set_header(key, value)
29+
self._apply_falcon_response(result_response, response)
3330
elif isinstance(result_response, FalconResponse): # pragma: no cover
34-
response.status = result_response.status_code
35-
response.media = result_response.media
36-
for key, value in result_response.headers.items():
37-
response.set_header(key, value)
31+
self._copy_falcon_response(result_response, response)
3832

3933
setattr(resource, method_name, handle)
4034
return resource

0 commit comments

Comments
 (0)