{"record":{"id":"968b34a46d88e84c","repo":"n8n-io/n8n","slug":"bad-gateway","errorCode":null,"errorMessage":"Bad Gateway","messagePattern":"Bad Gateway","errorType":"http","errorClass":null,"httpStatus":409,"severity":"error","filePath":"packages/cli/src/controllers/project.controller.ts","lineNumber":323,"sourceCode":"\t\t\t\t\tproject: { id: project.id, name: project.name },\n\t\t\t\t});\n\t\t\t}\n\n\t\t\tconst relations = await this.projectsService.getProjectRelations(projectId);\n\t\t\tthis.eventService.emit('team-project-updated', {\n\t\t\t\tuserId: req.user.id,\n\t\t\t\trole: req.user.role.slug,\n\t\t\t\tmembers: relations.map((r) => ({ userId: r.userId, role: r.role.slug })),\n\t\t\t\tprojectId,\n\t\t\t});\n\n\t\t\t// Response semantics:\n\t\t\t// - If at least one user was added, return 201. When there are also conflicts, include them in the body.\n\t\t\t// - If no users were added but conflicts exist, return 409 with conflicts.\n\t\t\tif (added.length > 0) {\n\t\t\t\treturn conflicts.length > 0 ? res.status(201).json({ conflicts }) : res.status(201).send();\n\t\t\t}\n\t\t\tif (conflicts.length > 0) return res.status(409).json({ conflicts });\n\t\t\treturn res.status(200).send();\n\t\t} catch (e) {\n\t\t\tif (e instanceof UnlicensedProjectRoleError) {\n\t\t\t\tthrow new BadRequestError(e.message);\n\t\t\t}\n\t\t\tthrow e;\n\t\t}\n\t}\n\n\t@Patch('/:projectId/users/:userId')\n\t@ProjectScope('project:update')\n\tasync changeProjectUserRole(\n\t\treq: AuthenticatedRequest,\n\t\tres: Response,\n\t\t@Param('projectId') projectId: string,\n\t\t@Param('userId') userId: string,\n\t\t@Body body: ChangeUserRoleInProject,\n\t) {","sourceCodeStart":305,"sourceCodeEnd":341,"githubUrl":"https://github.com/n8n-io/n8n/blob/5ac6606e81f67bb9534255570cd4e86fd8101eee/packages/cli/src/controllers/project.controller.ts#L305-L341","documentation":"CATALOG ANOMALY: the literal 'Bad Gateway' does not appear anywhere in project.controller.ts (it actually lives in telemetry.controller.ts:45). At the attributed line 323 the real code is the addProjectUsers response path: `if (conflicts.length > 0) return res.status(409).json({ conflicts });`, and the surrounding catch (lines 325-329) only converts UnlicensedProjectRoleError into a 400 and rethrows everything else as a generic 500. So the meaningful error surface here is either a structured 409 conflicts response or an unhandled 500 from an unexpected backend error during bulk user-add.","triggerScenarios":"POST /:projectId/users where some users cannot be added (returns 409 with a conflicts body), or where an unexpected non-UnlicensedProjectRole error escapes the catch and surfaces as 500 (DB constraint, missing role, FK violation).","commonSituations":"Adding users who are already members, or whose target role is invalid, producing conflicts; a transient DB/backend failure during the bulk add; the body must be inspected to distinguish 'partial success with conflicts' from a hard failure.","solutions":["Inspect the response status and body: 201 with {conflicts}, 409 with {conflicts}, or 500 (rethrown).","For 409, resolve each conflict (e.g. user already a member, role mismatch) and retry the remaining users.","For 500, check server logs for the rethrown root cause — do not retry unchanged."],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":"// Differentiate the documented response shapes before treating as error.\nfunction classifyAddUsersResponse(res) {\n  if (res.status === 201 || res.status === 200) return 'ok';\n  if (res.status === 409) return 'conflicts'; // res.body.conflicts[]\n  return 'error';\n}","typeGuard":"function hasConflicts(body) {\n  return body && Array.isArray(body.conflicts);\n}","tryCatchPattern":"try {\n  const res = await api.post(`/projects/${projectId}/users`, payload);\n  if (res.status === 409 && res.data?.conflicts) {\n    // resolve per-user conflicts, retry the subset that can be added\n  }\n} catch (e) {\n  if (e.status === 500) {\n    // inspect server logs; do not retry identical payload\n  } else if (e.status === 400 && /not licensed to use role/.test(e.message)) {\n    // switch to a licensed role\n  } else { throw e; }\n}","preventionTips":["Always inspect status + conflicts body, not just success/failure.","Note: the catalog 'Bad Gateway' label is mis-attributed here; treat responses by real status code."],"tags":["projects","conflicts","catalog-anomaly","memberships"],"backgroundTag":null,"analyzedSha":"5ac6606e81f67bb9534255570cd4e86fd8101eee","analyzedAt":"2026-08-12T05:26:35.080Z","schemaVersion":2},"datasetVersion":"2026-08-12T13:17:24.610Z"}