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路由未被后端匹配,或目标资源(数据库记录/文件)不存在,优先级从高到低排查:
- 后端控制器未配置与前端请求匹配的路由特性;
- 数据库查询因
IsApproved过滤条件导致未找到对应附件; - 文件路径拼接错误,导致实际文件不存在;
- 前端
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
相关产品推荐
相关产品推荐

