Slack APIでページネーション(cursor)が機能しない原因を突き止めてみた
はじめに
Google Apps Script(GAS)でスプレッドシートのメニューからSlackのチャンネル一覧を取得するツールを作っていたところ、conversations.listのページネーション(cursor)がうまく機能しないという問題にぶつかりました。原因を調べた結果、Slack APIのリクエスト設定に思わぬ落とし穴があることがわかったので、備忘録として記事にまとめます。
実現したかったこと
スプレッドシートのメニューから実行すると、Slackのパブリックチャンネルの名前を取得してログに出力する、というシンプルなGASです。
const MENU_NAME = 'チャンネル名';
const MENU_ITEM_NAME = '一覧表示';
function onOpen() {
const ui = SpreadsheetApp.getUi();
ui.createMenu(MENU_NAME).addItem(MENU_ITEM_NAME, "runFunc").addToUi();
}
function runFunc() {
const channelList = getChannelList();
Logger.log(channelList.length);
}
function getChannelList(){
let cursor = "";
const allChannels = [];
do {
const payload = {
types: "public_channel",
exclude_archived: true,
limit: 50,
cursor
};
const response = UrlFetchApp.fetch(
'https://slack.com/api/conversations.list',
{
method: "post",
contentType: 'application/json',
headers: { "Authorization": `Bearer ${getSlackToken()}` },
payload: JSON.stringify(payload),
muteHttpExceptions: true
});
const result = JSON.parse(response.getContentText());
if(!result.ok){
Logger.log(response.getResponseCode());
throw new Error('レスポンスが正しく取得できませんでした');
}
result.channels.forEach((item)=>{
allChannels.push(item.name);
Logger.log(item.name);
});
cursor = result.response_metadata?.next_cursor || "";
} while (cursor);
return allChannels;
}
function getSlackToken() {
const token = PropertiesService.getScriptProperties().getProperty(`BOT_TOKEN`);
if (!token) {
throw new Error(
"トークンが読み込めません"
);
}
return token;
}
チャンネル一覧の取得にはSlack APIのconversations.listを使います。チャンネル数が多い場合は一度に全件返ってこないため、レスポンスに含まれるresponse_metadata.next_cursorを次のリクエストのcursorパラメータに渡し、cursorが空になるまでループさせるという実装です。
発生した問題
一見よくある実装なのですが、実行すると次のような症状が出ました。
- 一度のリクエストで指定した50件ではなく100件のチャンネルを取得している
- next_cursorを使った2回目以降のリクエストではページが進まず、同じ結果が返り続ける


ログで確認すると、result.response_metadata?.next_cursorの値自体は取得できているように見えるため、「なぜcursorを渡しているのに正しく反映されないのか」が最初は謎でした。
「同じ結果が返り続ける」という症状からも何となく察せると思いますが、このnext_cursorの値を調べると、cursor変数に毎回まったく同じ値が代入されていることが確認できました。
また、limitで指定した50件ではなく、100件のチャンネルを取得していました。
なお、この100件という数値は、チャンネルの取得数を指定しなかった場合のデフォルトの値と同じでした。
これらのことから、そもそもpayload全体がAPI側に正しく認識されていないのではないかと考えました。
原因
原因は、リクエストメソッドとボディの形式の不適合でした。
GASのUrlFetchAppは、payloadにJavaScriptのオブジェクトをそのまま渡すと、自動的にapplication/x-www-form-urlencoded(もしくは multipart/form-data)としてエンコードして送信してくれます。
しかし今回のコードのように、
- contentType: 'application/json'を明示的に指定
- payloadをJSON.stringify(payload)でJSON文字列化して送信
としてしまうと、リクエストボディはJSON文字列としてそのまま送られます。
ところがSlackの読み取りメソッドでは、POSTリクエストにJSONのpayloadを送信しても、その属性を理解しないという仕様がありました。
payload全体が正しく認識されていないという推測も、上記の仕様と一致します。
今回はconversations.listのような読み取りメソッドを、POSTリクエスト&JSON形式で送信してしまったことにより、このパターンに該当してしまい、問題が発生したと考えられます。
解決策
対処はシンプルで、contentTypeの指定を削除し、payloadはオブジェクトのまま(JSON.stringifyせずに)渡すことです。
これによりUrlFetchAppがデフォルトのapplication/x-www-form-urlencodedとしてボディをエンコードして送信するようになり、Slack API側でもlimit、cursorを含む各パラメータが正しく解釈されるようになりました。
「読み取りメソッドで、POSTリクエスト&JSON形式」のパターンを回避した形になります。
Before
const response = UrlFetchApp.fetch(
'https://slack.com/api/conversations.list',
{
method: "post",
contentType: 'application/json',
headers: { "Authorization": `Bearer ${getSlackToken()}` },
payload: JSON.stringify(payload),
muteHttpExceptions: true
}
);
After
const response = UrlFetchApp.fetch(
'https://slack.com/api/conversations.list',
{
method: "post",
headers: { "Authorization": `Bearer ${getSlackToken()}` },
payload: payload, // JSON.stringify しない
muteHttpExceptions: true
}
);
contentTypeの行を削除し、payload: JSON.stringify(payload)をpayload: payloadに変更しただけですが、これでパブリックチャンネルを問題なく取得できるようになりました。
まとめ
今回はSlack APIのページネーション(cursor)がうまくいかない問題について、原因を調べて無事解決することができました。
Slack APIは、読み取りメソッドにおいてPOSTリクエスト&JSON形式だと属性を認識できないという事実は、意外と知られていない上に、陥りやすいポイントなのではないでしょうか。
この記事が同じような事象で悩んでいる方の参考になれば幸いです。
参考記事
クラスメソッドオペレーションズ株式会社について
クラスメソッドグループのオペレーション企業です。
運用・保守開発・サポート・情シス・バックオフィスの専門チームが、IT・AIをフル活用した「しくみ」を通じて、お客様の業務代行から課題解決や高付加価値サービスまでを提供するエキスパート集団です。
当社は様々な職種でメンバーを募集しています。
「オペレーション・エクセレンス」と「らしく働く、らしく生きる」を共に実現するカルチャー・しくみ・働き方にご興味がある方は、クラスメソッドオペレーションズ株式会社 コーポレートサイトをぜひご覧ください。
※2026年1月 アノテーション㈱から社名変更しました。




