乐于分享
好东西不私藏

上传pdf到oss并且实现在浏览器中预览的流程和踩坑

上传pdf到oss并且实现在浏览器中预览的流程和踩坑

上传pdf到oss并且实现在浏览器中预览的流程和踩坑

之前做过一个需求:实现在浏览器中预览 pdf。虽然需求比较简单,不过有一些小细节可以展开讲。

本文按照实际链路,梳理从上传 PDF 到浏览器预览的流程,以及其中遇到的几个问题。

1. 上传 PDF

上传文件前,服务端需要确定 Bucket、Object Key、访问凭证和文件内容。

下面使用 OSS Node.js SDK 上传一份 PDF,并在上传请求中设置与浏览器预览有关的请求头:

OSS Node.js SDK 官方文档[1]

const OSS = require("ali-oss");

const path = require("path");

async function uploadPdf() {

  const client = new OSS({

    region: "oss-<region-id>",

    accessKeyId: process.env.OSS_ACCESS_KEY_ID,

    accessKeySecret: process.env.OSS_ACCESS_KEY_SECRET,

    authorizationV4: true,

    bucket: "example-bucket",

  });

  const headers = {

    "Content-Type": "application/pdf",

    "Content-Disposition": "inline; filename=\"document.pdf\"",

    "x-oss-object-acl": "private",

    "x-oss-forbid-overwrite": "true",

  };

  const objectKey = "files/document.pdf";

  const localFilePath = path.normalize("document.pdf");

  const result = await client.put(objectKey, localFilePath, { headers });

  console.log("文件上传完成", result);

}

uploadPdf().catch(console.error);

Content-Type: application/pdf 告诉客户端文件内容是 PDF。Content-Disposition: inline 表示这份内容适合在浏览器中展示,filename 则提供建议文件名。

PutObject 官方文档[2]列出了上传时对于 content-disposition 的指定的请求头:

指定Object的展示形式。取值如下:

• Content-Disposition:inline:直接预览文件内容。

• Content-Disposition:attachment:以原文件名的形式下载到浏览器指定路径。

• Content-Disposition:attachment; filename=“yourFileName”:以自定义文件名的形式下载到浏览器指定路径。yourFileName用于自定义下载后的文件名称,例如example.jpg。

在 PutObject 请求中设置的 Content-TypeContent-Disposition 等字段,会被 OSS 保存为 Object Metadata。以后读取 Object 时,相应值会成为 HTTP 响应头。不过Object Metadata 不是浏览器最终响应的唯一来源。

OSS Metadata 到 HTTP 响应头

2. 获取访问 URL

公共读 Bucket 中的 Object 可以通过公开 URL 访问。私有 Bucket 则要求请求携带有效签名,通常由服务端生成有时效的预签名 URL。

使用预签名 URL 下载(Node.js SDK)[3]

const OSS = require("ali-oss");

async function generateSignatureUrl(fileName) {

  const client = new OSS({

    endpoint: "https://files.example.com", // 可以指定自定义域名

    accessKeyId: process.env.OSS_ACCESS_KEY_ID,

    accessKeySecret: process.env.OSS_ACCESS_KEY_SECRET,

    bucket: "example-bucket",

    region: "oss-<region-id>",

    authorizationV4: true,

    cname: true,

  });

  return client.signatureUrlV4(

    "GET",

    3600,

    { headers: {} },

    fileName,

  );

}

generateSignatureUrl("files/document.pdf")

  .then((url) => console.log("Generated URL:", url))

  .catch(console.error);

预签名 URL 解决的是私有 Object 的临时访问权限,不决定文件应该预览还是下载。浏览器最终怎样处理 PDF,仍取决于访问域名和最终响应头。

3. 访问域名如何改变最终响应

同一份 Object 可以通过 OSS 默认域名或已经绑定到 Bucket 的自定义域名访问。两类域名可能得到不同的最终响应。

3.1. OSS 默认域名可能增加强制下载头

阿里云文件预览排查文档[4]说明了 OSS 默认域名的强制下载规则。

为降低特定文件通过默认域名直接执行的安全风险,命中规则的请求会收到:

x-oss-force-download: true

Content-Disposition: attachment

是否命中强制下载规则,取决于 Bucket、账号、地域和文件类型等条件。OSS 默认域名并不是在所有情况下都会强制下载全部文件,具体范围应以阿里云文档中的规则为准。

如果 Object Metadata 原本保存的是:

Content-Disposition: inline

请求命中默认域名的强制下载策略后,浏览器最终可能收到:

x-oss-force-download: true

Content-Disposition: attachment

这意味着问题不一定出在上传代码。即使上传时保存了 inline,OSS 仍可能在形成最终响应时加入 attachment

3.2. 自定义域名不会增加默认域名下载头

自定义域名需要先绑定到 Bucket,再通过 CNAME 指向 OSS。它不是简单地把预签名 URL 中的域名替换掉。

阿里云自定义域名文档[5]说明,通过自定义域名访问时,OSS 不会增加默认域名使用的强制下载响应头。

但自定义域名不会主动添加 Content-Disposition: inline。它只是不会触发默认域名的强制下载规则,最终响应仍取决于 Object Metadata、Content-Type 和浏览器支持情况。

4. 浏览器如何处理 PDF 响应

浏览器收到响应后,主要通过 Content-TypeContent-Disposition 判断内容是什么,以及应该怎样处理。

4.1. Content-Type 描述内容是什么

Content-Type[6] 描述响应内容的媒体类型:

Content-Type: application/pdf

浏览器据此知道响应内容是 PDF。如果该字段缺失或被设置为 application/octet-stream,浏览器只能将它识别为通用二进制内容,可能无法选择正确的展示方式。

4.2. Content-Disposition 描述怎样处理内容

Content-Disposition[7] 用于说明响应内容应该内联展示,还是作为附件下载。

取值或参数 示例 含义
inline Content-Disposition: inline 内容可以在页面中展示
attachment Content-Disposition: attachment 内容应该作为附件下载
filename attachment; filename="document.pdf" 建议使用的 ASCII 文件名
filename* attachment; filename*=UTF-8''... 使用编码表示非 ASCII 文件名

filenamefilename* 是附加参数,不是与 inlineattachment 并列的处理方式。

下面两组响应的 Content-Type 相同,但处理语义不同:

Content-Type: application/pdf

Content-Disposition: inline

浏览器支持 PDF 时,通常会直接展示内容。

Content-Type: application/pdf

Content-Disposition: attachment

浏览器知道内容是 PDF,但仍会按照附件语义处理。

4.3. CORS

如果前端通过 fetch 读取不同源的 pdfUrl,浏览器会 CORS 校验:

项目发起跨域 fetch

→ 浏览器发送 Origin

→ OSS 根据 Bucket 的 CORS 规则返回响应头

→ 浏览器检查当前 Origin 是否被允许

→ 通过后 JavaScript 才能读取响应

OSS 的 CORS 规则通常需要关注:

配置 作用
Allowed Origin 允许哪些页面来源读取 Object
Allowed Methods 允许 GETHEAD 等哪些方法
Allowed Headers 允许请求携带哪些请求头
Expose Headers 允许 JavaScript 读取哪些响应头

配置入口是:进入阿里云 OSS 控制台的 Bucket 列表,选择目标 Bucket,再打开 数据安全 → 跨域设置,单击 创建规则

具体参数见阿里云 OSS 跨域设置文档[8]

5. 选择 PDF 预览方式

前端拿到 PDF URL 后,可以让 JavaScript 读取文件并生成 Blob URL,也可以直接让浏览器加载远程 URL。两种方式经过的链路不同。

5.1. 方式一:JavaScript 读取 PDF,再生成 Blob URL

如果远程 URL 返回 attachment,仍可以先读取文件,再用本地 Blob URL 打开 PDF:

const response = await fetch(pdfUrl);

if (!response.ok) {

  throw new Error(`PDF 请求失败:${response.status}`);

}

const pdfBlob = await response.blob();

const blobUrl = URL.createObjectURL(pdfBlob);

const previewWindow = window.open(blobUrl, "_blank");

if (!previewWindow) {

  URL.revokeObjectURL(blobUrl);

  throw new Error("浏览器阻止了预览窗口");

}

// 预览页面不再使用该 URL 后,再调用 URL.revokeObjectURL(blobUrl)。

这个方案重新构造了一个本地资源,能够避开远程 attachment 对顶层导航的影响,但没有改变远程响应本身。

它还要求 fetch 通过 CORS 校验,并需要等待整份 PDF 被读取。对于大文件,这会增加内存占用,也失去了浏览器直接加载远程 PDF 时可能使用的分段请求能力。

5.1.1. 踩坑:保存文件名退化为 UUID

Blob URL 形如下面这样:

blob:https://example.com/<uuid>

它不会自动继承远程响应中的 Content-Disposition 和文件名。

在本项目使用的浏览器中,打开 Blob URL 后,地址栏、页签和 PDF 查看器使用 UUID。保存文件时,默认文件名同样退化为 UUID,而不是远程响应建议的文件名。

Blob URL 文件名退化为 UUID 示意图

左:Blob URL、页签与 PDF 查看器使用 UUID;右:“另存为”没有获得远程 PDF 的建议文件名。

如果项目需要提供指定文件名的下载操作,可以另外创建带 download 属性的链接:

const downloadLink = document.createElement("a");

downloadLink.href = blobUrl;

downloadLink.download = "document.pdf";

downloadLink.click();

具体默认文件名和扩展名仍可能因浏览器而异。这里记录的是项目实测结果,不将它扩大为所有浏览器的统一行为。

5.2. 方式二:浏览器直接加载远程 PDF

如果远程 URL 返回正确的 Content-TypeContent-Disposition,更合适的方式是让浏览器直接处理 PDF:

window.open(pdfUrl, "_blank");

这条路径由浏览器直接请求远程 PDF,业务 JavaScript 不读取响应体,因此不依靠 CORS 完成预览。

配置 CORS 也不会把 Content-Disposition: attachment 改成 inline。如果直接打开 URL 仍然下载,需要检查 Object Metadata、实际访问域名和最终响应头。

6. 总结

OSS PDF 从上传到浏览器处理流程

排查 PDF 无法预览时,可以按同一顺序检查:

1. 上传时是否保存了正确的 Content-TypeContent-Disposition

2. 访问的是 OSS 默认域名还是已经绑定的自定义域名。

3. 浏览器实际收到的响应头是 inline 还是 attachment

4. 前端是在直接打开 URL,还是通过 fetch 读取文件。

5. 如果使用 fetch,Bucket 的 CORS 规则是否允许当前页面来源。

参考链接

[1] OSS Node.js SDK 官方文档
https://help.aliyun.com/zh/oss/developer-reference/nodejs-sdk/

[2] PutObject 官方文档
https://help.aliyun.com/zh/oss/developer-reference/putobject

[3] 使用预签名 URL 下载(Node.js SDK)
https://help.aliyun.com/zh/oss/developer-reference/download-objects-using-a-presigned-url-generated-with-oss-sdk-for-node-js

[4] 阿里云文件预览排查文档
https://help.aliyun.com/zh/oss/how-to-ensure-an-object-is-previewed-when-you-access-the-object

[5] 阿里云自定义域名文档
https://help.aliyun.com/zh/oss/user-guide/access-buckets-via-custom-domain-names

[6] Content-Type
https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Reference/Headers/Content-Type

[7] Content-Disposition
https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Reference/Headers/Content-Disposition

[8] 阿里云 OSS 跨域设置文档
https://help.aliyun.com/zh/oss/user-guide/configure-cross-origin-resource-sharing