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

Angular调用ASP.NET Core Web API下载文件时404错误的排查与解决

ASP.NET Core 6 + Angular 14 PDF下载404问题排查与解决

问题场景

使用ASP.NET Core 6 Web API做后端、Angular 14做前端实现PDF下载功能,点击下载按钮后前端控制台返回404错误,请求URL为https://localhost:44361/api/v1/download-leave-document-by-id/88f88414-e171-45db-8a8c-71456f251f83。

后端代码(ASP.NET Core 6)

public async Task<IActionResult> GetLeaveDocuments(Guid id)
{
    try
    {
        var leaveAttachment = _dbContext.LeaveApplications
            .Include(ma => ma.LeaveApproval)
            .ThenInclude(ma => ma.LeaveApprovalAttachments)
            .Where(ma => (bool)ma.IsApproved)
            .SelectMany(ma => ma.LeaveApproval.LeaveApprovalAttachments)
            .FirstOrDefault(maa => maa.Id == id);
        var filePath = Path.Combine(Directory.GetCurrentDirectory(), leaveAttachment.FilePath);
        var memory = new MemoryStream();
        using (var stream = new FileStream(filePath, FileMode.Open))
        {
            await stream.CopyToAsync(memory);
        }
        memory.Position = 0;
        var contentType = "application/octet-stream";
        var fileName = Path.GetFileName(filePath);
        return File(memory, contentType, fileName);
    }
    catch (Exception ex)
    {
        return StatusCode(500, $"Internal server error: {ex}");
    }
}

前端代码(Angular 14)

服务代码

getLeaveDocument(id: string): Observable<Blob> {
  return this.http.get(`${this.baseUrl}/download-leave-document-by-id/${id}`, {
    responseType: 'blob'
  });
}

组件.ts代码

downloadFile(id: string, contentType:string){
    this.leaveService.getLeaveDocument(id).subscribe({
      next: (res: Blob) => {
        const blob = new Blob([res], {type: contentType});
        const url = window.URL.createObjectURL(blob);
        window.open(url);
      }
    })
}

组件.html代码

<tr *ngFor="let mDocument of leave?.leaveApproval?.leaveApprovalAttachments; let i = index;">
  <td style="width: 60px">{{ i + 1}}.</td>
  <td>{{mDocument?.fileName || 'N/A'}}</td>
  <td>{{mDocument?.fileType || 'N/A'}}</td>
  <td><input type="button" class="btn btn-success" (click)="downloadFile(mDocument.id, mDocument.mimeType)" value="Download"/></td>
</tr>

错误信息

{
    "headers": {
        "normalizedNames": {},
        "lazyUpdate": null,
        "headers": {}
    },
    "status": 404,
    "statusText": "OK",
    "url": "https://localhost:44361/api/v1/download-leave-document-by-id/88f88414-e171-45db-8a8c-71456f251f83",
    "ok": false,
    "name": "HttpErrorResponse",
    "message": "Http failure response for https://localhost:44361/api/v1/download-leave-document-by-id/88f88414-e171-45db-8a8c-71456f251f83: 404 OK",
    "error": {}
}

问题原因分析

404错误核心是前端请求的API路由未被后端匹配,或目标资源(数据库记录/文件)不存在,优先级从高到低排查:

  1. 后端控制器未配置与前端请求匹配的路由特性;
  2. 数据库查询因IsApproved过滤条件导致未找到对应附件;
  3. 文件路径拼接错误,导致实际文件不存在;
  4. 前端baseUrl配置错误,导致请求路径偏差。

解决步骤

1. 确保API路由完全匹配

ASP.NET Core Web API的路由由控制器和方法上的特性定义,与方法名无关。需给控制器和目标方法添加对应路由:

[ApiController]
[Route("api/v1")] // 匹配前端baseUrl的/api/v1前缀
public class LeaveController : ControllerBase
{
    // 匹配前端请求的/download-leave-document-by-id/{id}路径
    [HttpGet("download-leave-document-by-id/{id:guid}")]
    public async Task<IActionResult> GetLeaveDocuments(Guid id)
    {
        // 原代码逻辑...
    }
}

2. 修复数据库查询逻辑并增加空值校验

原查询强制过滤IsApproved=true的申请,若目标附件对应的申请未批准,会返回null,后续访问leaveAttachment.FilePath会触发异常,但后端catch返回500而非404,需提前校验并返回明确的404:

public async Task<IActionResult> GetLeaveDocuments(Guid id)
{
    try
    {
        var leaveAttachment = _dbContext.LeaveApplications
            .Include(ma => ma.LeaveApproval)
            .ThenInclude(ma => ma.LeaveApprovalAttachments)
            .Where(ma => ma.IsApproved == true)
            .SelectMany(ma => ma.LeaveApproval.LeaveApprovalAttachments)
            .FirstOrDefault(maa => maa.Id == id);

        // 新增空值校验,返回明确的404
        if (leaveAttachment == null)
        {
            return NotFound("附件不存在或对应的休假申请未通过审批");
        }

        // 后续文件读取逻辑...
    }
    catch (Exception ex)
    {
        return StatusCode(500, $"Internal server error: {ex}");
    }
}

3. 修正文件路径拼接逻辑

Directory.GetCurrentDirectory()在ASP.NET Core中返回的是项目启动目录,若文件存储在wwwroot下,应使用IWebHostEnvironment.WebRootPath来拼接路径,避免路径错误:

// 先在控制器构造函数中注入IWebHostEnvironment
private readonly IWebHostEnvironment _webHostEnv;
private readonly YourDbContext _dbContext;

public LeaveController(IWebHostEnvironment webHostEnv, YourDbContext dbContext)
{
    _webHostEnv = webHostEnv;
    _dbContext = dbContext;
}

// 替换路径拼接代码
var filePath = Path.Combine(_webHostEnv.WebRootPath, leaveAttachment.FilePath);

// 新增文件存在校验
if (!System.IO.File.Exists(filePath))
{
    return NotFound("文件不存在");
}

4. 优化前端下载逻辑(避免弹窗拦截)

原代码用window.open()可能被浏览器拦截,改为创建<a>标签触发下载,同时传递文件名:

组件.ts代码修改

downloadFile(id: string, contentType:string, fileName:string){
    this.leaveService.getLeaveDocument(id).subscribe({
        next: (res: Blob) => {
            const blob = new Blob([res], {type: contentType});
            const url = window.URL.createObjectURL(blob);
            const downloadLink = document.createElement('a');
            downloadLink.href = url;
            downloadLink.download = fileName; // 指定下载文件名
            document.body.appendChild(downloadLink);
            downloadLink.click();
            window.URL.revokeObjectURL(url); // 释放URL对象
            document.body.removeChild(downloadLink);
        },
        error: (err) => {
            console.error('下载失败:', err);
            // 可添加用户提示,比如alert(err.error || '下载失败')
        }
    })
}

组件.html代码修改(传递fileName参数)

<td><input type="button" class="btn btn-success" (click)="downloadFile(mDocument.id, mDocument.mimeType, mDocument.fileName)" value="Download"/></td>

5. 验证前端baseUrl配置

确认Angular服务中的baseUrl为https://localhost:44361/api/v1,避免末尾多斜杠或路径错误,比如:

private baseUrl = 'https://localhost:44361/api/v1';

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.17 12:55:23