Skip to content

Create a new error for every aborted validation - #289

Merged
Cito merged 2 commits into
mainfrom
fix-validation-aborted-error-leak
Oct 25, 2026
Merged

Cito merged 2 commits into
mainfrom
fix-validation-aborted-error-leak

Conversation

@Cito

@Cito Cito commented Oct 25, 2026 •

Copy link
Copy Markdown
Member

Fixes #287.

Since 3.3.0a3, validate() raised one module-level ValidationAbortedError whenever the error limit was reached. Raising an existing exception again appends the new frames to its __traceback__, so every aborted validation lengthened the traceback of the shared error. That traceback kept the frames, and with them the document and validation context of every request, alive for the life of the process. Since any client can send a document with more than 100 validation errors, this could be used to exhaust the memory of a server. GraphQL.js also uses a shared error here, but JavaScript errors capture their stack only once, when they are created, so it is not affected.

This change:

  • creates a new ValidationAbortedError every time the error limit is reached, as GraphQL-core 3.2 did,
  • removes the traceback and the exception context from the aborted error before it is returned, so that it does not keep the frames of the validation alive. The context is set when the error limit is reached while an exception is being handled, e.g. when OverlappingFieldsCanBeMergedRule reports an exceeded comparison budget,
  • removes the module-level validation_aborted_error, which was not exported.

With the reproduction from #287, the memory retained after 200 aborted validations drops from about 25 MB to a constant 180 KiB.

Tests check that every aborted validation gets its own error without traceback and context, that the documents and validation contexts are released with weak references (also when the validation is aborted while an exception is being handled, and through the comparison budget of OverlappingFieldsCanBeMergedRule), and that ValidationAbortedError still takes the arguments of GraphQLError.

A second commit fixes similar cases found in the rest of the code:

  • A GraphQLError no longer adopts the traceback of the exception that is being handled when it is created. Python gives an error its traceback when it is raised, and reports the handled exception as its context. Errors that are created while handling an exception but never raised, like the error about an exceeded comparison budget, therefore no longer keep the frames of that exception alive.
  • The memoized error of an incremental computation is raised again with its original traceback every time its result is requested, as asyncio does for futures.
  • The reason of an abort signal is raised again with the traceback it had when the signal was aborted. Before, every execution that used an aborted signal extended the traceback of its reason.

Thanks to @riodw for the detailed report and to @Fire162 for the first fix in #288.

Cito added 2 commits October 25, 2026 09:28
Raising the shared ValidationAbortedError added the frames of every aborted
validation to its traceback, which kept them alive for the life of the process.
A GraphQLError no longer adopts the traceback of the exception that is being
handled when it is created, and the memoized errors of computations and the
reasons of abort signals are raised again with their original tracebacks.
@codspeed

codspeed Bot commented Oct 25, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 20 untouched benchmarks


Comparing fix-validation-aborted-error-leak (39f79f1) with main (f0598fe)

Open in CodSpeed

@Cito
Cito merged commit f9fc5ad into main Oct 25, 2026
20 checks passed
@Cito
Cito deleted the fix-validation-aborted-error-leak branch October 25, 2026 09:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Memory leak: the shared validation_aborted_error keeps every traceback it is raised through

1 participant