You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React+Spring Boot传大文件到Azure Blob遇400错误(InvalidBlobOrBlock)

分块上传Azure Blob Storage时InvalidBlobOrBlock错误排查与解决

问题场景

上传200-300MB的Zip文件到Azure Blob Storage,采用React前端分块、Spring Boot后端接收合并的方案。调用blockBlobClient.stageBlock()处理第11块(索引10)时返回400错误,错误码InvalidBlobOrBlock,提示指定的块内容无效。

错误原因分析

  1. ByteArrayInputStream流状态问题:ByteArrayInputStream的读取指针是单向的,Azure SDK调用stageBlock时可能改变流的状态;高并发场景下也可能出现流状态意外修改,导致块内容无法正确读取。
  2. 块顺序不可靠:后端通过List按接收顺序存储块,若前端因网络波动导致块乱序到达,会出现块ID与内容不匹配的情况,触发无效块错误。
  3. 内存存储风险:大文件的所有块都存储在内存中,不仅占用大量内存,还可能引发数据损坏。

解决方案

1. 替换流为直接使用字节数组

Azure Blob SDK提供了直接接受字节数组的stageBlock重载方法,无需使用ByteArrayInputStream,彻底规避流状态问题。

2. 按块编号有序存储

改用按块编号为Key的有序Map存储块数据,确保合并时严格按分块顺序处理,不受接收顺序影响。

3. 优化内存占用(可选)

对于超大型文件,建议将块存储到临时文件而非内存,避免内存溢出。

修改后的Spring Boot代码

import com.azure.storage.blob.BlobClient;
import com.azure.storage.blob.BlobServiceClient;
import com.azure.storage.blob.specialized.BlockBlobClient;
import com.app.exception.general.AppException;

import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentSkipListMap;

@Log4j2
@Service
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class FeatureAnalyticsService {

    // 按文件名存储块ID,key为块编号(保证有序)
    private static final Map<String, Map<Integer, String>> chunkIdMap = new ConcurrentHashMap<>();
    // 按文件名存储块字节数据,key为块编号(保证有序)
    private static final Map<String, Map<Integer, byte[]>> chunkDataMap = new ConcurrentHashMap<>();

    private final BlobServiceClient blobServiceClient;

    private static byte @NonNull [] getChunkBytes(@NonNull MultipartFile fileChunk) throws IOException {
        return fileChunk.getInputStream().readAllBytes();
    }

    public boolean uploadInsights(
            String filename, @NonNull MultipartFile fileChunk, int chunkNumber, int totalChunks) {
        BlobClient blobClient = getBlobClient(filename);
        String base64ChunkId = Base64.getEncoder().encodeToString(String.valueOf(chunkNumber).getBytes());

        // 初始化当前文件的块存储结构
        chunkIdMap.computeIfAbsent(filename, k -> new ConcurrentSkipListMap<>());
        chunkDataMap.computeIfAbsent(filename, k -> new ConcurrentSkipListMap<>());

        // 存储当前块的ID和数据
        chunkIdMap.get(filename).put(chunkNumber, base64ChunkId);
        try {
            byte[] chunkBytes = getChunkBytes(fileChunk);
            chunkDataMap.get(filename).put(chunkNumber, chunkBytes);
            log.info("Received chunk {} for file {}, size: {}", chunkNumber, filename, chunkBytes.length);
        } catch (IOException e) {
            log.error("Failed to read chunk {} for file {}", chunkNumber, filename, e);
            cleanup(filename);
            throw new AppException(e.getMessage());
        }

        // 所有块接收完成后合并
        if (chunkIdMap.get(filename).size() == totalChunks) {
            BlockBlobClient blockBlobClient = blobClient.getBlockBlobClient();
            List<String> sortedChunkIds = new ArrayList<>(chunkIdMap.get(filename).values());
            List<byte[]> sortedChunkData = new ArrayList<>(chunkDataMap.get(filename).values());

            try {
                for (int i = 0; i < sortedChunkIds.size(); i++) {
                    String blockId = sortedChunkIds.get(i);
                    byte[] chunkBytes = sortedChunkData.get(i);
                    log.info("Staging chunk {}, size: {}", i, chunkBytes.length);
                    // 使用字节数组重载方法,避免流的问题
                    blockBlobClient.stageBlock(blockId, chunkBytes, chunkBytes.length);
                }
                blockBlobClient.commitBlockList(sortedChunkIds);
                log.info("Successfully merged and uploaded file {}", filename);
                cleanup(filename);
                return true;
            } catch (Exception e) {
                log.error("Failed to merge chunks for file {}", filename, e);
                cleanup(filename);
                return false;
            }
        }
        return true;
    }

    private void cleanup(String filename) {
        chunkIdMap.remove(filename);
        chunkDataMap.remove(filename);
    }

    private @NonNull BlobClient getBlobClient(String filename) {
        return blobServiceClient.createBlobContainerIfNotExists("uploads").getBlobClient(filename);
    }
}

额外优化建议

  • 前端增加重试机制:对上传失败的块实现自动重试,避免因网络波动导致上传中断。
  • 后端增加块校验:接收块时计算MD5哈希值,与前端传入的哈希值对比,确保块数据完整性。
  • 临时文件存储块:对于200-300MB的文件,可将块写入临时文件减少内存占用,示例代码:
    // 替换内存存储为临时文件
    private static final Map<String, Map<Integer, File>> chunkFileMap = new ConcurrentHashMap<>();
    // 接收块时写入临时文件
    File tempFile = File.createTempFile("chunk-", ".tmp");
    Files.copy(fileChunk.getInputStream(), tempFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
    chunkFileMap.get(filename).put(chunkNumber, tempFile);
    // stageBlock时读取临时文件
    blockBlobClient.stageBlock(blockId, Files.newInputStream(tempFile.toPath()), tempFile.length());
    

内容的提问来源于stack exchange,提问作者this.srivastava

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.06 15:34:56