[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:
Julien Castiaux
2022-04-27 20:35:55 +02:00
parent 873b7b8a82
commit 034d01b2f3
3 changed files with 58 additions and 21 deletions
+9
View File
@@ -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
# =====================================================
+16 -4
View File
@@ -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
View File
@@ -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)
# =========================================================