Repository: camunda/camunda-modeler (JavaScript)

Issue:
Gracefully handle c8run `/v2/` path in the orchestration cluster endpoint
### What should we do?

* [x] I can connect to c8run if I provide  `http://localhost:8088` endpoint
* [ ] I can connect to c8run if I provide `http://localhost:8088/v2/` endpoint 

### Why should we do it?

c8run provides the orchestration cluster endpoint with the `/v2/` in the path:

<img width="1025" height="616" alt="Image" src="https://github.com/user-attachments/assets/0c2319a0-a980-4028-8358-342d2b19ac10" />

Desktop Modeler fails to recognize it, and reports improper configuration:

<img width="1114" height="880" alt="Image" src="https://github.com/user-attachments/assets/ef48605a-afd2-4050-8656-7c30a572c425" />

We know upfront that `/v2/` may be present in the URL. Let's be more helpful, and handle the path gracefully.


Discussion: its not only c8run, console also defaults to the v2 path (if you reopen a credential) (cf. slack https://camunda.slack.com/archives/GP70M0J6M/p1759244643413549?thread_ts=1759241005.289019&cid=GP70M0J6M)
but also note that even if we fix this, you can't connect to c8run (the java version) with rest. at least during my testing i was not able to make it work without cookie auth

Correction: I think c8run 8.8 should work with rest



Fix (patch):
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ab9aa6731c..9bdc6d38d2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,7 @@ All notable changes to the [Camunda Modeler](https://github.com/camunda/camunda-
 ___Note:__ Yet to be released changes appear here._
 
 * `FIX`: use `ZEEBE_GRPC_ADDRESS` in favor of deprecated `ZEEBE_ADDRESS` for cluster connections ([#5362](https://github.com/camunda/camunda-modeler/pull/5362))
+* `FEAT`: accept trailing `v2` or `/` for REST cluster URLs ([#5345](https://github.com/camunda/camunda-modeler/issues/5345))
 
 ## 5.40.1
 
diff --git a/app/lib/zeebe-api/camunda-client-factory.js b/app/lib/zeebe-api/camunda-client-factory.js
index 30e06ca68a..e3522d6e13 100644
--- a/app/lib/zeebe-api/camunda-client-factory.js
+++ b/app/lib/zeebe-api/camunda-client-factory.js
@@ -23,7 +23,8 @@ const {
 const {
   sanitizeCamundaClientOptions,
   isGrpcSaasUrl,
-  isRestSaasUrl
+  isRestSaasUrl,
+  removeV2OrSlashes
 } = require('./utils');
 
 /**
@@ -269,7 +270,7 @@ class CamundaClientFactory {
     case 'http':
     case 'https':
     default:
-      clientConfig.ZEEBE_REST_ADDRESS = url;
+      clientConfig.ZEEBE_REST_ADDRESS = removeV2OrSlashes(url);
       break;
     }
 
diff --git a/app/lib/zeebe-api/utils.js b/app/lib/zeebe-api/utils.js
index ee6524abed..1013f2a20e 100644
--- a/app/lib/zeebe-api/utils.js
+++ b/app/lib/zeebe-api/utils.js
@@ -137,4 +137,40 @@ module.exports.isGrpcSaasUrl = isGrpcSaasUrl;
 function isRestSaasUrl(url) {
   return /^https:\/\/[a-z]+-\d+\.zeebe\.camunda\.io(:443|)\/[a-z\d-]+\/?/.test(url);
 }
-module.exports.isRestSaasUrl = isRestSaasUrl;
\ No newline at end of file
+module.exports.isRestSaasUrl = isRestSaasUrl;
+
+/**
+ * Remove trailing slashes and '/v2' from a URL. This is mainly used for Camunda 8 REST API endpoints as we are inconsistent if we add /v2 or not.
+ *
+ * @param {string} url
+ *
+ * @returns {string}
+ *
+ * @examples
+ * removeV2OrSlashes('https://example.com') // returns 'https://example.com'
+ * removeV2OrSlashes('https://example.com/') // returns 'https://example.com'
+ * removeV2OrSlashes('https://example.com/v2') // returns 'https://example.com'
+ * removeV2OrSlashes('https://example.com/v2/') // returns 'https://example.com'
+ * removeV2OrSlashes('https://example.com/v2/v2') // returns 'https://example.com/v2' (super edge case, they have to specify v2 explicitly twice)
+ */
+module.exports.removeV2OrSlashes = function(url) {
+
+  if (!url) {
+    return url;
+  }
+  const parsed = new URL(url);
+
+  let pathname = parsed.pathname || '';
+
+  while (pathname.endsWith('/')) {
+    pathname = pathname.slice(0, -1);
+  }
+
+  if (pathname.endsWith('/v2')) {
+    pathname = pathname.slice(0, -3);
+  }
+
+  parsed.pathname = pathname || '/';
+
+  return parsed.href.replace(/\/(\?|#|$)/, '$1');
+};
\ No newline at end of file
diff --git a/client/src/plugins/zeebe-plugin/deployment-plugin/DeploymentConfigValidator.js b/client/src/plugins/zeebe-plugin/deployment-plugin/DeploymentConfigValidator.js
index 63d586298b..ab5cf9096c 100644
--- a/client/src/plugins/zeebe-plugin/deployment-plugin/DeploymentConfigValidator.js
+++ b/client/src/plugins/zeebe-plugin/deployment-plugin/DeploymentConfigValidator.js
@@ -24,7 +24,7 @@ export const VALIDATION_ERROR_MESSAGES = {
   CONTACT_POINT_MUST_BE_URL: 'Cluster endpoint must be a valid URL.',
   CONTACT_POINT_MUST_NOT_BE_EMPTY: 'Cluster endpoint must not be empty.',
   CLUSTER_URL_MUST_NOT_BE_EMPTY: 'Cluster URL must not be empty.',
-  CONTACT_POINT_MUST_START_WITH_PROTOCOL: 'Cluster endpoint must start with "http://", "grpc://", "https://", or "grpcs", .',
+  CONTACT_POINT_MUST_START_WITH_PROTOCOL: 'Cluster endpoint must start with "http://", "grpc://", "https://", or "grpcs://".',
   MUST_PROVIDE_A_VALUE: 'Must provide a value.',
   OAUTH_URL_MUST_NOT_BE_EMPTY: 'OAuth URL must not be empty.'
 };