url.el 关于 Multibyte text in HTTP request 的一件怪事

Emacs 生成的字符串,明明没有特殊字符却成了 Multibyte 的了,比如 symbol-name

(multibyte-string-p (symbol-name 'XXX))
;; t

以及从 Auth Source 中获得密码:

;; ~/.authinfo
;; machine example.com password 123456
(let ((pass
       (let ((plist (car (auth-source-search :max 1 :host "example.com"))))
         (let ((v (plist-get plist :secret)))
           (if (functionp v) (funcall v) v)))))
  (list pass (multibyte-string-p pass)))
;; => ("123456" t)

更加诡异的是(url-http-create-request 就是怎么干的),5 Bytes 是哪里来的?

(let ((s (concat (symbol-name 'a)
                 (encode-coding-string "λ" 'utf-8))))
  (list (length s)
        (string-bytes s)))
;; => (3 5)

url.el 发送请求时,如果 Header 中包含了上面的 Multibyte 数据,当 Body 包含 UTF-8 的 Unibyte 时会报错,否则则不会:

(defun test (author)
  (let ((url-request-extra-headers
         `(("Content-Type" . "application/json")
           ("XXX" . ,(symbol-name 'XXX))))
        (url-request-method "POST")
        (url-request-data (encode-coding-string
                           (json-encode `((author . ,author)))
                           'utf-8)))
    (display-buffer
     (url-retrieve-synchronously "http://httpbin.org/post"))))

(test "Li Bai") ;; 正常
(test "李白") ;; 报错 Multibyte text in HTTP request

目前正确的做法是把所有的传给 url.el 的参数都 Encode 一遍,比如上面的 (encode-coding-string (symbol-name 'XXX) 'utf-8),但是我实在是不明白怎么回事。


相关讨论:

2 个赞

看了下文档,multibyte好像是emacs内部使用的编码,codepoint包含了utf-8,但是字节序列不是utf-8方式,应该是用的比utf-8更高效一点的方式,更方便程序处理,编码名字叫utf-8-emacs.

此编码包含了utf-8,还包含了额外的codepoint,没法一对一转换到utf-8,可能因为这个,url.el里没有做自动转换。

(string-bytes (concat (symbol-name 'a) (encode-coding-string "λ" 'utf-8)))  ;; => 5
(string-bytes (concat "a" (encode-coding-string "λ" 'utf-8))) ;; => 3

第一种写法结果是5,有点奇怪。

草,写完才发现已经有过讨论了,请移步 (concat (symbol-name 'GET) (encode-coding-string “我” 'utf-8)) 几个 bytes?


有点挖坟了,但是我最近也遇到了这个问题,简单发个贴,希望之后碰到这个问题的人能够看到。

出现这个问题的主要原因就是 LZ 说的 multibyte 和 unibyte 字符串 concat 合并问题。调用 url-retrieve 发起 HTTP request 时最终会调用 url-http-create-request ,这个函数内部会对将要发送的字符串做检查来判断是否全是字节数据:

;; Bug#23750
  (unless (= (string-bytes request)
	     (length request))
    (error "Multibyte text in HTTP request: %s" request))

concat 在参数即包含 unibyte 和 multibyte 字符串时,返回的结果字符串是 multibyte 的,这就导致某些在 unibyte 字符串中可以使用一个 byte 表示的字符在 multibyte 中需要使用两个字符表示,这一点也写在 concat 函数的注释中了:

if (dest_multibyte && some_unibyte)
{
      /* Non-ASCII characters in unibyte strings take two bytes when
	 converted to multibyte -- count them and adjust the total.  */
      for (ptrdiff_t i = 0; i < nargs; i++)
	{
	  Lisp_Object arg = args[i];
	  if (STRINGP (arg) && !STRING_MULTIBYTE (arg))
	    {
	      ptrdiff_t bytes = SCHARS (arg);
	      const unsigned char *s = SDATA (arg);
	      ptrdiff_t nonascii = 0;
	      for (ptrdiff_t j = 0; j < bytes; j++)
		nonascii += s[j] >> 7;
	      if (STRING_BYTES_BOUND - result_len_byte < nonascii)
		string_overflow ();
	      result_len_byte += nonascii;
	    }
	}
}

下面是一个例子:

(string-bytes (unibyte-string 128)) => 1
(string-bytes "我") => 3
(string-bytes (concat "我" (unibyte-string 128))) => 5 ;; 3 + 1*2

一言以蔽之,请保证传递给 url-retrieve 的所有字符串都是 unibyte 以避免错误。

1 个赞