WordPressのREST API認証で1日溶かした話
先月、WordPressのREST API認証でまる1日を溶かした。同じ罠にハマる人を減らしたくて記録しておく。
何をやろうとしていたか
WordPressへの記事自動投稿システムを作ろうとしていた。PythonスクリプトからREST APIを叩いて、Markdownで書いた記事をHTMLに変換してWordPressに投稿するという仕組みだ。
構成としてはそんなに複雑ではない。でもこれが思わぬところで躓いた。
最初の躓き:401 Unauthorized
最初に試したのはユーザー名とパスワードによるBasic認証だった。
import requests
response = requests.post(
"https://example.com/wp-json/wp/v2/posts",
auth=("admin", "mypassword"),
json={"title": "テスト投稿", "status": "draft"}
)
print(response.status_code) # → 401
なぜか401が返ってくる。ユーザー名とパスワードは間違っていない。WordPressの管理画面には普通にログインできる。
調べると、WordPress 5.6以降はBasic認証でユーザーパスワードをそのまま使う方法は非推奨になっており、「アプリケーションパスワード」という別の認証情報を使う必要があるとわかった。
アプリケーションパスワードで再挑戦
WordPress管理画面のユーザー設定から「アプリケーションパスワード」を発行した。形式は xxxx xxxx xxxx xxxx xxxx xxxx のようなスペース区切りの文字列だ。
response = requests.post(
"https://example.com/wp-json/wp/v2/posts",
auth=("admin", "xxxx xxxx xxxx xxxx xxxx xxxx"),
json={"title": "テスト投稿", "status": "draft"}
)
print(response.status_code) # → 401
まだ401だ。
認証情報は間違っていないはずなのに。コピーし直してみた。スペースを除去してみた。それでもだめだった。
沼にハマっていく午後
昼食を食べ終えてから作業を再開した頃には、もう問題がわからなくなっていた。
curl で試してみた。なぜかcurlは通った。
curl -X POST https://example.com/wp-json/wp/v2/posts \
-u "admin:xxxx xxxx xxxx xxxx xxxx xxxx" \
-H "Content-Type: application/json" \
-d '{"title":"テスト","status":"draft"}'
# → 201 Created
curlは成功するのにPythonのrequestsは失敗する。意味がわからなかった。
StackOverflowを読み漁った。GitHubのIssueを見た。WordPressの公式ドキュメントを何度も読んだ。
原因判明:SSL証明書の検証とリダイレクト
夕方になってようやく気づいた。
私のWordPressはHTTPS(SSL)で動いているが、Pythonのrequestsライブラリがリダイレクトを処理する際に、POSTメソッドがGETに変換されてしまっていた。そして認証ヘッダーがリダイレクト後に引き継がれていなかった。
正確には二つの問題が絡み合っていた。
1. HTTPからHTTPSへのリダイレクトが発生していた(エンドポイントのURLをhttpで書いていた)
2. リダイレクト後の認証ヘッダーが消えていた
# 問題のあったコード
response = requests.post(
"http://example.com/wp-json/wp/v2/posts", # ← httpになっていた
auth=("admin", "xxxx xxxx xxxx xxxx xxxx xxxx"),
json={"title": "テスト投稿", "status": "draft"}
)
# 修正後
response = requests.post(
"https://example.com/wp-json/wp/v2/posts", # ← httpsに修正
auth=("admin", "xxxx xxxx xxxx xxxx xxxx xxxx"),
json={"title": "テスト投稿", "status": "draft"}
)
URLの先頭が http:// だったのが原因だった。typoと呼ぶにも情けない凡ミスだ。
curlが通っていたのはcurlが自動的にHTTPSにリダイレクトして認証情報を引き継いでいたためだった。
なぜ1日かかったのか
振り返ると、問題の特定に時間がかかった原因がいくつかある。
エラーメッセージが不親切だった。401 Unauthorizedというだけで、「リダイレクト後の認証ヘッダーが消えた」という原因は一切書かれていない。
curlが通ることで迷走した。「curlで動くのにPythonで動かない」という状況が、「Pythonの問題だ」という方向に思考を向けてしまった。本当の問題はURLだったのに。
思い込みがあった。「URLは正しいはず」という前提で、認証まわりばかりを調べ続けた。
教訓
1. エンドポイントのURLは必ずHTTPSで書く。自動リダイレクトに依存しない。
2. 問題の仮説を複数持つ。「認証が間違っている」だけでなく「URLが間違っている」「ヘッダーが正しく設定されていない」という仮説を並行して検証する。
3. curlとコードで差異があったら、差異そのものを調べる。「curlが通るのになぜ」という観点で差分を探す。
当たり前のことばかりだが、疲れていたり思い込みがあると見えなくなる。次にハマったときのために書いておいた。