nodejs
のhttp
モジュールで、リダイレクトをフォローするhttp
リクエストを行う。
リダイレクトでリクエストし直してくれるモジュールを追加すれば済む話だが、追加しない場合は自分で必要な分だけ実装する。
http.get()の概要
nodejs
といえばサーバーの構築だが、http
リクエストも行うことができる。
よく使われるGET
リクエストならhttp.get()
でメソッドの指定やリクエストの手順を簡略化することができる。
https
へのリクエストは、http
モジュールではなくhttps
モジュールを使う。http
のままだと次のようなエラーが表示される。
TypeError [ERR_INVALID_PROTOCOL]: Protocol "https:" not supported. Expected "http:"
なので、https://~~
のURLに対してはhttps
モジュールを使おう。
なお、実装ではhttps
を使ったので、以降のサンプルではhttps
が多く出現する。
http.get()やhttps.get()の仕様
http
もhttps
もget()
自体はあまり変わりはない。コールバック関数内でレスポンスの読み取りを行う。
https.get('https://blog.ikappio.com', (res) => {
const { statusCode, headers } = res;
console.log("code:", statusCode);
console.log("headers:", headers);
let data = "";
res.setEncoding("utf8")
// データが大きければ、dataイベントは複数回起きるはず
res.on("data", (chunk) => {
data += chunk;
});
// endイベントでデータの読み込みは終了
res.on("end", () => {
console.log("received data:", data);
});
});
基本的にはこのように行う。コールバックの中でデータの読み取りを行うので、リダイレクトに沿ってリクエストを再度行うのは、少し工夫が必要になると思う。(もっと簡単にできるのかもしれないが)
単純に実装するとコールバックの中でもう一度get()
することになり少し見にくいように思う。
http.get
自体もhttp.ClientRequest
インスタンスを返すが、これは個人的にはデータの読み取りには使いにくい。(というか取得できるのか?)
また、get()
は非同期に実行されるので、コールバックが実行されるのは、get()
がhttp.ClientRequest
を返したしばらく後になっている。
今回の実装は、特定URLにget
リクエストすると200
の前に302
が0回か1回返ってくることがある場合で行った。
実装の方針
get()
のコールバック関数の中でレスポンスのステータスコードをチェックして、その内容で次のリクエストを行うか行わないかを決定する。
ループと非同期処理を使う。Promise
とasync
とawait
を利用した。まずは、ループの方から。
リダイレクトはループで処理
2回目以降のリクエストは1回目と同じコードで行う。このため、無限ループ内でget()
を行うことにする。一定回数失敗したら終了。ステータスコード302
のリダイレクトでももう1回実行する。
ループに関するサンプルコードはこんな感じ。
// 一応関数の形にする。
const sampleLoop = () => {
let retryCount = 1; // リトライを数える
const MAX_RETRY = 5; // リトライ回数の上限
let response = null; // リクエストの結果の格納用変数
let url = "<URL to request>" // リダイレクトで書き換える可能性があるのでletで定義
while(true) {
if(retryCount > MAX_RETRY) {
// 5回やってダメなら諦める
// ..本当はちょっと処理があるけど省略
break;
}
// 以降はhttps.get()を使ってリクエストを行うが、これは後述する。
// ...
// カウントの更新
retryCount++;
continue;
}
// responseを使って処理を行う。
// ...
return response;
}
同期的処理に変換
https.get()
の処理は非同期なので、そのまま書くとコードの実行が進み、関数を抜けてしまう。
なので、レスポンスが来るまで待機するようにする必要がある。
そして、https.get()
の返り値はPromise
ではないので、https.get()
をPromise
でラップすることにした。(これは正しいのだろうか)
ラップして、これを実行する関数をasync
にすれば同期的に実行できる。
そんなわけで、https.get
の同期化したサンプルコードは次のようになる。
// awaitを使うので、asyncを関数に指定する
const syncGet = async (url) => {
const reqPromise = new Promise((resolve, reject) => {
https.get(url, (res) => {
// 色々処理。
// ...
res.on("end", () => {
// ここでresolveしておけばいい
resolve(someResult)
});
});
});
// awaitでresolveかrejectを待つ。
const result = await reqPromise.catch((reason) => {
// rejectされてたらここで返り値を設定できる。(上でチェーンしておいてもOK)
return "失敗"
});
return result;
}
この関数を使えば、同期的にhttpリクエストの結果を取得できる:
const result = syncGet("some url");
// リクエストが完了して結果を取得した後に下の行は実行される。
doSomething(result);
これらの2つを組み合わせればリダイレクトは処理できる。
また、実際のsyncGet
相当の関数のresolve
やreject
を行うときは一定のルールが必要になる。typescript
などなら型を定義すればいいが、今回はオブジェクトを返すようにした。
このオブジェクトにstatusCode
とステータスコードごとの追加プロパティを持たせる。
実装
あとはステータスコードをみて分岐したりを加えればいい。
サンプルコードは次のようになる。
const retrieveResourse = async () => {
let retryCount = 1;
const MAX_RETRY = 5;
let response = null;
let url = "<URL to request>"
while(true) {
if(retryCount > MAX_RETRY) {
// 5回やってダメなら諦める
response = { success: false, reason: "Retry Count Exceeded"};
break;
}
// カウントの更新
retryCount++;
const promise = new Promise((resolve, reject) => {
https.get(url, (res) => {
const { statusCode } = res;
if(statusCode === 302) {
// リダイレクトするのでもう一度ループする。
resolve({
statusCode,
url: res.headers.location, // リダイレクト先のURL
});
return;
}
if(statusCode === 200) {
// データを受け取り終了
let data = "";
res.setEncoding("utf8");
res.on("data", (chunk) => {
data += chunk;
});
res.on("end", () => {
resolve({
statusCode,
data,
});
return;
});
return;
}
// 他のステータスコードは想定外
resolve({
statusCode,
});
});
});
// 上のpromiseの終了を待つ。
const result = await promise.catch((reason) => {
// レスポンス以外のエラー(get自体に失敗など)
return { statusCode: 0, reason }
});
if(result.statusCode === 302) {
// もう1回ループする必要あり
url = result.url;
continue;
}
if(result.statusCode === 200) {
// 取得できた
response = {
success: true,
data: result.data,
};
break;
}
// エラーハンドリングする
console.log("不明なエラー?");
// 一応リトライ
continue;
// もしくはresponseを生成して終了させる
// response = {
// success: false,
// reason: result.reason,
// };
// break;
}
return response;
}
// あとはどこからか呼び出せばnullか取得できたデータが返ってくる
const res = retrieveResource();
if (res !== null && res.success) {
doSomething(res.data)
} else {
console.log("取得失敗:", res.reason)
}
少しネストが深いような気もするが、これでリダイレクトはフォローできる。
おわりに
あまり調べていないが、Python
のrequests
でのallow_redirects
オプションのようにリダイレクトでリクエストし直してくれるモジュールはあると思うので、そちらを使ったほうが安心だとは思う。
イベント中でresolve
してもいいものなのかよくわからないが、無事に動いているのでよしとする。
外部のモジュールをやたらと入れたくないのであれば上のような実装も仕方ないかなと思う。
長くなってしまった。以上。