Skip to content

fix(server): client-safe error bodies, accurate status codes and clearer OTel modals - #75

Merged
muzamilkm merged 5 commits into
arbisoft:main-arbisoftfrom
muzamilkm:fix/production-hardening-access-and-dashboards
Sep 30, 2026
Merged

muzamilkm merged 5 commits into
arbisoft:main-arbisoftfrom
muzamilkm:fix/production-hardening-access-and-dashboards

Conversation

@muzamilkm

@muzamilkm muzamilkm commented Sep 30, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Follows up the findings from the production deletion-guardrail run (after #74). API error responses no longer carry the errors package's internals, every error returns a status code that matches what actually went wrong, and the project delete and Claude Code OTel confirmation modals are clearer.

Client-safe error bodies

The run found error bodies that were accurate but not written for users: every message ended in the status code (… (409)), causes exposed the internal wrap chain (for example error executing delete hook for plugin claude_otel), and some bodies were built from err.Error(), which renders the whole chain, including stack frames when ENABLE_STACKTRACE is set.

  • message is the top-level message only, without the (NNN) suffix. The status is already the response's status code. Logs are unchanged and still carry the full chain and codes.
  • causes is no longer filled from the error chain. The Config UI never read it. The field stays on ApiBody because the GitHub multi-token test fills it itself.
  • Connection and scope helpers return a body alongside the error (the 409 with the list of blocking blueprints and projects). The plugin router now rewrites that body's message to the safe message, so the blueprint and project list is kept but err.Error() no longer reaches the client. The helpers themselves are untouched.
  • A 500 always returns the generic an unexpected error occurred. Its message is often a converted SQL, driver or runtime error. Every other status keeps its text.
Case Before After
Delete a scope while a pipeline runs 409 at least one pipeline is running (409) 409 at least one pipeline is running
Delete the final project of an active OTel connection (API) 400 error deleting project (400), with the real reason only in causes 409 revoke the active Claude Code OTel connection before deleting its final project placement
Delete a project with an unfinished pipeline 500 409, same message
Malformed JSON body on a plugin endpoint 500 with the JSON parser's text 400 bad request body format
Missing required fields on GitHub/Jira Test Connection 500 Key: 'JiraConn.Endpoint' Error:Field validation for 'Endpoint' failed on the 'required' tag… 400 Endpoint is required; AuthMethod is required
Converted SQL or driver error 500 with the raw error text 500 an unexpected error occurred

Status classification

  • Untyped validation and request-decoding errors that reach a plugin endpoint are reclassified from 500 to 400, with a readable message built from the failing fields. This is done centrally in the router, so no plugin code changes.
  • Project deletion returns a plugin's veto as the top-level message instead of wrapping it twice, and "There are unfinished pipelines in the current project" is now a 409 instead of a 500.

Test Connection statuses

Test Connection passed the remote service's status straight through, so a rejected Jira token made DevLake itself answer 401, and a wrong Jira endpoint answered 404. WrapTestConnectionErrResp now remaps the status and keeps the message, so users still see why the check failed:

  • Remote 4xx (credentials rejected, endpoint not found) → 400, for example Please check your credential.
  • Remote 5xx, or a host that cannot be resolved or reached → 502, for example Failed to resolve DNS.
  • Network timeout → 504 Failed to connect.

This applies to every plugin whose Test Connection uses that wrapper, including GitHub and Jira.

Claude Code OTel: 409 for state conflicts

These used to return 400 and are now 409, matching DevLake's own 409 for "referenced by blueprints":

  • a connection already exists for this team
  • only revoked connections can be removed
  • the connection is not active
  • the credential verifier is unavailable
  • finalize the current rotation before starting another
  • the final project of an active connection can't be removed, including through project deletion (the veto from fix(otel): remove project placements in the project deletion transaction #74)

Invalid input stays 400: an empty project list, a project that no longer exists, or an invalid team name.

Config UI

  • Project delete modal: the general warning and the OTel warning were laid out side by side. They now stack vertically.
  • OTel confirmation modals: Rotate, Apply, Finalize, Revoke and Remove now show which connection they act on: Team: <name> (<slug>).
  • OTel project picker: when no project is selected, the Generate and Save buttons are disabled with no explanation. Both modals now show Select at least one DevLake project.
  • Failed pipelines: the pipeline page printed the pipeline's stored message inline, which is err.Error(): the full wrap chain in the errors library's debug format (Wraps: (2) …, Error types: (1) *hintdetail.withDetail …), plus stack frames and file paths wherever ENABLE_STACKTRACE is on. The banner now reads Pipeline failed. Hover over a failed task below to see the reason., and the failed-task tooltip shows the task's errorName (the message chain only, for example subtask Collect Pull Requests ended unexpectedly caused by: graphql query got error) instead of its message. The pipeline's own errorName can't be used for the banner because it embeds each failed task's Error() text. Nothing is lost for debugging: every message in the chain is still in the tooltip, and the full message is unchanged in the API, the database and the pipeline logs.

Test plan

  • Unit tests: one table test for the plugin endpoint error contract (status, message without the suffix, no causes, helper body data kept, 500 generic, malformed JSON and validation errors → 400), one for the Test Connection remap, and the fix(otel): remove project placements in the project deletion transaction #74 hook and OTel veto tests updated to the unwrapped veto and 409.
  • Rebuilt devlake and config-ui locally and ran every case through the API on the local stack:
    • GitHub and Jira connection and scope deletes while referenced by a blueprint: 409 with the blueprint and project list, no suffix, no causes.
    • Connection delete with scopes remaining, scope delete during a running pipeline, and project delete with an unfinished pipeline: 409, and nothing was deleted.
    • Test Connection: bad Jira credentials and a bad GitHub token → 400; wrong Jira endpoint → 400 with the /rest/ hint; unresolvable host → 502; unreachable host → 504 after 10 s; missing fields and malformed JSON → 400.
    • OTel: duplicate team, hiding an active connection, rotating twice and rotating a revoked connection → 409; empty and unknown project lists → 400; unknown connection → 404. Deleting the final project of an active connection → 409 with the reason as the message, and the project was left intact.
    • Teardown: deleting a project whose OTel connection is revoked succeeds and removes the placement; deleting an already-deleted connection returns 404 with no causes.
  • Opened a failed local pipeline (stack traces enabled): the banner is the one-line summary, and the Stage 2 task tooltip shows the message chain with no stack frames or Wraps:/Error types: lines.
  • Checked in the Config UI: the scope-delete error modal shows the message without the suffix, the project delete warnings stack with Confirm disabled for the OTel case, all five OTel modals name the row's own team, and clearing the project picker shows the hint and disables Save and Generate.

Notes for reviewers

  • On the backend, only files the fork already modifies are touched. The upstream connection and scope helpers, srvhelper and the GitHub/Jira plugins are unchanged. The failed-pipeline fix does touch three upstream Config UI files (pipeline/components/info.tsx, task.tsx and types/task.ts), a few lines each, instead of changing how the upstream runner stores message.
  • A 500 no longer shows its message to clients. The full error is still in the DevLake logs.

@muzamilkm
muzamilkm merged commit 6e81e61 into arbisoft:main-arbisoft Sep 30, 2026
5 checks passed
@muzamilkm
muzamilkm deleted the fix/production-hardening-access-and-dashboards branch October 1, 2026 18:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants