EmacsのHTTPクライアント事情について考える
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
には標準のライブラリとサードパーティのパッケージの両方がある。
url-retrieve
Emacs
でHTTPのリクエストを送信する関数の代表格は url-retrieve
だろう1 。この関数は lisp/url/url.el.gz
で定義されており、Emacsに標準で梱包されている。第1引数には送信先のURL、第2引数には受け取ったレスポンスを元に呼び出すコールバックのシンボルを指定する。
HTTPメソッドを変更するには、送信したい値を url-request-method
に設定し、 url-retrieve
を呼び出す。
ヘッダを変更する場合は、 url-request-extra-headers
を設定する。またリクエストBODYを設定する場合は、 url-request-data
を設定する。
リクエストBODYを設定する場合は、 url-request-data
を設定する。
同期的にリクエストを送信するための url-retrieve-synchronously
も用意されている。
request.el
request.el
2は cURL
が使える環境であれば、リクエストの送信に 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)))))
plz.el
plz.el
も request.el
と同様に cURL
を使用可能であれば cURL
を使用し、そうでなければ url.el
の関数を使用する。こちらはELPAに登録されている。
(require 'plz)
(plz 'get "http://localhost:8000")
yay
HTTPリクエストの検証用としてのHTTPクライアント
HTTP(s)で提供するWeb APIを開発する場合、そのWeb APIが正しい動作をしているか、パラメータを変更して送信するとどうなるか等、試行錯誤しながら動作を確認したい。Emacs以外で使われる競合するツールとしては、 cURL
や Postman
が挙げられる。
http.el
HTTPの形式に似た独自の構文に従ってHTTPリクエストを記述でき、そのリクエストを送信しレスポンスを確認するといったことができる。それらが定義する通常は request.http
といったようなファイルを作成し、そのファイル内にリクエストの内容を記述する。検証時やドキュメントとして使用することが多い。
restclient.el
restclient.el
は http.el
と同様の機能を提供する。ただし細かい違いもある。変数展開などが使用できるため、大部分においては http.el
の上位互換と言えなくもない。ただし、具体的な事を忘れてしまったが、 restclient.el
を使っていて「ここは http.el
の方が優れているな」と思った記憶がある。とても小さなピンポイントの内容だった気がするが忘れてしまった。思い出したら書く事にする。
httprepl.el
これは存在を知らなかった。かなり古いコードだが、今度使ってみたい。
cURLを直接使う
HTTPリクエストを送信するような機能は、低レベルな機能を言語の標準ライブラリとして提供し、ユーザーやサードパーティのライブラリはそれを利用するという事が多い。
Emacsでも url-retrieve
は標準ライブラリとして提供されているけれど、HTTP送信のプログラムをサブプセスとして起動し利用すれば、そのコードやラッパーを使わずリクエストを送信する事はできる。
HTTPリクエストを送信するプログラムとしてcURLはよく使われる。cURLは送信する内容をコマンドの引数として渡すが、設定をファイルから読み込む事もでるし、その設定を標準入力から渡す事もできる。
ここでは、設定を標準入力から渡しcURLを起動する事でリクエストを送信する方法を試す。 make-process
関数でサブプセスとして呼び出し、cURLの設定をバッファに生成し、そのバッファのデータをサブプロセスの標準入力に流し込む。これの嬉しい所は、cURLの設定ファイルの形式に則り、リクエストを手動で変更できる所だ。
;;; curl --- cURL Utility -*- lexical-binding: t -*-
;; Copyright (C) 2024 TakesxiSximada
;; Author: TakesxiSximada <[email protected]>
;; Maintainer: TakesxiSximada <[email protected]>
;; Version: 3
;; Package-Version: 20240321.0000
;; Package-Requires:
;; Date: 2024-03-21
;; This file is not part of GNU Emacs.
;;; License:
;; This program is free software: you can redistribute it and/or
;; modify it under the terms of the GNU Affero General Public License as
;; published by the Free Software Foundation, either version 3 of the
;; License, or (at your option) any later version.
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
;; Affero General Public License for more details.
;; You should have received a copy of the GNU Affero General Public
;; License along with this program. If not, see
;; <https://www.gnu.org/licenses/>.
;;; Code:
(defcustom curl-executable "curl"
"Path to curl command"
:group 'curl
:type 'file)
(defcustom curl-config-buffer-name "*cURL Config*"
"Name of cURL Config Buffer"
:group 'curl
:type 'string)
(defcustom curl-process-buffer-name "*cURL Process*"
"Name of cURL Process Buffer"
:group 'curl
:type 'string)
(defcustom curl-output-buffer-name "*cURL Output*"
"Name of cURL Output Buffer"
:group 'curl
:type 'string)
(defcustom curl-error-buffer-name "*cURL Error*"
"Name of cURL Error Buffer"
:group 'curl
:type 'string)
(defvar curl-config-template
"# cURL Configuration for Emacs
url %s
request %s
header %s
data %s
")
(defvar curl-process nil)
(defvar curl-process-sentinel nil)
(defvar curl-config-buffer nil)
(defvar curl-process-sentinel-hook nil)
;;;###autoload
(defun curl-create-config ()
(interactive)
(with-current-buffer (get-buffer-create curl-config-buffer-name)
(erase-buffer)
(insert
(format curl-config-template
(json-encode-string "http://example.com")
(json-encode-string "GET")
(json-encode-string "Content-Type: application/json")
(json-encode-string "{}")))
(curl-config-mode))
(display-buffer curl-config-buffer-name))
;;;###autoload
(defun curl-send-request ()
"Launch curl as a subprocess and send requests by feeding settings into the standard input."
(interactive)
(setq curl-config-buffer (get-buffer curl-config-buffer-name))
(setq curl-output-buffer (get-buffer-create curl-output-buffer-name))
(setq curl-error-buffer (get-buffer-create curl-error-buffer-name))
(with-current-buffer curl-output-buffer
(erase-buffer))
(with-current-buffer curl-error-buffer
(erase-buffer))
(setq curl-process
(make-process
:name "curl"
:buffer (get-buffer-create curl-output-buffer-name)
:stderr (get-buffer-create curl-error-buffer-name)
:command `(,curl-executable "-v" "-K-")
:sentinel (lambda (process event)
(run-hook-with-args
'curl-process-sentinel-hook process event))))
(process-send-string curl-process
(with-current-buffer curl-config-buffer
(buffer-string)))
(process-send-eof curl-process)
(setq curl-result-output-window (split-window-horizontally))
(setq curl-result-error-window (split-window-below nil curl-result-output-window))
(set-window-buffer curl-result-output-window curl-output-buffer-name)
(set-window-buffer curl-result-error-window curl-error-buffer-name)
(select-window curl-result-output-window))
(defun curl-edit-config-value ()
"
'edit-indirect-after-commit-functions
"
(interactive)
(unwind-protect
(progn
(narrow-to-region (line-beginning-position) (line-end-position))
(when (re-search-forward "\s*\\\"\\(.*\\)\\\"\s*$" nil t)
(edit-indirect-region (match-beginning 1)
(match-end 1)
t)))
(widen)))
(defun curl-send-request-to-buffer ()
(interactive)
(process-send-string curl-request-target-process
"curl -v -K-\n")
(process-send-string curl-request-target-process
(with-current-buffer curl-config-buffer
(buffer-string)))
(process-send-eof curl-request-target-process)
(process-send-eof curl-request-target-process))
;;;###autoload
(defun curl-select-process (&optional buf)
(interactive "bBuffer: ")
(if-let ((proc (get-buffer-process buf)))
(setq-local curl-process proc)))
;;;###autoload
(defun curl-start-on-shell ()
(interactive)
(process-send-string curl-process "curl -v -K-\n"))
;;;###autoload
(defun curl-start-make-process ()
(interactive)
(make-process
:name "curl"
:buffer (get-buffer-create curl-output-buffer-name)
:stderr (get-buffer-create curl-error-buffer-name)
:command `(,curl-executable "-v" "-K-")))
;;;###autoload
(defun curl-send ()
(interactive)
(process-send-string curl-process (buffer-string))
(process-send-eof curl-process))
;;;###autoload
(defun curl-json-to-data-as-region-kill-new ()
(interactive)
(kill-new
(json-encode-string
(json-encode
(json-read-from-string
(buffer-substring-no-properties
(region-beginning) (region-end)))))))
(define-derived-mode curl-config-mode fundamental-mode
:group 'curl)
(define-key curl-config-mode-map (kbd "C-c C-s") #'curl-start-on-shell)
(define-key curl-config-mode-map (kbd "C-c C-p") #'curl-start-make-process)
(define-key curl-config-mode-map (kbd "C-c C-l") #'curl-select-process)
(define-key curl-config-mode-map (kbd "C-c C-c") #'curl-send)
(provide 'curl)
;; curl.el ends here
他の言語の場合は cURL
を直接サブプロセスとして呼び出す事はあまりしない。標準の機能のラッパーとしてのサードパーティを使う。仮にcURLの機能を使うとしても直接呼び出すのではなく、 libcurl
をリンクして使う事が多い。例えばPythonでcURLを使いたい場合、libcurlをリンクしたPycURLというサードパーティライブラリを使う。Emacs Lispにもlibcurlを呼び出す拡張はある。
https://github.com/syohex/emacs-curl
ここで試した方法は、libcurlを使うのではなく、サブコマンドとして呼び出すという、何と言ったらいいか大雑把というか思い切りの良い手段を取った。またEmacsのやり方としてもそれ程悪い方法ではないように思う。Emacsのライブラリは、外部プログラムをサブプロセスとして呼び出して実現している機能が結構あるからだ。この辺りは言語の考え方の特徴が現れていて面白い。
HTTPに拘らない
TCPのコネクション(HTTP/3の場合はUDPだけれど)を作成できれば、HTTPは自分の力で手書きすればいいのかもしれない。Emacsには当然トランスポート層の接続を作るための関数が用意されている。
make-network-process
make-network-process
はEmacsが提供する低レベルの関数で、 src/process.c
内で定義されており、Cで実装されている34。
(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 ")
network-connection
network-connection
は、 make-network-process
の更に上位の関数で、作成したバッファが comint-mode
になっている。そのため作成されたバッファに生のHTTPリクエストを記述し、=comint-send-input= を呼び出す事で、HTTPリクエストを送信できる。HTTPレスポンスは同じバッファに出力される。
注意点としては comint-send-input
が RET
に割り当てられている。その場合、改行を入力しようとして RET
を押すとリクエストが送信される事になる。これはキー割り当てを工夫する事で解消できるし、 C-q
を使ってエスケーピングする事でキャリッジリターンなども入力できる。
(network-connection "localhost" 8000)
他にも上位の関数はある
これらの実装はまだ幾つかある。それぞれに必要な機能を提供している。
- 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-retrieve
、 request
、 plz
で事足りるため、直接使う機会はあまりないかもしれない。ただ使えるという事だけでも知っておくと、トラブルシューティングやデバッグの助けになる。
まとめ
今回はEmacsの様々なHTTPクライアントを確認した。目的も違えば、使っている機構も、レイヤーも異なる様々な方法でHTTPリクエストを送信し、どのようなデータが送信されたのかを確認した。またOrg-modeとBabelの強みを生かしたHTTPの記述と送信についても確認した。Babelを用いた方法では、改行文字の問題などでハマる箇所もあった。またボディから署名を計算するための小さなEmacs Lispを実装した。
Emacsについてまた知る事ができた。