Return 409/400 instead of 500 when tag creation is rejected - #2539
Merged
Conversation
Creating a tag returned a 500 with no usable message in two situations, both of which are ordinary user error and should be reported as such. The constraint-violation handler assumed the cause chain was exactly two levels deep, that the inner cause carried a message, and that the message contained a PostgreSQL "Detail: " section. Any of those assumptions failing threw a NullPointerException out of the handler itself, so Spring reported a 500 rather than the 409 the handler was written to return. When the section was simply absent, indexOf returned -1 and substring silently dropped the first seven characters of the message instead. This matters for tags because there is a unique index on lower(name) across the whole tags table, which holds tag groups as well, so a name that collides with any existing tag or group lands in this handler. Walk the cause chain defensively, only strip the "Detail: " prefix when it is actually present, and fall back to the most specific cause. Separately, rejecting a tag whose group does not allow custom tags threw IllegalArgumentException, which fell through to the generic handler and became a 500 reading "An exception occurred: java.lang.IllegalArgumentException". Throw BadRequestAtlasException instead and register it on the existing bad-request handler, which already treated it as a 400 when it arrived wrapped in an UndeclaredThrowableException. The message now names the groups that refused the tag, since the client cannot otherwise tell which of the selected groups was the problem. Creating a tag with no groups at all NPEd on the same path and is now a 400 too. Reported in OHDSI/Atlas3#211.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Reported as OHDSI/Atlas3#211. Creating a tag with a name, a tag group and an icon returns a 500 with no usable message. Both routes into that 500 are ordinary user error and should be reported as such.
Cause 1: the constraint-violation handler throws from inside itself
GlobalExceptionHandler.handleDataIntegrityViolationdid:That assumes the cause chain is exactly two levels deep, that the inner cause carries a message, and that the message contains a PostgreSQL
Detail:section. When any of those does not hold it throws aNullPointerExceptionout of the handler, so Spring reports a 500 instead of the 409 the handler was written to return. When the section is merely absent,indexOfreturns -1 andsubstring(-1 + 8)silently drops the first seven characters of the message instead.This is reachable for tags in particular because
tags_name_idxis a unique index onlower(name)over the wholetagtable, and tag groups are rows in that same table (TagGroupServicepersists them through the sameTagentity and repository), so a name colliding with any existing tag or group lands in this handler. The index keeps its original plural name from beforeV2.9.0.20210812164224__assets_tags_renaming.sqlrenamed the table.Cause 2: a rejected tag group produces a generic 500
TagService.createthrewIllegalArgumentExceptionwhen a selected group hasallow_custom = false. Nothing handles that type, so it fell through to the generic handler and became a 500 readingAn exception occurred: java.lang.IllegalArgumentException. A tag posted with no groups at all NPE'd on the same path.Fix
Detail:prefix only when present, and fall back togetMostSpecificCause. A duplicate name now returns 409 with the reason.BadRequestAtlasExceptionfor a group that does not allow custom tags, and register that type on the existing bad-request handler. It was already treated as a 400 when it arrived wrapped in anUndeclaredThrowableException, so throwing it directly producing a 500 was inconsistent. The message names the groups that refused the tag, since the client cannot otherwise tell which selection was the problem.Verification
GlobalExceptionHandlerTestcovers the PostgreSQLDetail:case, a missing cause, a one-level chain, a cause with no message, and a non-PostgreSQL message with noDetail:section. Restoring the previous implementation fails four of the five, three with the originalNullPointerExceptionand one on the seven-character truncation.Note on a hypothesis that does not hold
It has been suggested that the 500 came from the client omitting
mandatory,showGroup,multiSelectionandallowCustomfrom the create request. That cannot be the cause:TagDTOdeclares all four as primitiveboolean, so Jackson deserializes an absent key tofalse, andTagDTOToTagConverterreads them throughisShowGroup()-style primitive getters, so there is no unboxing to fail. Sending those fields explicitly changes nothing.