{"record":{"id":"488a57b18bf58ec9","repo":"paperclipai/paperclip","slug":"configjson-is-required-and-must-be-an-object","errorCode":null,"errorMessage":"\"configJson\" is required and must be an object","messagePattern":"\"configJson\" is required and must be an object","errorType":"http","errorClass":null,"httpStatus":400,"severity":"error","filePath":"server/src/routes/plugins.ts","lineNumber":2320,"sourceCode":"   * Errors:\n   * - 400 if request validation fails\n   * - 404 if plugin not found\n   */\n  router.post(\"/plugins/:pluginId/config\", async (req, res) => {\n    assertInstanceAdmin(req);\n    assertPluginManagementVisible();\n    const { pluginId } = req.params;\n\n    const plugin = await resolvePlugin(registry, pluginId);\n    if (!plugin) {\n      res.status(404).json({ error: \"Plugin not found\" });\n      return;\n    }\n\n    const body = req.body as { companyId?: unknown; configJson?: Record<string, unknown> } | undefined;\n    const companyId = requirePluginConfigCompanyId(req, body?.companyId);\n    if (!body?.configJson || typeof body.configJson !== \"object\" || Array.isArray(body.configJson)) {\n      res.status(400).json({ error: '\"configJson\" is required and must be an object' });\n      return;\n    }\n\n    // Strip devUiUrl unless the caller is an instance admin. devUiUrl activates\n    // a dev-proxy in the static file route that could be abused for SSRF if any\n    // board-level user were allowed to set it.\n    if (\n      \"devUiUrl\" in body.configJson &&\n      !(req.actor.type === \"board\" && req.actor.isInstanceAdmin)\n    ) {\n      delete body.configJson.devUiUrl;\n    }\n\n    // Validate configJson against the plugin's instanceConfigSchema (if declared).\n    // This ensures CLI/API callers get the same validation the UI performs client-side.\n    const schema = plugin.manifestJson?.instanceConfigSchema;\n    if (schema && Object.keys(schema).length > 0) {\n      const validation = validateInstanceConfig(body.configJson, schema);","sourceCodeStart":2302,"sourceCodeEnd":2338,"githubUrl":"https://github.com/paperclipai/paperclip/blob/a7e689b3c35347b529cb9f54c9b9a8575a3dcab6/server/src/routes/plugins.ts#L2302-L2338","documentation":"Returned as HTTP 400 by POST /api/plugins/:pluginId/config when the request body has no configJson or configJson is not a plain object. The route (server/src/routes/plugins.ts:2298) explicitly rejects null, arrays, strings, and numbers; only a JSON object is accepted. Note the companyId check (requirePluginConfigCompanyId) runs first, so hitting this error means companyId already passed.","triggerScenarios":"Calling POST /api/plugins/:pluginId/config with body {companyId: 'c1'} (configJson omitted), configJson: null, configJson: '[1,2]' (array), or a double-stringified body like configJson: '{\"k\":1}'. Also triggered when the request lacks Content-Type: application/json so Express never parsed the body and req.body is undefined.","commonSituations":"Stringifying the config twice (JSON.stringify of an already-stringified value), sending the array form-data variant, forgetting the field in a curl/CLI call, or using a client that defaults to text/plain so req.body stays undefined.","solutions":["Send configJson as a real JSON object in the request body, e.g. {\"companyId\":\"c1\",\"configJson\":{\"apiKey\":\"...\"}}","Set the Content-Type: application/json header so Express JSON middleware parses the body","If the config arrives as a string in your code, JSON.parse it before putting it into the request body","Confirm you are not sending an array or null; wrap values in an object ({items: [...]}, not [...])"],"exampleFix":"// before\nawait fetch(`/api/plugins/${pluginId}/config`, {\n  method: 'POST',\n  headers: { 'Content-Type': 'application/json' },\n  body: JSON.stringify({ companyId, configJson: JSON.stringify(cfg) }),\n});\n// after\nawait fetch(`/api/plugins/${pluginId}/config`, {\n  method: 'POST',\n  headers: { 'Content-Type': 'application/json' },\n  body: JSON.stringify({ companyId, configJson: cfg }),\n});","handlingStrategy":"validation","validationCode":"function isPlainObject(v: unknown): v is Record<string, unknown> {\n  return typeof v === 'object' && v !== null && !Array.isArray(v);\n}\n// before POST /api/plugins/:id/config\nif (!isPlainObject(configJson)) throw new Error('configJson must be a plain object');\nawait fetch(`/api/plugins/${pluginId}/config`, {\n  method: 'POST',\n  headers: { 'Content-Type': 'application/json' },\n  body: JSON.stringify({ companyId, configJson }),\n});","typeGuard":"function isPlainObject(v: unknown): v is Record<string, unknown> {\n  return typeof v === 'object' && v !== null && !Array.isArray(v);\n}","tryCatchPattern":"try { await savePluginConfig(...); } catch (e) { if (e.status === 400 && /configJson/.test(e.body.error)) fixBodyShape(); else throw e; }","preventionTips":["Always construct request bodies with JSON.stringify of a plain object, never pre-stringified nested values","Default to Content-Type: application/json on every plugin API call","Keep a typed PluginConfigPayload interface so TypeScript rejects stringified configs at compile time"],"tags":["http-400","request-validation","plugins","api","config"],"backgroundTag":"request-body-validation","analyzedSha":"a7e689b3c35347b529cb9f54c9b9a8575a3dcab6","analyzedAt":"2026-08-18T22:49:45.177Z","schemaVersion":2},"datasetVersion":"2026-08-22T09:17:25.309Z"}