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

Errors thrown within <await><@catch> are not handled #1813

Closed
vwong opened this issue Jun 22, 2022 · 6 comments
Closed

Errors thrown within <await><@catch> are not handled #1813

vwong opened this issue Jun 22, 2022 · 6 comments
Labels
type:unverified bug A bug report that has not been verified

Comments

@vwong
Copy link
Contributor

vwong commented Jun 22, 2022

Marko Version

5.21.2

Details

In an <await> tag, if I don't specify the <@catch>, then the error is forwarded to the server (ExpressJS, in my case), so that the server can handle it correctly - such as prematurely terminating the response stream, so that clients know the response is bad. If I do specify the <@catch>, then the error is swallowed, and not forwarded to the server; as far as the stream is concerned, nothing is wrong.

I want to be able to render a user-friendly message AND indicate to the server to terminate the response stream.

I try re-throwing the error...

<await(somePromise)>
  <@then|res|>
    <success res=res/>
  </@then>
  <@catch|err|>
    <failure err=err/>
    $ throw err
  </@catch>
</await>

Expected Behavior

Thrown error is handled, and does not crash the server

Actual Behavior

Error is uncaught, and crashes the server (makes it unresponsive)

Possible Fix

Handle the (re) thrown error (or any other newly thrown one), the same way as if when there was no <@catch>.

diff --git a/node_modules/marko/dist/core-tags/core/await/renderer.js b/node_modules/marko/dist/core-tags/core/await/renderer.js
index 58722fd..5a329f3 100644
--- a/node_modules/marko/dist/core-tags/core/await/renderer.js
+++ b/node_modules/marko/dist/core-tags/core/await/renderer.js
@@ -220,7 +220,11 @@ function renderContents(err, data, input, out) {
   if (err) {
     if (input.catch) {
       if (errorRenderer) {
-        errorRenderer(out, err);
+        var renderErr = safeRenderBody(errorRenderer, out, err);
+
+        if (renderErr) {
+          out.error(err);
+        }
       }
     } else {
       out.error(err);

I'm happy to help create a PR to fix this (with the appropriate tests), if this is deemed desirable and correct.

@vwong vwong added the type:unverified bug A bug report that has not been verified label Jun 22, 2022
@tigt
Copy link
Contributor

tigt commented Jun 22, 2022

This definitely seems like it should work, but we may have to make the fix back-compat friendly. We're figuring out how.

@vwong
Copy link
Contributor Author

vwong commented Jun 22, 2022

Another point to add to your thinking: I find myself adding the following (in addition to the patch above)

// main usage
<await(somePromise)>
  <@then|res|>
    <success res=res/>
  </@then>
  <@catch|err|>
    <failure err=err/>
    <break-chunk/>
    $ throw err;
  </@catch>
</await>

// break-chunk.marko
$ const delay = new Promise((resolve) => setTimeout(resolve, 0));
<await(delay)><@then>${" "}</@then></await>

It seems that I need to add in another <await> in the error handler to force the stream to break the chunk, otherwise, if the main <await> is the last one on the page, it seems to be flushed together with the null chunk anyway. By forcing the chunk to break, I give the server (ExpressJS in my case) a chance to omit the null chunk. It's a bit fiddly, and may be a case of a leaky abstraction. It would be good if the patch itself could include something like out.beginAsync() or something to automatically break the chunk.

This last bit, (the flushing of chunks) is somewhat guess work at this point, I haven't dug deep enough into the code yet. So, I could be wrong entirely! Apologies if this causes a wild goose chase!

@DylanPiercey
Copy link
Contributor

@vwong what issue are you having with the null chunk being flushed with the rest of the await?

@DylanPiercey
Copy link
Contributor

FYI @mlrawlings was floating the idea of having something like:

<@catch|err| rethrow>

However I think if this manual throw case isn't working it probably should also.

@vwong
Copy link
Contributor Author

vwong commented Jun 23, 2022

If I just had:

<@catch|error|>
  $ throw error;
</@catch>

The stream, in some cases, completes fine (with the patch applied), with null terminating chunk and all. I haven't had time to dig into exactly why, but I suspect this happens when this is already the last <await>, and any remaining data are just closing tags and synchronous content. By the time ExpressJS (really finalhandler) gets to terminate the stream, the null chunk has already been sent (I suspect this without proof).

That's why I attempted to inject another dummy <await>:

<@catch|error|>
  <component-that-awaits-with-settimeout-zero />
  $ throw error;
</@catch>

I was hoping that this would cause the stream to pause, enough for it to disconnect before the null chunk. In my testing, this seems to work.

I've been thinking about <@catch|err| rethrow> too, or eof, or similar. It's certainly less leaky than having to explicit throw, and having to know that an uncaught exception terminates the stream.

A better end-result, I think, is to continue rendering the rest of the page, but keep track that an error occurred at all, then corrupt the page right at the end. But I'd accept the mid-stream termination if we can't achieve the near-end-termination.

@vwong
Copy link
Contributor Author

vwong commented Aug 9, 2023

Looks like this is now handled by #2005.

@vwong vwong closed this as completed Aug 9, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
type:unverified bug A bug report that has not been verified
Projects
None yet
Development

No branches or pull requests

3 participants