-
-
Notifications
You must be signed in to change notification settings - Fork 773
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[aiohttp] Multipart handling #987
Changes from all commits
fdbd6b0
f7c3a93
2497f75
5324620
5b316f3
a9b8342
8b52da5
c67498a
6f0cc06
6fead32
614af93
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -211,8 +211,9 @@ def redirect(request): | |
@aiohttp_jinja2.template('index.j2') | ||
@asyncio.coroutine | ||
def _get_swagger_ui_home(self, req): | ||
return {'openapi_spec_url': (self.base_path + | ||
self.options.openapi_spec_path)} | ||
return {'openapi_spec_url': ( | ||
self.base_path + self.options.openapi_spec_path, | ||
)} | ||
|
||
def add_auth_on_not_found(self, security, security_definitions): | ||
""" | ||
|
@@ -264,13 +265,50 @@ def get_request(cls, req): | |
:rtype: ConnexionRequest | ||
""" | ||
url = str(req.url) | ||
logger.debug('Getting data and status code', | ||
extra={'has_body': req.has_body, 'url': url}) | ||
content_type = req.content_type | ||
|
||
logger.debug( | ||
'Getting data and status code', | ||
extra={ | ||
# has_body | can_read_body report if | ||
# body has been read or not | ||
# body_exists refers to underlying stream of data | ||
'body_exists': req.body_exists, | ||
'can_read_body': req.can_read_body, | ||
'content_type': content_type, | ||
'url': url, | ||
}, | ||
) | ||
|
||
query = parse_qs(req.rel_url.query_string) | ||
headers = req.headers | ||
body = None | ||
if req.body_exists: | ||
|
||
# if request is not multipart, `data` will be empty dict | ||
# and stream will not be consumed | ||
post_data = yield from req.post() | ||
|
||
# set those up beforehand, they are needed anyway | ||
files = {} | ||
form = {} | ||
|
||
if post_data: | ||
logger.debug('Reading multipart data from request') | ||
for k, v in post_data.items(): | ||
if isinstance(v, web.FileField) and k not in files: | ||
files[k] = v | ||
elif isinstance(v, web.FileField) and k in files: | ||
try: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Any concerns of using There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. TBH I have doubts if that is correct. It works for many files but if I mix normal fields and files everything goes wrong. Also form is processed in weird way, I will post details łatwe for that. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Interesting thing. I noticed that if you try to submit multiple fields for Flask it will ignore them and take just the first value. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. FYI: #992 |
||
files[k].append(v) | ||
except AttributeError: | ||
files[k] = [files[k], v] | ||
elif not isinstance(v, web.FileField): | ||
# put normal fields as an array, that's how werkzeug does that for Flask | ||
# and that's what Connexion expects in its processing functions | ||
form[k] = [v] | ||
body = b'' | ||
else: | ||
logger.debug('Reading data from request') | ||
body = yield from req.read() | ||
|
||
return ConnexionRequest(url=url, | ||
|
@@ -280,7 +318,8 @@ def get_request(cls, req): | |
headers=headers, | ||
body=body, | ||
json_getter=lambda: cls.jsonifier.loads(body), | ||
files={}, | ||
form=form, | ||
files=files, | ||
context=req) | ||
|
||
@classmethod | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -273,7 +273,7 @@ def _get_body_argument(self, body, arguments, has_kwargs, sanitize): | |
|
||
if x_body_name in arguments or has_kwargs: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @kornicameister, you and I are both trying to get aiohttp to work well with connexion. I can't wait until this PR is accepted. Possibly another thing to add in this area of change: There's a similar condition (but not problematic for me) also on line 263 above. The original change doesn't describe why. It's just "connexion 2.0" in the blame. I would propose removing the 'or has_kwargs' so that parameters are the same whether you break them out as named arguments in the handler or accept any named argument in the handler. Thoughts? For now I've worked around it by expanding 'body' into the kwargs at the beginning of the decorator's wrapper, but I don't know if that handles all cases, and it just seems inconsistent of connexion. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @ddurham2 I have tried to untagle the code in different parts of connexion without too much luck. I am afraid I can't help you. Due to various issues, like this one, we're forced to move away from Although, I am tracking what's going on, and my original opinion on state of affairs in connexion had not changed. Without strong API contract enforced & required by There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If it were up to me, I'd have ditched that PR and focus on work to prepare strong internal API contract toward making There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Did you find an alternative to connexion? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There are alternatives but concept is a mirror reflection, essentially it is code that comes first. |
||
return {x_body_name: res} | ||
return {} | ||
return res | ||
|
||
def _sanitize_body_argument(self, body_arg, body_props, additional_props, sanitize): | ||
""" | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
import asyncio | ||
import os | ||
|
||
import aiohttp | ||
import pytest | ||
|
||
from connexion import AioHttpApp | ||
|
||
try: | ||
import ujson as json | ||
except ImportError: | ||
import json | ||
|
||
|
||
@pytest.fixture | ||
def aiohttp_app(aiohttp_api_spec_dir): | ||
app = AioHttpApp(__name__, port=5001, | ||
specification_dir=aiohttp_api_spec_dir, | ||
debug=True) | ||
app.add_api( | ||
'openapi_multipart.yaml', | ||
validate_responses=True, | ||
strict_validation=True, | ||
pythonic_params=True, | ||
pass_context_arg_name='request_ctx', | ||
) | ||
return app | ||
|
||
|
||
@asyncio.coroutine | ||
def test_single_file_upload(aiohttp_app, aiohttp_client): | ||
app_client = yield from aiohttp_client(aiohttp_app.app) | ||
|
||
resp = yield from app_client.post( | ||
'/v1.0/upload_file', | ||
data=aiohttp.FormData(fields=[('funky_funky', open(__file__, 'rb'))])(), | ||
) | ||
|
||
data = yield from resp.json() | ||
assert resp.status == 200 | ||
assert data['fileName'] == f'{__name__}.py' | ||
|
||
|
||
@asyncio.coroutine | ||
def test_many_files_upload(aiohttp_app, aiohttp_client): | ||
app_client = yield from aiohttp_client(aiohttp_app.app) | ||
|
||
dir_name = os.path.dirname(__file__) | ||
files_field = [('files', open(f'{dir_name}/{file_name}', 'rb')) for file_name in os.listdir(dir_name) if file_name.endswith('.py')] | ||
|
||
form_data = aiohttp.FormData(fields=files_field) | ||
|
||
resp = yield from app_client.post( | ||
'/v1.0/upload_files', | ||
data=form_data(), | ||
) | ||
|
||
data = yield from resp.json() | ||
|
||
assert resp.status == 200 | ||
assert data['filesCount'] == len(files_field) | ||
|
||
|
||
@asyncio.coroutine | ||
def test_mixed_multipart_single_file(aiohttp_app, aiohttp_client): | ||
app_client = yield from aiohttp_client(aiohttp_app.app) | ||
|
||
form_data = aiohttp.FormData() | ||
form_data.add_field('dirName', os.path.dirname(__file__)) | ||
form_data.add_field('funky_funky', open(__file__, 'rb')) | ||
|
||
resp = yield from app_client.post( | ||
'/v1.0/mixed_single_file', | ||
data=form_data(), | ||
) | ||
|
||
data = yield from resp.json() | ||
|
||
assert resp.status == 200 | ||
assert data['dirName'] == os.path.dirname(__file__) | ||
assert data['fileName'] == f'{__name__}.py' | ||
|
||
|
||
@asyncio.coroutine | ||
def test_mixed_multipart_many_files(aiohttp_app, aiohttp_client): | ||
app_client = yield from aiohttp_client(aiohttp_app.app) | ||
|
||
dir_name = os.path.dirname(__file__) | ||
files_field = [('files', open(f'{dir_name}/{file_name}', 'rb')) for file_name in os.listdir(dir_name) if file_name.endswith('.py')] | ||
|
||
form_data = aiohttp.FormData(fields=files_field) | ||
form_data.add_field('dirName', os.path.dirname(__file__)) | ||
form_data.add_field('testCount', str(len(files_field))) | ||
|
||
resp = yield from app_client.post( | ||
'/v1.0/mixed_many_files', | ||
data=form_data(), | ||
) | ||
|
||
data = yield from resp.json() | ||
|
||
assert resp.status == 200 | ||
assert data['dirName'] == os.path.dirname(__file__) | ||
assert data['testCount'] == len(files_field) | ||
assert data['filesCount'] == len(files_field) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,132 @@ | ||
--- | ||
openapi: 3.0.0 | ||
servers: | ||
- url: /v1.0 | ||
info: | ||
title: "{{title}}" | ||
version: "1.0" | ||
paths: | ||
"/mixed_single_file": | ||
post: | ||
summary: Reads multipart data | ||
description: Handles multipart data reading | ||
operationId: fakeapi.aiohttp_handlers.aiohttp_multipart_mixed_single_file | ||
responses: | ||
"200": | ||
description: OK response | ||
content: | ||
'application/json': | ||
schema: | ||
type: object | ||
properties: | ||
dirName: | ||
type: string | ||
fileName: | ||
type: string | ||
default: | ||
description: unexpected error | ||
requestBody: | ||
required: true | ||
content: | ||
multipart/form-data: | ||
schema: | ||
type: object | ||
properties: | ||
dirName: | ||
type: string | ||
funky_funky: | ||
type: string | ||
format: binary | ||
"/mixed_many_files": | ||
post: | ||
summary: Reads multipart data | ||
description: Handles multipart data reading | ||
operationId: fakeapi.aiohttp_handlers.aiohttp_multipart_mixed_many_files | ||
responses: | ||
"200": | ||
description: OK response | ||
content: | ||
'application/json': | ||
schema: | ||
type: object | ||
properties: | ||
dirName: | ||
type: string | ||
testCount: | ||
type: number | ||
filesCount: | ||
type: number | ||
default: | ||
description: unexpected error | ||
requestBody: | ||
required: true | ||
content: | ||
multipart/form-data: | ||
schema: | ||
type: object | ||
properties: | ||
dirName: | ||
type: string | ||
testCount: | ||
type: number | ||
files: | ||
type: array | ||
items: | ||
type: string | ||
format: binary | ||
"/upload_file": | ||
post: | ||
summary: Uploads single file | ||
description: Handles multipart file upload. | ||
operationId: fakeapi.aiohttp_handlers.aiohttp_multipart_single_file | ||
responses: | ||
"200": | ||
description: OK response | ||
content: | ||
'application/json': | ||
schema: | ||
type: object | ||
properties: | ||
fileName: | ||
type: string | ||
default: | ||
description: unexpected error | ||
requestBody: | ||
required: true | ||
content: | ||
multipart/form-data: | ||
schema: | ||
type: object | ||
properties: | ||
funky_funky: | ||
type: string | ||
format: binary | ||
"/upload_files": | ||
post: | ||
summary: Uploads many files | ||
description: Handles multipart file upload. | ||
operationId: fakeapi.aiohttp_handlers.aiohttp_multipart_many_files | ||
responses: | ||
"200": | ||
description: OK response | ||
content: | ||
'application/json': | ||
schema: | ||
type: object | ||
properties: | ||
filesCount: | ||
type: number | ||
default: | ||
description: unexpected error | ||
requestBody: | ||
required: true | ||
content: | ||
multipart/form-data: | ||
schema: | ||
type: object | ||
properties: | ||
file: | ||
type: array | ||
items: | ||
type: string | ||
format: binary |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unrelated but
flake8
was unhappy about it.