{"record":{"id":"bd93e469bf792cd0","repo":"paperclipai/paperclip","slug":"failed-to-serve-file","errorCode":null,"errorMessage":"Failed to serve file","messagePattern":"Failed to serve file","errorType":"http","errorClass":null,"httpStatus":500,"severity":"error","filePath":"server/src/routes/plugin-ui-static.ts","lineNumber":503,"sourceCode":"      res.set(\"Content-Type\", contentType);\n    }\n\n    // Step 9: Set CORS headers (plugin UI may be loaded from different origin in dev)\n    res.set(\"Access-Control-Allow-Origin\", \"*\");\n\n    // Step 10: Send the file\n    // The plugin source can live in Git worktrees (e.g. \".worktrees/...\").\n    // `send` defaults to dotfiles:\"ignore\", which treats dot-directories as\n    // not found. We already enforce traversal safety above, so allow dot paths.\n    res.sendFile(resolvedFilePath, { dotfiles: \"allow\" }, (err) => {\n      if (err) {\n        log.error(\n          { err, pluginId: plugin.id, filePath: resolvedFilePath },\n          \"plugin-ui-static: error sending file\",\n        );\n        // Only send error if headers haven't been sent yet\n        if (!res.headersSent) {\n          res.status(500).json({ error: \"Failed to serve file\" });\n        }\n      }\n    });\n  });\n\n  return router;\n}\n","sourceCodeStart":485,"sourceCodeEnd":511,"githubUrl":"https://github.com/paperclipai/paperclip/blob/120ae5428fa29bee300bcf806491cd4d965fbb7c/server/src/routes/plugin-ui-static.ts#L485-L511","documentation":"Returned as HTTP 500 by GET /_plugins/:pluginId/ui/* (server/src/routes/plugin-ui-static.ts:503) when res.sendFile's completion callback reports an error — the stat/symlink/containment checks all passed but the actual stream failed (EACCES, EMFILE, ENOSPC, or the file vanished between stat and send). If headers were already sent the status cannot be changed, so the connection is destroyed instead.","triggerScenarios":"The plugin package is replaced/deleted on disk between the statSync check and the sendFile stream opening (reinstall in flight); the server process lacks read permission on the file itself (execute-only directory traversal rights); file-descriptor exhaustion (EMFILE) under heavy asset traffic; disk/IO errors mid-stream.","commonSituations":"Concurrent plugin upgrade + active browser tabs loading assets; hardened filesystems where directory access and file access differ; fd limits hit on busy dev servers; antivirus/quarantine removing files mid-request.","solutions":["Retry the request once — most causes (file swap race, transient EMFILE) are self-healing after the reinstall finishes","If persistent, check read permissions on the file and every parent directory for the server user","Raise the process file-descriptor limit (ulimit -n) if EMFILE appears in server logs","Avoid uninstalling/reinstalling plugins while their UI is being served, or expect transient 500s during that window"],"exampleFix":null,"handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"const res = await fetch(url, { cache: \"no-store\" });\nif (res.status === 500) {\n  await new Promise((r) => setTimeout(r, 500));\n  const retry = await fetch(url, { cache: \"no-store\" });\n  if (retry.ok) return retry;\n  throw new Error(\"Plugin asset serve failed twice — plugin may be mid-reinstall\");\n}","preventionTips":["Treat a single 500 from /_plugins/:id/ui/* as transient (file swap race) and retry once without cache","Avoid uninstall/reinstall of plugins while their assets are in active use","Monitor server logs for 'error sending file' entries to catch permission/fd-limit issues early"],"tags":["plugins","static-files","http-500","file-stream","race-condition"],"backgroundTag":"file-stream-error","analyzedSha":"120ae5428fa29bee300bcf806491cd4d965fbb7c","analyzedAt":"2026-08-18T22:49:45.177Z","schemaVersion":2},"datasetVersion":"2026-08-22T04:17:13.399Z"}