[IMP] core: send json to http controllers
With this work we relax the http controller so that it accepts all requests, including `Content-Type: application/json`. We also enrich the framework with two new helper methods dedicated to serialaze json requests and responses: - `request.get_json_body()`, loads the json content from the request's body and returns the corresponding python object (usually a dict). - `request.make_json_response(data)`, dumps `data` to json and makes an http response out of it. closes odoo/odoo#86300 Task: 2779837 Signed-off-by: Julien Castiaux <juc@odoo.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
import json
|
||||
import logging
|
||||
import werkzeug
|
||||
from odoo import http
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.http import request
|
||||
@@ -73,6 +74,14 @@ class TestHttp(http.Controller):
|
||||
def echo_json_context(self, **kwargs):
|
||||
return request.env.context
|
||||
|
||||
@http.route('/test_http/echo-json-over-http', type='http', auth='none', methods=['POST'], csrf=False)
|
||||
def echo_json_over_http(self):
|
||||
try:
|
||||
data = request.get_json_data()
|
||||
except ValueError as exc:
|
||||
raise werkzeug.exceptions.BadRequest("Invalid JSON data") from exc
|
||||
return request.make_json_response(data)
|
||||
|
||||
# =====================================================
|
||||
# Models
|
||||
# =====================================================
|
||||
|
||||
@@ -187,14 +187,24 @@ class TestHttpEchoReplyHttpNoDB(TestHttpBase):
|
||||
|
||||
@mute_logger('odoo.http')
|
||||
def test_echohttp4_post_json_nodb(self):
|
||||
res = self.nodb_url_open('/test_http/echo-http-post', data='{}', headers=CT_JSON)
|
||||
self.assertIn("Bad Request", res.text)
|
||||
payload = json.dumps({'commander': 'Thor'})
|
||||
res = self.nodb_url_open('/test_http/echo-http-post', data=payload, headers=CT_JSON)
|
||||
self.assertEqual(res.status_code, 200)
|
||||
self.assertEqual(res.text, '{}')
|
||||
|
||||
def test_echohttp5_post_csrf(self):
|
||||
res = self.nodb_url_open('/test_http/echo-http-csrf?race=Asgard', data={'commander': 'Thor'})
|
||||
self.assertEqual(res.status_code, 303)
|
||||
self.assertEqual(urlparse(res.headers.get('Location', '')).path, '/web/database/selector')
|
||||
|
||||
def test_echohttp6_json_over_http(self):
|
||||
payload = json.dumps({'commander': 'Thor'})
|
||||
res = self.nodb_url_open('/test_http/echo-json-over-http', data=payload, headers=CT_JSON)
|
||||
self.assertEqual(res.status_code, 200)
|
||||
self.assertEqual(res.text, payload)
|
||||
mimetype = res.headers['Content-Type'].partition(';')[0]
|
||||
self.assertEqual(mimetype, 'application/json')
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestHttpEchoReplyJsonNoDB(TestHttpBase):
|
||||
@@ -247,8 +257,10 @@ class TestHttpEchoReplyHttpWithDB(TestHttpBase):
|
||||
|
||||
@mute_logger('odoo.http')
|
||||
def test_echohttp4_post_json_db(self):
|
||||
res = self.db_url_open('/test_http/echo-http-post', data='{}', headers=CT_JSON)
|
||||
self.assertIn("Bad Request", res.text)
|
||||
payload = json.dumps({'commander': 'Thor'})
|
||||
res = self.db_url_open('/test_http/echo-http-post', data=payload, headers=CT_JSON)
|
||||
self.assertEqual(res.status_code, 200)
|
||||
self.assertEqual(res.text, '{}')
|
||||
|
||||
@mute_logger('odoo.http')
|
||||
def test_echohttp5_post_no_csrf(self):
|
||||
|
||||
+33
-17
@@ -1164,6 +1164,9 @@ class Request:
|
||||
params.pop('session_id', None)
|
||||
return params
|
||||
|
||||
def get_json_data(self):
|
||||
return json.loads(self.httprequest.get_data(as_text=True))
|
||||
|
||||
def _get_profiler_context_manager(self):
|
||||
"""
|
||||
Get a profiler when the profiling is enabled and the requested
|
||||
@@ -1201,7 +1204,7 @@ class Request:
|
||||
response.headers.extend(self.future_response.headers)
|
||||
return response
|
||||
|
||||
def make_response(self, data, headers=None, cookies=None):
|
||||
def make_response(self, data, headers=None, cookies=None, status=200):
|
||||
""" Helper for non-HTML responses, or HTML responses with custom
|
||||
response headers or cookies.
|
||||
|
||||
@@ -1210,17 +1213,39 @@ class Request:
|
||||
complete response object, or the returned data will not be correctly
|
||||
interpreted by the clients.
|
||||
|
||||
:param basestring data: response body
|
||||
:param str data: response body
|
||||
:param int status: http status code
|
||||
:param headers: HTTP headers to set on the response
|
||||
:type headers: ``[(name, value)]``
|
||||
:param collections.Mapping cookies: cookies to set on the client
|
||||
:returns: a response object.
|
||||
:rtype: :class:`~odoo.http.Response`
|
||||
"""
|
||||
response = Response(data, headers=headers)
|
||||
response = Response(data, status=status, headers=headers)
|
||||
if cookies:
|
||||
for k, v in cookies.items():
|
||||
response.set_cookie(k, v)
|
||||
return response
|
||||
|
||||
def make_json_response(self, data, headers=None, cookies=None, status=200):
|
||||
""" Helper for JSON responses, it json-serializes ``data`` and
|
||||
sets the Content-Type header accordingly if none is provided.
|
||||
|
||||
:param data: the data that will be json-serialized into the response body
|
||||
:param int status: http status code
|
||||
:param List[(str, str)] headers: HTTP headers to set on the response
|
||||
:param collections.Mapping cookies: cookies to set on the client
|
||||
:rtype: :class:`~odoo.http.Response`
|
||||
"""
|
||||
data = json.dumps(data, ensure_ascii=False, default=date_utils.json_default)
|
||||
|
||||
headers = werkzeug.datastructures.Headers(headers)
|
||||
headers['Content-Length'] = len(data)
|
||||
if 'Content-Type' not in headers:
|
||||
headers['Content-Type'] = 'application/json; charset=utf-8'
|
||||
|
||||
return self.make_response(data, headers.to_wsgi_list(), cookies, status)
|
||||
|
||||
def not_found(self, description=None):
|
||||
""" Shortcut for a `HTTP 404
|
||||
<http://tools.ietf.org/html/rfc7231#section-6.5.4>`_ (Not Found)
|
||||
@@ -1468,7 +1493,7 @@ class HttpDispatcher(Dispatcher):
|
||||
|
||||
@classmethod
|
||||
def is_compatible_with(cls, request):
|
||||
return request.httprequest.mimetype not in JSON_MIMETYPES
|
||||
return True
|
||||
|
||||
def dispatch(self, endpoint, args):
|
||||
"""
|
||||
@@ -1568,14 +1593,10 @@ class JsonRPCDispatcher(Dispatcher):
|
||||
<-- {"jsonrpc": "2.0", "error": {"code": 1, "message": "End user error message.", "data": {"code": "codestring", "debug": "traceback" } }, "id": null}
|
||||
|
||||
"""
|
||||
httprequest = self.request.httprequest
|
||||
body = httprequest.get_data().decode(httprequest.charset)
|
||||
try:
|
||||
self.jsonrequest = json.loads(body)
|
||||
except ValueError:
|
||||
_logger.info('%s: Invalid JSON data\n%s', httprequest.path,
|
||||
body)
|
||||
raise werkzeug.exceptions.BadRequest(f"Invalid JSON data:\n{body}")
|
||||
self.jsonrequest = self.request.get_json_data()
|
||||
except ValueError as exc:
|
||||
raise BadRequest("Invalid JSON data") from exc
|
||||
|
||||
self.request.params = dict(self.jsonrequest.get('params', {}), **args)
|
||||
ctx = self.request.params.pop('context', None)
|
||||
@@ -1627,12 +1648,7 @@ class JsonRPCDispatcher(Dispatcher):
|
||||
if result is not None:
|
||||
response['result'] = result
|
||||
|
||||
body = json.dumps(response, default=date_utils.json_default)
|
||||
|
||||
return Response(body, status=status, headers=[
|
||||
('Content-Type', 'application/json'),
|
||||
('Content-Length', len(body)),
|
||||
])
|
||||
return self.request.make_json_response(response)
|
||||
|
||||
|
||||
# =========================================================
|
||||
|
||||
Reference in New Issue
Block a user