-
Notifications
You must be signed in to change notification settings - Fork 48.4k
Cross-origin error handling in DEV #10353
New issue
Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? # to your account
Conversation
Cross-origin errors aren't accessible by React in DEV mode because we catch errors using a global error handler, in order to preserve the "Pause on exceptions" behavior of the DevTools. When this happens, we should use a custom error object that explains what happened. For uncaught errors, the actual error message is logged to the console by the browser, so React should skip logging the message again.
'behavior of the DevTools. This is only an issue in DEV-mode; ' + | ||
'in production, React uses a normal try-catch statement.\n\n' + | ||
"It's recommended to serve JavaScript files from the same " + | ||
'origin as your application.', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This might be interpreted as advice against CDNs. Which I'm not sure we want to give? Do we have opinions on this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In that case, you would use an Access Control Allow Origin header. Do you think we should add this detail? Might be better to link to a page with more details?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's keep it like this for now.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should mention something about CORS headers and how to set them up on a script tag. CORS will always be required for modules so people might as well start getting familiar with configuring it for cross-origin. whatwg/html#2440 (comment)
The issue isn't that it is cross-origin but that it is cross-origin + CORS not being set up. CDNs should support it and users that rely on CDNs should configure their script tags accordingly. Or CRA and Webpack should do it automatically.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
cc @mjackson, have you had a chance to look into this for unpkg
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
unpkg currently sets Access-Control-Allow-Origin: *
on responses which should be sufficient for all requests w/out credentials.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you have to change your script tag to set the crossorigin
attribute to anonymous
for this to be respected by the browser though?
<script src="https://example.com/example-framework.js"
crossorigin="anonymous"></script>
I think that could be the issue.
EDIT: Or maybe it's enough to set it to anything? E.g.:
<script src="https://example.com/example-framework.js"
crossorigin></script>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think so, @sebmarkbage. I created a test page that loads 16.0.0-beta.2 from unpkg w/out using <script crossorigin>
and it seems to work ok (i.e. I can use "Pause on exceptions" w/out any problem) and React is able to catch and report the error in the console.
Is that what I'm supposed to be testing?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's only if the error is thrown from a script other than the origin of the page...?
Look how @acdlite used the external expect
library here. https://github.com/facebook/react/pull/10353/files#diff-f2b0545941b151d6a1f95ada1639d6f2R147
Triggering an "invariant" inside React, if React is hosted on unpkg, should probably also work similarly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can confirm just crossorigin
is enough.
willRetry, | ||
}); | ||
|
||
if (__DEV__) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Probably worth leaving a comment explaining why we need these two paths.
I don't quite get this. Aren't we only skipping logging for our own custom fallback error? That's what the logic reads like to me. |
@gaearon Yeah. I meant we should skip logging the custom error, since that would be confusing. I considered just always skipping the message in DEV. Wasn't sure if the current double-logging behavior was desired for some reason. |
It is useful in case your app swallows errors. Then you won’t see the message unless we also print it. IMO we should keep it, even if it’s a bit annoying. |
But in DEV, the error is logged regardless because it's considered unhandled by the browser. |
Hmm, I’m not sure what you mean. try {
this.setState()
} catch (err) {
this.setState()
} which is usually found in async code. People think they’re handling network errors, but they also swallow JS errors. I think it’s important we keep logging the error in this case. Even if the browser thinks it’s caught. |
Even in that case, I believe the error is treated as unhandled because of the When I get back to my computer I'll make a GIF |
Hmm. Was this the case before? I believe such errors don’t surface in 15. |
It's different in 16 because we wrap everything in one big |
willRetry, | ||
shouldIgnoreErrorMessage: error != null && | ||
typeof error.__reactShouldIgnoreErrorMessage === 'boolean' && | ||
error.__reactShouldIgnoreErrorMessage, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why typeof error.__reactShouldIgnoreErrorMessage === 'boolean' && error.__reactShouldIgnoreErrorMessage
instead of error.__reactShouldIgnoreErrorMessage === true
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Flow
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Really 😐
willRetry, | ||
shouldIgnoreErrorMessage: false, | ||
}); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Might be easier to read if the if/else condition above were collapsed into:
capturedErrors.set(boundary, {
componentName,
componentStack,
error,
errorBoundary: errorBoundaryFound ? boundary.stateNode : null,
errorBoundaryFound,
errorBoundaryName,
willRetry,
shouldIgnoreErrorMessage: __DEV__ && error != null && error.__reactShouldIgnoreErrorMessage === true
});
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will that be dead code eliminated?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No. During stripEnvironmentVariables
, __DEV__
would be converted to true/false.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I want it to be DCE'd, but if we change this to always ignore the message in DEV then this doesn't matter. #10353 (comment)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm confused. You have it in an if(DEV)/else currently, which won't be DCEd.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the production build, it will be DCE'd the same as the rest of the DEV blocks
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm confused. This PR adds the following:
if (__DEV__) {
capturedErrors.set(boundary, {...});
} else {
capturedErrors.set(boundary, {...});
}
I suggested collapsing this if/else branch b'c I thought it was more readable. I don't understand how DCE applies here, since neither approach would actually get DCE'd.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What I want to DCE is the extra checks
error != null && error.__reactShouldIgnoreErrorMessage === true
that only happen in DEV.
But this convo is moot anyway, because I'm going to remove this branch and just always ignore the error message in DEV.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, I misunderstood your concern then. So you could have done:
__DEV__ ? error != null && error.__reactShouldIgnoreErrorMessage === true : false
But yeah 😁
@bvaughn Do you object to skipping the error message in the error logger in DEV mode? Given that it's always logged by the browser. #10353 (comment) |
I'd prefer to skip it if it really still captures those nasty cases. But then the message should make it clear the actual error is just above. Since in my experience people look for first error above and don't scroll further up. |
One potentially confusing wording issue: we say "React caught an error" but it says "Uncaught error" right above. Would be nice to somehow make this seem less contradictory for users. |
I'll let y'all come up with improved messaging, or I'll pick this up again when I get back from PTO. |
I don't think the messages should block landing this. Just an observation. |
43793a1
to
dc24f32
Compare
Pushed a commit that removes the double logging in DEV. Unit tests for the error logger are now failing because messages don't match. I'll fix 'em later, gotta go to bed :D |
When are you going on PTO? Maybe I can take over this if you'd like. |
@gaearon I'll be back Monday. And sure, no problem! |
container = document.createElement('div'); | ||
|
||
triggerErrorAndCatch = () => { | ||
// Use setImmediate so that the render is syncrhonous |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"syncrhonous"
Gah, every time. I don't even know how this happens because 'c' and 'r' are both left-hand keys 🤦♂️
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also just realized this isn't an amazing explanation. Without the setImmediate
, the update is batched because it's inside an event handler. With the setImmediate
, the update is deferred until the next tick, but within that tick, the setState
is synchronous.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
setImmediate
isn't sync in an async-by-default world. Why no ReactDOM.flushSync
instead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because for some reason I was thinking that wasn't in master yet :D Good call
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Need to tweak error message. Tests failing but I think direction is fine.
@sebmarkbage Which tweak in particular? I can take over this tomorrow. |
Something about CORS and the use of crossorigin attribute. |
// In production, we print the error directly. | ||
// This will include the message, the JS stack, and anything the browser wants to show. | ||
// We pass the error object instead of custom message so that the browser displays the error natively. | ||
console.error(error); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should remove the message
and name
variables now. They don't seem to be used anymore.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice improvements! 🎉
Everything passed locally. |
We could also "sandwich" those errors. i.e. we could log the first message immediately and then if there were errors during teardown (between first log and commit) we could log another message saying "Scroll up to find the original error" or something. |
I don't think anyone is on this yet. You can make it yours. |
'error handler, in order to preserve the "Pause on exceptions" ' + | ||
'behavior of the DevTools. This is only an issue in DEV-mode; ' + | ||
'in production, React uses a normal try-catch statement.\n\n' + | ||
'If you are using React from a CDN, ensure that the <script> tag ' + |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It might be nice to say "If you are using React from a CDN in development, ensure ..." just to re-emphasize that this is only an issue in dev.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isn't it only a dev issue though? Since in production we use a regular try/catch instead of the funky global handler
Sorry. I read your statement twice as "to re-emphasize that this isn't only an issue in dev."
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
PR welcome :-)
I'm awfully confused by this and the search for an answer lead me here. I'm not getting the "correct" (as I would suspect) error information in my error boundary.
I always get the above message even though everything is served from The error information is there in the console log together with additional stuff which is fine but I was expecting the error boundary |
Can you provide a project reproducing this, with browser information? |
@gaearon I'll try to whip something together. Chrome 60. |
Any chance you're serving something else from other origin? For example you'd see this if another library (not React) throws but it's on CDN. |
@gaearon I don't think so but of course, that thought has crossed my mind. I managed to get a repo working, I'll keep investigating, you if wanna take a quick look you can https://github.com/tessin/tessin-mini
This is a very complicated webpack setup that I've been experimenting with but there should be no CORS involved, I also noted different behavior between beta 2 and 5. Running Chrome Please ask if I need to clarify anything. |
Also, it doesn't appear as if the server-side rendering is trapping the error, it's propagated and not caught by the error boundary component. Works as expected in a production build, you can run the production version and test it if you do.
Feel free to send me down various rabbit holes if it helps, I don't know what else to do about this ATM. |
Demo link -> http://i.imgur.com/O7M3O2V.gif |
Beta 3 changed our wording around the component stack stuff. We now log an additional message below the actual error containing the component stack info. Is this what you're referring to? |
@bvaughn The issue isn't with the console output, the issue is with that get's passed to the ...or are you saying that you are not getting that CORS error? (you should be able to see it if you follow the link to my demo) |
@bvaughn You can see here that I get additional errors in the console relating to CORS (which should not occur) and that the error information is different.
The behavior is different between development and production builds. You can see this in the demo I put together. |
Thanks for clarifying. I was focused on the console output. Yes, I see the cross-origin error in the body as well if I click on the "Dashboard" link. (Loading the dashboard page directly seems to display the correct error message FWIW.) Looking at the network tab, it seems like everything is coming from localhost so this seems to be a bug. Let's take the discussion to a new GH issue! 😄 Edit I've filed #10441 |
Cross-origin errors aren't accessible by React in DEV mode because we catch errors using a global error handler, in order to preserve the "Pause on exceptions" behavior of the DevTools. When this happens, we should use a custom error object that explains what happened.
For uncaught errors, the actual error message is logged to the console by the browser, so React should skip logging the custom error message, to avoid confusion.
Includes a new DOM test case:
Closes #10321