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が通るのになぜ」という観点で差分を探す。

当たり前のことばかりだが、疲れていたり思い込みがあると見えなくなる。次にハマったときのために書いておいた。