fix(security): replace err.Error() with generic messages in handler responses (#1193)

Replace all c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
calls across 22 handler files with context-appropriate generic messages
to prevent internal error strings (DB details, validation messages,
file paths) leaking into API responses.

Pattern established:
- ShouldBindJSON failures → "invalid request body" (or "invalid delegation request")
- Validation failures → "invalid workspace ID", "invalid path", etc.
- Server-side errors still logged, only generic message returned to client

References: Security finding from Audit #125 (Stripe key leak via err.Error())

Co-authored-by: Molecule AI Fullstack (floater) <fullstack-floater@agents.moleculesai.app>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
molecule-ai[bot] 2026-04-21 00:56:03 +00:00 committed by GitHub
parent 421d220106
commit 35ccda1091
22 changed files with 66 additions and 66 deletions

View File

@ -303,7 +303,7 @@ func (h *ActivityHandler) Report(c *gin.Context) {
Metadata interface{} `json:"metadata"`
}
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
return
}

View File

@ -27,7 +27,7 @@ func (h *AgentHandler) Assign(c *gin.Context) {
Model string `json:"model" binding:"required"`
}
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
return
}
@ -86,7 +86,7 @@ func (h *AgentHandler) Replace(c *gin.Context) {
Model string `json:"model" binding:"required"`
}
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
return
}
@ -165,7 +165,7 @@ func (h *AgentHandler) Move(c *gin.Context) {
TargetWorkspaceID string `json:"target_workspace_id" binding:"required"`
}
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
return
}

View File

@ -30,7 +30,7 @@ func (h *ApprovalsHandler) Create(c *gin.Context) {
Context map[string]interface{} `json:"context"`
}
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
return
}
@ -170,7 +170,7 @@ func (h *ApprovalsHandler) Decide(c *gin.Context) {
DecidedBy string `json:"decided_by"`
}
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
return
}

View File

@ -141,7 +141,7 @@ func (h *ArtifactsHandler) Create(c *gin.Context) {
var req createArtifactsRepoRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
return
}
@ -302,7 +302,7 @@ func (h *ArtifactsHandler) Fork(c *gin.Context) {
var req forkArtifactsRepoRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
return
}
@ -367,7 +367,7 @@ func (h *ArtifactsHandler) Token(c *gin.Context) {
var req artifactsTokenRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
return
}

View File

@ -90,7 +90,7 @@ func (h *BudgetHandler) PatchBudget(c *gin.Context) {
// so we unmarshal into a raw map first.
var raw map[string]interface{}
if err := c.ShouldBindJSON(&raw); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
return
}

View File

@ -35,7 +35,7 @@ func (h *BundleHandler) Export(c *gin.Context) {
b, err := bundle.Export(ctx, workspaceID, h.configsDir, h.docker)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
c.JSON(http.StatusNotFound, gin.H{"error": "bundle not found"})
return
}
@ -46,7 +46,7 @@ func (h *BundleHandler) Export(c *gin.Context) {
func (h *BundleHandler) Import(c *gin.Context) {
var b bundle.Bundle
if err := c.ShouldBindJSON(&b); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid bundle"})
return
}

View File

@ -136,7 +136,7 @@ func (h *ChannelHandler) Create(c *gin.Context) {
}
if err := adapter.ValidateConfig(body.Config); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid config: " + err.Error()})
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid channel config"})
return
}
@ -294,7 +294,8 @@ func (h *ChannelHandler) Send(c *gin.Context) {
}
if err := h.manager.SendOutbound(ctx, channelID, body.Text); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
log.Printf("Channels: send outbound failed for channel %s: %v", channelID, err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "send failed"})
return
}
@ -307,7 +308,8 @@ func (h *ChannelHandler) Test(c *gin.Context) {
ctx := c.Request.Context()
if err := h.manager.SendOutbound(ctx, channelID, "🔔 Molecule AI channel test — connection successful!"); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
log.Printf("Channels: test message failed for channel %s: %v", channelID, err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "test message failed"})
return
}
@ -436,7 +438,7 @@ func (h *ChannelHandler) Webhook(c *gin.Context) {
// Parse the webhook first to get the chat_id
msg, err := adapter.ParseWebhook(c, nil)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "parse error: " + err.Error()})
c.JSON(http.StatusBadRequest, gin.H{"error": "webhook parse failed"})
return
}
if msg == nil {

View File

@ -71,7 +71,7 @@ func (h *CheckpointsHandler) Upsert(c *gin.Context) {
Payload json.RawMessage `json:"payload"`
}
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
return
}

View File

@ -114,7 +114,7 @@ func (h *DelegationHandler) Delegate(c *gin.Context) {
// the 400 response and returns the error so the caller can return.
func bindDelegateRequest(c *gin.Context, body *delegateRequest) error {
if err := c.ShouldBindJSON(body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid delegation request"})
return err
}
if _, err := uuid.Parse(body.TargetID); err != nil {
@ -344,7 +344,7 @@ func (h *DelegationHandler) Record(c *gin.Context) {
DelegationID string `json:"delegation_id" binding:"required"`
}
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
return
}
if _, err := uuid.Parse(body.TargetID); err != nil {
@ -392,7 +392,7 @@ func (h *DelegationHandler) UpdateStatus(c *gin.Context) {
ResponsePreview string `json:"response_preview,omitempty"`
}
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
return
}
if body.Status != "completed" && body.Status != "failed" {

View File

@ -302,7 +302,7 @@ func (h *DiscoveryHandler) CheckAccess(c *gin.Context) {
TargetID string `json:"target_id" binding:"required"`
}
if err := c.ShouldBindJSON(&payload); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
return
}

View File

@ -145,7 +145,7 @@ func (h *MemoriesHandler) Commit(c *gin.Context) {
Namespace string `json:"namespace,omitempty"` // optional; defaults to "general"
}
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
return
}

View File

@ -116,7 +116,7 @@ func (h *MemoryHandler) Set(c *gin.Context) {
IfMatchVersion *int64 `json:"if_match_version"`
}
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
return
}

View File

@ -250,7 +250,7 @@ func (h *OrgHandler) Import(c *gin.Context) {
Template OrgTemplate `json:"template"` // or inline template
}
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
return
}
@ -264,7 +264,7 @@ func (h *OrgHandler) Import(c *gin.Context) {
// letting an unauthenticated caller probe arbitrary filesystem paths.
resolved, err := resolveInsideRoot(h.orgDir, body.Dir)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("invalid dir: %v", err)})
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid org directory"})
return
}
orgBaseDir = resolved
@ -279,11 +279,11 @@ func (h *OrgHandler) Import(c *gin.Context) {
// refactor. Fails loudly on missing / cyclic / escaping includes.
expanded, err := resolveYAMLIncludes(data, orgBaseDir)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("!include expansion failed: %v", err)})
c.JSON(http.StatusBadRequest, gin.H{"error": "org template expansion failed"})
return
}
if err := yaml.Unmarshal(expanded, &tmpl); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("invalid YAML: %v", err)})
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid org template"})
return
}
} else if body.Template.Name != "" {

View File

@ -237,7 +237,7 @@ func (h *OrgPluginAllowlistHandler) PutAllowlist(c *gin.Context) {
var req putAllowlistRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
return
}
if req.EnabledBy == "" {

View File

@ -45,7 +45,7 @@ func (h *PluginsHandler) Install(c *gin.Context) {
var req installRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
return
}
@ -56,11 +56,9 @@ func (h *PluginsHandler) Install(c *gin.Context) {
c.JSON(he.Status, he.Body)
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
c.JSON(http.StatusInternalServerError, gin.H{"error": "plugin install failed"})
return
}
// On success, we own stagedDir cleanup. On error, resolveAndStage
// has already cleaned it up (and its returned result is nil).
defer os.RemoveAll(result.StagedDir)
// Org plugin allowlist gate (#591).
@ -77,7 +75,7 @@ func (h *PluginsHandler) Install(c *gin.Context) {
c.JSON(he.Status, he.Body)
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
c.JSON(http.StatusInternalServerError, gin.H{"error": "plugin deliver failed"})
return
}
@ -96,7 +94,7 @@ func (h *PluginsHandler) Uninstall(c *gin.Context) {
ctx := c.Request.Context()
if err := validatePluginName(pluginName); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid plugin name"})
return
}
@ -179,7 +177,7 @@ func (h *PluginsHandler) Download(c *gin.Context) {
ctx := c.Request.Context()
if err := validatePluginName(pluginName); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid plugin name"})
return
}
@ -223,7 +221,7 @@ func (h *PluginsHandler) Download(c *gin.Context) {
c.JSON(he.Status, he.Body)
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
c.JSON(http.StatusInternalServerError, gin.H{"error": "plugin download failed"})
return
}
defer os.RemoveAll(result.StagedDir)

View File

@ -126,13 +126,13 @@ func validateAgentURL(rawURL string) error {
func (h *RegistryHandler) Register(c *gin.Context) {
var payload models.RegisterPayload
if err := c.ShouldBindJSON(&payload); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
return
}
// C6: reject SSRF-capable URLs before persisting or caching them.
if err := validateAgentURL(payload.URL); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
return
}
@ -251,7 +251,7 @@ func (h *RegistryHandler) Register(c *gin.Context) {
func (h *RegistryHandler) Heartbeat(c *gin.Context) {
var payload models.HeartbeatPayload
if err := c.ShouldBindJSON(&payload); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
return
}
@ -390,7 +390,7 @@ func (h *RegistryHandler) evaluateStatus(c *gin.Context, payload models.Heartbea
func (h *RegistryHandler) UpdateCard(c *gin.Context) {
var payload models.UpdateCardPayload
if err := c.ShouldBindJSON(&payload); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
return
}

View File

@ -114,7 +114,7 @@ func (h *ScheduleHandler) Create(c *gin.Context) {
// Validate and compute next run
nextRun, err := scheduler.ComputeNextRun(body.CronExpr, body.Timezone, time.Now())
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
return
}
@ -198,7 +198,7 @@ func (h *ScheduleHandler) Update(c *gin.Context) {
}
nextRun, err := scheduler.ComputeNextRun(cronExpr, tz, time.Now())
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
return
}
nextRunAt = &nextRun

View File

@ -222,7 +222,7 @@ func (h *SecretsHandler) Set(c *gin.Context) {
Value string `json:"value" binding:"required"`
}
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
return
}
@ -335,7 +335,7 @@ func (h *SecretsHandler) SetGlobal(c *gin.Context) {
Value string `json:"value" binding:"required"`
}
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
return
}

View File

@ -122,7 +122,7 @@ func (h *TemplatesHandler) Import(c *gin.Context) {
Files map[string]string `json:"files" binding:"required"`
}
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
return
}
@ -140,7 +140,7 @@ func (h *TemplatesHandler) Import(c *gin.Context) {
}
if err := writeFiles(destDir, body.Files); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
return
}
@ -164,7 +164,7 @@ func (h *TemplatesHandler) ReplaceFiles(c *gin.Context) {
Files map[string]string `json:"files" binding:"required"`
}
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
return
}
@ -183,7 +183,7 @@ func (h *TemplatesHandler) ReplaceFiles(c *gin.Context) {
// Validate all paths first
for relPath := range body.Files {
if err := validateRelPath(relPath); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
return
}
}
@ -227,7 +227,7 @@ func (h *TemplatesHandler) ReplaceFiles(c *gin.Context) {
}
os.MkdirAll(destDir, 0o755)
if err := writeFiles(destDir, body.Files); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
return
}
c.JSON(http.StatusOK, gin.H{"status": "replaced", "workspace": workspaceID, "files": len(body.Files), "source": "template"})

View File

@ -134,7 +134,7 @@ func (h *TemplatesHandler) ListFiles(c *gin.Context) {
subPath := c.DefaultQuery("path", "")
if subPath != "" {
if err := validateRelPath(subPath); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid path"})
return
}
}
@ -258,7 +258,7 @@ func (h *TemplatesHandler) ReadFile(c *gin.Context) {
}
if err := validateRelPath(filePath); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid path"})
return
}
@ -318,7 +318,7 @@ func (h *TemplatesHandler) WriteFile(c *gin.Context) {
}
if err := validateRelPath(filePath); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid path"})
return
}
@ -326,7 +326,7 @@ func (h *TemplatesHandler) WriteFile(c *gin.Context) {
Content string `json:"content"`
}
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
return
}
@ -367,7 +367,7 @@ func (h *TemplatesHandler) DeleteFile(c *gin.Context) {
}
if err := validateRelPath(filePath); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid path"})
return
}

View File

@ -39,7 +39,7 @@ func (h *ViewportHandler) Save(c *gin.Context) {
Zoom float64 `json:"zoom"`
}
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid viewport data"})
return
}

View File

@ -76,14 +76,14 @@ func (h *WorkspaceHandler) TokenRegistry() *provisionhook.Registry {
func (h *WorkspaceHandler) Create(c *gin.Context) {
var payload models.CreateWorkspacePayload
if err := c.ShouldBindJSON(&payload); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid workspace payload"})
return
}
// #685/#688: validate field lengths and reject injection characters before
// any DB or provisioner interaction.
if err := validateWorkspaceFields(payload.Name, payload.Role, payload.Model, payload.Runtime); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid workspace fields"})
return
}
@ -133,7 +133,7 @@ func (h *WorkspaceHandler) Create(c *gin.Context) {
var workspaceDir interface{}
if payload.WorkspaceDir != "" {
if err := validateWorkspaceDir(payload.WorkspaceDir); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid workspace directory"})
return
}
workspaceDir = payload.WorkspaceDir
@ -145,7 +145,7 @@ func (h *WorkspaceHandler) Create(c *gin.Context) {
workspaceAccess = provisioner.WorkspaceAccessNone
}
if err := provisioner.ValidateWorkspaceAccess(workspaceAccess, payload.WorkspaceDir); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid workspace access"})
return
}
@ -411,7 +411,7 @@ func (h *WorkspaceHandler) Get(c *gin.Context) {
// #687: reject non-UUID IDs before hitting the DB.
if err := validateWorkspaceID(id); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid workspace ID"})
return
}
@ -569,13 +569,13 @@ func (h *WorkspaceHandler) Update(c *gin.Context) {
// #687: reject non-UUID IDs before hitting the DB.
if err := validateWorkspaceID(id); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid workspace ID"})
return
}
var body map[string]interface{}
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
return
}
@ -591,7 +591,7 @@ func (h *WorkspaceHandler) Update(c *gin.Context) {
if err := validateWorkspaceFields(
strField("name"), strField("role"), "" /*model not patchable*/, strField("runtime"),
); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid workspace fields"})
return
}
@ -644,7 +644,7 @@ func (h *WorkspaceHandler) Update(c *gin.Context) {
if wsDir != nil {
if dirStr, isStr := wsDir.(string); isStr && dirStr != "" {
if err := validateWorkspaceDir(dirStr); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid workspace directory"})
return
}
}
@ -707,7 +707,7 @@ func (h *WorkspaceHandler) Delete(c *gin.Context) {
// #687: reject non-UUID IDs before hitting the DB.
if err := validateWorkspaceID(id); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid workspace ID"})
return
}
@ -864,7 +864,7 @@ func (h *WorkspaceHandler) Delete(c *gin.Context) {
// Hard delete the workspace row
if _, err := db.DB.ExecContext(ctx, "DELETE FROM workspaces WHERE id = ANY($1::uuid[])", purgeIDs); err != nil {
log.Printf("Purge workspace row error for %v: %v", allIDs, err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "purge failed: " + err.Error()})
c.JSON(http.StatusInternalServerError, gin.H{"error": "purge failed"})
return
}
c.JSON(http.StatusOK, gin.H{"status": "purged", "cascade_deleted": len(descendantIDs)})