Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Stop throwing ParseError when throwOnError is false #1169

Merged
merged 7 commits into from
May 9, 2018

Conversation

edemaine
Copy link
Member

This PR is motivated by the recent security issue reported in #1160. I would argue that almost anyone who uses KaTeX on a user-specified string doesn't want an exception to happen, but rather wants KaTeX to do its best job at rendering the given LaTeX. throwOnError is the obvious "solution", but it actually only fixes invalid commands, but any other type of ParseError still gets thrown. This is nice because you get partial renderings, but not nice because it doesn't fix the original problem of not throwing exceptions. As a result, I believe many KaTeX users (myself included) write our own try ... catch ... block to render something else when KaTeX throws an error. And if done poorly, this leads to security holes like #1160.

This PR makes throwOnError more useful by rendering HTML for all types of ParseError. If it's just an unknown command, you get a partial render as before. But if you have some other type of error, you get a <span class="katex-error"> whose text is the LaTeX source code, its color is options.errorColor (defaulting to red as usual), and it has a hover title that displays the actual error message. This is what I did in my own KaTeX error handler. Crucially, the output HTML is properly escaped (thanks to our existing domTree infrastructure -- see the test snapshot).

There's an obvious alternative: show the error message directly, and hover to see the LaTeX. Or some other combination. But IMHO this PR is probably closest to the intent of the author: if you can't render nice LaTeX, at least render the source code.

Another question is naming. I took over throwOnError because it is the natural name for this feature (and probably what most people expect). But it breaks backwards compatibility, so would need to be a new version. We could use another name to keep backward compatibility. We could also provide the old behavior, via something like throwOnBadCommand. I'm not sure of a great use for this... If one wants to reformat the error style, they could use CSS, or (in the worst case) parse the resulting <span class="katex-error"> and restructure.

Along the way:

  • Use new katex.__renderToDomTree in katex-spec's _getBuilt. (This was necessary to get the new error handling in _getBuilt.)
  • Fix jest results which must always be functions, not strings.

`render`, `renderToString`, etc. now catch ParseError and render it to the
raw LaTeX (with proper escaping) and a hover title with the error.

Along the way:
* Use new `katex.__renderToDomTree` in katex-spec's `_getBuilt`.
  (This was necessary to get the new error handling in `_getBuilt`.)
* Fix jest results which must always be functions, not strings.
@codecov
Copy link

codecov bot commented Feb 18, 2018

Codecov Report

Merging #1169 into master will increase coverage by 0.01%.
The diff coverage is 93.33%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master    #1169      +/-   ##
==========================================
+ Coverage   83.76%   83.78%   +0.01%     
==========================================
  Files          61       61              
  Lines        3978     3989      +11     
  Branches      662      663       +1     
==========================================
+ Hits         3332     3342      +10     
- Misses        547      548       +1     
  Partials       99       99
Impacted Files Coverage Δ
katex.js 79.41% <93.33%> (+5.49%) ⬆️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 3e529dd...761cbe6. Read the comment docs.

kevinbarabash and others added 3 commits February 18, 2018 13:20
Leave error type unspecified, as we check it with instanceof.
@ylemkimon
Copy link
Member

ylemkimon commented Mar 9, 2018

How about using error levels:
0 - suppress all errors, instead print LaTeX source code, this PR's behavior when !throwOnError
1 - suppress unsupported command error, current behavior when !throwOnError
2 - throw all errors, current/PR behavior when throwOnError

@kevinbarabash
Copy link
Member

@edemaine thanks for the PR. This is definitely a pain point for users. I will review this weekend.

@@ -98,10 +100,10 @@ const parseAndSetResult = function(expr, result, settings) {
} catch (e) {
result.pass = false;
if (e instanceof ParseError) {
result.message = "'" + expr + "' failed " +
result.message = () => "'" + expr + "' failed " +
"parsing with error: " + e.message;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did result.message change to a callback?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is necessary since some update to Jest: Jest requires all messages to be callable. Obviously not directly related to this PR, but I ran into this (presumably long-standing) bug when my tests were failing.

`;

exports[`A parser that does not throw on unsupported commands should properly escape LaTeX in errors 1`] = `"<span class=\\"katex-error\\" title=\\"ParseError: KaTeX parse error: Expected group after &#x27;^&#x27; at position 2: 2^̲&amp;&quot;&lt;&gt;\\" style=\\"color:#933\\">2^&amp;&quot;&lt;&gt;</span>"`;

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice

static/main.js Outdated
@@ -21,7 +21,7 @@ function init() {
const macros = {};
// TODO: Add toggle for displayMode.
// https://github.com/Khan/KaTeX/issues/1035
const options = {displayMode: true};
const options = {displayMode: true, throwOnError: false};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool.

options: Settings,
) {
if (options.throwOnError || !(error instanceof ParseError)) {
throw error;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does re-throwing maintain the call stack?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kevinbarabash Yes 😄

* renders the invalid LaTeX as a span with hover title giving the KaTeX
* error message. Otherwise, simply throws the error.
*/
const renderError = function(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a really slick solution.

@kevinbarabash
Copy link
Member

How about using error levels:
0 - suppress all errors, instead print LaTeX source code, this PR's behavior when !throwOnError
1 - suppress unsupported command error, current behavior when !throwOnError
2 - throw all errors, current/PR behavior when throwOnError

I can't think of a situation where people would actually want level 1. They'd still need to use a try/catch to avoid errors other than unsupported command errors.

@ylemkimon
Copy link
Member

ylemkimon commented Mar 12, 2018

@kevinbarabash A user may want to implement custom error handler, but in the custom error handler, it cannot access KaTeX internals, i.e., if it's unsupported command error print the plain command name. Though as you said, I'm not sure there are people want it.

@edemaine
Copy link
Member Author

I could see wanting to build a custom handler for custom formatting in the error case. An alternative would be to provide a callback to call instead of an error handler. Maybe throwOnError could receive a function argument to handle this case?

@ylemkimon
Copy link
Member

This will also fix #475.

Related: #538

@ylemkimon
Copy link
Member

ylemkimon commented Mar 22, 2018

With #1226, I think we should have an error callback function(handler) and handle errors accordingly.

@kevinbarabash
Copy link
Member

@edemaine looks like there's some lint:

/home/travis/build/Khan/KaTeX/test/katex-spec.js
  3120:16  error  'buildTree' is not defined  no-undef

@edemaine
Copy link
Member Author

edemaine commented May 9, 2018

Thanks for the review, @ylemkimon. Do you think I should add an error callback function for custom error handling/rendering? Or leave that for consideration in another PR?

@ylemkimon
Copy link
Member

Now I think currently there is no way to access KaTeX internals, and custom error handler probably should be another issue/PR. Thank you for the PR!

@ylemkimon ylemkimon merged commit 49e673a into KaTeX:master May 9, 2018
@edemaine
Copy link
Member Author

edemaine commented May 9, 2018

Sounds good! By the way, this is a breaking change compared to old behavior (rendering instead of throwing an exception), so should we advance to 0.10? @kevinbarabash

@edemaine edemaine mentioned this pull request May 9, 2018
edemaine added a commit to edemaine/KaTeX that referenced this pull request May 9, 2018
PR KaTeX#1169 forgot to document itself.  This fixes the README accordingly.
ylemkimon pushed a commit that referenced this pull request May 9, 2018
PR #1169 forgot to document itself.  This fixes the README accordingly.
@ylemkimon ylemkimon mentioned this pull request Aug 18, 2018
1 task
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants