alex-oleshkevich / starception

Beautiful exception page for Starlette apps.
MIT License
97 stars 6 forks source link

starception template broken if I have a typo using url_for #24

Closed euri10 closed 1 year ago

euri10 commented 1 year ago

I think this should work, it's easily reproducible using url_for it seems

I have an endpoint that renders a template

If I make a mistake in the template on the line below (it should be order.conversations_uuid with an a and not converstions_uuid), then starception template is not correctly displayed (see screen)

<a href="{{ url_for("conversation_get", uuid=order.converstions_uuid ) }}">

20221107_1652_1908x1031_1667836369

alex-oleshkevich commented 1 year ago

Looks like escaping issue. Will check soon

alex-oleshkevich commented 1 year ago

@euri10 could you please PR a test suite? It works for me. There is something special in your case that I don't know about. image

euri10 commented 1 year ago

I can trigger it in my project very easily but have issues replicating on a simpler template, I think it happens on the stack frame trying to access an asyncpg Record attribute that does not exists, the generated FrameInfo might be causing it I'll try a MRE later

euri10 commented 1 year ago

ok I have troubles to come up with an MRE but hopefuly this manual debug will help @alex-oleshkevich

in generate_html the limit param is set at 15, so I put a breakpoint and incrementally set it manually to 1, 2 etc until it breaks the template.

it happens to break the template at 5 so there might be something in the lines of the frame code context that is not properly escaped (limit parameter s passed down getinnerframes and retrieve limit lines of context)

My exception has 28 frames, error seems to happen on the 5th line of code context, so the below is[z.code_context[4] for z in frames] when setting limit to 5. Escaping issue might happen in one of those 28 items.

pprint([z.code_context[4] for z in frames])
['            if not response_started:\n',
 '\n',
 '    def should_autoload(self, connection: HTTPConnection) -> bool:\n',
 '            if response_started:\n',
 '            handler = None\n',
 '                # This exception was possibly handled by the dependency but '
 'it should\n',
 '                    dependency_exception = e\n',
 '            elif match == Match.PARTIAL and partial is None:\n',
 '    def __eq__(self, other: typing.Any) -> bool:\n',
 '            response = await run_in_threadpool(func, request)\n',
 '            )\n',
 '        return await run_in_threadpool(dependant.call, **values)\n',
 '        {\n',
 '            context,\n',
 '\n',
 '        return wrapper\n',
 '\n',
 '    async def render_async(self, *args: t.Any, **kwargs: t.Any) -> str:\n',
 '    def join_path(self, template: str, parent: str) -> str:\n',
 '    <link href="{{ url_for(\'static\', path=\'/orders.css\') }}" '
 'rel="stylesheet">\n',
 '        </div>\n',
 '                        unread\n',
 '        loader = jinja2.FileSystemLoader(directory)\n',
 '\n',
 '                pass\n',
 '        )\n',
 '            path_params.pop(key)\n',
 '\n']

here the full code context for the 28 frames, the previous is the 5th and last element of all lines code context

pprint([z.code_context for z in frames])
[['\n',
  '        try:\n',
  '            await self.app(scope, receive, _send)\n',
  '        except Exception as exc:\n',
  '            if not response_started:\n'],
 ['            await send(message)\n',
  '\n',
  '        await self.app(scope, receive, send_wrapper)\n',
  '\n',
  '\n'],
 ['            await load_session(connection)\n',
  '\n',
  '        await self.app(scope, receive, send)\n',
  '\n',
  '    def should_autoload(self, connection: HTTPConnection) -> bool:\n'],
 ['\n',
  '            if handler is None:\n',
  '                raise exc\n',
  '\n',
  '            if response_started:\n'],
 ['\n',
  '        try:\n',
  '            await self.app(scope, receive, sender)\n',
  '        except Exception as exc:\n',
  '            handler = None\n'],
 ['                except Exception as e:\n',
  '                    dependency_exception = e\n',
  '                    raise e\n',
  '            if dependency_exception:\n',
  '                # This exception was possibly handled by the dependency but '
  'it should\n'],
 ['                scope[self.context_name] = stack\n',
  '                try:\n',
  '                    await self.app(scope, receive, send)\n',
  '                except Exception as e:\n',
  '                    dependency_exception = e\n'],
 ['            if match == Match.FULL:\n',
  '                scope.update(child_scope)\n',
  '                await route.handle(scope, receive, send)\n',
  '                return\n',
  '            elif match == Match.PARTIAL and partial is None:\n'],
 ['            await response(scope, receive, send)\n',
  '        else:\n',
  '            await self.app(scope, receive, send)\n',
  '\n',
  '    def __eq__(self, other: typing.Any) -> bool:\n'],
 ['        request = Request(scope, receive=receive, send=send)\n',
  '        if is_coroutine:\n',
  '            response = await func(request)\n',
  '        else:\n',
  '            response = await run_in_threadpool(func, request)\n'],
 ['            raise RequestValidationError(errors, body=body)\n',
  '        else:\n',
  '            raw_response = await run_endpoint_function(\n',
  '                dependant=dependant, values=values, '
  'is_coroutine=is_coroutine\n',
  '            )\n'],
 ['\n',
  '    if is_coroutine:\n',
  '        return await dependant.call(**values)\n',
  '    else:\n',
  '        return await run_in_threadpool(dependant.call, **values)\n'],
 ['        buyer_username=session["username"],\n',
  '    )\n',
  '    return templates.TemplateResponse(\n',
  '        "orders/orders.html",\n',
  '        {\n'],
 ['            raise ValueError(\'context must include a "request" key\')\n',
  '        template = self.get_template(name)\n',
  '        return _TemplateResponse(\n',
  '            template,\n',
  '            context,\n'],
 ['        self.template = template\n',
  '        self.context = context\n',
  '        content = template.render(context)\n',
  '        super().__init__(content, status_code, headers, media_type, '
  'background)\n',
  '\n'],
 ['    def _with_tracer(tracer):\n',
  '        def wrapper(wrapped, instance, args, kwargs):\n',
  '            return func(tracer, wrapped, instance, args, kwargs)\n',
  '\n',
  '        return wrapper\n'],
 ['            template_name = instance.name or DEFAULT_TEMPLATE_NAME\n',
  '            span.set_attribute(ATTRIBUTE_JINJA2_TEMPLATE_NAME, '
  'template_name)\n',
  '        return wrapped(*args, **kwargs)\n',
  '\n',
  '\n'],
 ['            return self.environment.concat(self.root_render_func(ctx))  # '
  'type: ignore\n',
  '        except Exception:\n',
  '            self.environment.handle_exception()\n',
  '\n',
  '    async def render_async(self, *args: t.Any, **kwargs: t.Any) -> str:\n'],
 ['        from .debug import rewrite_traceback_stack\n',
  '\n',
  '        raise rewrite_traceback_stack(source=source)\n',
  '\n',
  '    def join_path(self, template: str, parent: str) -> str:\n'],
 ['{% extends "base.html" %}\n',
  '{% block title %}Order{% endblock %}\n',
  '{% block head %}\n',
  '    {{ super() }}\n',
  '    <link href="{{ url_for(\'static\', path=\'/orders.css\') }}" '
  'rel="stylesheet">\n'],
 ['    <div>\n',
  '        <div id="content" class="content">\n',
  '            {% block content %}\n',
  '            {% endblock %}\n',
  '        </div>\n'],
 ['                <td><a href="{{ url_for(\'orders_get\', '
  'order_uuid=order.orders_uuid) }}">{{ order.orders_uuid }}</a></td>\n',
  '                <td class="{% if not order.message_count '
  '%}order-conversation-alert{% endif %}">\n',
  '                    <a href="{{ url_for("conversation_get", '
  'uuid=order.converstions_uuid ) }}">\n',
  '                        {{ order.conversations_uuid }}\n',
  '                        unread\n'],
 ['        def url_for(context: dict, name: str, **path_params: typing.Any) -> '
  'str:\n',
  '            request = context["request"]\n',
  '            return request.url_for(name, **path_params)\n',
  '\n',
  '        loader = jinja2.FileSystemLoader(directory)\n'],
 ['    def url_for(self, name: str, **path_params: typing.Any) -> str:\n',
  '        router: Router = self.scope["router"]\n',
  '        url_path = router.url_path_for(name, **path_params)\n',
  '        return url_path.make_absolute_url(base_url=self.base_url)\n',
  '\n'],
 ['        for route in self.routes:\n',
  '            try:\n',
  '                return route.url_path_for(name, **path_params)\n',
  '            except NoMatchFound:\n',
  '                pass\n'],
 ['            raise NoMatchFound(name, path_params)\n',
  '\n',
  '        path, remaining_params = replace_params(\n',
  '            self.path_format, self.param_convertors, path_params\n',
  '        )\n'],
 ['        if "{" + key + "}" in path:\n',
  '            convertor = param_convertors[key]\n',
  '            value = convertor.to_string(value)\n',
  '            path = path.replace("{" + key + "}", value)\n',
  '            path_params.pop(key)\n'],
 ['        value = str(value)\n',
  '        assert "/" not in value, "May not contain path separators"\n',
  '        assert value, "Must not be empty"\n',
  '        return value\n',
  '\n']]
euri10 commented 1 year ago

it happens on the 21th frame, 5th code context line ie manually setting this:

frames = inspect.getinnerframes(exc.__traceback__, 5)[:21] if exc.__traceback__ else []
stack = [
        StackItem(
            exc=exc,
            solution=getattr(exc, 'solution', ''),
            frames=frames,
            has_vendor_frames=any(is_vendor(f) for f in frames),
        )
    ]
frames[-1].code_context
['    <div>\n', '        <div id="content" class="content">\n', '            {% block content %}\n', '            {% endblock %}\n', '        </div>\n']

and I've got the template failing and redering like this 20221108_1129_1865x1463_1667903381

alex-oleshkevich commented 1 year ago

can you install pygments and test if the bug still exists?

also, test if escaping this line solves it: https://github.com/alex-oleshkevich/starception/blob/master/starception/templates/code_snippet.html#L21

alex-oleshkevich commented 1 year ago

or email me the page as HTML to i can inspect where the problem is

euri10 commented 1 year ago

can you install pygments and test if the bug still exists?

also, test if escaping this line solves it: master/starception/templates/code_snippet.html#L21

pygments solves it, it is not in the dependencies ? replacing the code class by foobar also, same as commenting the line I suppose

euri10 commented 1 year ago

roo I should rtfm, missed https://github.com/alex-oleshkevich/starception#with-syntax-highlight-support

alex-oleshkevich commented 1 year ago

Fixed in update. Please confirm. ;)

euri10 commented 1 year ago

Fixed in update. Please confirm. ;)

upgraded to 0.6.2 and it's working fine ! thanks for the quick fix