wscalls: add possibility to set a timeout on named calls (#63803)

This commit is contained in:
Frédéric Péters 2022-04-13 09:31:52 +02:00
parent daf9efebda
commit a2c825ea2b
5 changed files with 83 additions and 9 deletions

View File

@ -214,3 +214,26 @@ def test_wscalls_empty_param_values(pub):
wscall = NamedWsCall.get('a_new_webservice_call')
assert wscall.request['qs_data'] == {'foo': ''}
assert wscall.request['post_data'] == {'bar': ''}
def test_wscalls_timeout(pub):
create_superuser(pub)
NamedWsCall.wipe()
app = login(get_app(pub))
resp = app.get('/backoffice/settings/wscalls/')
resp = resp.click('New webservice call')
resp.form['name'] = 'a new webservice call'
resp.form['description'] = 'description'
resp.form['request$timeout'] = 'plop'
resp = resp.form.submit('submit')
assert resp.pyquery('[data-widget-name="request$timeout"].widget-with-error')
assert (
resp.pyquery('[data-widget-name="request$timeout"] .error').text()
== 'Timeout must be empty or a number.'
)
resp.form['request$timeout'] = '10'
resp = resp.form.submit('submit')
wscall = NamedWsCall.get('a_new_webservice_call')
assert wscall.request['timeout'] == '10'

View File

@ -249,3 +249,20 @@ def test_webservice_empty_param_values(http_requests, pub):
wscall.call()
assert http_requests.get_last('url') == 'http://remote.example.net/json?titi='
assert http_requests.get_last('body') == '{"toto": ""}'
def test_webservice_timeout(http_requests, pub):
NamedWsCall.wipe()
wscall = NamedWsCall()
wscall.name = 'Hello world'
wscall.request = {
'method': 'GET',
'url': 'http://remote.example.net/connection-error',
'timeout': '10',
}
try:
wscall.call()
except Exception:
pass
assert http_requests.get_last('timeout') == 10

View File

@ -2875,7 +2875,7 @@ def test_sms_with_passerelle(two_pubs):
with mock.patch('wcs.qommon.misc._http_request') as mocked_http_post:
mocked_http_post.return_value = (mock.Mock(headers={}), 200, 'data', 'headers')
item.perform(formdata)
url = mocked_http_post.call_args[0][0]
url = mocked_http_post.call_args[1]['url']
payload = mocked_http_post.call_args[1]['body']
assert 'http://passerelle.example.com' in url
assert '?nostop=1' in url

View File

@ -29,6 +29,9 @@
</li>
{% endif %}
<li>{% trans "Method:" %} {% if wscall.request.method == 'POST' %}POST{% else %}GET{% endif %}</li>
{% if wscall.request.timeout %}
<li>{% trans "Timeout:" %} {{ wscall.request.timeout }}s</li>
{% endif %}
</ul>
</div>

View File

@ -19,6 +19,7 @@ import json
import urllib.parse
import xml.etree.ElementTree as ET
from django.conf import settings
from django.utils.encoding import force_text
from quixote import get_publisher, get_request
@ -69,6 +70,7 @@ def call_webservice(
post_formdata=None,
formdata=None,
cache=False,
timeout=None,
notify_on_errors=False,
record_on_errors=False,
handle_connection_errors=True,
@ -145,17 +147,20 @@ def call_webservice(
payload = formdata_dict
try:
request_kwargs = {
'url': url,
'headers': headers,
'timeout': int(timeout) if timeout else None,
}
if method in ('PATCH', 'PUT', 'POST'):
if payload:
headers['Content-type'] = 'application/json'
payload = json.dumps(payload, cls=JSONEncoder)
response, status, data, dummy = misc._http_request(
url, method=method, body=payload, headers=headers
)
response, status, data, dummy = misc._http_request(method=method, body=payload, **request_kwargs)
elif method == 'DELETE':
response, status, data, dummy = misc._http_request(url, method='DELETE', headers=headers)
response, status, data, dummy = misc._http_request(method='DELETE', **request_kwargs)
else:
response, status, data, dummy = misc.http_get_page(url, headers=headers)
response, status, data, dummy = misc.http_get_page(**request_kwargs)
request = get_request()
if cache is True and request and hasattr(request, 'wscalls_cache'):
request.wscalls_cache[unsigned_url] = (status, data)
@ -225,7 +230,7 @@ class NamedWsCall(XmlStorableObject):
def export_request_to_xml(self, element, attribute_name, charset, **kwargs):
request = getattr(self, attribute_name)
for attr in ('url', 'request_signature_key', 'method'):
for attr in ('url', 'request_signature_key', 'method', 'timeout'):
ET.SubElement(element, attr).text = force_text(request.get(attr) or '', charset)
for attr in ('qs_data', 'post_data'):
data_element = ET.SubElement(element, attr)
@ -238,7 +243,7 @@ class NamedWsCall(XmlStorableObject):
def import_request_from_xml(self, element, **kwargs):
request = {}
for attr in ('url', 'request_signature_key', 'method'):
for attr in ('url', 'request_signature_key', 'method', 'timeout'):
request[attr] = ''
if element.find(attr) is not None and element.find(attr).text:
request[attr] = force_str(element.find(attr).text)
@ -359,9 +364,35 @@ class WsCallRequestWidget(CompositeWidget):
},
)
def validate_timeout(value):
if value and not value.isdecimal():
raise ValueError(_('Timeout must be empty or a number.'))
self.add(
StringWidget,
'timeout',
title=_('Timeout'),
value=value.get('timeout'),
size=20,
hint=_(
'Stop waiting for a response after this number of seconds. '
'Leave empty to get default timeout (%ss).'
)
% settings.REQUESTS_TIMEOUT,
validation_function=validate_timeout,
)
def _parse(self, request):
values = {}
for name in ('url', 'request_signature_key', 'qs_data', 'method', 'post_formdata', 'post_data'):
for name in (
'url',
'request_signature_key',
'qs_data',
'method',
'post_formdata',
'post_data',
'timeout',
):
if not self.include_post_formdata and name == 'post_formdata':
continue
value = self.get(name)