ASP Web API返回405错误:PDF无法在第三方预览工具显示
咱们先搞清楚为啥第三方工具用不了:
- PDFescape报405错误:这个工具大概率会发送
HEAD请求(用来探测文件的存在性和大小),但你的接口只标注了[HttpGet],不支持HEAD方法,所以返回405不允许访问。 - Google Docs Viewer不显示文件:这类预览工具通常会发送带
Range请求头的分块请求(实现渐进加载),而你的接口直接返回整个文件的200响应,没有处理分块请求,导致工具无法正确解析内容。另外如果你的API和预览工具跨域,CORS限制也可能导致工具拿不到文件。
下面给你两种解决方案:
方案一:改造现有接口兼容第三方工具
这种方式不需要生成直接链接,调整接口逻辑就能让预览工具正常工作。
1. 允许HEAD请求
把接口的[HttpGet]改成支持GET和HEAD,或者在方法内判断请求类型,HEAD请求只返回响应头:
[AcceptVerbs("GET", "HEAD")] public HttpResponseMessage GetAttachment(string id) { try { string mapping = @"\\192.168.3.3\Archieve"; string sourcedir = @"\Digital\"; string filename = id + ".pdf"; string sourceFullPath = mapping + sourcedir + filename; var credential = new NetworkCredential("user", "pass", "192.168.3.3"); HttpResponseMessage response; if (Request.Method == HttpMethod.Head) { // HEAD请求只返回响应头,不返回内容 response = Request.CreateResponse(HttpStatusCode.OK); using (new NetworkConnection(mapping, credential)) { var fileInfo = new FileInfo(sourceFullPath); response.Content.Headers.ContentLength = fileInfo.Length; } response.Content.Headers.ContentType = new MediaTypeHeaderValue("application/pdf"); response.Content.Headers.ContentDisposition = new ContentDispositionHeaderValue("inline") { FileName = filename }; return response; } // 原GET请求逻辑 byte[] dataBytes; using (new NetworkConnection(mapping, credential)) { dataBytes = File.ReadAllBytes(sourceFullPath); } response = Request.CreateResponse(HttpStatusCode.OK); response.Content = new StreamContent(new MemoryStream(dataBytes)); response.Content.Headers.ContentDisposition = new ContentDispositionHeaderValue("inline") { FileName = filename }; response.Content.Headers.ContentType = new MediaTypeHeaderValue("application/pdf"); response.Content.Headers.ContentLength = dataBytes.Length; return response; } catch (Exception ex) { return Request.CreateResponse(HttpStatusCode.Gone, ex.Message); } }
这里把ContentDisposition改成inline,更适合预览工具直接读取(之前的attachment是提示下载,部分工具可能会识别为下载而非预览)。
2. 处理Range分块请求
为了支持Google Docs Viewer这类工具的渐进加载,需要解析Range请求头,返回206 Partial Content响应,代码示例如下:
[AcceptVerbs("GET", "HEAD")] public HttpResponseMessage GetAttachment(string id) { try { string mapping = @"\\192.168.3.3\Archieve"; string sourcedir = @"\Digital\"; string filename = id + ".pdf"; string sourceFullPath = mapping + sourcedir + filename; var credential = new NetworkCredential("user", "pass", "192.168.3.3"); HttpResponseMessage response; using (new NetworkConnection(mapping, credential)) { var fileInfo = new FileInfo(sourceFullPath); long fileLength = fileInfo.Length; if (Request.Method == HttpMethod.Head) { response = Request.CreateResponse(HttpStatusCode.OK); response.Content.Headers.ContentLength = fileLength; response.Content.Headers.ContentType = new MediaTypeHeaderValue("application/pdf"); response.Content.Headers.ContentDisposition = new ContentDispositionHeaderValue("inline") { FileName = filename }; return response; } // 处理Range请求 var rangeHeader = Request.Headers.Range; if (rangeHeader != null && rangeHeader.Ranges.Count > 0) { var range = rangeHeader.Ranges.First(); long start = range.From ?? 0; long end = range.To ?? fileLength - 1; if (start >= fileLength) { return Request.CreateResponse(HttpStatusCode.RequestedRangeNotSatisfiable); } end = Math.Min(end, fileLength - 1); long contentLength = end - start + 1; // 读取指定范围的字节 byte[] rangeBytes = new byte[contentLength]; using (var fs = new FileStream(sourceFullPath, FileMode.Open, FileAccess.Read)) { fs.Seek(start, SeekOrigin.Begin); fs.Read(rangeBytes, 0, (int)contentLength); } response = Request.CreateResponse(HttpStatusCode.PartialContent); response.Content = new StreamContent(new MemoryStream(rangeBytes)); response.Content.Headers.ContentRange = new ContentRangeHeaderValue(start, end, fileLength); response.Content.Headers.ContentLength = contentLength; } else { // 返回完整文件 byte[] dataBytes = File.ReadAllBytes(sourceFullPath); response = Request.CreateResponse(HttpStatusCode.OK); response.Content = new StreamContent(new MemoryStream(dataBytes)); response.Content.Headers.ContentLength = dataBytes.Length; } response.Content.Headers.ContentType = new MediaTypeHeaderValue("application/pdf"); response.Content.Headers.ContentDisposition = new ContentDispositionHeaderValue("inline") { FileName = filename }; return response; } } catch (Exception ex) { return Request.CreateResponse(HttpStatusCode.Gone, ex.Message); } }
3. 配置CORS(跨域资源共享)
如果你的API和预览工具不在同一个域名下,需要允许跨域访问。先通过NuGet安装Microsoft.AspNet.WebApi.Cors,然后在WebApiConfig.cs中添加:
public static void Register(HttpConfiguration config) { // 允许指定域名跨域访问,支持GET和HEAD方法 var cors = new EnableCorsAttribute("https://docs.google.com,https://www.pdfescape.com", "*", "GET,HEAD"); config.EnableCors(cors); // 其他Web API配置... }
方案二:生成直接的文件链接
如果一定要生成静态文件的直接链接,可以用以下两种方式:
1. 临时复制到Web静态目录
把网络共享的PDF复制到Web服务器的静态文件目录,返回该文件的URL,之后记得定时清理临时文件:
[HttpGet] public HttpResponseMessage GetAttachmentDirectLink(string id) { try { string mapping = @"\\192.168.3.3\Archieve"; string sourcedir = @"\Digital\"; string filename = id + ".pdf"; string sourceFullPath = mapping + sourcedir + filename; // 静态临时目录,确保Web.config允许访问 string tempStaticDir = HttpContext.Current.Server.MapPath("~/Content/TempPDFs/"); Directory.CreateDirectory(tempStaticDir); string targetPath = Path.Combine(tempStaticDir, filename); var credential = new NetworkCredential("user", "pass", "192.168.3.3"); using (new NetworkConnection(mapping, credential)) { File.Copy(sourceFullPath, targetPath, overwrite: true); } // 生成直接访问URL string directUrl = Url.Content($"~/Content/TempPDFs/{filename}"); return Request.CreateResponse(HttpStatusCode.OK, new { DirectFileUrl = directUrl }); } catch (Exception ex) { return Request.CreateResponse(HttpStatusCode.Gone, ex.Message); } }
注意:需要定时清理TempPDFs目录的文件,避免磁盘空间被占满,可以写个后台任务或者用Windows计划任务实现。
2. IIS虚拟目录映射
如果你的Web服务器(IIS)有权限访问网络共享,可以直接在IIS中添加虚拟目录,指向\\192.168.3.3\Archieve\Digital,配置虚拟目录的访问凭据(和你代码中的NetworkCredential一致),这样就能直接通过http://myserver/你的虚拟目录名/0317101532.pdf访问文件,不需要写任何代码。
总结
优先推荐方案一,改造现有接口兼容第三方工具,不需要额外维护静态文件,也更安全;如果业务必须需要直接链接,可以选择方案二的临时复制或IIS虚拟目录方式。
内容的提问来源于stack exchange,提问作者iha

