Skip to content

fix(auth): send JSON object body to /api/v1/changePassword#35440

Merged
oidacra merged 3 commits intomainfrom
35269-fix-change-password-button
Apr 24, 2026
Merged

fix(auth): send JSON object body to /api/v1/changePassword#35440
oidacra merged 3 commits intomainfrom
35269-fix-change-password-button

Conversation

@oidacra
Copy link
Copy Markdown
Member

@oidacra oidacra commented Apr 23, 2026

Summary

Fixes the "Change Password" button on the Password Recovery page, which silently failed with HTTP 415 Unsupported Media Type and a console TypeError: Cannot read properties of undefined (reading '0').

Root cause: LoginService.changePassword was calling HttpClient.post(url, JSON.stringify({...})). When given a string body, Angular's HttpClient auto-sets Content-Type: text/plain, but the backend ResetPasswordResource declares @Consumes(MediaType.APPLICATION_JSON) — so every request was rejected with 415. The error handler at reset-password.component.ts:125 then crashed reading errors[0] on the non-JSON 415 body.

Fix: Pass the plain object so Angular serializes it and sets Content-Type: application/json automatically. Matches the pattern of every other auth call in LoginService (recoverPassword, loginUser, loginAs, etc.).

Regression origin: PR #34165 (df97483c4e) — the CoreWebServiceHttpClient refactor. CoreWebService.request() handled JSON headers internally; the migrated code kept the JSON.stringify but lost the headers.

Verification

Reproduced locally against http://localhost:8080:

Request body Status Response
JSON.stringify({password, token}) (before) 415 {"message":"HTTP 415 Unsupported Media Type"}
{password, token} (after) 400 {"errors":[{"errorCode":"reset-password-token-invalid","message":"Invalid token..."}]}

The 400 response shape matches what reset-password.component.ts expects — no more TypeError.

Closes #35269

Acceptance Criteria

  • POST /api/v1/changePassword is sent with Content-Type: application/json
  • "Change Password" button triggers the request successfully (no 415)
  • Error responses are parsed without a TypeError in the console
  • Happy path updates the password and allows login with the new one

Test plan

  • yarn nx test dotcms-ui — 2109 passing
  • Manual: trigger password recovery → open email link → submit new password → verify successful change and subsequent login

Changed files

  • core-web/libs/dotcms-js/src/lib/core/login.service.ts — remove JSON.stringify on the changePassword body

This PR fixes: #35269

The change-password HTTP call was stringifying the payload before passing
it to HttpClient.post. HttpClient auto-sets Content-Type: text/plain for
string bodies, so the backend (which declares @consumes APPLICATION_JSON)
rejected the request with 415 Unsupported Media Type — silently breaking
the "Change Password" button on the password recovery page. The error
handler then threw "Cannot read properties of undefined (reading '0')"
when reading errors[0] on the non-JSON 415 body.

Pass the plain object so Angular serializes it and sets the correct
Content-Type, matching sibling auth calls (recoverPassword, loginUser).

Regression from PR #34165 (CoreWebService → HttpClient refactor).

Closes #35269
@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Apr 23, 2026

Claude finished @oidacra's task in 1m 19s —— View job


PR Review

  • Read changed files and diff
  • Analyze correctness, edge cases, and test coverage

The fix is correct and the approach is sound. A few things worth a second look:

login.service.ts

No issues. Dropping JSON.stringify and passing the plain object is the right call — matches recoverPassword, loginUser, loginAs, and getLoginFormInfo in the same file. The PR description correctly traces the regression to PR #34165.

reset-password.component.ts — error fallback

// reset-password.component.ts:125-126
this.message =
    response.error?.errors?.[0]?.message ?? response.error?.message ?? '';

The ?? '' final fallback means if the backend returns an error body with neither shape, the user sees a blank error message. Silent failure is marginally better than a TypeError, but it hides real errors (e.g. a network failure where response.error is null entirely). A generic fallback like 'An error occurred' or a i18n key would be more defensive. Minor — not a blocker.

Also: the deprecated two-argument subscribe(successFn, errorFn) at line 116 is the natural place to fix this fallback properly using catchError, but the PR author correctly flags this as pre-existing and out of scope.

reset-password.component.spec.ts — new HTTP contract tests

The approach of using HttpTestingController with the real LoginService is exactly right for catching this class of regression. typeof req.request.body === 'object' is a solid guard — a future JSON.stringify reintroduction would break it.

One gap: the success path test (req.flush({ entity: 'ok' })) doesn't assert that dotRouterService.goToLogin was called with { queryParams: { changedPassword: true } }. This is a pre-existing gap, not introduced here, but worth noting since the contract test is otherwise thorough.

The afterEach(() => httpMock.verify()) is correct and will catch any unflushed requests.

Structural note (acknowledged in PR)

The PR author explains why the test lives in the component spec rather than login.service.spec.ts — that file has skip:test and skip:lint in project.json, so its Karma tests don't run in CI. That's the underlying tech debt that allowed this regression to slip. The component-level Jest test is a reasonable workaround, but the skip flags in dotcms-js/project.json should be tracked as a follow-up. Fix this →


Overall: clean fix, good regression test. The only actionable item is the ?? '' empty-string fallback — consider a non-empty fallback message for better UX.

oidacra added 2 commits April 23, 2026 16:48
… array

The error handler read response.error?.errors[0]?.message — the optional
chain covered .error but not .errors, so if the backend returns an error
body without an errors array (e.g. the 415 body {message: "..."}), the
handler crashes with "Cannot read properties of undefined (reading '0')".

Guard the array access too.
- Add regression tests that mount the real LoginService with
  HttpTestingController and verify POST /api/v1/changePassword is sent
  with a plain-object body (string bodies cause Content-Type: text/plain
  and trigger the 415 this PR fixes) and that a 415-shaped error body
  without an `errors` array is handled without crashing.

- Fall back to response.error.message when the errors array is absent,
  so the user sees the backend's message instead of a blank error state.
@oidacra
Copy link
Copy Markdown
Member Author

oidacra commented Apr 23, 2026

Addressed in 59481e6:

1. Silent blank message on non-standard errors — Done. Added ?? response.error?.message ?? '' fallback so a 415-style body surfaces the backend message instead of a blank state.

2. No test for absent-errors case — Done. New test should not crash when the error body has no "errors" array in reset-password.component.spec.ts flushes a 415 with { message: "..." } and asserts the component survives and shows the fallback message.

3. No unit test for changePassword in the service — Done (in the component spec, not the service spec). New describe('ResetPasswordComponent — HTTP contract') mounts the real LoginService with HttpTestingController and asserts:

  • POST /api/v1/changePassword
  • typeof req.request.body === 'object' (catches any future JSON.stringify regression — a string body triggers text/plain and the 415)
  • Body shape { password, token }

The natural home for this test would be libs/dotcms-js/login.service.spec.ts, but that project has skip:test and skip:lint in project.json so karma specs there don't run in CI — which is the structural reason this regression slipped. The component-level HTTP test runs in the existing Jest/CI pipeline and catches the same class of bug.

4. Deprecated subscribe(successFn, errorFn) — Skipping. Pre-existing, not introduced by this PR; out of scope.

@oidacra oidacra marked this pull request as ready for review April 23, 2026 21:06
@oidacra oidacra enabled auto-merge April 23, 2026 21:32
@oidacra oidacra added this pull request to the merge queue Apr 24, 2026
Merged via the queue into main with commit af5330c Apr 24, 2026
29 checks passed
@oidacra oidacra deleted the 35269-fix-change-password-button branch April 24, 2026 19:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

AI: Safe To Rollback Area : Frontend PR changes Angular/TypeScript frontend code

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

"Change Password" button on Password Recovery page no longer works.

2 participants