-
Notifications
You must be signed in to change notification settings - Fork 846
feat: incomplete with node on which an error occurred #4863
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
Conversation
| description: 'An error occured while running this rule', | ||
| message: err.message, | ||
| stack: err.stack, | ||
| error: err, | ||
| // Add a serialized reference to the node the rule failed on for easier debugging. | ||
| // See https://github.com/dequelabs/axe-core/issues/1317. | ||
| errorNode: err.errorNode |
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.
Note that I got rid of most of these, and updated how error works:
- error is now serialized so that it can be carried across frame boundaries
- description is clobbered by the reporter. It never even left axe (plus it has a typo)
- message and stack are duplicated on error
- errorNode wasn't getting pulled through nodeSerializer
ErrorNode in particular was a problem. Because it wasn't serialized, it A. doesn't have the frame selector, and B. leaves the _element property on it, which doesn't play well with serialization (axe Extension would completely error out trying to serialize 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.
Pull Request Overview
This PR implements error handling for axe-core checks and rules by catching errors and converting them to incomplete results instead of failing completely. This allows the scanner to report what it could successfully check while noting where errors occurred.
- Introduces a new
SupportErrorclass to wrap errors with additional context - Adds error handling to rule matching and check evaluation phases
- Creates a dedicated "error-occurred" check type for incomplete results
Reviewed Changes
Copilot reviewed 22 out of 23 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| lib/core/utils/support-error.js | New SupportError class for wrapping errors with context |
| lib/core/utils/serialize-error.js | Utility for serializing errors to JSON |
| lib/core/base/rule.js | Updated to catch errors and wrap them in SupportError |
| lib/core/base/audit.js | Added error handling in audit run and after phases |
| lib/core/utils/merge-results.js | Support for merging error information from frames |
| lib/checks/shared/error-occurred.json | New check definition for error cases |
| test/integration/full/error-occurred/* | Integration tests for error handling |
| test/core/utils/* | Unit tests for new utilities |
Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.
Co-authored-by: Copilot <[email protected]>
Co-authored-by: Copilot <[email protected]>
lib/core/utils/serialize-error.js
Outdated
| if (typeof err !== 'object' || err === null) { | ||
| return { message: String(err) }; | ||
| } | ||
| const serial = { ...err }; // Copy all "own" properties |
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 ...err is going to mean that any (potentially non-serializable) properties of the original error will stay as-is in the returned object, no? Doesn't this defeat the purpose? I'd have expected that whole point of this function would be to make sure the following test case passes:
it('should not include unserializable errorNode properties', () => {
const baseError = new Error('test');
const errorNode = new axe.utils.DqElement(document.documentElement);
const supportError = new axe.utils.SupportError({ error: baseError, errorNode })
const serialized = serializeError(supportError);
assert.isUndefined(serialized.errorNode?._element);
})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.
(presumably by only passing through properties known to be serializable and specifically handling errorNode by passing it through nodeSerializer.dqElmToSpec)
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.
Yeah it's not perfect. I'm not sure there's an easy way to ensure this. Considering this is internal, this seemed sufficient to me.
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.
if you leave an unserializable property on the "serialized" error, doesn't that defeat the purpose of the PR and still leave you in the same place as you were originally, where you'll get errors when trying to pass an axe result containing the partially-unserializable error across a serialization boundary?
If we're satisfied with not having errorNode and instead relying on the node in the synthetic check result, I think the "easy way to ensure this" is to replace { ...err } with {} here and add the non-errorNode properties of RuleError to the list of props on L12. (maybe also add code to the list, it's common enough in things that could be error causes)
| ]; | ||
| const node = errorNode || new DqElement(document.documentElement); | ||
| return Object.assign(new RuleResult(rule), { | ||
| error: serialError, |
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.
The issue suggests that this should also include errorNode here (for compatibility with existing error handling behavior). Note that if we do this, we'd need to pass it through nodeSerializer at frame boundaries.
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.
Yeah, that's why I chose not to do that. It's a pain to properly serialize, with little benefit. See PR description.
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 "it's a pain to do" is a good enough reason to break backcompat with existing users' error handlers. The specification in the #4860 (in "What to change" 1) mentioned "In addition to using an error and errorNode property" and "the error and errorNode properties should continue to be set." I think it'd be better to keep it for backcompat.
I'm not sure what you're referring to by "See PR description", I don't see anything relevant there - did you mean this comment? That comment mentions that it's a problem that errorNode isn't going through nodeSerializer like it should, which I agree with, but I don't think it justifies why breaking the existing property is a preferable solution to "update it to go through nodeSerializer". There aren't that many places where we need to do nodeSerializer.mergeSpecs at frame boundaries, I'd expect it to be a reasonably contained change to update those places to make sure they also mergeSpecs on error nodes.
Looking at the current code (before this PR) that creates errorNodes, I'm actually a bit surprised/confused that we see any current scenarios where they contain _element properties that confuse serialization; the only places that appear to do an initial set of errorNode are in check.js where they come from a nodeSerializer.toSpec output. It makes sense that the original values would be missing ancestor frame information, but it surprises me that they'd end up with _element properties.
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 agree it's a breaking change:
- It's already an optional prop: not every method sets it, so not having it can't possibly break things.
- It's an undocumented property, changing those isn't a breaking change.
- If this was breaking, than so would removing
messageandstackbe.
errorNode is also much buggier than I realized it was when I wrote the issue. It doesn't work across frames, and it doesn't run through NodeSerializer. Everything errorNode does can now be done in a better way. errorNode is a "feature" that has never worked, and that we keep making mistakes with. Better to drop it IMO.
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 1 is a bit of a sketchy argument - I think if someone had a UI for errors that attempted to display which node an error occurred at, and that UI worked fine for errors in checks in <4.11 but stopped working for errors in checks in >=4.11, I think they could reasonably argue that the change broke them.
But points 2, 3, and that it's already really buggy anyway are all fair enough. I verified that I don't think we have any products with such an error UI already (we do have one product that parses errorNode if it's present, but it doesn't do anything with it). I can live with dropping it since it sounds like you feel strongly about it, it's not a hill I'm going to fight further on.
Co-authored-by: Dan Bjorge <[email protected]>
WilcoFiers
left a 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.
Approve Dan's changes
dbjorge
left a 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.
Approving + security reviewing everything except my commit (which Wilco approved above)
## [4.11.0](v4.10.3...v4.11.0) (2025-10-07) ### Features - add RGAA tags to rules ([#4862](#4862)) ([53a925a](53a925a)) - **aria-prohibited-attr:** add support for fallback roles ([#4325](#4325)) ([62a19a9](62a19a9)) - **axe.d.ts:** add nodeSerializer typings ([#4551](#4551)) ([a2f3a48](a2f3a48)), closes [#4093](#4093) - **DqElement:** deprecate fromFrame function ([#4881](#4881)) ([374c376](374c376)), closes [#4093](#4093) - **DqElement:** Truncate large `html` strings when the element has a large outerHTML string ([#4796](#4796)) ([404a4fb](404a4fb)), closes [#4544](#4544) - **get-xpath:** return proper relative selector for id ([#4846](#4846)) ([1035f9e](1035f9e)), closes [#4845](#4845) - **i18n:** Add Portugal Portuguese translation ([#4725](#4725)) ([5b6a65a](5b6a65a)) - incomplete with node on which an error occurred ([#4863](#4863)) ([32ed8da](32ed8da)) - **locale:** Added ru locale ([#4565](#4565)) ([067b01d](067b01d)) - **tap:** some best practice rules map to RGAA ([#4895](#4895)) ([bc33f4c](bc33f4c)) - **td-headers-attr:** report headers attribute referencing other <td> elements as unsupported ([#4589](#4589)) ([ec7c6c8](ec7c6c8)), closes [#3987](#3987) ### Bug Fixes - **aria-allowed-role:** add form to allowed roles of form element ([#4588](#4588)) ([8aa47ac](8aa47ac)), closes [/github.com/dequelabs/axe-core/blob/develop/lib/standards/html-elms.js#L264](https://github.com/dequelabs//github.com/dequelabs/axe-core/blob/develop/lib/standards/html-elms.js/issues/L264) - **aria-allowed-role:** Add math to allowed roles for img element ([#4658](#4658)) ([95b6c18](95b6c18)), closes [#4657](#4657) - **autocomplete-valid :** Ignore readonly autocomplete field ([#4721](#4721)) ([491f4ec](491f4ec)), closes [#4708](#4708) - **autocomplete-valid:** treat values "xon" and "xoff" as non-WCAG-violations ([#4878](#4878)) ([52bc611](52bc611)), closes [#4877](#4877) - **axe.d.ts:** add typings for preload options object ([#4543](#4543)) ([cfd2974](cfd2974)) - **button-name,input-button-name,input-img-alt:** allow label to give accessible name ([#4607](#4607)) ([a9710d7](a9710d7)), closes [#4472](#4472) [#3696](#3696) [#3696](#3696) - **captions:** fix grammar in captions check incomplete message ([#4661](#4661)) ([11de515](11de515)) - **color-contrast:** do not run on elements with font-size: 0 ([#4822](#4822)) ([d77c885](d77c885)), closes [#4820](#4820) - consistently parse tabindex, following HTML 5 spec ([#4637](#4637)) ([645a850](645a850)), closes [#4632](#4632) - **core:** measure perf for async checks ([#4609](#4609)) ([7e9bacf](7e9bacf)) - fix grammar when using "alternative text" in a sentence ([#4811](#4811)) ([237a586](237a586)), closes [#4394](#4394) - **get-ancestry:** add nth-child selector for multiple siblings of shadow root ([#4606](#4606)) ([1cdd6c3](1cdd6c3)), closes [#4563](#4563) - **get-ancestry:** don't error when there is no parent ([#4617](#4617)) ([a005703](a005703)) - **locale:** fix typos in japanese (ja) locale ([#4856](#4856)) ([3462cc5](3462cc5)) - **locale:** fixed typos in german (DE) locale ([#4631](#4631)) ([b7736de](b7736de)) - **locale:** proofread and updated de.json ([#4643](#4643)) ([8060ada](8060ada)) - **meta-viewport:** lower impact to moderate ([#4887](#4887)) ([2f32aa5](2f32aa5)), closes [#4714](#4714) - **no-autoplay-audio:** don't timeout for preload=none media elements ([#4684](#4684)) ([cdc871e](cdc871e)) - **performanceTimer:** throwing in axe catch clause ([#4852](#4852)) ([a4ade04](a4ade04)), closes [/github.com/dequelabs/axe-core/blob/e7dae4ec48cbfef74de9f833fdcfb178c1002985/lib/core/base/rule.js#L297-L300](https://github.com/dequelabs//github.com/dequelabs/axe-core/blob/e7dae4ec48cbfef74de9f833fdcfb178c1002985/lib/core/base/rule.js/issues/L297-L300) - **performanceTimer:** work in frames ([#4834](#4834)) ([d7dfebc](d7dfebc)) - **rules:** Change "alternate text" to "alternative text" ([#4582](#4582)) ([b03ada3](b03ada3)) - **target-size:** do not treat focusable tabpanels as targets ([#4702](#4702)) ([60d11f2](60d11f2)), closes [#4421](#4421) [#4701](#4701) - **type:** correct RuleError type ([#4893](#4893)) ([d1aa8e2](d1aa8e2)) - **types:** correct raw types ([#4903](#4903)) ([3eade11](3eade11)) This PR was opened by a robot 🤖 🎉
Part of #4860
If a check or matches method throws, catch the error and add a node with an
error-occurrednone check. This way incomplete results are always reported.This only happens for axe.run / axe.runPartial.
rule.runSync()still throws if an occurs in a rule or check.