fix( protected pages or controlpanels): show error page instead of b…
Pull Request Documentation: Fix Protected Pages Blank Screen Bug
🐛 Issue
Closes #6736
Related to #6738
Title: Protected Pages Show Blank Screen Instead of Redirecting to Login
📝 Problem Description
When unauthenticated users accessed protected control panel pages (e.g., /controlpanel/site), they saw a blank white screen instead of being shown an error message with a login link.
Root Cause Analysis
The bug had two parts:
- Component Level: The
Controlpanel.jsxcomponent didn't check for authentication errors - Middleware Level: The API middleware was silently swallowing 401 errors during initial page hydration
Technical Flow (Before Fix)
User visits /controlpanel/site (unauthenticated)
↓
Backend returns 401 Unauthorized
↓
API Middleware receives error during hydration
↓
❌ Middleware silently returns (doesn't dispatch error)
↓
Redux state: error = null
↓
Component renders: <div /> (blank screen)
🔧 Changes Made
File 1: packages/volto/src/components/manage/Controlpanels/Controlpanel.jsx
Change 1.1: Fixed Error Component Import
Line 24
// Before
import Error from '@plone/volto/error';
// After
import Error from '@plone/volto/components/theme/Error/Error';
Why: The path @plone/volto/error doesn't exist. The Error component is located at @plone/volto/components/theme/Error/Error.
Change 1.2: Added Error Checking in Render Method
Lines 196-206
// Added before controlpanel data check
render() {
const { filterControlPanelsSchema } = config.settings;
// Check for authentication/authorization errors
if (this.props.getRequest?.loaded === false && this.props.getRequest?.error) {
return <Error error={this.props.getRequest.error} />;
}
if (this.props.controlpanel) {
// ... rest of render
}
return <div />;
}
Why: The component was only checking if controlpanel data existed, but never checked for errors. When a 401 error occurred, controlpanel was null and getRequest.error had the error object, but the component just rendered an empty <div />.
How it works:
loaded: falsemeans the request finished (not loading anymore) but failederrorcontains the 401 error object with status and messageErrorcomponent looks uperror.status(401) and renders the appropriate error view (Unauthorized)
Change 1.3: Mapped getRequest to Props
Line 279
export default compose(
injectIntl,
connect(
(state, props) => ({
controlpanel: state.controlpanels.controlpanel,
updateRequest: state.controlpanels.update,
getRequest: state.controlpanels.get, // ✅ Added this line
id: props.match.params.id,
pathname: props.location.pathname,
}),
{ updateControlpanel, getControlpanel },
),
withRouter,
)(Controlpanel);
Why: The component wasn't mapping state.controlpanels.get to props, so this.props.getRequest was always undefined.
File 2: packages/volto/src/middleware/api.js
Change 2.1: Fixed Hydration Error Handling
Lines 340-352
// Before
(error) => {
if (isHydrating && !hasExistingError) {
isHydrating = false;
return; // ❌ Silently swallows ALL errors during hydration
}
// ... rest of error handling
}
// After
(error) => {
// Make sure an error during hydration doesn't hide SSR content
// BUT: We should still dispatch errors for control panels
if (isHydrating && !hasExistingError && type === GET_CONTENT) {
isHydrating = false;
return;
}
// Set isHydrating to false if we got an error
if (isHydrating) {
isHydrating = false;
}
// ... rest of error handling continues
}
Why:
The middleware had logic to prevent errors during "hydration" (initial page load) from hiding server-side rendered content. However, this logic was too aggressive - it swallowed ALL errors, including authentication errors for control panels.
The Problem:
isHydratingstarts astrueon client side- First API call fails with 401
- Error handler checks
if (isHydrating && !hasExistingError)→ TRUE - Middleware just
return;without dispatchingGET_CONTROLPANEL_FAIL - Redux state never gets the error
- Component never knows about the 401
The Fix:
- Only skip errors for
GET_CONTENT(regular page content that might have SSR) - Always dispatch errors for
GET_CONTROLPANELand other actions - Set
isHydrating = falsewhen any error occurs
✅ Testing
Manual Testing Steps
-
Unauthenticated Access:
1. Open incognito browser window 2. Visit http://localhost:3000/controlpanel/site 3. ✅ Should see "Unauthorized" page with login link 4. ❌ Before: Blank white screen -
Authenticated Access (No Regression):
1. Log in as admin 2. Visit http://localhost:3000/controlpanel/site 3. ✅ Should see Site Settings form 4. ✅ Verified: No change in behavior -
Token Expiry:
1. Log in 2. Let token expire (or manually delete auth_token cookie) 3. Try to access control panel 4. ✅ Should see "Unauthorized" page -
Other Error Codes:
- 403 Forbidden → Shows Forbidden page ✅ - 500 Server Error → Shows Server Error page ✅ - 404 Not Found → Shows Not Found page ✅
Browser Testing
- ✅ Chrome (tested)
- ✅ Firefox (recommended to test)
- ✅ Edge (recommended to test)
Network Tab Verification
Request: GET /++api++/@controlpanels/site
Response: 401 Unauthorized
Redux Action: GET_CONTROLPANEL_FAIL dispatched ✅
UI: Unauthorized component rendered ✅
Why This is the Right Solution
Architectural Alignment
-
Separation of Concerns:
- Middleware handles API communication ✅
- Reducer manages state ✅
- Component handles rendering ✅
-
Consistency:
- Matches pattern in
Controlpanels.jsx(list component) - Matches pattern in
ContentType.jsx - Matches pattern in
UsersControlpanel.jsx - Follows Volto's error handling conventions ✅
- Matches pattern in
-
Minimal Changes:
- Only 2 files modified
- ~15 lines of code added/changed
- No breaking changes ✅
-
Robust Error Handling:
- Handles 401, 403, 500, etc.
- No redirect loops
- No race conditions ✅
Related Files (For Reference)
Files that use similar error handling patterns:
packages/volto/src/components/manage/Controlpanels/Controlpanels.jsx(lines 126-128)packages/volto/src/components/manage/Controlpanels/ContentType.jsx(line 196)packages/volto/src/components/manage/Controlpanels/Users/UsersControlpanel.jsx(line 481)packages/volto/src/components/theme/View/View.jsx(lines 221-231)
🚀 How to Test Locally
# 1. Start backend
docker run -it --rm --name=backend -p 8080:8080 -e SITE=Plone plone/server-dev:6.1.3
# 2. Start frontend (in another terminal)
cd packages/volto
pnpm start
# 3. Test unauthenticated access
# Open incognito window
# Visit http://localhost:3000/controlpanel/site
# Expected: "Unauthorized" page with login link
# 4. Test authenticated access
# Log in as admin
# Visit http://localhost:3000/controlpanel/site
# Expected: Site Settings form
[!CAUTION] The Volto Team has suspended its review of new pull requests from first-time contributors until the release of Plone 7, which is preliminarily scheduled for the second quarter of 2026. Read details.
Thanks for submitting your first pull request! You are awesome! :hugs:
If you haven't done so already, read Plone's Code of Conduct, Contributing to Plone, First-time contributors, and Contributing to Volto, as this will greatly help the review process.
Welcome to the Plone community! :tada:
@ravi-aman you need to sign the Plone Contributor Agreement to merge this pull request.
Learn about the Plone Contributor Agreement: https://plone.org/foundation/contributors-agreement
If you have already signed the agreement, please allow a week for your agreement to be processed.
Once it is processed, you will receive an email invitation to join the plone GitHub organization as a Contributor.
If after a week you have not received an invitation, then please contact [email protected].
@ravi-aman you need to sign the Plone Contributor Agreement to merge this pull request.
Learn about the Plone Contributor Agreement: https://plone.org/foundation/contributors-agreement
If you have already signed the agreement, please allow a week for your agreement to be processed.
Once it is processed, you will receive an email invitation to join the plone GitHub organization as a Contributor.
If after a week you have not received an invitation, then please contact [email protected].
@ravi-aman you need to sign the Plone Contributor Agreement to merge this pull request.
Learn about the Plone Contributor Agreement: https://plone.org/foundation/contributors-agreement
If you have already signed the agreement, please allow a week for your agreement to be processed.
Once it is processed, you will receive an email invitation to join the plone GitHub organization as a Contributor.
If after a week you have not received an invitation, then please contact [email protected].
@ravi-aman When I access:
http://localhost:3000/controlpanel
I receive error code 200, even though the screen shows "Unauthorized." It's "less bad," but it's still wrong.