Fix: Categories /_import endpoint — silent 200 OK, broken transaction, bad validation#34718
Open
Fix: Categories /_import endpoint — silent 200 OK, broken transaction, bad validation#34718
/_import endpoint — silent 200 OK, broken transaction, bad validation#34718Conversation
…rns 200 OK - Move @WrapInTransaction from private processImport() to public importCategories() so that ByteBuddy AOP can properly manage the database transaction - Fix exception handling in processImport(): return 500/403 error response instead of silently swallowing exceptions and returning 200 OK - Fix CategoryHelper.addOrUpdateCategory() to return long success count (previously void, so BulkResultView always showed 0 successes) - Change private addOrUpdateCategory() to return boolean indicating save success - Use returned success count in BulkResultView response Co-authored-by: fmontes <751424+fmontes@users.noreply.github.com>
Copilot
AI
changed the title
[WIP] Fix categories import endpoint to return errors correctly
Fix: Categories import endpoint silently swallows exceptions and returns 200 OK
Feb 21, 2026
Co-authored-by: fmontes <751424+fmontes@users.noreply.github.com>
Contributor
Author
Added
Warning Firewall rules blocked me from connecting to one or more addresses (expand for details)I tried to connect to the following addresses, but was blocked by firewall rules:
If you need me to access, download, or install something from one of these locations, you can either:
|
Contributor
There was a problem hiding this comment.
Pull request overview
This PR fixes a critical bug where the categories import endpoint (POST /api/v1/categories/_import) was silently swallowing exceptions and always returning HTTP 200 with successCount: 0, even when imports failed. The root cause was that @WrapInTransaction was on a private method (ineffective for proxy-based AOP) and exceptions were caught but not properly propagated.
Changes:
- Moved
@WrapInTransactionfrom privateprocessImport()to publicimportCategories()for proper transaction management - Updated exception handling to return HTTP 500 for general errors and HTTP 403 for security violations instead of silently returning 200 OK
- Changed return types to track and return actual success counts:
CategoryHelper.addOrUpdateCategory()now returnslong(count of successful imports), and the private overload returnsboolean(whether individual save succeeded) - Added comprehensive unit tests covering merge/replace modes, existing-key updates, security exception handling, and edge cases
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
CategoriesResource.java |
Moved @WrapInTransaction to public method, improved exception handling to return proper HTTP error codes, and uses actual success count in response |
CategoryHelper.java |
Changed method signatures to return success counts (long for public method, boolean for private method), enabling accurate tracking of import results |
CategoryHelperTest.java |
Added comprehensive unit tests covering merge mode, replace mode, security exceptions, and empty CSV scenarios |
…nvalid CSV, leading spaces in names Co-authored-by: fmontes <751424+fmontes@users.noreply.github.com>
Co-authored-by: fmontes <751424+fmontes@users.noreply.github.com>
Co-authored-by: fmontes <751424+fmontes@users.noreply.github.com>
…ture, field trimming, exportType validation, multi-catch, test style) Co-authored-by: fmontes <751424+fmontes@users.noreply.github.com>
Copilot
AI
changed the title
Fix: Categories import endpoint silently swallows exceptions and returns 200 OK
Fix: Categories Feb 21, 2026
/_import endpoint — silent 200 OK, broken transaction, bad validation
…r, overly broad throws) - Add missing DoesNotExistException import - Fix ambiguous BadRequestException constructor calls by removing null first arg - Narrow throws signatures from Exception to specific checked exceptions Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Use Date.now() with unique prefixes instead of _.random() to generate tag names, eliminating the possibility of duplicate names causing the "Create Tags in Site-Specific Storage" test to intermittently fail. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The import tests referenced a hardcoded local path that does not exist in CI. Previously this was masked because the old code silently swallowed the missing file exception and returned 200. Now that the endpoint correctly returns 400 for missing files, the test needs a real CSV resource. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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.
POST /api/v1/categories/_importalways returned 200 OK withsuccessCount: 0regardless of outcome. Root causes: exceptions swallowed,@WrapInTransactionon a private method (no-op), success count hardcoded, no input validation.Proposed Changes
CategoriesResource.java@WrapInTransactionmoved from privateprocessImport→ publicimportCategories(ByteBuddy only intercepts public methods)processImportnow throwsBadRequestException/ForbiddenException/DotRuntimeExceptioninstead of catching and returningResponse— exceptions now propagate through the transaction boundary, triggering rollback on failure (critical: previously areplacethat deleted all categories before a bad CSV would commit the deletion)"replace".equals(exportType)/"merge".equals(exportType);elsebranch throwsBadRequestException— covers null and unknown valuesuploadedFilethrowsBadRequestException("File is required")before any DB workDoesNotExistExceptionimportCategoryHelper.javaaddOrUpdateCategoryreturnslongsuccess count instead ofvoidaddOrUpdateCategoryreturnsboolean(true=saved, false=error caught)throws Exception→throws DotDataException, DotSecurityExceptionon private method andthrows IOException, DotDataException, DotSecurityExceptionon public methodDotSecurityException/DotDataExceptioncatch blocks into a single multi-catchCategoryImporter.javafindByKey(" boys")mismatchesIOExceptionfor missing/wrong columns or empty/garbage filescsvreader.close()in finally blockCategoryHelperTest.java(new — 13 unit tests)DotSecurityException/DotDataExceptionhandling (failure doesn't abort remaining rows)@Test(expected = IOException.class)Manual Testing
All tests were run against a live dotCMS instance. Replace
$TOKENwith a valid API token and$BASEwith your server URL.Test 1: Valid CSV merge (expect 200, successCount=2)
Result:
{"entity":{"fails":[],"skippedCount":0,"successCount":2},...}— HTTP 200Test 2: No file provided (expect 400)
Result:
{"error":"dotcms.api.error.bad_request: File is required"}— HTTP 400Test 3: Invalid CSV headers (expect 400)
Result:
{"error":"dotcms.api.error.bad_request: Invalid CSV format: missing required columns (name, key, variable, sort)"}— HTTP 400Test 4: Empty file (expect 400)
Result:
{"error":"dotcms.api.error.bad_request: Invalid CSV format: missing required columns (name, key, variable, sort)"}— HTTP 400Test 5: Invalid exportType (expect 400)
Result:
{"error":"dotcms.api.error.bad_request: Invalid exportType: bogus. Must be 'replace' or 'merge'"}— HTTP 400Test 6: Leading/trailing spaces in name are trimmed (expect 200, name="SpaceyName")
Result: successCount=1, category name stored as
"SpaceyName"(no leading/trailing spaces)Test 7: Replace mode (expect 200)
Result:
{"entity":{"fails":[],"skippedCount":0,"successCount":1},...}— HTTP 200Test 8: Garbage file content (expect 400)
Result:
{"error":"dotcms.api.error.bad_request: Invalid CSV format: missing required columns (name, key, variable, sort)"}— HTTP 400Test Results Summary
Checklist
Additional Info
The rollback fix is the most impactful change:
WrapInTransactionAdvice.exit()only callsrollbackTransaction()whent != null. Previously,processImportcaught all exceptions and returned aResponsenormally, so the transaction always committed — including partialreplaceoperations (delete committed, import not).BadRequestException/ForbiddenException/DotRuntimeExceptionall extendWebApplicationException(aRuntimeException), so they propagate through@WrapInTransactionand are handled by the existing JAX-RS exception mappers (400 / 403 / 500).