Repository navigation
Display backend error message on enrollment failure - #3918
Conversation
OpenAPI ChangesNo changes detected Unexpected changes? Ensure your branch is up-to-date with |
d5fdd27 to
83be81e
Compare
There was a problem hiding this comment.
🟢 Approval recommended
The scoped error handling preserves existing fallbacks and is supported by focused unit and integration coverage.
Pull request overview
Surfaces learner-facing MITx Online 400-level enrollment errors while preserving existing fallback messaging for other failures.
Changes:
- Adds reusable extraction of valid 400
detailmessages. - Propagates mutation errors through enrollment hooks and UI surfaces.
- Adds coverage for backend details, fallbacks, and basket error precedence.
File summaries
| File | Description |
|---|---|
frontends/api/package.json |
Exports the mutation-error utility. |
frontends/api/src/mutations/errorMessages.ts |
Extracts safe 400 response details. |
frontends/api/src/mutations/errorMessages.test.ts |
Tests detail extraction and fallbacks. |
frontends/main/src/common/mitxonline/useReplaceBasketItem.ts |
Exposes the relevant basket error. |
frontends/main/src/common/mitxonline/useReplaceBasketItem.test.tsx |
Tests basket error precedence. |
frontends/main/src/page-components/EnrollmentDialogs/CourseEnrollmentDialog.tsx |
Displays backend enrollment details. |
frontends/main/src/page-components/EnrollmentDialogs/CourseEnrollmentDialog.test.tsx |
Tests dialog error messaging. |
frontends/main/src/app-pages/ProductPages/useCourseEnrollment.ts |
Returns course enrollment errors. |
frontends/main/src/app-pages/ProductPages/useProgramEnrollment.ts |
Returns program enrollment errors. |
frontends/main/src/app-pages/ProductPages/HeaderEnrollButton.tsx |
Renders detailed header errors. |
frontends/main/src/app-pages/ProductPages/EnrollOfferingBoxes.tsx |
Renders detailed offering errors. |
frontends/main/src/app-pages/ProductPages/enrollTypes.ts |
Centralizes fallback copy. |
frontends/main/src/app-pages/ProductPages/CoursePage.tsx |
Passes course errors to the header. |
frontends/main/src/app-pages/ProductPages/CoursePage.test.tsx |
Tests course-header error details. |
frontends/main/src/app-pages/ProductPages/CourseEnrollArea.tsx |
Passes course errors to offering boxes. |
frontends/main/src/app-pages/ProductPages/CourseEnrollArea.test.tsx |
Tests paid-enrollment details. |
frontends/main/src/app-pages/ProductPages/ProgramHeaderEnrollButton.tsx |
Passes program errors to the header. |
frontends/main/src/app-pages/ProductPages/ProgramHeaderEnrollButton.test.tsx |
Tests program-header messages. |
frontends/main/src/app-pages/ProductPages/ProgramEnrollArea.tsx |
Passes program errors to offering boxes. |
frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/hooks/useEnrollmentHandler.ts |
Configures dashboard toast messages. |
frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/EnrolledCourseCard.tsx |
Displays detailed upgrade failures. |
frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/UnenrolledCourseCard.test.tsx |
Tests detailed dashboard toasts. |
Review details
- Files reviewed: 22/22 changed files
- Comments generated: 0
- Review effort level: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
83be81e to
4758b9b
Compare
MITx Online answers a rejected enrollment with a 400 whose detail is written for the learner, but Learn discarded the body and rendered the same generic "please try again" for every failure — including ones that cannot succeed on retry. Adds badRequestDetail/badRequestDetailOr, which read a detail only from a 400 and return undefined otherwise, so a 500 or a detail-less 400 keeps today's copy. The enroll hooks now expose the error object rather than an isError boolean, and the enroll areas, header CTA, enrollment dialog, and dashboard toasts prefer the server's string over their own. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
An enrollment 400 detail is often only actionable by a human ("Error code:
CS_700"), but no enrollment error surface offered a way to reach support.
Add ErrorMessageWithSupport, which appends a "Contact Support" link to a
message, and use it on every enrollment failure surface: the product page
InfoBox and header alerts, both enrollment dialog alerts, and - via a new
opt-in `meta.contactSupport` read by the global mutation-error handler - the
dashboard enrollment toast.
The link points at the support site's request form (new urls.SUPPORT_REQUEST)
rather than the mailto: used by the dashboard's upgrade banners, which are
left as they are.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
4758b9b to
a877204
Compare
gumaerc
left a comment
There was a problem hiding this comment.
I tested this fairly exhaustively. I set my local mitxonline to the branch from mitodl/mitxonline#3938, manually set my user to have a denied compliance check and exercised the errors in the frontend everywhere I thought they could appear. Everything on the product pages seems to be working. There were a couple edge cases I discovered, but I think the fault there lies in mitxonline and not Learn. I will leave a comment on the parent issue about that, but I think this is good to merge. I left one nitpick about some punctuation below:
What are the relevant tickets?
https://github.com/mitodl/hq/issues/13250
Description (What does it do?)
MITx Online answers a rejected enrollment with a 400 whose
detailis written for the learner, but Learn discarded the body and rendered the same generic "please try again" for every failure — including ones that cannot succeed on retry.detail.detail— today's copy is unchanged.Pairs with mitodl/mitxonline#3938, which gives compliance rejections a
detailworth showing. Neither depends on the other landing first.Implementation details
New
frontends/api/src/mutations/errorMessages.ts, exported asapi/mutation-errors:badRequestDetail(error)— thedetailstring from a 400 body, elseundefined. Returnsundefined(not a fallback) so each call site keeps its own copy via?? FALLBACK. Guards a non-axios error, a missing response, any status but 400, a non-object body, and adetailthat is missing/blank/non-string; joins a DRFErrorDetaillist. Never throws — it runs inside the global mutation-error handler, where an exception would swallow the failure entirely.badRequestDetailOr(fallback)— builds aMutationErrorMeta["getErrorMessage"]. This is the first production use of that hook;mutationMeta.tsdocuments it for exactly this case. The no-arg form returns"", which fails the handler'sisMessagecheck and falls through to the existing generic copy rather than restating it.useCourseEnrollment,useProgramEnrollment, anduseReplaceBasketItemnow exposeerror: unknownin place ofisError: boolean.error != nullisisError, so keeping both would be two derived fields that have to agree. InuseReplaceBasketItemthe add-to-basket error wins over clear-basket: it is the later step,reset()means the two are never errored from one attempt, and its 400 carries the product-specific explanation.The dialog keeps two distinct fallbacks (basket vs. enroll), which is why the hooks return the error object rather than a pre-resolved string.
Pre-existing and untouched, but more visible now that the copy is specific: a failed paid attempt keeps its alert up while the user clicks the free CTA. Worth a follow-up.
Screenshots (if appropriate):
How can this be tested?
End-to-end against a local MITx Online: enable the
EXPORT_COMPLIANCE_CHECK_ENABLEDflag and makeverify_user_with_exportsreturn a non-accepted result, then click "Start Learning" on a course product page. I have not run this — the evidence above is unit-level only.Additional Context
On the dashboard upgrade path,
HomeEnrollmentsDisplayandProgramEnrollmentDisplayappend" Contact Support for assistance."to whatever message arrives, so a server detail now renders as"<detail> Contact Support for assistance.". Reads fine for MITx Online's details (they end in punctuation), but it is server-controlled text feeding client-side copy — flagging rather than plumbing a flag through.