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

ASP.NET Web API文件验证属性重复读取请求内容报错解决方案咨询

解决ActionFilter中读取文件后Action无法重复读取流的问题

这个问题我之前也碰到过!核心原因就是HttpRequest的Content流是单向只读的,一旦在过滤器里读完验证,Action里再尝试读取就会因为流已经到末尾而报错。下面是完整的解决方案,同时优化了原代码里的异步处理和细节问题:

核心思路

在过滤器中一次性完成文件读取、验证,然后把验证通过的文件数据(文件名、字节数组)存入ActionContext.Items字典中,供后续的Action方法直接使用,完全避免二次读取Request流。

修改后的ValidateFileAttribute代码

public class ValidateFileAttribute : ActionFilterAttribute
{
    // 定义允许的文件扩展名和对应魔数
    private readonly List<string> _validExtensions = new() { "pdf", "doc", "docx" };
    private readonly List<string> _validMagicNumbers = new() { "25-50-44-46", "D0-CF-11-E0", "50-4B-03-04" };
    private const int MaxFileSize = 3 * 1024 * 1024; // 3MB

    public override async Task OnActionExecutingAsync(HttpActionContext actionContext, CancellationToken cancellationToken)
    {
        // 先判断是否为多部分表单请求
        if (!actionContext.Request.Content.IsMimeMultipartContent())
        {
            actionContext.Response = actionContext.Request.CreateErrorResponse(
                HttpStatusCode.BadRequest, "请求格式错误,请使用multipart/form-data格式上传文件");
            return;
        }

        try
        {
            // 读取所有文件内容
            var provider = await actionContext.Request.Content.ReadAsMultipartAsync(cancellationToken);
            var validatedFiles = new List<(string FileName, byte[] FileBytes)>();

            foreach (var content in provider.Contents)
            {
                // 获取文件名(处理可能带的双引号)
                var fileName = content.Headers.ContentDisposition?.FileName?.Trim('"') ?? string.Empty;
                if (string.IsNullOrEmpty(fileName))
                {
                    actionContext.Response = actionContext.Request.CreateErrorResponse(
                        HttpStatusCode.BadRequest, "存在未命名的上传文件");
                    return;
                }

                // 读取文件字节数组
                var fileBytes = await content.ReadAsByteArrayAsync(cancellationToken);
                if (fileBytes == null || fileBytes.Length == 0)
                {
                    actionContext.Response = actionContext.Request.CreateErrorResponse(
                        HttpStatusCode.BadRequest, $"文件{fileName}为空,请重新上传");
                    return;
                }

                // 1. 验证扩展名
                var extension = Path.GetExtension(fileName).TrimStart('.').ToLowerInvariant();
                if (!_validExtensions.Contains(extension))
                {
                    actionContext.Response = actionContext.Request.CreateErrorResponse(
                        HttpStatusCode.BadRequest, $"文件{fileName}格式不支持,仅允许:{string.Join(", ", _validExtensions)}");
                    return;
                }

                // 2. 验证魔数(取前4个字节)
                var magicNumber = BitConverter.ToString(fileBytes, 0, 4);
                if (!_validMagicNumbers.Contains(magicNumber, StringComparer.OrdinalIgnoreCase))
                {
                    actionContext.Response = actionContext.Request.CreateErrorResponse(
                        HttpStatusCode.BadRequest, $"文件{fileName}格式与扩展名不符,请检查文件是否损坏");
                    return;
                }

                // 3. 验证文件大小
                if (fileBytes.Length > MaxFileSize)
                {
                    actionContext.Response = actionContext.Request.CreateErrorResponse(
                        HttpStatusCode.BadRequest, $"文件{fileName}大小超过3MB限制");
                    return;
                }

                // 验证通过,加入列表
                validatedFiles.Add((fileName, fileBytes));
            }

            // 将验证后的文件存入ActionContext,供Action使用
            actionContext.ActionContext.Items["ValidatedFiles"] = validatedFiles;
        }
        catch (Exception ex)
        {
            actionContext.Response = actionContext.Request.CreateErrorResponse(
                HttpStatusCode.InternalServerError, $"文件上传处理失败:{ex.Message}");
        }
    }
}

修改后的Upload Action代码

[ValidateFile]
[Route("upload")]
[HttpPost]
public async Task<DocumentUploadResponse> Upload()
{
    // 从ActionContext中获取验证后的文件数据
    if (!ActionContext.Items.TryGetValue("ValidatedFiles", out var filesObj) || 
        filesObj is not List<(string FileName, byte[] FileBytes)> validatedFiles)
    {
        return new DocumentUploadResponse 
        { 
            Success = false, 
            Message = "未获取到有效的上传文件" 
        };
    }

    var response = new DocumentUploadResponse 
    { 
        Success = true, 
        UploadedFileNames = new List<string>() 
    };

    foreach (var (fileName, fileBytes) in validatedFiles)
    {
        // 转换为Base64字符串
        var base64File = Convert.ToBase64String(fileBytes);

        // 执行数据库存储操作(示例)
        // var document = new Document 
        // { 
        //     FileName = fileName, 
        //     FileContentBase64 = base64File,
        //     UploadTime = DateTime.UtcNow
        // };
        // _dbContext.Documents.Add(document);
        // await _dbContext.SaveChangesAsync();

        response.UploadedFileNames.Add(fileName);
    }

    return response;
}

// 假设的响应模型
public class DocumentUploadResponse
{
    public bool Success { get; set; }
    public string? Message { get; set; }
    public List<string>? UploadedFileNames { get; set; }
}

关键优化点说明

  • 避免重复读流:过滤器中一次性读取所有文件并验证,将结果存入ActionContext.Items,Action直接取用,彻底解决流只能读一次的问题。
  • 异步处理优化:去掉了原代码中Task.Factory.StartNew和.Result的错误用法,改用await避免异步死锁和线程阻塞。
  • 细节处理:
    • 处理文件名可能带的双引号(ContentDisposition.FileName默认会用双引号包裹)。
    • 用Path.GetExtension更可靠地获取文件扩展名。
    • 魔数验证直接取前4个字节生成字符串,比Substring更准确。
  • 错误提前终止:一旦某一步验证失败,立即返回错误响应,不再继续处理其他文件。

内容的提问来源于stack exchange,提问作者rajk

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.15 04:47:27