-
Bug
-
Resolution: Unresolved
-
Major
-
None
-
quay-v3.17.0
Problem Statement
When label operations (create/delete) fail in the new React UI, users see a generic error message:
Error displayed: "Unable to complete request - Undefined"
Expected: Specific server error message explaining why the operation failed (e.g., "Cannot add label: tag is immutable", "Invalid label format", "Permission denied", etc.)
This makes debugging and troubleshooting {}impossible{} for users.

Visual Evidence
See attached screenshot: Error3.jpg
- Repository: quayqe/demo
- Action: Attempting to add label to tag
- Error shown: "Unable to complete request" + "Undefined"
Root Cause Analysis
Frontend Error Handling Bug
File: web/src/components/labels/LabelsEditable.tsx:56-95
The label error display code directly accesses error.error.message, which retrieves the generic Axios error message instead of the server's specific error message.
Unable to find source-code formatter for language: typescript. Available languages are: actionscript, ada, applescript, bash, c, c#, c++, cpp, css, erlang, go, groovy, haskell, html, java, javascript, js, json, lua, none, nyan, objc, perl, php, python, r, rainbow, ruby, scala, sh, sql, swift, visualbasic, xml, yaml// Line 56-72: Create label error display if (errorCreatingLabels) { const errorCreatingLabelsMessage = ( <> {Array.from(errorCreatingLabelsDetails.getErrors()).map( ([label, error]) => ( <p key={label}> Could not create label {label}: {error.error.message} // ❌ WRONG </p> ), )} </> ); } // Line 79-95: Delete label error display (same bug) if (errorDeletingLabels) { const errorDeletingLabelsMessage = ( <> {Array.from(errorDeletingLabelsDetails.getErrors()).map( ([label, error]) => ( <p key={label}> Could not delete label {label}: {error.error.message} // ❌ WRONG </p> ), )} </> ); }
Why This is Wrong
ResourceError Structure:
Unable to find source-code formatter for language: typescript. Available languages are: actionscript, ada, applescript, bash, c, c#, c++, cpp, css, erlang, go, groovy, haskell, html, java, javascript, js, json, lua, none, nyan, objc, perl, php, python, r, rainbow, ruby, scala, sh, sql, swift, visualbasic, xml, yamlclass ResourceError extends Error { error: AxiosError; // Contains server response resource: string; // e.g., "test=value" message: string; // e.g., "Unable to create label" }
AxiosError Structure:
Unable to find source-code formatter for language: typescript. Available languages are: actionscript, ada, applescript, bash, c, c#, c++, cpp, css, erlang, go, groovy, haskell, html, java, javascript, js, json, lua, none, nyan, objc, perl, php, python, r, rainbow, ruby, scala, sh, sql, swift, visualbasic, xml, yamlinterface AxiosError { message: string; // Generic: "Request failed with status code 400" code: string; // e.g., "ERR_BAD_REQUEST" response: { // ← SERVER ERROR DETAILS ARE HERE! status: number; data: { error_message?: string; // ← REAL ERROR MESSAGE message?: string; detail?: string; } } }
What Happens:
- error.error.message → Request failed with status code 400 (useless)
- error.error.response.data.error_message → Cannot add label: tag is immutable (useful!)
The code accesses the wrong field, so users see the generic Axios error instead of the server's explanation.
Correct Pattern Exists in Codebase
Quay has a dedicated helper function for extracting server error messages:
File: web/src/resources/ErrorHandling.ts:80-118
Unable to find source-code formatter for language: typescript. Available languages are: actionscript, ada, applescript, bash, c, c#, c++, cpp, css, erlang, go, groovy, haskell, html, java, javascript, js, json, lua, none, nyan, objc, perl, php, python, r, rainbow, ruby, scala, sh, sql, swift, visualbasic, xml, yamlexport function getErrorMessage(error: AxiosError<ErrorResponse>) { // Check if server sent a response if (error.response) { // Extract server message from multiple possible fields const serverMessage = error.response.data?.detail || error.response.data?.error_message || error.response.data?.message || error.response.data?.error_description || (typeof error.response.data === 'string' ? error.response.data : null); // For 5xx errors, return generic message (security) if (error.response.status >= 500) { return 'an unexpected issue occurred...'; } // For 4xx, return specific message if (serverMessage) { return serverMessage; // ← THIS IS WHAT WE NEED! } } // Network errors if (error.code && ...) { return getNetworkError(error.code); } // Fallback return 'unable to make request'; }
This helper is used correctly in other parts of the UI (e.g., account creation errors - PROJQUAY-10418), but the Labels component doesn't use it.
Impact Assessment
Severity: MAJOR
User Experience Impact
- Users cannot diagnose why label operations fail
- Generic "Undefined" error provides no actionable information
- Increases support tickets and user frustration
Affected Operations
- Creating labels on manifests
- Deleting labels from manifests
- All label error scenarios show the same useless error
Common Error Scenarios Hidden
- Permission denied (user lacks write access)
- Invalid label format (key/value validation failures)
- Reserved label prefix (quay.* labels restricted)
- Tag immutability conflicts
- Database constraints violations
- Network/server errors
Debugging Difficulty
- Support teams cannot help users troubleshoot
- Developers cannot reproduce issues from user reports
- No visibility into actual error cause
Steps to Reproduce
Scenario 1: Add Label to Immutable Tag
- Create repository quayqe/demo
- Push image and make tag stable immutable
- Navigate to Tags tab
- Click tag actions → "Edit labels"
- Try to add label test=value
- Observe: Error shows "Unable to complete request - Undefined"
- Expected: Specific error explaining immutability restriction
Scenario 2: Invalid Label Format
- Navigate to any repository tag
- Click "Edit labels"
- Try to add label with invalid format: invalid label format
- Observe: Error shows "Unable to complete request - Undefined"
- Expected: Error shows "Invalid label format, must be key=value"
Scenario 3: Permission Denied
- Navigate to repository where user has read-only access
- Try to add label to tag
- Observe: Error shows "Unable to complete request - Undefined"
- Expected: Error shows "Permission denied" or "Requires write access"
Expected Behavior
- Error message should display server's specific error message
- Users should understand why the operation failed
- Error should provide actionable guidance
Actual Behavior
- All errors show generic "Unable to complete request - Undefined"
- No information about root cause
- Users cannot troubleshoot
Affected Code Files
Frontend:
- web/src/components/labels/LabelsEditable.tsx - Lines 56-95 (error display)
- web/src/hooks/UseTagLabels.ts - Lines 45-67 (mutation hooks)
- web/src/resources/TagResource.ts - Lines 248-270 (createLabel/deleteLabel)
Error Handling (Existing Helper):
- web/src/resources/ErrorHandling.ts - Lines 80-118 (getErrorMessage helper)
Recommended Fix
Fix: Use Existing Error Helper Function
File: web/src/components/labels/LabelsEditable.tsx
Unable to find source-code formatter for language: typescript. Available languages are: actionscript, ada, applescript, bash, c, c#, c++, cpp, css, erlang, go, groovy, haskell, html, java, javascript, js, json, lua, none, nyan, objc, perl, php, python, r, rainbow, ruby, scala, sh, sql, swift, visualbasic, xml, yamlimport { getErrorMessage } from 'src/resources/ErrorHandling'; // ✅ ADD THIS // Line 56-72: Fix create error display if (errorCreatingLabels) { const errorCreatingLabelsMessage = ( <> {Array.from(errorCreatingLabelsDetails.getErrors()).map( ([label, error]) => ( <p key={label}> Could not create label {label}: {getErrorMessage(error.error)} {/* ✅ CHANGED */} </p> ), )} </> ); addAlert({ variant: AlertVariant.Failure, title: `Could not create labels`, message: errorCreatingLabelsMessage, }); } // Line 79-95: Fix delete error display if (errorDeletingLabels) { const errorDeletingLabelsMessage = ( <> {Array.from(errorDeletingLabelsDetails.getErrors()).map( ([label, error]) => ( <p key={label}> Could not delete label {label}: {getErrorMessage(error.error)} {/* ✅ CHANGED */} </p> ), )} </> ); addAlert({ variant: AlertVariant.Failure, title: `Could not delete labels`, message: errorDeletingLabelsMessage, }); }
Changes Required
- Import getErrorMessage helper from ErrorHandling.ts
- Replace error.error.message with getErrorMessage(error.error) (2 places)
- Add unit tests for error display
Testing Requirements
Manual Testing
Test Case 1: Invalid Label Format
- Add label with invalid key: invalid label!@#
- Verify error shows: "Invalid label format, key must match ^[0-9A-Za-z/\\-_.]=.$"
Test Case 2: Reserved Prefix
- Add label: quay.reserved=test
- Verify error shows: "Label has a reserved prefix"
Test Case 3: Permission Denied
- Try to add label to repo with read-only access
- Verify error shows: "You do not have permission" or similar
Test Case 4: Network Error
- Disconnect network
- Try to add label
- Verify error shows network-related message (not "Undefined")
Test Case 5: Server Error (5xx)
- Simulate server error
- Verify error shows: "an unexpected issue occurred. Please try again or contact support"
Automated Testing
Unit Test: web/src/components/labels/LabelsEditable.test.tsx
Unable to find source-code formatter for language: typescript. Available languages are: actionscript, ada, applescript, bash, c, c#, c++, cpp, css, erlang, go, groovy, haskell, html, java, javascript, js, json, lua, none, nyan, objc, perl, php, python, r, rainbow, ruby, scala, sh, sql, swift, visualbasic, xml, yamldescribe('LabelsEditable error handling', () => { it('displays server error message on create failure', async () => { // Mock API error with specific message mockAxios.post.mockRejectedValue({ response: { status: 400, data: { error_message: 'Invalid label format' } } }); // Try to create label render(<EditableLabels {...props} />); // ... user interaction ... // Verify specific error message is displayed expect(screen.getByText(/Invalid label format/)).toBeInTheDocument(); expect(screen.queryByText(/Undefined/)).not.toBeInTheDocument(); }); it('displays server error message on delete failure', async () => { // Similar test for delete errors }); it('handles network errors gracefully', async () => { // Test network error scenarios }); });
❌ Broken Error Handling (This Issue)
File: web/src/components/labels/LabelsEditable.tsx
Labels component still uses the old broken pattern of accessing error.error.message directly.
Regression risk: Other components may have the same bug and need auditing.
Related Code Pattern Issues
Other Components to Audit
Search codebase for similar pattern:
grep -r "error.error.message" web/src/
Potentially affected:
- Tag operations error handling
- Repository settings error handling
- Team management error handling
- Any component using BulkOperationError<ResourceError>
Preventive Measures
Recommendation: Create ESLint rule to prevent this pattern:
// .eslintrc.js rules: { 'no-restricted-syntax': [ 'error', { selector: 'MemberExpression[object.property.name="error"][property.name="message"]', message: 'Use getErrorMessage() helper instead of error.error.message' } ] }
Workaround (Until Fixed)
For users encountering errors:
- Open browser DevTools (F12)
- Go to Network tab
- Retry the failed operation
- Find the failed API request (POST .../labels)
- Click on it → Response tab
- Read the error_message field for actual error
For support teams:
Ask users to provide:
- Browser console errors
- Network tab screenshots
- Detailed steps to reproduce
Acceptance Criteria
Success metrics:
- [ ] Error messages display server's specific error text
- [ ] No "Undefined" errors shown to users
- [ ] All error scenarios show actionable messages
- [ ] Unit tests verify correct error extraction
- [ ] Manual testing confirms improved UX
- [ ] ESLint rule prevents future regressions (optional)
Priority Justification
Why MAJOR:
- User Impact: Affects all label operations across all repositories
- Debugging Difficulty: Users cannot troubleshoot without seeing real errors
- Support Burden: Increases support tickets and response time
- Regression Risk: Similar pattern may exist in other components
- Simple Fix: 2-line change with existing helper function
Not CRITICAL because:
- Workaround exists (browser DevTools)
- Does not cause data loss
- Does not block core functionality
Reported by: Luffy Zhang (lzha1981)
Generated by: Claude Code Automated Bug Analysis