Skip to content

Commit d699ca0

Browse files
author
Sean Roberts
committed
fix: 404ing list results should return empty iterators, not throw
1 parent 769c60f commit d699ca0

File tree

4 files changed

+118
-7
lines changed

4 files changed

+118
-7
lines changed

src/list.test.ts

+54
Original file line numberDiff line numberDiff line change
@@ -762,4 +762,58 @@ describe('list', () => {
762762
expect(directories).toEqual([])
763763
expect(mockStore.fulfilled).toBeTruthy()
764764
})
765+
766+
test('Handles missing content automatic pagination', async () => {
767+
const mockStore = new MockFetch().get({
768+
headers: { authorization: `Bearer ${edgeToken}` },
769+
response: new Response('<not_found>', { status: 404 }),
770+
url: `${edgeURL}/${siteID}/site:${storeName}?prefix=group%2F`,
771+
})
772+
773+
globalThis.fetch = mockStore.fetch
774+
775+
const store = getStore({
776+
edgeURL,
777+
name: storeName,
778+
token: edgeToken,
779+
siteID,
780+
})
781+
782+
const { blobs } = await store.list({
783+
prefix: 'group/',
784+
})
785+
786+
expect(blobs).toEqual([])
787+
expect(mockStore.fulfilled).toBeTruthy()
788+
})
789+
790+
test('Handles missing content manual pagination', async () => {
791+
const mockStore = new MockFetch().get({
792+
headers: { authorization: `Bearer ${edgeToken}` },
793+
response: new Response('<not_found>', { status: 404 }),
794+
url: `${edgeURL}/${siteID}/site:${storeName}`,
795+
})
796+
797+
globalThis.fetch = mockStore.fetch
798+
799+
const store = getStore({
800+
edgeURL,
801+
name: storeName,
802+
token: edgeToken,
803+
siteID,
804+
})
805+
const result: ListResult = {
806+
blobs: [],
807+
directories: [],
808+
}
809+
810+
for await (const entry of store.list({ paginate: true })) {
811+
result.blobs.push(...entry.blobs)
812+
result.directories.push(...entry.directories)
813+
}
814+
815+
expect(result.blobs).toEqual([])
816+
expect(result.directories).toEqual([])
817+
expect(mockStore.fulfilled).toBeTruthy()
818+
})
765819
})

src/store.ts

+20-7
Original file line numberDiff line numberDiff line change
@@ -409,21 +409,34 @@ export class Store {
409409
parameters: nextParameters,
410410
storeName,
411411
})
412-
const page = (await res.json()) as ListResponse
413412

414-
if (page.next_cursor) {
415-
currentCursor = page.next_cursor
416-
} else {
417-
done = true
413+
let blobs: ListResponseBlob[] = []
414+
let directories: string[] = []
415+
416+
if (![200, 204, 404].includes(res.status)) {
417+
throw new BlobsInternalError(res)
418418
}
419419

420-
const blobs = (page.blobs ?? []).map(Store.formatListResultBlob).filter(Boolean) as ListResponseBlob[]
420+
if (res.status === 404) {
421+
done = true
422+
} else {
423+
const page = (await res.json()) as ListResponse
424+
425+
if (page.next_cursor) {
426+
currentCursor = page.next_cursor
427+
} else {
428+
done = true
429+
}
430+
431+
blobs = (page.blobs ?? []).map(Store.formatListResultBlob).filter(Boolean) as ListResponseBlob[]
432+
directories = page.directories ?? []
433+
}
421434

422435
return {
423436
done: false,
424437
value: {
425438
blobs,
426-
directories: page.directories ?? [],
439+
directories,
427440
},
428441
}
429442
},

src/store_list.test.ts

+39
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,45 @@ describe('listStores', () => {
136136
})
137137
})
138138

139+
test('Handles missing content for auto pagination', async () => {
140+
const mockStore = new MockFetch().get({
141+
headers: { authorization: `Bearer ${apiToken}` },
142+
response: new Response('<not found>', { status: 404 }),
143+
url: `https://api.netlify.com/api/v1/blobs/${siteID}?prefix=site%3A`,
144+
})
145+
146+
globalThis.fetch = mockStore.fetch
147+
148+
const { stores } = await listStores({
149+
token: apiToken,
150+
siteID,
151+
})
152+
153+
expect(stores).toStrictEqual([])
154+
expect(mockStore.fulfilled).toBeTruthy()
155+
})
156+
157+
test('Handles missing content with manual pagination', async () => {
158+
const mockStore = new MockFetch().get({
159+
headers: { authorization: `Bearer ${apiToken}` },
160+
response: new Response('<not found>', { status: 404 }),
161+
url: `https://api.netlify.com/api/v1/blobs/${siteID}?prefix=site%3A`,
162+
})
163+
164+
globalThis.fetch = mockStore.fetch
165+
166+
const result: ListStoresResponse = {
167+
stores: [],
168+
}
169+
170+
for await (const entry of listStores({ token: apiToken, siteID, paginate: true })) {
171+
result.stores.push(...entry.stores)
172+
}
173+
174+
expect(result.stores).toStrictEqual([])
175+
expect(mockStore.fulfilled).toBeTruthy()
176+
})
177+
139178
describe('With edge credentials', () => {
140179
test('Lists site stores', async () => {
141180
const mockStore = new MockFetch().get({

src/store_list.ts

+5
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,11 @@ const getListIterator = (client: Client, prefix: string): AsyncIterable<ListStor
5656
method: HTTPMethod.GET,
5757
parameters: nextParameters,
5858
})
59+
60+
if (res.status === 404) {
61+
return { done: true, value: undefined }
62+
}
63+
5964
const page = (await res.json()) as ListStoresResponse
6065

6166
if (page.next_cursor) {

0 commit comments

Comments
 (0)