{"record":{"id":"4e6377657df2ea84","repo":"flowable/flowable-engine","slug":"processdiagramgenerator-already-closed","errorCode":null,"errorMessage":"ProcessDiagramGenerator already closed","messagePattern":"ProcessDiagramGenerator already closed","errorType":"exception","errorClass":"FlowableImageException","httpStatus":null,"severity":"error","filePath":"modules/flowable-image-generator/src/main/java/org/flowable/image/impl/DefaultProcessDiagramCanvas.java","lineNumber":263,"sourceCode":"            ESCALATION_CATCH_IMAGE = ImageIO.read(ReflectUtil.getResource(\"org/flowable/icons/escalation.png\", customClassLoader));\n            MESSAGE_THROW_IMAGE = ImageIO.read(ReflectUtil.getResource(\"org/flowable/icons/message-throw.png\", customClassLoader));\n            MESSAGE_CATCH_IMAGE = ImageIO.read(ReflectUtil.getResource(\"org/flowable/icons/message.png\", customClassLoader));\n            SIGNAL_THROW_IMAGE = ImageIO.read(ReflectUtil.getResource(\"org/flowable/icons/signal-throw.png\", customClassLoader));\n            SIGNAL_CATCH_IMAGE = ImageIO.read(ReflectUtil.getResource(\"org/flowable/icons/signal.png\", customClassLoader));\n            \n        } catch (IOException e) {\n            LOGGER.warn(\"Could not load image for process diagram creation: {}\", e.getMessage());\n        }\n    }\n\n    /**\n     * Generates an image of what currently is drawn on the canvas.\n     * \n     * Throws an {@link FlowableImageException} when {@link #close()} is already called.\n     */\n    public InputStream generateImage(String imageType) {\n        if (closed) {\n            throw new FlowableImageException(\"ProcessDiagramGenerator already closed\");\n        }\n\n        try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {\n            ImageIO.write(processDiagram, imageType, out);\n            return new ByteArrayInputStream(out.toByteArray());\n        } catch (IOException e) {\n            throw new FlowableImageException(\"Error while generating process image\", e);\n        }\n    }\n\n    /**\n     * Generates an image of what currently is drawn on the canvas.\n     * \n     * Throws an {@link FlowableImageException} when {@link #close()} is already called.\n     */\n    public BufferedImage generateBufferedImage(String imageType) {\n        if (closed) {\n            throw new FlowableImageException(\"ProcessDiagramGenerator already closed\");","sourceCodeStart":245,"sourceCodeEnd":281,"githubUrl":"https://github.com/flowable/flowable-engine/blob/d6d39ce1c69ff244f2d9dc6af756a9b95e865586/modules/flowable-image-generator/src/main/java/org/flowable/image/impl/DefaultProcessDiagramCanvas.java#L245-L281","documentation":"DefaultProcessDiagramCanvas buffers the process diagram in memory and is single-use: once close() is called the internal image is released and closed is set to true. generateImage() checks this flag and throws FlowableImageException to prevent rendering from a dead canvas. This signals a lifecycle misuse — callers must generate all images before closing.","triggerScenarios":"Calling generateImage(String imageType) after close() has already been invoked on the same DefaultProcessDiagramCanvas instance; reusing a closed canvas from a cached/pooled generator; calling generateImage twice with a close in between.","commonSituations":"Wrapping the canvas in try-with-resources (which auto-closes it) and then attempting to render afterwards; sharing one canvas across requests where a first handler closed it; refactorings that moved close() earlier in the flow.","solutions":["Ensure generateImage()/generateBufferedImage() are called before close() on the canvas","If using try-with-resources, generate the image inside the try block","If the canvas is shared, remove the premature close() or create a new canvas per render","Check for double-close paths in custom ProcessDiagramGenerator implementations"],"exampleFix":"// before\ncanvas.close();\nInputStream is = canvas.generateImage(\"png\"); // throws\n// after\nInputStream is = canvas.generateImage(\"png\");\ncanvas.close();","handlingStrategy":"try-catch","validationCode":"if (canvas == null) throw new IllegalStateException(\"canvas not initialized\");\n// track close state yourself, or generate before closing:\n// InputStream is = canvas.generateImage(\"png\"); canvas.close();","typeGuard":"boolean isCanvasUsable(DefaultProcessDiagramCanvas c) {\n    return c != null; // closed flag is private; enforce usage order instead\n}","tryCatchPattern":"try {\n    InputStream is = canvas.generateImage(\"png\");\n} catch (FlowableImageException e) {\n    // canvas was closed; recreate and regenerate\n    canvas = new DefaultProcessDiagramCanvas(...);\n}","preventionTips":["Generate all images before closing the canvas","Never rely on a closed canvas; create a new one instead","Avoid try-with-resources unless the image is produced inside the try block","Document canvas ownership in code that shares it"],"tags":["image-generation","lifecycle","flowable","closed-resource"],"backgroundTag":"invalid-state-transition","analyzedSha":"d6d39ce1c69ff244f2d9dc6af756a9b95e865586","analyzedAt":"2026-09-11T06:41:19.413Z","contentChangedAt":"2026-09-11T06:41:19.413Z","schemaVersion":2},"datasetVersion":"2026-09-18T11:17:12.947Z"}