首页
/ Electron PostBody 对象详解:理解 target=_blank 表单提交中携带的 POST 数据

Electron PostBody 对象详解:理解 target=_blank 表单提交中携带的 POST 数据

2026-09-06 14:22:38作者:曹令琨Iris

PostBody 是 Electron 主进程中描述"新窗口将携带哪些 POST 数据"的数据结构,它出现在 webContents.setWindowOpenHandler()did-create-window 事件的 details 参数中。阅读本文后,你将完整掌握 datacontentTypeboundary 三个字段的确切含义与取值规则,理解它是如何由 HTML 表单的 enctype 属性生成、Electron 又是如何在源码层面解析出 content-type 与 boundary 的,并学会在主进程侧检验、拦截或转发这些数据。

PostBody 何时出现:只有 target=_blank 的表单提交才会产生

PostBody 并非 window.open() 的产物,而是专属于"用 <form target="_blank"> 提交表单"这一场景。WebContents 文档中对 setWindowOpenHandlerdetails 参数有明确说明(见 web-contents.md):

postBody PostBody (optional) - The post data that will be sent to the new window, along with the appropriate headers that will be set. If no post data is to be sent, the value will be null. Only defined when the window is being created by a form that set target=_blank.

也就是说:

  • 只有渲染进程中的表单设置了 target="_blank" 且方法为 POST 时,主进程收到的 details.postBody 才会被定义;
  • 没有 POST 数据时该值为 null
  • window.open()、普通链接点击等场景下不会出现 PostBody。

同样的说明也存在于 did-create-window 事件的 details 中(见 web-contents.md),因此无论是"开窗前拦截"还是"开窗后跟踪",都能拿到同一份 PostBody 信息。关于窗口如何从渲染进程被创建的整体机制,可参考 Opening windows from the renderer

字段逐一解析

PostBody 结构文档的定义,该对象包含三个字段:

data:实际要发送的负载数组

类型为 ([UploadRawData](https://gitcode.com/GitHub_Trending/el/electron/blob/640ecbf5570998bf1483bac1b1509ffac3cbcd40/docs/api/structures/upload-raw-data.md?utm_source=gitcode_repo_files) | [UploadFile](https://gitcode.com/GitHub_Trending/el/electron/blob/640ecbf5570998bf1483bac1b1509ffac3cbcd40/docs/api/structures/upload-file.md?utm_source=gitcode_repo_files))[],即"要发送到新窗口的 post 数据",是一个联合类型数组。两个成员结构各自如下:

UploadRawData(见 upload-raw-data.md):

字段 类型 说明
type 'rawData' 固定为 'rawData',用于区分两种上传项
bytes Buffer 待上传的原始数据

UploadFile(见 upload-file.md):

字段 类型 默认值 说明
type 'file' 固定为 'file'
filePath string 要上传的文件路径
offset Integer 0 读取起点偏移
length Integer 0 offset 起读取的字节数
modificationTime Double 0 文件最后修改时间,UNIX 纪元起的秒数

upload 类型的表单文件控件会以 UploadFile 形式出现在数组中,普通文本/隐藏字段则以 UploadRawData 形式出现。

contentType:与表单 enctype 一一对应

contentType 是字符串,取值为二选一:

  • application/x-www-form-urlencoded —— 对应未设置 enctype 或设置为 application/x-www-form-urlencoded 的普通表单;
  • multipart/form-data —— 对应 enctype="multipart/form-data" 的文件上传表单。

文档明确指出它"对应于所提交 HTML 表单的 enctype 属性"。这一点在 Electron 的测试中有直接印证:spec/guest-window-manager-spec.ts 中"includes post body"用例提交了一个不含 enctype 的默认表单,断言得到的正是 contentType: 'application/x-www-form-urlencoded'

boundary:仅对 multipart 有效

boundary 为可选字符串,用于分隔 multipart 消息中的各个 part。只有当 contentTypemultipart/form-data 时才有意义。其值形如 WebKitFormBoundary12345678(不含前导的 --),最终会以 content-type: multipart/form-data; boundary=... 的形式出现在请求头中。

源码视角:PostBody 是如何被构造出来的

从源码结构看,PostBody 的构造发生在主进程对 -new-window / -will-add-new-contents 内部事件的响应里。lib/browser/api/web-contents.ts 中的处理逻辑是:

this.on('-new-window', (event, url, frameName, disposition, rawFeatures, referrer, postData, sandboxFlags) => {
  const postBody = postData
    ? {
        data: postData,
        ...parseContentTypeFormat(postData)
      }
    : undefined;
  const details: Electron.HandlerDetails = {
    url,
    frameName,
    features: rawFeatures,
    referrer,
    postBody,
    disposition
  };
  // ...随后调用 _callWindowOpenHandler(event, details)

可见 data 字段直接来自 Chromium 层上抛的 postData 数组,而 contentType/boundary 则由 parseContentTypeFormat() 补齐——这正是文档中"contentTypeboundary 由数据本身推导"的底层原因。

parseContentTypeFormat 的完整实现在 lib/browser/guest-window-manager.ts,其判定策略值得细看:

const MULTIPART_CONTENT_TYPE = 'multipart/form-data';
const URL_ENCODED_CONTENT_TYPE = 'application/x-www-form-urlencoded';

// Figure out appropriate headers for post data.
export const parseContentTypeFormat = function (postData: Exclude<PostData, undefined>) {
  if (postData.length) {
    if (postData[0].type === 'rawData') {
      // For multipart forms, the first element will start with the boundary
      // notice, which looks something like `------WebKitFormBoundary12345678`
      const postDataFront = postData[0].bytes.toString();
      const boundary = /^--.*[^-\r\n]/.exec(postDataFront);
      if (boundary) {
        return {
          boundary: boundary[0].substr(2),
          contentType: MULTIPART_CONTENT_TYPE
        };
      }
    }
  }
  // Either the form submission didn't contain any inputs (the postData array
  // was empty), or we couldn't find the boundary and thus we can assume this is
  // a key=value style form.
  return {
    contentType: URL_ENCODED_CONTENT_TYPE
  };
};

从源码可以得出几条实用结论:

  1. multipart 的识别依据是"首元素以 -- 开头的 boundary 标记":浏览器在 multipart/form-data 编码下,第一个上传项的字节流会以 ------WebKitFormBoundaryXXXX 这样的边界行开头;代码用正则 /^--.*[^-\r\n]/ 提取它,并 substr(2) 去掉前导 -- 后作为 boundary 字段返回;
  2. 无法识别 boundary 时一律退化为 urlencoded:包括空表单(postData 为空数组)的情况,这与文档"contentType 只能是两种值之一"的描述严格吻合;
  3. 源码注释还指出一个边界情况:如果 urlencoded 表单中存在名为 --theKey 的输入项,理论上可能误触发 boundary 正则——注释作者以一句"don't do that?"带过,属于已知但不处理的边角。

提取出的 contentType/boundary 最终如何变成请求头,见同文件的 formatPostDataHeaders

function formatPostDataHeaders(postData: PostData) {
  if (!postData) return;

  const { contentType, boundary } = parseContentTypeFormat(postData);
  if (boundary != null) {
    return `content-type: ${contentType}; boundary=${boundary}`;
  }

  return `content-type: ${contentType}`;
}

这也解释了文档"boundary 仅在 contentTypemultipart/form-data 时有效"的表述:boundary 非空时会被拼进 content-type 头,urlencoded 场景下则不会出现。

实战:在主进程中检查与消费 PostBody

1. 在 setWindowOpenHandler 中检查

结合上面源码链路,一个可直接运行的检查示例如下:

const { app, BrowserWindow } = require('electron')

app.whenReady().then(() => {
  const win = new BrowserWindow()

  win.webContents.setWindowOpenHandler((details) => {
    if (details.postBody) {
      console.log('即将以 POST 数据打开新窗口:', details.url)
      console.log('content-type:', details.postBody.contentType)
      if (details.postBody.boundary) {
        console.log('boundary:', details.postBody.boundary)
      }
      // 逐项查看负载:rawData 项是 Buffer,file 项含 filePath
      for (const part of details.postBody.data) {
        if (part.type === 'rawData') {
          console.log('rawData:', part.bytes.toString())
        } else if (part.type === 'file') {
          console.log('file:', part.filePath, part.offset, part.length)
        }
      }
    }

    // 返回 { action: 'deny' } 可取消开窗
    return { action: 'deny' }
  })

  win.loadFile('index.html')
})

2. 用 data: URL 表单复现测试场景

Electron 官方测试 spec/guest-window-manager-spec.ts 给出了一个不依赖真实 HTML 文件、可完整复现 PostBody 生成过程的最小用例:

const details = await new Promise((resolve) => {
  browserWindow.webContents.setWindowOpenHandler((details) => {
    resolve(details)
    return { action: 'deny' }
  })

  browserWindow.loadURL(`data:text/html,${encodeURIComponent(`
    <form action="http://example.com" target="_blank" method="POST" id="form">
      <input name="key" value="value">
    </form>
    <script>form.submit()</script>
  `)}`)
})

该用例断言了完整结果:dispositionforeground-tab,且 postBody 深度等于:

{
  contentType: 'application/x-www-form-urlencoded',
  data: [
    { type: 'rawData', bytes: Buffer.from('key=value') }
  ]
}

这直接验证了三件事:target="_blank" + POST 表单是 PostBody 的唯一来源;默认编码下 contentType 为 urlencoded;单个 key=value 输入项最终呈现为一个 rawData 项,其 bytes 就是 key=value 的 Buffer。

3. 常见用法:拦截后改由主进程转发

PostBody 的典型消费场景是:渲染进程把表单 POST 到 target="_blank"(例如登录、上传回调),主进程拦截开窗动作,改为用 net 模块或 fetch 把同样的负载转发给后端:

const { net } = require('electron')

win.webContents.setWindowOpenHandler((details) => {
  if (details.postBody) {
    const { contentType, boundary, data } = details.postBody
    // 按 data 中的 rawData/file 项重建请求体后 POST 到 details.url
    // 具体编码需区分 urlencoded 与 multipart 两种 contentType
  }
  return { action: 'deny' }
})

小结

  • PostBody 描述"新窗口要发送的 POST 数据",字段为 dataUploadRawData/UploadFile 数组)、contentType(仅 application/x-www-form-urlencodedmultipart/form-data,对应表单 enctype)、boundary(可选,仅 multipart 有效)。
  • 产生条件唯一:渲染进程中 target="_blank" 的表单提交;window.open() 与普通导航不产生 PostBody,无数据时为 null
  • contentTypeboundary 由 Electron 从负载字节推导parseContentTypeFormat 依据首个 rawData 项是否以 -- 边界标记开头来判定 multipart 并提取 boundary,否则退回 urlencoded;formatPostDataHeaders 负责将其拼入 content-type 请求头。
  • 验证与复现spec/guest-window-manager-spec.ts 提供了用 data: URL 表单端到端验证 PostBody 结构的最小可运行用例,适合作为自定义测试的模板。
登录后查看全文
热门项目推荐
相关项目推荐