jQuery(form).serialize() failing with "URIError: malformed URI sequence"

jQuery(form).serialize() failing with "URIError: malformed URI sequence"

我有一个 Web 应用程序,可以让用户评论彼此的 post。我们使用 jQuery.ajax() 向服务器发送新的评论,在我们的测试中它似乎工作可靠。

jQuery(".post form.add-comment").on("submit", function(event) {
  event.preventDefault();
  jQuery.ajax({
    type: "POST",
    url: "/comment",
    data: jQuery(this).serialize()
  });
});

但是,我们会自动从用户那里收集客户端 JavaScript 错误日志(使用 Sentry),并且偶尔会出现如下所示的错误:

URIError: malformed URI sequence jquery.min.js:4:25041

这个错误似乎阻止了评论被发送到我们的服务器,所以我们无法判断用户正在尝试什么 post 可能导致了这个错误。

什么可能导致此错误发生,我们如何防止它?

出于某种原因,some of your users 正在尝试提交包含我们可能称之为 "invalid characters" 的评论。从 \uD800\uDFFF 的 Unicode 代码点被保留,以便 UCS-2 和 UTF-16 文本编码可以使用它们的成对来识别其他有效的 Unicode 字符代码点,否则这些代码点将超出- 这些编码的范围。对于大多数现代编码,包括 UTF-16,这些代码点只允许出现在有效的对中,在转换为另一种编码时可以映射到有效的字符代码点;它们永远不可能独立存在 "characters".

不幸的是,JavaScript 在 UTF-16 标准化之前选择了 UCS-2,而 UCS-2 确实 允许您自己包含代理字符,而无需配对以产生有效的代码点。因为 JavaScript 允许,浏览器也接受它作为输入。这是一个复杂的问题,但在大多数情况下,它实际上并没有像您所遇到的那样妨碍用户。如果您的表单没有使用 JavaScript,您的用户将能够提交包含未配对代理项的评论而不会出现错误。那将如何运作?

浏览器采用一种通用方法来解决编码不兼容问题:任何无法转换为目标编码的字符都将替换为 Unicode 替换字符 \uFFFD。浏览器在对典型的表单数据进行编码以供提交时自动执行此替换。但是,jQuery.serialize() 没有任何此类逻辑,它调用的内置函数 encodeURIComponent 也没有对表单值进行编码。相反,它只是抛出你看到的 URIError 。您可以在 ECMAScript 9 规范的 Section 18.2.6.1.1: Runtime Semantics: Encode 中找到此错误。

encodeURIComponent('\uD83D') // URIError: malformed URI sequence

要在 JavaScript 中重现类似浏览器的表单行为,您需要查找并替换 \uD800\uDBFF 范围内出现 "high surrogate" 的所有实例,而不后跟 \uDC00\uDFFF 范围内的 "low surrogate",反之亦然。这可能看起来像这样:

const replaceUnpairedSurrogates = s => s
  .replace(/[\uD800-\uDBFF]+([^\uDC00-\uDFFF]|$)/g, '�')
  .replace(/(^|[^\uD800-\uDBFF])[\uDC00-\uDFFF]+/g, '�');

(此函数满足 Unicode 标准要求的 "Constraints on Conversion Processes",因为它确保后面的有效字符不会被替换破坏。它不符合可选的 "Substitution of Maximal Subparts" 约定,因为它可能会将连续的未配对代理字符折叠为单个替换字符。)

您目前正在使用 jQuery.serialize(this) 对表单数据进行编码,这不允许我们在编码之前转换表单值。但是 jQuery.serialize(this)jQuery.param(jQuery.serializeArray(this)) 一样,给了我们一个地方来应用我们的替换:

jQuery(".post form.add-comment").on("submit", function(event) {
  event.preventDefault();
  const data = jQuery.param(
    jQuery.serializeArray(this).map(
      ({name, value}) => {
        name: replaceUnpairedSurrogates(name),
        value: replaceUnpairedSurrogates(value),
      })
    )
  );
  jQuery.ajax({
    type: "POST",
    url: "/comment",
    data: data
  });
});

为了测试,你可以运行下面显示一个"invalid character"用于复制:

prompt('Copy this:', '\uD83D');