« ^ »

EmacsのHTTPクライアント事情について考える

所要時間: 約 12分

Emacs にはHTTPリクエストを送信する方法はいくつかある。それぞれ目的や想定している状況が異なる。これを使えばよいというものは本来無いので、自分の頭で考えて選択していくと良い。

ダミーサーバー

HTTPクライアントを使うには、当然リクエストを送信する先のサーバーが必要になる。今回は yay と返すだけのシンプルなサーバーを Python で実装した。

from socket import socket

soc = socket()
soc.bind(("localhost", 8000))

try:
    soc.listen(1)
    while True:
        (conn, addr) = soc.accept()
        print(conn, addr)
        print(conn.recv(10000))
        conn.send(b"HTTP/1.1 200 OK\r\nContent-Length: 3\r\n\r\nyay")
        conn.close()
finally:
    soc.close()

このサーバーを起動しておく。

python simple-http.py

SSL/TLS については一旦考えない事にする。

一般的なHTTPクライアントライブラリ

プログラミング言語にはそれぞれHTTPクライアントライブラリが、サードパーティとして実装されていたり、標準ライブラリとして組み込まれていたりする事が多い。 Emacs には標準のライブラリとサードパーティのパッケージの両方がある。

この手のツールで多いのは、言語の標準として低レベルな機能を提供し、サードパーティはそれを用いるというものだろう。しかし Emacs の場合は少し事情が異なり、 Emacs の標準ライブラリとして提供している以外の方法として外部コマンドである cURL を呼び出す方法を使うものがある。

これは cURL が非常に良くできたHTTPクライアントである事と、 Emacs の外で出来る事は外で行うという考え方の両方が影響しているのだろう。Pythonの場合は cURL を直接呼び出す事はあまりしない。標準の機能のラッパーとしてのサードパーティを使うか、 cURL を使うにしても libcurl をリンクしたPycURLを使う。Emacsはリンクせずコマンドをサブプロセスとして呼び出すという、何と言ったらいいか大雑把というか思い切りの良い手段を取る。この辺りは言語の考え方の特徴が現れていて面白い。

url-retrieve

Emacs でHTTPのリクエストを送信する関数の代表格は url-retrieve だろう1 。この関数は lisp/url/url.el.gz で定義されており、Emacsに標準で梱包されている。第1引数には送信先のURL、第2引数には受け取ったレスポンスを元に呼び出すコールバックのシンボルを指定する。

(require 'url)

(url-retrieve "http://localhost:8000" (lambda (r) nil))
#<buffer  *http localhost:8000*-37929>
url-retrieve を使ってHTTPリクエストを送信する。
b'GET / HTTP/1.1\r\nMIME-Version: 1.0\r\nConnection: keep-alive\r\nHost: localhost:8000\r\nAccept-encoding: gzip\r\nAccept: */*\r\nUser-Agent: URL/Emacs Emacs/30.0.50 (OpenStep; x86_64-apple-darwin21.6.0)\r\n\r\n'
サーバーが受け取ったリクエストのデータ。

HTTPメソッドを変更するには、送信したい値を url-request-method に設定し、 url-retrieve を呼び出す。

(require 'url)

(let ((url-request-method "POST" ))
  (url-retrieve "http://localhost:8000" (lambda (r) nil)))
#<buffer  *http localhost:8000*-568096>
メソッドを変更してHTTPリクエストを送信する。
b'POST / HTTP/1.1\r\nMIME-Version: 1.0\r\nConnection: keep-alive\r\nHost: localhost:8000\r\nAccept-encoding: gzip\r\nAccept: */*\r\nUser-Agent: URL/Emacs Emacs/30.0.50 (OpenStep; x86_64-apple-darwin21.6.0)\r\n\r\n'
サーバーが受け取ったリクエストのデータ。

ヘッダを変更する場合は、 url-request-extra-headers を設定する。またリクエストBODYを設定する場合は、 url-request-data を設定する。

(require 'url)

(let ((url-request-extra-headers '(("User-Agent" . "DUMMY"))))
  (url-retrieve "http://localhost:8000" (lambda (r) nil)))
#<buffer  *http localhost:8000*-889520>
リクエストヘッダを変更してHTTPリクエストを送信する。
b'GET / HTTP/1.1\r\nMIME-Version: 1.0\r\nConnection: keep-alive\r\nHost: localhost:8000\r\nAccept-encoding: gzip\r\nAccept: */*\r\nUser-Agent: URL/Emacs Emacs/30.0.50 (OpenStep; x86_64-apple-darwin21.6.0)\r\nUser-Agent: DUMMY\r\n\r\n'
サーバーが受け取ったリクエストのデータ。

リクエストBODYを設定する場合は、 url-request-data を設定する。

(require 'url)

(let ((url-request-data "DUMMY"))
  (url-retrieve "http://localhost:8000" (lambda (r) nil)))
#<buffer  *http localhost:8000*-305063>
リクエストヘッダを変更してHTTPリクエストを送信する。
b'GET / HTTP/1.1\r\nMIME-Version: 1.0\r\nConnection: keep-alive\r\nHost: localhost:8000\r\nAccept-encoding: gzip\r\nAccept: */*\r\nUser-Agent: URL/Emacs Emacs/30.0.50 (OpenStep; x86_64-apple-darwin21.6.0)\r\nContent-length: 5\r\n\r\nDUMMY'
サーバーが受け取ったリクエストのデータ。

同期的にリクエストを送信するための url-retrieve-synchronously も用意されている。

request.el

request.el 2cURL が使える環境であれば、リクエストの送信に cURL を使用する。 cURL が使えない環境では url.el に縮退して動作する。このパッケージはMELPAに登録されている。

(require 'request)

(request "http://localhost:8000/get")
  :parser 'json-read
  :success (cl-function
            (lambda (&key data &allow-other-keys)
              (message "I sent: %S" (assoc-default 'args data)))))
b'GET /get?key=value&key2=value2 HTTP/1.1\r\nHost: localhost:8000\r\nUser-Agent: curl/8.1.2\r\nAccept: */*\r\nAccept-Encoding: deflate, gzip\r\n\r\n'
サーバーが受け取ったリクエストのデータ。

plz.el

plz.elrequest.el と同様に cURL を使用可能であれば cURL を使用し、そうでなければ url.el の関数を使用する。こちらはELPAに登録されている。

(require 'plz)

(plz 'get "http://localhost:8000")
yay
b'GET / HTTP/1.1\r\nHost: localhost:8000\r\nUser-Agent: curl/8.1.2\r\nAccept: */*\r\nAccept-Encoding: deflate, gzip\r\n\r\n'
サーバーが受け取ったリクエストのデータ。

HTTPリクエストの検証用としてのHTTPクライアント

HTTP(s)で提供するWeb APIを開発する場合、そのWeb APIが正しい動作をしているか、パラメータを変更して送信するとどうなるか等、試行錯誤しながら動作を確認したい。Emacs以外で使われる競合するツールとしては、 cURLPostman が挙げられる。

http.el

HTTPの形式に似た独自の構文に従ってHTTPリクエストを記述でき、そのリクエストを送信しレスポンスを確認するといったことができる。それらが定義する通常は request.http といったようなファイルを作成し、そのファイル内にリクエストの内容を記述する。検証時やドキュメントとして使用することが多い。

GET http://localhost:8000
HTTP/1.1 200 OK
Content-Length: 3

yay
test.http
b'GET / HTTP/1.1\r\nHost: localhost:8000\r\nUser-Agent: curl/8.1.2\r\nAccept: */*\r\n\r\n'
サーバーが受け取ったリクエストのデータ。

restclient.el

restclient.elhttp.el と同様の機能を提供する。ただし細かい違いもある。変数展開などが使用できるため、大部分においては http.el の上位互換と言えなくもない。ただし、具体的な事を忘れてしまったが、 restclient.el を使っていて「ここは http.el の方が優れているな」と思った記憶がある。とても小さなピンポイントの内容だった気がするが忘れてしまった。思い出したら書く事にする。

GET http://localhost:8000
yay
// GET http://localhost:8000
// HTTP/1.1 200 OK
// Content-Length: 3
// Request duration: 0.002433s
test.restclient
b'GET / HTTP/1.1\r\nMIME-Version: 1.0\r\nConnection: keep-alive\r\nHost: localhost:8000\r\nAccept-encoding: gzip\r\nAccept: */*\r\nContent-length: 0\r\n\r\n'
サーバーが受け取ったリクエストのデータ。

httprepl.el

これは存在を知らなかった。かなり古いコードだが、今度使ってみたい。

https://github.com/gregsexton/httprepl.el

Babelと文芸的プログラミング

EmacsにはOrg-modeという強力なドキュメンテーションシステムがあり、Org-modeにはコードブロックに記述された処理を実行するためのBabelという機構がある。この機構を用いるとOrg-modeの文書で実行が可能なコードを記述し、その結果を即座にドキュメントに挿入できる。それは実行可能なドキュメントとなるため、文書の保守がとてもやりやすくなる。いわゆる文芸的プログラミングのようなことができる3。コードブロックに記述する内容は、Babel用のプラグインが用意されているかどうかで、実行が可能かどうかが決まる。もしプラグインが無ければ自分で実装することもできる。

http.elやrestclient.elとBabelを組み合わせる

http.elのためのBabelプラグインとしてはob-httpがある。またrestclient.elのBabelプラグインとしてはob-restclient.elがある。これらは別途インストールし org-babel-load-languages に登録することで利用可能となる。

(org-babel-do-load-languages
 'org-babel-load-languages
 '(
   (restclient . t)
   (http . t)))
org-babel-load-languagesにhttpとrestclientを登録する例

この設定を行うとOrg-modeのコードブロックでhttp.elやrestclient.elの構文のリクエストを記述しそれを送信できるようになる。

#+begin_src http
GET http://example.com
#+end_src
ob-httpを利用する
#+begin_src restclient
GET http://example.com
#+end_src
ob-restclientを利用する

multipart/form-dataでのキャリッジリターン文字扱いが微妙に異るため注意する必要がある

これまでEmacsでのHTTPリクエストの送信のための機構のいくつかを説明した。HTTPリクエストにはJSON形式であったりx-www-form-urlencoded形式であったりといったいろいろな形式のBODYを記述できる。このBODYの形式の中でファイルのアップロードなどに用いられるmultipart/form-dataというものがある。multipart/form-dataはHTMLのFORMタグを指定し、そのenctype属性をmultipart/form-dataとすることでブラウザから送信できるようになる。実際のデータは以下のような形式だ。

POST / HTTP/1.1
Host: localhost:8001
User-Agent: curl/7.64.1
Accept: */*
Content-Length: 189
Content-Type: multipart/form-data; boundary=------------------------44644daa7f63ea32

--------------------------44644daa7f63ea32
Content-Disposition: form-data; name="file"; filename="foo.txt"
Content-Type: text/plain

aaa

--------------------------44644daa7f63ea32--

これと同様のリクエストをhttp.elやrestclient.elの構文では以下のように記述できる。

POST http://localhost:8001
Content-Type: multipart/form-data; boundary=BOUNDARY

--BOUNDARY
Content-Disposition: form-data; name="file"; filename="foo.txt"
Content-Type: text/plain

aaa

--BOUNDARY--

今回は制御文字であるキャリッジリターンが問題になった。HTTPの仕様上この文字は必須であり、各ヘッダーの終端やヘッダーとボディの境界にはキャリッジリターンとラインフィードを用いて指定する。http.elやrestclient.elの構文では、これらの文字を適切に使用しなくても各ヘッダーの終端やヘッダーとボディの境界については、それらのライブラリがそれらを判断して制御文字を適切に設定した状態でリクエストを送信する。

しかしmultipart/form-dataの場合、HTTPのボディ部の中にさらなるヘッダーのような値を設定する。http.elやrestclient.elはそれらを解釈しないため、適宜リクエスト内部に制御文字を明示的に指定する必要がある。http.elやrestclient.elはファイル単体で独立できるため、これらの改行をCRLF(キャリッジリターンとラインフィード)にすれば特に気にする必要はない。

では文書はどうだろうか。文書の改行はLinuxのスタイルに従いLF(ラインフィード)のみにしている人が多いのではないだろうか。Org-modeの文書の改行をLFで記述しており、更にその文書でBabelの機構を用いてhttp.elやrestclient.elを利用する場合に問題が発生する。文書の改行はLFだが、http.elやrestclient.elスタイルのブロックの改行はCRLFにしなければならない。ob-httpやob-restclientはこれらの要求に上手く対応できず、期待するリクエストを送信できない。今のところ上手くこの問題を回避する方法がわからない。

restclientで変数を展開した後のデータを取得する

外部APIを使用する場合で、HTTPリクエストのBODYの署名をHTTPヘッダーに付与しなければならない場合がある。そういった場合、署名を計算するために送信するBODY全体のデータが必要なのだが、http.elやrestclient.elでBODYに変数を埋め込んでいると、変数展開前のBODYを元に署名を計算することになってしまい、正しい署名が計算できない。正しい署名を計算するには変数展開後のBODY全体が必要になる。このデータを取得できないかを試すことにした。

restclientの内部を読んでみると、実装済みの使えそうな関数がいくつかあった。

restclient-narrow-to-current
現在のポイントのrestclientの範囲をナローイングする。
restclient-parse-body
restclient形式の文字列を変数展開して返す。
restclient-find-vars-before-point
restclient形式で宣言された変数の連想リストを返す。

これらを組み合せ、変数展開後の文字列を専用のバッファに書き出せば良い。完成形はこのよになった。これは本当に小さなEmacs Lispのため、init.elにでも直接書いておこうと思う。

(defun restclient-show-request-on-new-buffer ()
  (interactive)
  (save-excursion
    (restclient-narrow-to-current)
    (let ((req-txt (restclient-parse-body
		    (buffer-substring-no-properties (point-min) (point-max))
		    (restclient-find-vars-before-point))))
      (widen)
      (with-current-buffer (get-buffer-create "*Restclient Request*")
	(erase-buffer)
	(insert req-txt)
	(switch-to-buffer (current-buffer))))))

HTTPに拘らない

TCPのコネクション(HTTP/3の場合はUDPだけれど)を作成できれば、HTTPは自分の力で手書きすればいいのかもしれない。Emacsには当然トランスポート層の接続を作るための関数が用意されている。

make-network-process

make-network-process はEmacsが提供する低レベルの関数で、 src/process.c 内で定義されており、Cで実装されている45

(make-network-process
 :name "*TESTING*"
 :buffer "*TESTING*"
 :server nil
 :nowait nil
 :host "localhost"
 :coding '(raw-text-unix . raw-text-unix)
 :family 'ipv4
 :service 8000))

関数を呼び出すと、TCP接続を行ったサブプロセスと、それに関連付けられたバッファが作成される。そのプロセスに対してHTTP形式の文字列を送信する事で、HTTPリクエストを送信できる。またHTTPレスポンスはバッファに書き込まれる。

(process-send-string
  (get-buffer-process (get-buffer "*TESTING*"))
  "GET / HTTP/1.1

")
b'GET / HTTP/1.1\r\n\r\n'
サーバーが受け取ったリクエストのデータ。

network-connection

network-connection は、 make-network-process の更に上位の関数で、作成したバッファが comint-mode になっている。そのため作成されたバッファに生のHTTPリクエストを記述し、=comint-send-input= を呼び出す事で、HTTPリクエストを送信できる。HTTPレスポンスは同じバッファに出力される。

注意点としては comint-send-inputRET に割り当てられている。その場合、改行を入力しようとして RET を押すとリクエストが送信される事になる。これはキー割り当てを工夫する事で解消できるし、 C-q を使ってエスケーピングする事でキャリッジリターンなども入力できる。

(network-connection "localhost" 8000)
GET / HTT/1.1
HTTP/1.1 200 OK
Content-Length: 3

yay
Process Network Connection [localhost 8000] connection broken by remote peer
network-connectionで作られたバッファの例。
b'GET / HTT/1.1\r\n'
サーバーが受け取ったリクエストのデータ。

他にも上位の関数はある

これらの実装はまだ幾つかある。それぞれに必要な機能を提供している。

open-network-stream
lisp/net/network-stream.el.gzで定義。
url-open-stream
lisp/url/url-gw.el.gzで定義。
url-http
lisp/url/url-http.el.gzで定義。

必要に応じて使い分ける事が望ましいが、基本的には前述した url-retrieverequestplz で事足りるため、直接使う機会はあまりないかもしれない。ただ使えるという事だけでも知っておくと、トラブルシューティングやデバッグの助けになる。

まとめ

今回はEmacsの様々なHTTPクライアントを確認した。目的も違えば、使っている機構も、レイヤーも異なる様々な方法でHTTPリクエストを送信し、どのようなデータが送信されたのかを確認した。またOrg-modeとBabelの強みを生かしたHTTPの記述と送信についても確認した。Babelを用いた方法では、改行文字の問題などでハマる箇所もあった。またボディから署名を計算するための小さなEmacs Lispを実装した。

Emacsについてまた知る事ができた。


3

特にOrg-mode, Babel, org-tangleを組み合せることで、本当に文芸的プログラミングを実践できそうに思えることがある。これはかなり魅力的にも感じるが、実際に複数人のチームで開発を行う場合には現実的ではない。スタイルが大きく異なる事、皆がEmacsやOrg-modeに好意的ではない事、デバッグのやりにくさ等がその理由になる。一方で、システムのテスト用ドキュメント兼ツールとしては結構使えるのではないかとも思う。本来http.elやrestclient.elはEmacs Lispでは実装せず、他のエディタや環境からも使用できるようにすると、もっと広がりがあるのではないかと思う。ここについて今回は踏み込まないけれど、時間ができたらもう少し掘り下げてみたい。