Skip to content

Comments

Fix: Categories /_import endpoint — silent 200 OK, broken transaction, bad validation#34718

Open
Copilot wants to merge 12 commits intomainfrom
copilot/fix-categories-import-endpoint
Open

Fix: Categories /_import endpoint — silent 200 OK, broken transaction, bad validation#34718
Copilot wants to merge 12 commits intomainfrom
copilot/fix-categories-import-endpoint

Conversation

Copy link
Contributor

Copilot AI commented Feb 21, 2026

POST /api/v1/categories/_import always returned 200 OK with successCount: 0 regardless of outcome. Root causes: exceptions swallowed, @WrapInTransaction on a private method (no-op), success count hardcoded, no input validation.

Proposed Changes

CategoriesResource.java

  • @WrapInTransaction moved from private processImport → public importCategories (ByteBuddy only intercepts public methods)
  • processImport now throws BadRequestException / ForbiddenException / DotRuntimeException instead of catching and returning Response — exceptions now propagate through the transaction boundary, triggering rollback on failure (critical: previously a replace that deleted all categories before a bad CSV would commit the deletion)
  • Null-safe "replace".equals(exportType) / "merge".equals(exportType); else branch throws BadRequestException — covers null and unknown values
  • Null uploadedFile throws BadRequestException("File is required") before any DB work
  • Added missing DoesNotExistException import

CategoryHelper.java

  • addOrUpdateCategory returns long success count instead of void
  • Private addOrUpdateCategory returns boolean (true=saved, false=error caught)
  • Narrowed throws Exceptionthrows DotDataException, DotSecurityException on private method and throws IOException, DotDataException, DotSecurityException on public method
  • Collapsed identical DotSecurityException / DotDataException catch blocks into a single multi-catch
  • Removed stale unused imports

CategoryImporter.java

  • All four CSV fields trimmed (was name-only); prevents findByKey(" boys") mismatches
  • Header validation throws IOException for missing/wrong columns or empty/garbage files
  • Null-safe csvreader.close() in finally block

CategoryHelperTest.java (new — 13 unit tests)

  • Merge/replace success counting
  • Per-row DotSecurityException/DotDataException handling (failure doesn't abort remaining rows)
  • CSV header validation (wrong headers, too few columns)
  • Name trimming verification
  • Empty/garbage content detection
  • Exception-expecting tests use @Test(expected = IOException.class)

Manual Testing

All tests were run against a live dotCMS instance. Replace $TOKEN with a valid API token and $BASE with your server URL.

Test 1: Valid CSV merge (expect 200, successCount=2)

cat > /tmp/categories_test.csv << 'CSV'
"name","key","variable","sort"
"TestCatA","testcata","testcata","0"
"TestCatB","testcatb","testcatb","1"
CSV

curl -s -w "\nHTTP_STATUS: %{http_code}\n" \
  -H "Authorization: Bearer $TOKEN" \
  -F "file=@/tmp/categories_test.csv" \
  -F "filter=" \
  -F "exportType=merge" \
  "$BASE/api/v1/categories/_import"

Result: {"entity":{"fails":[],"skippedCount":0,"successCount":2},...} — HTTP 200

Test 2: No file provided (expect 400)

curl -s -w "\nHTTP_STATUS: %{http_code}\n" \
  -H "Authorization: Bearer $TOKEN" \
  -F "filter=" \
  -F "exportType=merge" \
  "$BASE/api/v1/categories/_import"

Result: {"error":"dotcms.api.error.bad_request: File is required"} — HTTP 400

Test 3: Invalid CSV headers (expect 400)

cat > /tmp/bad_headers.csv << 'CSV'
"bad1","bad2","bad3","bad4"
"x","y","z","0"
CSV

curl -s -w "\nHTTP_STATUS: %{http_code}\n" \
  -H "Authorization: Bearer $TOKEN" \
  -F "file=@/tmp/bad_headers.csv" \
  -F "filter=" \
  -F "exportType=merge" \
  "$BASE/api/v1/categories/_import"

Result: {"error":"dotcms.api.error.bad_request: Invalid CSV format: missing required columns (name, key, variable, sort)"} — HTTP 400

Test 4: Empty file (expect 400)

> /tmp/empty.csv

curl -s -w "\nHTTP_STATUS: %{http_code}\n" \
  -H "Authorization: Bearer $TOKEN" \
  -F "file=@/tmp/empty.csv" \
  -F "filter=" \
  -F "exportType=merge" \
  "$BASE/api/v1/categories/_import"

Result: {"error":"dotcms.api.error.bad_request: Invalid CSV format: missing required columns (name, key, variable, sort)"} — HTTP 400

Test 5: Invalid exportType (expect 400)

cat > /tmp/valid.csv << 'CSV'
"name","key","variable","sort"
"X","x","x","0"
CSV

curl -s -w "\nHTTP_STATUS: %{http_code}\n" \
  -H "Authorization: Bearer $TOKEN" \
  -F "file=@/tmp/valid.csv" \
  -F "filter=" \
  -F "exportType=bogus" \
  "$BASE/api/v1/categories/_import"

Result: {"error":"dotcms.api.error.bad_request: Invalid exportType: bogus. Must be 'replace' or 'merge'"} — HTTP 400

Test 6: Leading/trailing spaces in name are trimmed (expect 200, name="SpaceyName")

cat > /tmp/spaces.csv << 'CSV'
"name","key","variable","sort"
"  SpaceyName  ","spaceyname","spaceyname","0"
CSV

curl -s -w "\nHTTP_STATUS: %{http_code}\n" \
  -H "Authorization: Bearer $TOKEN" \
  -F "file=@/tmp/spaces.csv" \
  -F "filter=" \
  -F "exportType=merge" \
  "$BASE/api/v1/categories/_import"

# Verify the name was trimmed:
curl -s -H "Authorization: Bearer $TOKEN" \
  "$BASE/api/v1/categories?filter=spaceyname&per_page=5"

Result: successCount=1, category name stored as "SpaceyName" (no leading/trailing spaces)

Test 7: Replace mode (expect 200)

cat > /tmp/replace.csv << 'CSV'
"name","key","variable","sort"
"ReplacedCat","replacedcat","replacedcat","0"
CSV

curl -s -w "\nHTTP_STATUS: %{http_code}\n" \
  -H "Authorization: Bearer $TOKEN" \
  -F "file=@/tmp/replace.csv" \
  -F "filter=" \
  -F "exportType=replace" \
  "$BASE/api/v1/categories/_import"

Result: {"entity":{"fails":[],"skippedCount":0,"successCount":1},...} — HTTP 200

Test 8: Garbage file content (expect 400)

echo "??random??binary??garbage??content??no??csv??here" > /tmp/garbage.csv

curl -s -w "\nHTTP_STATUS: %{http_code}\n" \
  -H "Authorization: Bearer $TOKEN" \
  -F "file=@/tmp/garbage.csv" \
  -F "filter=" \
  -F "exportType=merge" \
  "$BASE/api/v1/categories/_import"

Result: {"error":"dotcms.api.error.bad_request: Invalid CSV format: missing required columns (name, key, variable, sort)"} — HTTP 400


Test Results Summary

# Scenario Expected Actual
1 Valid CSV merge 200, successCount=2 PASS
2 No file provided 400 PASS
3 Invalid CSV headers 400 PASS
4 Empty file 400 PASS
5 Invalid exportType 400 PASS
6 Spaces trimmed in name 200, trimmed name PASS
7 Replace mode 200, successCount=1 PASS
8 Garbage file 400 PASS

Checklist

  • Tests
  • Translations
  • Security Implications Contemplated (add notes if applicable)

Additional Info

The rollback fix is the most impactful change: WrapInTransactionAdvice.exit() only calls rollbackTransaction() when t != null. Previously, processImport caught all exceptions and returned a Response normally, so the transaction always committed — including partial replace operations (delete committed, import not).

BadRequestException / ForbiddenException / DotRuntimeException all extend WebApplicationException (a RuntimeException), so they propagate through @WrapInTransaction and are handled by the existing JAX-RS exception mappers (400 / 403 / 500).

…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
Copilot AI requested a review from fmontes February 21, 2026 02:16
Copy link
Member

@fmontes fmontes left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot we need to add tests here.

Co-authored-by: fmontes <751424+fmontes@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Feb 21, 2026

@copilot we need to add tests here.

Added CategoryHelperTest in eea95dd3 with 6 unit tests covering addOrUpdateCategory():

  • Merge mode: new categories → success count equals CSV row count
  • Merge mode: existing key found → updates in-place and counts success
  • Merge mode: DotSecurityException on first save → counted as failure, continues processing remaining rows
  • All saves throw → returns 0
  • Replace mode: findByKey never called, count equals row count
  • Empty CSV (headers only) → returns 0, no save calls

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:

  • dotcms-npm.b-cdn.net
    • Triggering command: /home/REDACTED/work/core/core/installs/node/node node /home/REDACTED/work/core/core/installs/node/yarn/dist/bin/yarn.js --frozen-lockfile -Dsurefire.failIfNoSpecifiedTests=false --no-transfer-progress (dns block)
    • Triggering command: /home/REDACTED/work/core/core/installs/node/node node /home/REDACTED/work/core/core/installs/node/yarn/dist/bin/yarn.js --frozen-lockfile (dns block)
  • repo.dotcms.com
    • Triggering command: /usr/lib/jvm/temurin-21-jdk-amd64/bin/java /usr/lib/jvm/temurin-21-jdk-amd64/bin/java -classpath /home/REDACTED/work/core/core/.mvn/wrapper/maven-wrapper.jar -Dmaven.multiModuleProjectDirectory=/home/REDACTED/work/core/core org.apache.maven.wrapper.MavenWrapperMain test -pl :dotcms-core -Dtest=CategoryHelperTest -DfailIfNoTests=false --no-transfer-progress (dns block)
    • Triggering command: /usr/lib/jvm/temurin-21-jdk-amd64/bin/java /usr/lib/jvm/temurin-21-jdk-amd64/bin/java -classpath /home/REDACTED/work/core/core/.mvn/wrapper/maven-wrapper.jar -Dmaven.multiModuleProjectDirectory=/home/REDACTED/work/core/core org.apache.maven.wrapper.MavenWrapperMain test -pl :dotcms-core --am -Dtest=CategoryHelperTest -DfailIfNoTests=false --no-transfer-progress (dns block)
    • Triggering command: /usr/lib/jvm/temurin-21-jdk-amd64/bin/java /usr/lib/jvm/temurin-21-jdk-amd64/bin/java -classpath /home/REDACTED/work/core/core/.mvn/wrapper/maven-wrapper.jar -Dmaven.multiModuleProjectDirectory=/home/REDACTED/work/core/core org.apache.maven.wrapper.MavenWrapperMain test -pl :dotcms-core --am -Dtest=CategoryHelperTest -DfailIfNoTests=false -Dsurefire.failIfNoSpecifiedTests=false --no-transfer-progress (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

Copy link
Contributor

Copilot AI left a 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 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 @WrapInTransaction from private processImport() to public importCategories() 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 returns long (count of successful imports), and the private overload returns boolean (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 /_import endpoint — silent 200 OK, broken transaction, bad validation Feb 21, 2026
…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>
@github-actions github-actions bot added the Area : Backend PR changes Java/Maven backend code label Feb 23, 2026
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>
@fmontes fmontes marked this pull request as ready for review February 24, 2026 17:47
@fmontes fmontes requested a review from hassandotcms February 24, 2026 17:47
fmontes and others added 3 commits February 24, 2026 12:02
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>
@fmontes fmontes dismissed their stale review February 24, 2026 21:13

Is myself

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Area : Backend PR changes Java/Maven backend code

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

Categories import endpoint silently swallows exceptions and returns 200 OK

2 participants