Repository navigation
getAllContents() が取得中のコンテンツ減少時に終了しない可能性がある #116
Description
概要
getAllContents() のページング中に、対象コンテンツが削除・非公開化されるなどして件数が減ると、処理が終了せず、リクエストを送り続ける可能性があります。
現行実装では、最初の limit: 0 リクエストで取得した totalCount を固定の終了条件として使っています。一方、各ページの取得後は、実際に取得できた件数だけ contents.length が増え、offset は常に 100 ずつ増えます。
そのため、最初の totalCount に達する前に空ページが返ると、contents.length < totalCount が永続的に真となります。
対象箇所:
microcms-js-sdk/src/createClient.ts
Lines 303 to 328 in cbd80f4
| endpoint, | |
| queries: { ...queries, limit: 0 }, | |
| requestInit: customRequestInit, | |
| }); | |
| let contents: (T & MicroCMSListContent)[] = []; | |
| let offset = 0; | |
| const sleep = (ms: number) => | |
| new Promise((resolve) => setTimeout(resolve, ms)); | |
| while (contents.length < totalCount) { | |
| const { contents: _contents } = (await makeRequest({ | |
| endpoint, | |
| queries: { ...queries, limit, offset }, | |
| requestInit: customRequestInit, | |
| })) as MicroCMSListResponse<T>; | |
| contents = contents.concat(_contents); | |
| offset += limit; | |
| if (contents.length < totalCount) { | |
| await sleep(1000); // sleep for 1 second before the next request | |
| } | |
| } | |
対象バージョン
microcms-js-sdkv3.5.0main(commitcbd80f4623eed2b416ef61f817a59ead4d6106cc)
再現シナリオ
レスポンスを次の順序でモックすると再現できます。
limit=0の件数取得がtotalCount: 101を返すlimit=100&offset=0が 100 件を返す(累計 100 件)- 残りの 1 件を削除または非公開化する
limit=100&offset=100が 0 件を返す- 以降の
offset=200,offset=300, ... も 0 件を返す
手順 4 以降は contents.length が 100 のまま、最初に取得した totalCount は 101 のままなので、ループを抜けられません。現行実装の sleep(1000) により、外部からタイムアウトまたは中断されるまで、1 秒間隔でリクエストが続きます。
実際の動作
getAllContents()の Promise が完了しない- 空ページへのリクエストが無制限に続く
- SSG、ビルド、ワーカー、サーバーリクエストなどが外部タイムアウトまで停止する可能性がある
期待する動作
取得中に対象件数が変化しても、リクエスト回数が有界であり、Promise が必ず完了(resolve または reject)することを期待します。
例えば、次のいずれかで無限ループを防げると思います。
- 取得したページが
limit未満なら終了する - 最初の
totalCountから最大ページ数を決め、offsetを基準にループを制限する
回帰テストとして、上記のレスポンス列で offset=100 の空ページ取得後に完了し、offset=200 をリクエストしないことを確認できると思います。
関連箇所
getAllContentIds() にも同様の終了条件があるため、同じ事象が発生する可能性があります。
microcms-js-sdk/src/createClient.ts
Lines 234 to 286 in cbd80f4
| const getAllContentIds = async ({ | |
| endpoint, | |
| alternateField, | |
| draftKey, | |
| filters, | |
| orders, | |
| customRequestInit, | |
| }: GetAllContentIdsRequest): Promise<string[]> => { | |
| const limit = 100; | |
| const defaultQueries: MicroCMSQueries = { | |
| draftKey, | |
| filters, | |
| orders, | |
| limit, | |
| fields: alternateField ?? 'id', | |
| depth: 0, | |
| }; | |
| const { totalCount } = await makeRequest({ | |
| endpoint, | |
| queries: { ...defaultQueries, limit: 0 }, | |
| requestInit: customRequestInit, | |
| }); | |
| let contentIds: string[] = []; | |
| let offset = 0; | |
| const sleep = (ms: number) => | |
| new Promise((resolve) => setTimeout(resolve, ms)); | |
| const isStringArray = (arr: unknown[]): arr is string[] => | |
| arr.every((item) => typeof item === 'string'); | |
| while (contentIds.length < totalCount) { | |
| const { contents } = (await makeRequest({ | |
| endpoint, | |
| queries: { ...defaultQueries, offset }, | |
| requestInit: customRequestInit, | |
| })) as MicroCMSListResponse<Record<string, unknown>>; | |
| const ids = contents.map((content) => content[alternateField ?? 'id']); | |
| if (!isStringArray(ids)) { | |
| throw new Error( | |
| 'The value of the field specified by `alternateField` is not a string.', | |
| ); | |
| } | |
| contentIds = [...contentIds, ...ids]; | |
| offset += limit; | |
| if (contentIds.length < totalCount) { | |
| await sleep(1000); // sleep for 1 second before the next request | |
| } |
Activity
Sinhalite commentedon Sep 17, 2026
@gopalkriagg
ご報告ありがとうございます。
ご指摘いただいたとおり、取得中にコンテンツが削除・非公開化され、初回取得時の
totalCountより実際の件数が少なくなると、リクエストが終了しない可能性がありました。getAllContents() と getAllContentIds() の終了条件を、取得済み件数ではなく、初回の
totalCountとoffsetを基準に判定するよう修正しました。これにより、取得中に件数が減少した場合も、リクエスト回数が有限になります。修正は
microcms-js-sdk@3.5.1に含まれています。具体的な再現手順と修正案をご提示いただき、ありがとうございました。
gopalkriagg commentedon Sep 17, 2026
@Sinhalite 早速修正し、リリースして頂きありがとうございました!