Skip to content

Commit

Permalink
Based on request attribute, toggle surfacing more error information t…
Browse files Browse the repository at this point in the history
…o client (#36)

* Based on request attribute, toggle surfacing more error information to client
* display_error_stack value set via function passed through tween
  • Loading branch information
P-K authored Jan 6, 2023
1 parent 7128323 commit 7229e6e
Show file tree
Hide file tree
Showing 6 changed files with 90 additions and 29 deletions.
22 changes: 16 additions & 6 deletions pyramid_hypernova/batch.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,21 @@
from pyramid_hypernova.types import JobResult


def create_fallback_response(jobs, throw_client_error, json_encoder, error=None):
def create_fallback_response(jobs, throw_client_error, json_encoder, error=None, display_error_stack=False):
"""Create a response dict for falling back to client-side rendering.
:rtype: Dict[str, Job]
"""
return {
identifier: JobResult(
error=error,
html=render_blank_markup(identifier, job, throw_client_error, json_encoder),
html=render_blank_markup(
identifier,
job,
throw_client_error,
json_encoder,
error=error if display_error_stack else None,
),
job=job,
)
for identifier, job in jobs.items()
Expand Down Expand Up @@ -52,14 +58,16 @@ def __init__(
plugin_controller,
pyramid_request,
max_batch_size=None,
json_encoder=JSONEncoder()
json_encoder=JSONEncoder(),
display_error_stack=False
):
self.get_job_group_url = get_job_group_url
self.jobs = {}
self.plugin_controller = plugin_controller
self.max_batch_size = max_batch_size
self.json_encoder = json_encoder
self.pyramid_request = pyramid_request
self.display_error_stack = display_error_stack

def render(self, name, data, context=None):
if context is None: # pragma: no cover
Expand Down Expand Up @@ -119,7 +127,10 @@ def process_responses(self, query, jobs):
message=response_json['error']['message'],
stack=response_json['error']['stack'],
)
pyramid_response = create_fallback_response(jobs, True, self.json_encoder, error)

pyramid_response = create_fallback_response(
jobs, True, self.json_encoder, error, self.display_error_stack
)
self.plugin_controller.on_error(error, jobs, self.pyramid_request)
else:
pyramid_response = self._parse_response(response_json)
Expand All @@ -135,7 +146,7 @@ def process_responses(self, query, jobs):
[line.rstrip('\n') for line in traceback.format_tb(exc_traceback)],
)
self.plugin_controller.on_error(error, jobs, self.pyramid_request)
pyramid_response = create_fallback_response(jobs, True, self.json_encoder, error)
pyramid_response = create_fallback_response(jobs, True, self.json_encoder, error, self.display_error_stack)

return pyramid_response

Expand All @@ -145,7 +156,6 @@ def submit(self):
:rtype: Dict[str, JobResult]
"""
self.jobs = self.plugin_controller.prepare_request(self.jobs, self.pyramid_request)

response = {}

if self.jobs and self.plugin_controller.should_send_request(self.jobs, self.pyramid_request):
Expand Down
8 changes: 5 additions & 3 deletions pyramid_hypernova/rendering.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,16 @@
FALLBACK_ERROR = dedent('''
<script type="text/javascript">
(function () {{
function ServerSideRenderingError(component) {{
function ServerSideRenderingError(component, error) {{
this.name = 'ServerSideRenderingError';
this.component = component;
this.cause = error;
}}
ServerSideRenderingError.prototype = Object.create(ServerSideRenderingError.prototype);
ServerSideRenderingError.prototype.constructor = ServerSideRenderingError;
throw new ServerSideRenderingError('{component} failed to render server-side, and fell back to client-side rendering.');
throw new ServerSideRenderingError('{component} failed to render server-side, and fell back to client-side rendering.', {error});
}}());
</script>
''') # noqa: ignore=E501
Expand All @@ -39,7 +40,7 @@ def encode(data, json_encoder):
return text.replace('&', '&amp;').replace('>', '&gt;')


def render_blank_markup(identifier, job, throw_client_error, json_encoder):
def render_blank_markup(identifier, job, throw_client_error, json_encoder, error=None):
"""This will be called as a fallback when server-side rendering fails."""
# Hypernova server strips out non-word characters from the name
key = re.sub(r'\W', '', job.name)
Expand All @@ -53,6 +54,7 @@ def render_blank_markup(identifier, job, throw_client_error, json_encoder):
if throw_client_error:
blank_markup += FALLBACK_ERROR.format(
component=key,
error=encode(error, json_encoder) if error else 'undefined',
)

return blank_markup
Expand Down
4 changes: 4 additions & 0 deletions pyramid_hypernova/tweens.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,14 @@ def configure_hypernova_batch(registry, request):
)

json_encoder = registry.settings.get('pyramid_hypernova.json_encoder', JSONEncoder())
should_display_error_stack = registry.settings.get(
'pyramid_hypernova.should_display_error_stack', lambda request: False
)

return batch_request_factory(
get_job_group_url=get_job_group_url,
plugin_controller=plugin_controller,
json_encoder=json_encoder,
pyramid_request=request,
display_error_stack=should_display_error_stack(request),
)
33 changes: 22 additions & 11 deletions tests/batch_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,25 +42,36 @@
}


@pytest.mark.parametrize('jobs,throw_client_error,json_encoder', [
(test_jobs, True, JSONEncoder()),
(test_jobs, False, JSONEncoder()),
(test_jobs, True, ComplexJSONEncoder()),
(test_jobs, False, ComplexJSONEncoder()),
(test_jobs_with_complex_numbers_in_data, True, ComplexJSONEncoder()),
(test_jobs_with_complex_numbers_in_data, False, ComplexJSONEncoder()),
@pytest.mark.parametrize('jobs,throw_client_error,json_encoder,error,display_error_stack', [
(test_jobs, True, JSONEncoder(), HypernovaError('Error', 'Error msg', ['1: Error', '2: stack']), True),
(test_jobs, False, JSONEncoder(), HypernovaError('Error', 'Error msg', ['1: Error', '2: stack']), False),
(test_jobs, True, ComplexJSONEncoder(), None, True),
(test_jobs, False, ComplexJSONEncoder(), None, False),
(test_jobs_with_complex_numbers_in_data, True, ComplexJSONEncoder(), None, None),
(test_jobs_with_complex_numbers_in_data, False, ComplexJSONEncoder(), None, None),
])
def test_create_fallback_response(jobs, throw_client_error, json_encoder):
def test_create_fallback_response(jobs, throw_client_error, json_encoder, error, display_error_stack):
expected_response = {
identifier: JobResult(
error=None,
html=render_blank_markup(identifier, job, throw_client_error, json_encoder),
error=error,
html=render_blank_markup(
identifier,
job,
throw_client_error,
json_encoder,
(display_error_stack and error)
),
job=job,
)
for identifier, job in jobs.items()
}

assert create_fallback_response(jobs, throw_client_error, json_encoder) == expected_response
assert create_fallback_response(
jobs,
throw_client_error,
json_encoder,
error,
display_error_stack) == expected_response


@pytest.mark.parametrize('max_batch_size,expected', [
Expand Down
33 changes: 24 additions & 9 deletions tests/rendering_test.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
from json import JSONEncoder
from textwrap import dedent

import pytest

from pyramid_hypernova.rendering import encode
from pyramid_hypernova.rendering import render_blank_markup
from pyramid_hypernova.rendering import RenderToken
from pyramid_hypernova.types import HypernovaError
from pyramid_hypernova.types import Job
from testing.json_encoder import ComplexJSONEncoder

Expand Down Expand Up @@ -59,29 +62,41 @@ def test_render_blank_markup_with_custom_json_encoder():
''')


def test_render_blank_markup_with_error():
@pytest.mark.parametrize('error, error_markup', [
(
HypernovaError('Error', 'Error msg', ['1: Error', '2: stack']),
'["Error", "Error msg", ["1: Error", "2: stack"]]'
),
(None, 'undefined'),
])
def test_render_blank_markup_when_throw_client_error_true(error, error_markup):
job = Job('MyCoolComponent.js', data={'title': 'sup'}, context={})
markup = render_blank_markup('my-unique-token', job, True, JSONEncoder())
markup = render_blank_markup('my-unique-token', job, True, JSONEncoder(), error)

assert markup == dedent('''
expected_markup = dedent('''
<div data-hypernova-key="MyCoolComponentjs" data-hypernova-id="my-unique-token"></div>
<script
type="application/json"
data-hypernova-key="MyCoolComponentjs"
data-hypernova-id="my-unique-token"
><!--{"title": "sup"}--></script>
''')

expected_markup += dedent('''
<script type="text/javascript">
(function () {
function ServerSideRenderingError(component) {
(function () {{
function ServerSideRenderingError(component, error) {{
this.name = 'ServerSideRenderingError';
this.component = component;
}
this.cause = error;
}}
ServerSideRenderingError.prototype = Object.create(ServerSideRenderingError.prototype);
ServerSideRenderingError.prototype.constructor = ServerSideRenderingError;
throw new ServerSideRenderingError('MyCoolComponentjs failed to render server-side, and fell back to client-side rendering.');
}());
throw new ServerSideRenderingError('MyCoolComponentjs failed to render server-side, and fell back to client-side rendering.', {error_markup});
}}());
</script>
''') # noqa: ignore=E501
''').format(error_markup=error_markup) # noqa: ignore=E501

assert markup == expected_markup
19 changes: 19 additions & 0 deletions tests/tweens_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,22 @@ def mock_setup(self):

self.mock_request = mock.Mock()

def test_configure_hypernova_batch_sets_display_error_stack_value_through_function(self):
self.should_display_error_stack = mock.Mock(return_value=True)
self.mock_registry.settings['pyramid_hypernova.should_display_error_stack'] = self.should_display_error_stack
response = self.tween(self.mock_request)

# Access the response's body to ensure the batch request is made
response.body

self.mock_batch_request_factory.assert_called_once_with(
get_job_group_url=self.mock_get_job_group_url,
plugin_controller=mock.ANY,
json_encoder=self.mock_json_encoder,
pyramid_request=self.mock_request,
display_error_stack=True
)

def test_tween_replaces_tokens_when_disable_hypernova_tween_not_set(self):
del self.mock_request.disable_hypernova_tween

Expand All @@ -54,6 +70,7 @@ def test_tween_replaces_tokens_when_disable_hypernova_tween_not_set(self):
plugin_controller=mock.ANY,
json_encoder=self.mock_json_encoder,
pyramid_request=self.mock_request,
display_error_stack=False
)
assert self.mock_batch_request_factory.return_value.submit.called
assert response.text == '<div>REACT!</div>'
Expand All @@ -68,6 +85,7 @@ def test_tween_replaces_tokens_when_disable_hypernova_tween_set_false(self):
plugin_controller=mock.ANY,
json_encoder=self.mock_json_encoder,
pyramid_request=self.mock_request,
display_error_stack=False
)

# Access the response's body to ensure the batch request is made
Expand All @@ -89,6 +107,7 @@ def test_tween_replaces_tokens_when_disable_hypernova_tween_set_true(self):
plugin_controller=mock.ANY,
json_encoder=self.mock_json_encoder,
pyramid_request=self.mock_request,
display_error_stack=False
)
assert not self.mock_batch_request_factory.return_value.submit.called
assert response.text == str(self.token)
Expand Down

0 comments on commit 7229e6e

Please sign in to comment.