注:この記事では htmx.org@2.0.10 を使います。
htmx v4 は現在 beta 版として開発中で、4.0.0-beta4 も公開されています。
ただし、この記事は初心者向けに HTTP ヘッダー、CORS、HX-* ヘッダーの観察を目的とするため、安定版の htmx v2 系で動作確認します。
v4 系を試す場合は、CDN や npm のバージョン指定を変更したうえで、selfRequestsOnly、CORS、swap 挙動、イベントまわりの違いを確認してください。
はじめに
htmx は、HTML の属性だけで HTTP リクエストを送れる便利なライブラリです。
たとえば、次のように書くと、ボタンを押したときに GET リクエストを送れます。
<button hx-get="/hello" hx-target="#result">
取得する
</button>
<div id="result"></div>
この手軽さだけを見ると、htmx は「画面を簡単に更新するライブラリ」に見えます。
しかし、実際に使ってみると、htmx は ブラウザーから HTTP リクエストを送る練習道具としても便利です。
一方で、curl とは違う注意点もあります。
curl はターミナルから直接 HTTP リクエストを送ります。
htmx はブラウザーから HTTP リクエストを送ります。
そのため、htmx では次のような確認が必要になります。
- CORS
- Origin
- Sec-Fetch-* ヘッダー
- htmx が付ける HX-* ヘッダー
- Cookie の自動送信
- ブラウザーのセキュリティ制約
この記事では、curl と htmx を比較しながら、HTTP ヘッダーと CORS の基本を確認します。
この記事で使う練習サイト
この記事では、次の URL を使います。
https://nghttp2.org/httpbin
nghttp2.org は、HTTP/2 ライブラリである nghttp2 のプロジェクトサイトです。
また、HTTP/3 関連では nghttp3 というライブラリもあり、curl の HTTP/2 / HTTP/3 対応を学ぶときにも名前が出てきます。
つまり、nghttp2.org/httpbin は、単なる httpbin 互換サービスとしてだけでなく、HTTP/2 や HTTP/3 の学習文脈にもつなげやすい練習先です。
今回はまず、HTTP/2 / HTTP/3 の深い話には入りません。
初心者向けに、次のことを確認します。
- curl で GET リクエストする
- htmx で同じ GET リクエストを送る
- 返ってきた HTTP ヘッダーを見る
- CORS に関係するヘッダーを見る
- htmx が自動で送るヘッダーを見る
今回のゴール
この記事のゴールは、htmx で複雑な UI を作ることではありません。
ゴールは次の3つです。
1. curl で HTTP リクエストの基本形を見る
2. htmx でブラウザーから HTTP リクエストを送る
3. curl では意識しにくいブラウザー由来のヘッダーを確認する
特に重要なのは、次の教訓です。
curl で動く HTTP リクエストが、ブラウザーからもそのまま動くとは限らない。
htmx を使うと、この違いを実際に観察できます。
curl と htmx の違い
まず、curl と htmx の違いを整理します。
| 観点 | curl | htmx |
|---|---|---|
| 実行場所 | ターミナル | ブラウザー |
| CORS | 基本的に関係ない | 影響を受ける |
| Cookie | 明示しない限り送らない | ブラウザーが自動送信する場合がある |
| Origin | 通常は付かない | 外部 API では付く |
| Sec-Fetch-* | 付かない | ブラウザーが付ける |
| HX-* | 付かない | htmx が付ける |
| 用途 | HTTP の最小形を確認 | ブラウザー文脈込みの HTTP を確認 |
curl は、HTTP リクエストの最小形を見る道具です。
htmx は、ブラウザーから送られる HTTP リクエストを見る道具です。
どちらも HTTP の学習に役立ちますが、見えるものが違います。
1. curl で GET リクエストする
まずは curl で GET リクエストを送ります。
curl -s 'https://nghttp2.org/httpbin/get?keyword=htmx&page=1'
JSON を見やすくするために、jq が使える場合は次のようにします。
curl -s 'https://nghttp2.org/httpbin/get?keyword=htmx&page=1' | jq .
たとえば、次のようなレスポンスが返ります。
{
"args": {
"keyword": "htmx",
"page": "1"
},
"headers": {
"Accept": "*/*",
"Host": "nghttp2.org",
"User-Agent": "curl/..."
},
"origin": "<your-ip-address>",
"url": "https://nghttp2.org/httpbin/get?keyword=htmx&page=1"
}
ここで確認したいのは、args です。
"args": {
"keyword": "htmx",
"page": "1"
}
URL のクエリ文字列が、サーバー側ではこのように解釈されています。
?keyword=htmx&page=1
つまり、curl で次のことを確認できます。
- URL が正しいか
- クエリパラメーターが届いているか
- API が生きているか
- JSON レスポンスが返るか
2. curl でレスポンスヘッダーを見る
次に、レスポンスヘッダーを確認します。
curl -I 'https://nghttp2.org/httpbin/get'
私の環境では、次のようなヘッダーが返りました。
HTTP/2 200
content-type: application/json
access-control-allow-origin: *
access-control-allow-credentials: true
strict-transport-security: max-age=31536000
server: nghttpx
alt-svc: h3=":443"; ma=3600
ここで注目したいのは、次のヘッダーです。
access-control-allow-origin: *
これは CORS に関係するヘッダーです。
ざっくり言うと、外部のブラウザーからこのリソースを読めるかどうかに関係します。
* は、認証情報を含まないリクエストであれば、広い origin からのアクセスを許可するという意味です。
ただし、これだけで何でも安全に呼べるわけではありません。
Cookie や Authorization ヘッダーなど、認証情報を含むリクエストでは話が変わります。
3. htmx で GET リクエストする
次に、htmx で同じ URL にリクエストしてみます。
test.html を作ります。
<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title>htmx HTTP header demo</title>
<script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.10/dist/htmx.min.js"></script>
<script>
htmx.config.selfRequestsOnly = false;
</script>
</head>
<body>
<h1>htmx HTTP header demo</h1>
<button
hx-get="https://nghttp2.org/httpbin/get?keyword=htmx&page=1"
hx-target="#result"
hx-swap="innerHTML">
GETする
</button>
<h2>Result</h2>
<pre id="result">ここに結果が表示されます。</pre>
</body>
</html>
ローカルサーバーで開きます。Node.js、Python、PHP などの選択肢があります。
npx serve -l 8888
python3 -m http.server 8888
php -S localhost:8888
ブラウザーで開きます。
http://localhost:8888/test.html
ボタンを押すと、https://nghttp2.org/httpbin/get?keyword=htmx&page=1 にリクエストが送られ、レスポンス JSON が <pre id="result"> に表示されます。
4. selfRequestsOnly = false とは何か
先ほどの HTML には、次の設定を入れました。
<script>
htmx.config.selfRequestsOnly = false;
</script>
htmx v2 では、デフォルトでは同一オリジンへのリクエストだけを許可します。
今回のページは次の URL で開いています。
http://localhost:8888/test.html
リクエスト先は次です。
https://nghttp2.org/httpbin/get?keyword=htmx&page=1
これは外部サイトです。
そのため、htmx 側の制限を外すために selfRequestsOnly = false を設定しています。
ただし、注意点があります。
selfRequestsOnly = false は、htmx 側の制限を外すだけです。
ブラウザーの CORS 制約を解除するものではありません。
つまり、外部 API 側が CORS を許可していなければ、やはり失敗します。
5. htmx から送られたリクエストを確認する
htmx で GET した結果は、たとえば次のようになります。
{
"args": {
"keyword": "htmx",
"page": "1"
},
"headers": {
"Accept": "*/*",
"Accept-Encoding": "gzip, deflate, br, zstd",
"Accept-Language": "ja,en;q=0.9,en-US;q=0.8,fr;q=0.7",
"Host": "nghttp2.org",
"Hx-Current-Url": "http://localhost:8888/test.html",
"Hx-Request": "true",
"Hx-Target": "result",
"Origin": "http://localhost:8888",
"Referer": "http://localhost:8888/",
"Sec-Fetch-Dest": "empty",
"Sec-Fetch-Mode": "cors",
"Sec-Fetch-Site": "cross-site",
"User-Agent": "Mozilla/5.0 ..."
},
"origin": "<your-ip-address>",
"url": "https://nghttp2.org/httpbin/get?keyword=htmx&page=1"
}
curl のときより、たくさんのヘッダーが増えています。
これは、htmx が悪いわけではありません。
ブラウザーから送っているためです。
6. htmx が付ける HX-* ヘッダー
まず、htmx が付けるヘッダーを見ます。
"Hx-Current-Url": "http://localhost:8888/test.html",
"Hx-Request": "true",
"Hx-Target": "result"
それぞれの意味は次の通りです。
| ヘッダー | 意味 |
|---|---|
Hx-Request: true |
htmx からのリクエストである |
Hx-Target: result |
差し替え対象の要素が #result
|
Hx-Current-Url |
htmx が認識している現在のページ URL |
サーバー側では、Hx-Request を見て、htmx からのリクエストかどうかを判定できます。
たとえば、Node.js なら次のような分岐ができます。
if (req.headers["hx-request"] === "true") {
// htmx 向けに HTML 断片を返す
} else {
// 通常アクセス向けにページ全体を返す
}
これが htmx の重要なポイントです。
htmx は単に HTTP リクエストを送るだけではありません。
サーバー側から見ると、「これは htmx から来たリクエストです」と判定できる情報も送っています。
7. ブラウザーが付ける Origin
次に、Origin を見ます。
"Origin": "http://localhost:8888"
これは、ブラウザーが付けたヘッダーです。
意味は次の通りです。
このリクエストは
http://localhost:8888のページから送られました。
CORS では、この Origin が重要です。
ブラウザーは外部 API にリクエストするとき、Origin を送ります。
外部 API は、それに対して「許可します」というレスポンスヘッダーを返す必要があります。
その代表がこれです。
Access-Control-Allow-Origin: *
または、特定の origin だけを許可する場合は次のようになります。
Access-Control-Allow-Origin: http://localhost:8888
初心者向けには、こう覚えるとよいです。
Origin:
ブラウザーが「このページから来ました」と送る
Access-Control-Allow-Origin:
サーバーが「そのページから来てもよいです」と返す
8. Sec-Fetch-* ヘッダーを見る
次に、Sec-Fetch-* ヘッダーを見ます。
"Sec-Fetch-Dest": "empty",
"Sec-Fetch-Mode": "cors",
"Sec-Fetch-Site": "cross-site"
これらは、ブラウザーが自動で付けるヘッダーです。
Sec-Fetch-Site
"Sec-Fetch-Site": "cross-site"
これは、リクエスト元とリクエスト先の関係を示します。
今回のページは次です。
http://localhost:8888/test.html
リクエスト先は次です。
https://nghttp2.org/httpbin/get?keyword=htmx&page=1
別サイトなので、cross-site になっています。
もし、同じ origin の API にリクエストする場合は、same-origin になります。
Sec-Fetch-Mode
"Sec-Fetch-Mode": "cors"
これは、CORS モードのリクエストであることを示します。
今回のように、ブラウザーから外部 API にリクエストする場合に関係します。
Sec-Fetch-Dest
"Sec-Fetch-Dest": "empty"
これは、リクエストの用途を示します。
empty は、画像や CSS や script の読み込みではなく、fetch / XMLHttpRequest のようなプログラム的なリクエストであることを意味します。
htmx の hx-get は HTML 属性で書きますが、内部的にはブラウザーからプログラム的な HTTP リクエストを送っています。
そのため、Sec-Fetch-Dest: empty になります。
9. curl と htmx のヘッダーを比較する
curl のリクエストはシンプルです。
curl -s 'https://nghttp2.org/httpbin/get?keyword=htmx&page=1' | jq '.headers'
一方、htmx から送ると、次のようなヘッダーが追加されます。
Hx-Request
Hx-Target
Hx-Current-Url
Origin
Referer
Sec-Fetch-Site
Sec-Fetch-Mode
Sec-Fetch-Dest
Sec-Ch-Ua
Sec-Ch-Ua-Platform
Cookie があれば Cookie
この違いが重要です。
curl は HTTP の最小形を見るのに向いています。
htmx はブラウザーが実際に送る HTTP リクエストを見るのに向いています。
同じ URL に GET していても、送られるヘッダーは同じではありません。
10. よくあるエラー
ここからは、初心者がつまずきやすいエラーを整理します。
htmx:invalidPath
外部 URL にリクエストしようとして、htmx 側で拒否されることがあります。
原因は、htmx v2 の selfRequestsOnly です。
対応は次です。
<script>
htmx.config.selfRequestsOnly = false;
</script>
ただし、これは htmx 側の制限を外すだけです。
CORS エラーを解決するものではありません。
CORS エラー
ブラウザーの Console に次のようなエラーが出ることがあります。
Access to XMLHttpRequest at 'https://example.com/...'
from origin 'http://localhost:8888'
has been blocked by CORS policy
これは、外部 API 側がブラウザーからのアクセスを許可していないときに起きます。
curl では成功しても、htmx では失敗することがあります。
理由は、curl は CORS の対象ではないからです。
Request header field hx-target is not allowed
次のようなエラーが出ることがあります。
Request header field hx-target is not allowed by Access-Control-Allow-Headers
これは、htmx が送る HX-Target などのヘッダーを、外部 API 側が CORS で許可していない場合に起きます。
外部 API 側で次のようなヘッダーが必要になります。
Access-Control-Allow-Headers: HX-Request, HX-Target, HX-Current-URL
ただし、外部 API の設定は自分では変更できません。
その場合は、直接ブラウザーから叩くのではなく、自分のサーバーを中継させます。
11. 外部 API を htmx で試す前の確認手順
今回の作業でわかった教訓は、確認順序が大事だということです。
おすすめのワークフローは次です。
1. curl で API が生きているか確認する
2. curl -I でレスポンスヘッダーを見る
3. Access-Control-Allow-Origin を確認する
4. htmx v2 では selfRequestsOnly = false を設定する
5. まず GET だけで試す
6. DevTools の Network タブを見る
7. Origin と Access-Control-Allow-Origin を確認する
8. HX-* ヘッダーが CORS で許可されるか確認する
9. API トークンが必要なものはブラウザーから直接呼ばない
特に、最初は GET だけで確認するのがおすすめです。
POST、JSON、独自ヘッダー、Authorization などを入れると、CORS preflight が絡んで難しくなります。
12. API トークンを HTML に書いてはいけない
htmx を使うと、HTML だけで外部 API を呼べるように見えます。
しかし、次のように API トークンを HTML に書いてはいけません。
<!-- 実運用ではやってはいけない例 -->
<button
hx-get="https://api.example.com/data"
hx-headers='{"Authorization":"Bearer sk-xxxxx"}'>
呼び出す
</button>
HTML はブラウザーに配布されます。
つまり、利用者から見えます。
AI API や業務 API のトークンを使う場合は、ブラウザーから直接外部 API を呼ぶのではなく、自分のサーバーを中継させます。
ブラウザー / htmx
↓
自社 API サーバー
↓
外部 API
API トークンは、自社 API サーバー側の環境変数などに置きます。
13. htmx は curl の代わりになるのか
今回の実験を通して、htmx は curl のような練習道具として便利だと感じました。
ただし、完全な代わりではありません。
curl は、HTTP の最小形を確認するのに向いています。
curl -i 'https://nghttp2.org/httpbin/get?keyword=htmx&page=1'
htmx は、ブラウザーから送られる HTTP を確認するのに向いています。
<button
hx-get="https://nghttp2.org/httpbin/get?keyword=htmx&page=1"
hx-target="#result">
GETする
</button>
curl では、CORS や Sec-Fetch-* を意識する必要はあまりありません。
htmx では、ブラウザーから送るため、それらを確認する必要があります。
つまり、次のように使い分けるとよいです。
| 用途 | 道具 |
|---|---|
| API が生きているか確認する | curl |
| HTTP ヘッダーを最小構成で見る | curl |
| ブラウザーから送ると何が増えるか見る | htmx |
| CORS を確認する | htmx + DevTools |
| サーバー側で htmx 判定をしたい | HX-* ヘッダー |
まとめ
この記事では、curl と htmx を比較しながら、HTTP ヘッダーと CORS を確認しました。
学んだことは次の通りです。
- curl は HTTP リクエストの最小形を見るのに便利
- htmx はブラウザーから HTTP リクエストを送る練習に便利
- htmx v2 で外部 URL を呼ぶには
selfRequestsOnly = falseが必要 - 外部 API では CORS の確認が必要
- htmx は
HX-Request、HX-Target、HX-Current-URLなどを送る - ブラウザーは
OriginやSec-Fetch-*ヘッダーを送る - curl で成功しても、ブラウザーから成功するとは限らない
- API トークンを HTML に書いてはいけない
今回の教訓は、次の一文にまとめられます。
htmx は curl のように HTTP リクエストを試せる便利な道具だが、ブラウザーのセキュリティ境界の中で動くため、curl では求められない確認作業が必要になる。
htmx を使うと、HTTP リクエストが HTML から見えるようになります。
同時に、ブラウザーがどのようなヘッダーを付け、どのような制約をかけているのかも見えてきます。
初心者が HTTP を学ぶ入口として、curl と htmx を並べて使うのはかなり有効だと思います。