我有一个Web应用程序,可让用户对彼此的帖子发表评论。我们使用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
此错误似乎阻止了评论发送到我们的服务器,因此我们无法确定用户尝试发布的内容可能导致此错误。
是什么原因导致此错误发生,我们如何防止它发生?
答案 0 :(得分:4)
出于某种原因,some of your users试图提交包含我们可能称为“无效字符”的评论。保留从\uD800
到\uDFFF
的Unicode代码点,以便UCS-2和UTF-16文本编码可以使用它们对来标识其他有效的Unicode字符代码点,否则它们将不在此范围内。 -range表示这些编码。对于大多数现代编码,包括UTF-16,这些代码点仅允许成对出现,并在转换为另一种编码时可以映射为有效的字符代码点。它们永远不能作为独立的“字符”存在。
不幸的是,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
范围内的“高替代”的所有实例,而后没有“低替代”的情况。在\uDC00
到\uDFFF
范围内,反之亦然。可能看起来像这样:
const replaceUnpairedSurrogates = s => s
.replace(/[\uD800-\uDBFF]+([^\uDC00-\uDFFF]|$)/g, '�$1')
.replace(/(^|[^\uD800-\uDBFF])[\uDC00-\uDFFF]+/g, '$1�');
(此函数满足Unicode标准要求的“转换过程约束”,因为它确保替换后的有效字符不会被替换。它不符合可选的“替换最大子部分”约定,因为它可能会将连续的不成对的替代字符折叠成单个替换字符。)
您当前正在使用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
});
});
为了进行测试,可以运行以下命令以显示一个“无效字符”进行复制:
prompt('Copy this:', '\uD83D');