diff --git a/diagram-converter/webapp/src/main/java/io/camunda/migration/diagram/converter/webapp/ConverterExceptionHandler.java b/diagram-converter/webapp/src/main/java/io/camunda/migration/diagram/converter/webapp/ConverterExceptionHandler.java
new file mode 100644
index 000000000..be54e7471
--- /dev/null
+++ b/diagram-converter/webapp/src/main/java/io/camunda/migration/diagram/converter/webapp/ConverterExceptionHandler.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH under
+ * one or more contributor license agreements. See the NOTICE file distributed
+ * with this work for additional information regarding copyright ownership.
+ * Licensed under the Camunda License 1.0. You may not use this file
+ * except in compliance with the Camunda License 1.0.
+ */
+package io.camunda.migration.diagram.converter.webapp;
+
+import jakarta.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import org.apache.tomcat.util.http.fileupload.impl.FileCountLimitExceededException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.web.bind.annotation.ControllerAdvice;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.multipart.MultipartException;
+
+@ControllerAdvice
+public class ConverterExceptionHandler {
+
+ private static final Logger LOG = LoggerFactory.getLogger(ConverterExceptionHandler.class);
+
+ @ExceptionHandler(MultipartException.class)
+ public void handleMultipartException(MultipartException ex, HttpServletResponse response)
+ throws IOException {
+ Throwable rootCause = getRootCause(ex);
+
+ if (rootCause instanceof FileCountLimitExceededException fileCountEx) {
+ LOG.warn("File count limit exceeded: {}", rootCause.getMessage());
+ writeJsonError(
+ response,
+ HttpStatus.PAYLOAD_TOO_LARGE,
+ "FILE_COUNT_LIMIT_EXCEEDED",
+ fileCountEx.getLimit());
+ return;
+ }
+
+ LOG.error("Multipart request processing failed", ex);
+ writeJsonError(response, HttpStatus.BAD_REQUEST, "MULTIPART_ERROR", -1);
+ }
+
+ private void writeJsonError(
+ HttpServletResponse response, HttpStatus status, String errorCode, long maxPartCount)
+ throws IOException {
+ response.setStatus(status.value());
+ response.setContentType(MediaType.APPLICATION_JSON_VALUE);
+
+ String json =
+ "{\"errorCode\":\""
+ + errorCode
+ + "\",\"status\":"
+ + status.value()
+ + ",\"maxPartCount\":"
+ + maxPartCount
+ + "}";
+ response.getWriter().write(json);
+ }
+
+ private Throwable getRootCause(Throwable throwable) {
+ Throwable cause = throwable;
+ while (cause.getCause() != null && cause.getCause() != cause) {
+ cause = cause.getCause();
+ }
+ return cause;
+ }
+}
diff --git a/diagram-converter/webapp/src/main/javascript/src/App.jsx b/diagram-converter/webapp/src/main/javascript/src/App.jsx
index 3723f52e9..2f0eed68e 100644
--- a/diagram-converter/webapp/src/main/javascript/src/App.jsx
+++ b/diagram-converter/webapp/src/main/javascript/src/App.jsx
@@ -11,7 +11,7 @@ import {
ProgressIndicator,
ProgressStep,
Button,
- Callout,
+ ActionableNotification,
DataTable,
Table,
TableHead,
@@ -46,6 +46,9 @@ function App() {
const [previewTableHeader, setPreviewTableHeader] = useState([]);
const [previewTableRows, setPreviewTableRows] = useState([]);
+ const [downloadError, setDownloadError] = useState(null);
+ const [downloadErrorTitle, setDownloadErrorTitle] = useState("");
+
const [showConfig, setShowConfig] = useState(false);
const [configOptions, setConfigOptions] = useState({
defaultJobType: "camunda-7-job",
@@ -199,10 +202,40 @@ function App() {
setValidFiles(validFiles);
}
+ function buildErrorMessage(errorBody) {
+ switch (errorBody.errorCode) {
+ case "FILE_COUNT_LIMIT_EXCEEDED":
+ return <>
+ Too many files uploaded. The online version supports up to {errorBody.maxPartCount} parts per request.
+ {" "}To learn how to run the diagram converter locally with a custom limit, consult the{" "}
+ diagram converter guide.
+ >;
+ default:
+ return "Download failed. Please try again.";
+ }
+ }
+
+ async function handleDownloadResponse(filename, response, title) {
+ if (!response.ok) {
+ let errorMessage = "Download failed. Please try again.";
+ try {
+ const errorBody = await response.json();
+ errorMessage = buildErrorMessage(errorBody);
+ } catch {
+ // Response body is not JSON, use default message
+ }
+ setDownloadErrorTitle(title);
+ setDownloadError(errorMessage);
+ return;
+ }
+ setDownloadError(null);
+ await download1(filename, response);
+ }
+
async function downloadXLS() {
const formData = createFormData(validFiles);
- //validFiles.forEach((file) => formData.append("file", file));
- await download1("analysis.xlsx",
+ await handleDownloadResponse("analysis.xlsx",
await fetch(baseUrl + "/check", {
body: formData,
method: "POST",
@@ -210,30 +243,31 @@ function App() {
Accept:
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
},
- })
+ }),
+ "Downloading XLSX failed"
);
}
async function downloadCSV() {
const formData = createFormData(validFiles);
- //validFiles.forEach((file) => formData.append("file", file));
- await download1("analysis.csv",
+ await handleDownloadResponse("analysis.csv",
await fetch(baseUrl + "/check", {
body: formData,
method: "POST",
headers: {
Accept: "text/csv",
},
- })
+ }),
+ "Downloading CSV failed"
);
}
async function downloadZIP() {
const formData = createFormData(validFiles);
- //validFiles.forEach((file) => formData.append("file", file));
- await download1("converted-models.zip",
+ await handleDownloadResponse("converted-models.zip",
await fetch(baseUrl + "/convertBatch", {
body: formData,
method: "POST",
- })
+ }),
+ "Downloading ZIP failed"
);
}
@@ -522,6 +556,17 @@ function App() {
}
/>
))}
+ {downloadError && (
+ setDownloadError(null)}
+ className="download-error-notification"
+ >
+ {downloadError}
+
+ )}