or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

configuration.mdinbound-endpoints.mdindex.mdjava-dsl.mdmanagement.mdmessage-conversion.mdmultipart-handling.mdoutbound-handlers.md
tile.json

multipart-handling.mddocs/

Multipart File Handling

Support for multipart file uploads with various strategies for reading and storing file content. The module provides flexible options for handling multipart requests, including in-memory storage, temporary file creation, and simplified content reading.

Key Information for Agents

Required Configuration:

  • MultipartAwareFormHttpMessageConverter must be configured on inbound endpoint
  • MultipartFileReader must be set on converter
  • Request mapping must accept multipart/form-data content type
  • MultipartResolver can be configured (optional, uses default if not set)

Storage Strategies:

  • DefaultMultipartFileReader: Stores files in memory as byte arrays
  • FileCopyingMultipartFileReader: Stores files on disk as temporary files
  • SimpleMultipartFileReader: Returns content as String or byte[] (no file metadata)

File Availability:

  • Standard MultipartFile only available during request scope
  • UploadedMultipartFile available after request scope ends
  • Files stored in memory or on disk persist beyond request
  • Temporary files should be cleaned up manually (or use auto-delete)

Memory vs. Disk:

  • Memory: Faster, but limited by heap size
  • Disk: Slower, but handles large files
  • Choose based on file size and memory constraints
  • Can implement custom strategy via MultipartFileReader interface

File Metadata:

  • Original filename: getOriginalFilename()
  • Content type: getContentType()
  • Size: getSize()
  • Form parameter name: getName()
  • All metadata preserved in UploadedMultipartFile

Temporary Files:

  • Created with configurable prefix and suffix
  • Default prefix: "si_", default suffix: ".tmp"
  • Created in system temp directory or specified directory
  • Must be cleaned up manually (not auto-deleted)

Edge Cases:

  • Empty file uploads: isEmpty() returns true, size is 0
  • Multiple files with same parameter name: Use MultiValueMap to access all
  • Missing file parameter: Returns null in MultiValueMap
  • File too large: May cause OutOfMemoryError (memory strategy) or disk full (disk strategy)
  • Invalid content type: May not be recognized as multipart

Performance Considerations:

  • Memory strategy: Fast but limited by heap
  • Disk strategy: Slower I/O but handles large files
  • Consider file size limits and concurrent uploads
  • Temporary files should be cleaned up to prevent disk space issues

Security Considerations:

  • Validate file types and sizes
  • Sanitize filenames to prevent path traversal
  • Limit upload size via servlet container configuration
  • Consider virus scanning for uploaded files

Capabilities

MultipartFileReader Interface

Strategy interface for reading MultipartFile content in different ways. Implementations can store files in memory, write to filesystem, or extract content as strings or byte arrays.

public interface MultipartFileReader<T> {

    /**
     * Reads MultipartFile content and returns result of type T.
     * Type T depends on implementation strategy (File, MultipartFile, String, byte[], etc.).
     *
     * @param multipartFile the multipart file to read
     * @return the processed file content
     * @throws IOException if read error occurs
     */
    T readMultipartFile(MultipartFile multipartFile) throws IOException;
}

This interface allows custom implementations for different file handling strategies based on application requirements.

DefaultMultipartFileReader

MultipartFileReader implementation that reads MultipartFile content into a new MultipartFile instance not restricted to HTTP request scope. Files are stored in memory, making them available after request completion.

public class DefaultMultipartFileReader
    implements MultipartFileReader<MultipartFile> {

    /**
     * Creates default multipart file reader.
     * Files are read into memory as UploadedMultipartFile instances.
     */
    public DefaultMultipartFileReader();

    /**
     * Reads multipart file and returns UploadedMultipartFile.
     * Content is stored in memory (byte array).
     *
     * @param multipartFile the source multipart file
     * @return UploadedMultipartFile with content in memory
     * @throws IOException if read error occurs
     */
    public MultipartFile readMultipartFile(MultipartFile multipartFile)
        throws IOException;
}

Usage Example:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.integration.http.multipart.DefaultMultipartFileReader;
import org.springframework.integration.http.converter.MultipartAwareFormHttpMessageConverter;
import org.springframework.integration.http.inbound.HttpRequestHandlingMessagingGateway;
import org.springframework.integration.http.inbound.RequestMapping;
import org.springframework.http.HttpMethod;

@Configuration
public class DefaultMultipartConfig {

    @Bean
    public DefaultMultipartFileReader defaultFileReader() {
        return new DefaultMultipartFileReader();
    }

    @Bean
    public MultipartAwareFormHttpMessageConverter multipartConverter() {
        MultipartAwareFormHttpMessageConverter converter =
            new MultipartAwareFormHttpMessageConverter();
        converter.setMultipartFileReader(defaultFileReader());
        return converter;
    }

    @Bean
    public HttpRequestHandlingMessagingGateway uploadGateway() {
        HttpRequestHandlingMessagingGateway gateway =
            new HttpRequestHandlingMessagingGateway();

        RequestMapping mapping = new RequestMapping();
        mapping.setPathPatterns("/upload");
        mapping.setMethods(HttpMethod.POST);
        mapping.setConsumes("multipart/form-data");
        gateway.setRequestMapping(mapping);

        gateway.setMessageConverters(List.of(multipartConverter()));
        gateway.setRequestChannel(uploadChannel());

        return gateway;
    }
}

FileCopyingMultipartFileReader

MultipartFileReader implementation that copies MultipartFile content to a new temporary File on the filesystem. Useful for large files or when file persistence is required beyond request scope.

public class FileCopyingMultipartFileReader
    implements MultipartFileReader<MultipartFile> {

    /**
     * Creates reader using default temporary directory.
     * Files are created in system temp directory.
     */
    public FileCopyingMultipartFileReader();

    /**
     * Creates reader using specified directory.
     * Files are created in the provided directory.
     *
     * @param directory the directory for temporary files
     */
    public FileCopyingMultipartFileReader(File directory);

    /**
     * Sets prefix for temporary files.
     * Default: "si_".
     *
     * @param prefix the file prefix
     */
    public void setPrefix(String prefix);

    /**
     * Sets suffix for temporary files.
     * Default: ".tmp".
     *
     * @param suffix the file suffix
     */
    public void setSuffix(String suffix);

    /**
     * Reads and copies multipart file to temporary File.
     * File is created with configured prefix and suffix.
     * Returns UploadedMultipartFile wrapping the temporary file.
     *
     * @param multipartFile the source multipart file
     * @return MultipartFile (UploadedMultipartFile) containing copied content
     * @throws IOException if copy error occurs
     */
    public MultipartFile readMultipartFile(MultipartFile multipartFile)
        throws IOException;
}

Usage Example - Default Temp Directory:

import org.springframework.context.annotation.Bean;
import org.springframework.integration.http.multipart.FileCopyingMultipartFileReader;

@Configuration
public class FileCopyingConfig {

    @Bean
    public FileCopyingMultipartFileReader fileCopyingReader() {
        FileCopyingMultipartFileReader reader =
            new FileCopyingMultipartFileReader();
        reader.setPrefix("upload_");
        reader.setSuffix(".dat");
        return reader;
    }

    @Bean
    public MultipartAwareFormHttpMessageConverter fileConverter() {
        MultipartAwareFormHttpMessageConverter converter =
            new MultipartAwareFormHttpMessageConverter();
        converter.setMultipartFileReader(fileCopyingReader());
        return converter;
    }
}

Usage Example - Custom Directory:

@Bean
public FileCopyingMultipartFileReader customDirectoryReader() {
    File uploadDir = new File("/var/app/uploads");
    if (!uploadDir.exists()) {
        uploadDir.mkdirs();
    }

    FileCopyingMultipartFileReader reader =
        new FileCopyingMultipartFileReader(uploadDir);
    reader.setPrefix("user_upload_");
    reader.setSuffix(".bin");

    return reader;
}

Usage Example - Java DSL:

import org.springframework.integration.dsl.IntegrationFlow;
import org.springframework.integration.http.dsl.Http;

@Bean
public IntegrationFlow fileUploadFlow() {
    FileCopyingMultipartFileReader reader =
        new FileCopyingMultipartFileReader();
    reader.setPrefix("doc_");
    reader.setSuffix(".pdf");

    MultipartAwareFormHttpMessageConverter converter =
        new MultipartAwareFormHttpMessageConverter();
    converter.setMultipartFileReader(reader);

    return IntegrationFlow
        .from(Http.inboundGateway("/documents/upload")
            .requestMapping(m -> m
                .methods(HttpMethod.POST)
                .consumes("multipart/form-data"))
            .messageConverters(converter))
        .<MultiValueMap<String, Object>>handle((payload, headers) -> {
            MultipartFile uploadedFile = (MultipartFile) payload.getFirst("file");
            return documentService.processDocument(uploadedFile);
        })
        .get();
}

SimpleMultipartFileReader

MultipartFileReader implementation that reads file content as String or byte array depending on Content-Type, without maintaining file metadata. Simplifies file handling when only content is needed.

public class SimpleMultipartFileReader
    implements MultipartFileReader<Object> {

    /**
     * Creates simple multipart file reader.
     * Returns String for text content, byte[] for binary content.
     */
    public SimpleMultipartFileReader();

    /**
     * Sets default charset for text content.
     * Used when Content-Type doesn't specify charset.
     * Default: ISO-8859-1.
     *
     * @param defaultCharset the default charset
     */
    public void setDefaultMultipartCharset(String defaultCharset);

    /**
     * Reads multipart file and returns String or byte[].
     * Returns String for text/* content types, byte[] otherwise.
     *
     * @param multipartFile the source multipart file
     * @return String for text content, byte[] for binary content
     * @throws IOException if read error occurs
     */
    public Object readMultipartFile(MultipartFile multipartFile)
        throws IOException;
}

Usage Example:

import org.springframework.context.annotation.Bean;
import org.springframework.integration.http.multipart.SimpleMultipartFileReader;

@Configuration
public class SimpleMultipartConfig {

    @Bean
    public SimpleMultipartFileReader simpleFileReader() {
        SimpleMultipartFileReader reader = new SimpleMultipartFileReader();
        reader.setDefaultMultipartCharset("UTF-8");
        return reader;
    }

    @Bean
    public MultipartAwareFormHttpMessageConverter simpleConverter() {
        MultipartAwareFormHttpMessageConverter converter =
            new MultipartAwareFormHttpMessageConverter();
        converter.setMultipartFileReader(simpleFileReader());
        return converter;
    }

    @Bean
    public IntegrationFlow simpleUploadFlow() {
        return IntegrationFlow
            .from(Http.inboundGateway("/upload/simple")
                .messageConverters(simpleConverter()))
            .<MultiValueMap<String, Object>>handle((payload, headers) -> {
                Object fileContent = payload.getFirst("file");
                if (fileContent instanceof String) {
                    // Text file content
                    String text = (String) fileContent;
                    return processTextFile(text);
                } else {
                    // Binary file content
                    byte[] data = (byte[]) fileContent;
                    return processBinaryFile(data);
                }
            })
            .get();
    }
}

UploadedMultipartFile

MultipartFile implementation representing an uploaded file with content in memory (byte array) or in a File. Provides access to file content after the HTTP request scope ends.

public class UploadedMultipartFile implements MultipartFile {

    /**
     * Creates instance with File-based content.
     * Content is stored in the provided file.
     *
     * @param file the file containing content
     * @param size the file size
     * @param contentType the content type
     * @param formParameterName the form parameter name
     * @param originalFilename the original filename
     */
    public UploadedMultipartFile(
        File file,
        long size,
        String contentType,
        String formParameterName,
        String originalFilename);

    /**
     * Creates instance with byte array content.
     * Content is stored in memory.
     *
     * @param bytes the file content
     * @param contentType the content type
     * @param formParameterName the form parameter name
     * @param originalFilename the original filename
     */
    public UploadedMultipartFile(
        byte[] bytes,
        String contentType,
        String formParameterName,
        String originalFilename);

    /**
     * Returns form parameter name.
     *
     * @return the parameter name
     */
    public String getName();

    /**
     * Returns file content as byte array.
     * Reads from file or returns in-memory bytes.
     *
     * @return the file content
     * @throws IOException if read error occurs
     */
    public byte[] getBytes() throws IOException;

    /**
     * Returns content type.
     *
     * @return the content type
     */
    public String getContentType();

    /**
     * Returns InputStream for file content.
     *
     * @return the input stream
     * @throws IOException if error occurs
     */
    public InputStream getInputStream() throws IOException;

    /**
     * Returns original filename from upload.
     *
     * @return the original filename
     */
    public String getOriginalFilename();

    /**
     * Returns file size.
     *
     * @return the size in bytes
     */
    public long getSize();

    /**
     * Checks if file is empty.
     *
     * @return true if size is 0
     */
    public boolean isEmpty();

    /**
     * Transfers file content to destination File.
     * Copies content to the specified file.
     *
     * @param dest the destination file
     * @throws IOException if transfer error occurs
     * @throws IllegalStateException if transfer not supported
     */
    public void transferTo(File dest) throws IOException, IllegalStateException;
}

Usage Example:

import org.springframework.integration.http.multipart.UploadedMultipartFile;
import org.springframework.web.multipart.MultipartFile;

public class FileProcessor {

    public void processUploadedFile(MultipartFile multipartFile)
            throws IOException {
        if (multipartFile instanceof UploadedMultipartFile) {
            UploadedMultipartFile uploaded = (UploadedMultipartFile) multipartFile;

            // Access file properties
            String originalName = uploaded.getOriginalFilename();
            String contentType = uploaded.getContentType();
            long size = uploaded.getSize();

            System.out.println("Processing: " + originalName);
            System.out.println("Type: " + contentType);
            System.out.println("Size: " + size + " bytes");

            // Read content
            byte[] content = uploaded.getBytes();

            // Or get input stream
            try (InputStream input = uploaded.getInputStream()) {
                // Process stream
                processStream(input);
            }

            // Transfer to permanent location
            File destination = new File("/var/app/files/" + originalName);
            uploaded.transferTo(destination);
        }
    }
}

MultipartHttpInputMessage

Implementation of ServletServerHttpRequest that wraps a MultipartHttpServletRequest. Provides access to multipart content through Spring's MultipartRequest interface.

public class MultipartHttpInputMessage
    extends ServletServerHttpRequest
    implements MultipartRequest {

    /**
     * Creates instance wrapping multipart request.
     *
     * @param multipartServletRequest the multipart HTTP servlet request
     */
    public MultipartHttpInputMessage(
        MultipartHttpServletRequest multipartServletRequest);

    /**
     * Returns MultipartFile for given name.
     *
     * @param name the parameter name
     * @return the multipart file or null
     */
    public MultipartFile getFile(String name);

    /**
     * Returns Map of parameter name to MultipartFile.
     *
     * @return map of files
     */
    public Map<String, MultipartFile> getFileMap();

    /**
     * Returns MultiValueMap of all MultipartFiles.
     * Single parameter may have multiple files.
     *
     * @return multi-value map of files
     */
    public MultiValueMap<String, MultipartFile> getMultiFileMap();

    /**
     * Returns Iterator of file parameter names.
     *
     * @return iterator of file names
     */
    public Iterator<String> getFileNames();

    /**
     * Returns List of MultipartFiles for given name.
     * Supports multiple files with same parameter name.
     *
     * @param name the parameter name
     * @return list of multipart files
     */
    public List<MultipartFile> getFiles(String name);

    /**
     * Returns MultiValueMap of request parameters.
     *
     * @return parameter map
     */
    public MultiValueMap<String, String> getParameterMap();

    /**
     * Returns content type for given parameter or file name.
     *
     * @param paramOrFileName the parameter or file name
     * @return the content type
     */
    public String getMultipartContentType(String paramOrFileName);
}

This class is typically used internally by the framework but can be used in custom converters or handlers.

Advanced Configuration Patterns

Multiple File Upload

Handle multiple file uploads in a single request:

@Bean
public IntegrationFlow multipleFileUploadFlow() {
    FileCopyingMultipartFileReader reader =
        new FileCopyingMultipartFileReader();

    MultipartAwareFormHttpMessageConverter converter =
        new MultipartAwareFormHttpMessageConverter();
    converter.setMultipartFileReader(reader);

    return IntegrationFlow
        .from(Http.inboundGateway("/upload/multiple")
            .requestMapping(m -> m
                .methods(HttpMethod.POST)
                .consumes("multipart/form-data"))
            .messageConverters(converter)
            .requestPayloadType(MultiValueMap.class))
        .<MultiValueMap<String, Object>>handle((payload, headers) -> {
            // Get all files
            List<MultipartFile> files = new ArrayList<>();

            // Extract multiple files from payload
            for (String key : payload.keySet()) {
                List<?> values = payload.get(key);
                for (Object value : values) {
                    if (value instanceof MultipartFile) {
                        files.add((MultipartFile) value);
                    }
                }
            }

            // Process all files
            return fileService.processMultipleFiles(files);
        })
        .get();
}

File Upload with Metadata

Handle file uploads with additional form fields:

@Bean
public IntegrationFlow fileWithMetadataFlow() {
    DefaultMultipartFileReader reader = new DefaultMultipartFileReader();

    MultipartAwareFormHttpMessageConverter converter =
        new MultipartAwareFormHttpMessageConverter();
    converter.setMultipartFileReader(reader);

    return IntegrationFlow
        .from(Http.inboundGateway("/upload/with-metadata")
            .messageConverters(converter)
            .requestPayloadType(MultiValueMap.class))
        .<MultiValueMap<String, Object>>handle((payload, headers) -> {
            // Extract file
            MultipartFile file =
                (MultipartFile) payload.getFirst("file");

            // Extract metadata from form fields
            String title = (String) payload.getFirst("title");
            String description = (String) payload.getFirst("description");
            String category = (String) payload.getFirst("category");

            // Create metadata object
            FileMetadata metadata = new FileMetadata();
            metadata.setTitle(title);
            metadata.setDescription(description);
            metadata.setCategory(category);

            // Process file with metadata
            return fileService.processFileWithMetadata(file, metadata);
        })
        .get();
}

Conditional File Storage

Choose storage strategy based on file size or type:

@Configuration
public class ConditionalStorageConfig {

    @Bean
    public MultipartFileReader<?> conditionalFileReader() {
        return new MultipartFileReader<Object>() {
            private final DefaultMultipartFileReader memoryReader =
                new DefaultMultipartFileReader();
            private final FileCopyingMultipartFileReader fileReader =
                new FileCopyingMultipartFileReader();

            @Override
            public Object readMultipartFile(MultipartFile multipartFile)
                    throws IOException {
                // Store large files on disk (returns MultipartFile/UploadedMultipartFile)
                if (multipartFile.getSize() > 5 * 1024 * 1024) { // 5MB
                    return fileReader.readMultipartFile(multipartFile);
                }
                // Store small files in memory (returns MultipartFile/UploadedMultipartFile)
                else {
                    return memoryReader.readMultipartFile(multipartFile);
                }
            }
        };
    }

    @Bean
    public IntegrationFlow conditionalStorageFlow() {
        MultipartAwareFormHttpMessageConverter converter =
            new MultipartAwareFormHttpMessageConverter();
        converter.setMultipartFileReader(conditionalFileReader());

        return IntegrationFlow
            .from(Http.inboundGateway("/upload/smart")
                .messageConverters(converter))
            .handle((payload, headers) -> {
                // All files returned as MultipartFile (UploadedMultipartFile)
                MultipartFile fileContent =
                    (MultipartFile) ((MultiValueMap<?, ?>) payload).getFirst("file");
                return fileService.processFile(fileContent);
            })
            .get();
    }
}

File Type Validation

Validate file types before processing:

@Bean
public IntegrationFlow fileValidationFlow() {
    DefaultMultipartFileReader reader = new DefaultMultipartFileReader();

    MultipartAwareFormHttpMessageConverter converter =
        new MultipartAwareFormHttpMessageConverter();
    converter.setMultipartFileReader(reader);

    return IntegrationFlow
        .from(Http.inboundGateway("/upload/validated")
            .messageConverters(converter)
            .requestPayloadType(MultiValueMap.class))
        .<MultiValueMap<String, Object>>handle((payload, headers) -> {
            MultipartFile file =
                (MultipartFile) payload.getFirst("file");

            // Validate file type
            String contentType = file.getContentType();
            if (!isAllowedContentType(contentType)) {
                throw new IllegalArgumentException(
                    "File type not allowed: " + contentType);
            }

            // Validate file size
            if (file.getSize() > 10 * 1024 * 1024) { // 10MB
                throw new IllegalArgumentException(
                    "File too large: " + file.getSize());
            }

            // Validate file extension
            String filename = file.getOriginalFilename();
            if (filename != null && !hasAllowedExtension(filename)) {
                throw new IllegalArgumentException(
                    "File extension not allowed: " + filename);
            }

            return fileService.processValidatedFile(file);
        })
        .get();
}

private boolean isAllowedContentType(String contentType) {
    return contentType != null && (
        contentType.startsWith("image/") ||
        contentType.equals("application/pdf") ||
        contentType.equals("text/plain")
    );
}

private boolean hasAllowedExtension(String filename) {
    String[] allowedExtensions = {".jpg", ".png", ".pdf", ".txt"};
    for (String ext : allowedExtensions) {
        if (filename.toLowerCase().endsWith(ext)) {
            return true;
        }
    }
    return false;
}

Async File Processing

Process uploaded files asynchronously:

@Configuration
public class AsyncFileProcessingConfig {

    @Bean
    public IntegrationFlow asyncFileUploadFlow() {
        FileCopyingMultipartFileReader reader =
            new FileCopyingMultipartFileReader();

        MultipartAwareFormHttpMessageConverter converter =
            new MultipartAwareFormHttpMessageConverter();
        converter.setMultipartFileReader(reader);

        return IntegrationFlow
            .from(Http.inboundGateway("/upload/async")
                .messageConverters(converter)
                .requestPayloadType(MultiValueMap.class))
            .<MultiValueMap<String, Object>>handle((payload, headers) -> {
                MultipartFile uploadedFile = (MultipartFile) payload.getFirst("file");

                // Generate tracking ID
                String trackingId = UUID.randomUUID().toString();

                // Store tracking info
                uploadTracking.put(trackingId, "PROCESSING");

                // Return tracking ID immediately
                Map<String, Object> response = new HashMap<>();
                response.put("trackingId", trackingId);
                response.put("status", "ACCEPTED");

                return response;
            })
            // Send to async processing channel
            .channel("fileProcessingChannel")
            .get();
    }

    @Bean
    public IntegrationFlow fileProcessingFlow() {
        return IntegrationFlow
            .from("fileProcessingChannel")
            .handle(Message.class, (message, headers) -> {
                String trackingId = (String) headers.get("trackingId");
                MultipartFile file = (MultipartFile) headers.get("uploadedFile");

                try {
                    // Process file asynchronously
                    fileService.processFile(file);
                    uploadTracking.put(trackingId, "COMPLETED");
                } catch (Exception e) {
                    uploadTracking.put(trackingId, "FAILED");
                }

                return null;
            })
            .get();
    }
}

Image Upload with Transformation

Handle image uploads with transformation:

@Bean
public IntegrationFlow imageUploadFlow() {
    FileCopyingMultipartFileReader reader =
        new FileCopyingMultipartFileReader();
    reader.setPrefix("img_");
    reader.setSuffix(".tmp");

    MultipartAwareFormHttpMessageConverter converter =
        new MultipartAwareFormHttpMessageConverter();
    converter.setMultipartFileReader(reader);

    return IntegrationFlow
        .from(Http.inboundGateway("/upload/image")
            .requestMapping(m -> m
                .methods(HttpMethod.POST)
                .consumes("multipart/form-data"))
            .messageConverters(converter)
            .requestPayloadType(MultiValueMap.class))
        .<MultiValueMap<String, Object>>handle((payload, headers) -> {
            MultipartFile imageFile = (MultipartFile) payload.getFirst("image");

            // Validate it's an image
            String contentType = imageFile.getContentType();
            if (contentType == null || !contentType.startsWith("image/")) {
                throw new IllegalArgumentException("Not an image file");
            }

            // Transform image (resize, optimize, etc.)
            // Convert MultipartFile to File if needed for processing
            File processedImage = imageService.processImage(imageFile);

            // Generate thumbnail
            File thumbnail = imageService.generateThumbnail(processedImage);

            // Return both files
            Map<String, Object> response = new HashMap<>();
            response.put("original", processedImage.getAbsolutePath());
            response.put("thumbnail", thumbnail.getAbsolutePath());

            return response;
        })
        .get();
}

Batch File Upload

Handle batch file uploads with progress tracking:

@Bean
public IntegrationFlow batchFileUploadFlow() {
    DefaultMultipartFileReader reader = new DefaultMultipartFileReader();

    MultipartAwareFormHttpMessageConverter converter =
        new MultipartAwareFormHttpMessageConverter();
    converter.setMultipartFileReader(reader);

    return IntegrationFlow
        .from(Http.inboundGateway("/upload/batch")
            .messageConverters(converter)
            .requestPayloadType(MultiValueMap.class))
        .<MultiValueMap<String, Object>>handle((payload, headers) -> {
            List<MultipartFile> files = new ArrayList<>();

            // Collect all uploaded files
            payload.values().forEach(values ->
                values.forEach(value -> {
                    if (value instanceof MultipartFile) {
                        files.add((MultipartFile) value);
                    }
                }));

            // Generate batch ID
            String batchId = UUID.randomUUID().toString();

            // Process files with progress tracking
            List<String> processedFiles = new ArrayList<>();
            int total = files.size();
            int current = 0;

            for (MultipartFile file : files) {
                current++;
                updateProgress(batchId, current, total);

                String processedPath = fileService.processFile(file);
                processedFiles.add(processedPath);
            }

            // Return results
            Map<String, Object> response = new HashMap<>();
            response.put("batchId", batchId);
            response.put("totalFiles", total);
            response.put("processedFiles", processedFiles);

            return response;
        })
        .get();
}