Skip to content

Commit fae19ac

Browse files
piehgatsbybotwardpeet
authoredJul 2, 2020
fix(gatsby-source-drupal): end activities if errors are thrown (#25447)
* fix(gatsby-source-drupal): end activities if errors are thrown * Update packages/gatsby-source-drupal/src/__tests__/index.js Co-authored-by: Ward Peeters <ward@coding-tech.com> Co-authored-by: gatsbybot <mathews.kyle+gatsbybot@gmail.com> Co-authored-by: Ward Peeters <ward@coding-tech.com>
1 parent 9875dfd commit fae19ac

File tree

2 files changed

+234
-101
lines changed

2 files changed

+234
-101
lines changed
 

‎packages/gatsby-source-drupal/src/__tests__/index.js

+97-3
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
jest.mock(`axios`, () => {
22
return {
3-
get: path => {
3+
get: jest.fn(path => {
44
const last = path.split(`/`).pop()
55
try {
66
return { data: require(`./fixtures/${last}.json`) }
77
} catch (e) {
88
console.log(`Error`, e)
99
return null
1010
}
11-
},
11+
}),
1212
}
1313
})
1414

@@ -18,13 +18,16 @@ jest.mock(`gatsby-source-filesystem`, () => {
1818
}
1919
})
2020

21+
const normalize = require(`../normalize`)
22+
const downloadFileSpy = jest.spyOn(normalize, `downloadFile`)
23+
2124
const { createRemoteFileNode } = require(`gatsby-source-filesystem`)
2225

2326
const { sourceNodes } = require(`../gatsby-node`)
2427
const { handleWebhookUpdate } = require(`../utils`)
2528

2629
describe(`gatsby-source-drupal`, () => {
27-
const nodes = {}
30+
let nodes = {}
2831
const createNodeId = id => `generated-id-${id}`
2932
const baseUrl = `http://fixture`
3033
const createContentDigest = jest.fn().mockReturnValue(`contentDigest`)
@@ -362,4 +365,95 @@ describe(`gatsby-source-drupal`, () => {
362365
expect(nodes[createNodeId(`tag-3`)]).toBeDefined()
363366
expect(nodes[createNodeId(`article-5`)]).toBeDefined()
364367
})
368+
369+
describe(`Error handling`, () => {
370+
describe(`Does end activities if error is thrown`, () => {
371+
const axios = require(`axios`)
372+
beforeEach(() => {
373+
nodes = {}
374+
reporter.activityTimer.mockClear()
375+
activity.start.mockClear()
376+
activity.end.mockClear()
377+
axios.get.mockClear()
378+
downloadFileSpy.mockClear()
379+
})
380+
381+
it(`during data fetching`, async () => {
382+
axios.get.mockImplementationOnce(() => {
383+
throw new Error(`data fetching failed`)
384+
})
385+
expect.assertions(5)
386+
387+
try {
388+
await sourceNodes(args, { baseUrl })
389+
} catch (e) {
390+
expect(e).toMatchInlineSnapshot(`[Error: data fetching failed]`)
391+
}
392+
393+
expect(reporter.activityTimer).toHaveBeenCalledTimes(1)
394+
expect(reporter.activityTimer).toHaveBeenNthCalledWith(
395+
1,
396+
`Fetch data from Drupal`
397+
)
398+
399+
expect(activity.start).toHaveBeenCalledTimes(1)
400+
expect(activity.end).toHaveBeenCalledTimes(1)
401+
})
402+
403+
it(`during file downloading`, async () => {
404+
downloadFileSpy.mockImplementationOnce(() => {
405+
throw new Error(`file downloading failed`)
406+
})
407+
408+
expect.assertions(6)
409+
410+
try {
411+
await sourceNodes(args, { baseUrl })
412+
} catch (e) {
413+
expect(e).toMatchInlineSnapshot(`[Error: file downloading failed]`)
414+
}
415+
416+
expect(reporter.activityTimer).toHaveBeenCalledTimes(2)
417+
expect(reporter.activityTimer).toHaveBeenNthCalledWith(
418+
1,
419+
`Fetch data from Drupal`
420+
)
421+
expect(reporter.activityTimer).toHaveBeenNthCalledWith(
422+
2,
423+
`Remote file download`
424+
)
425+
426+
expect(activity.start).toHaveBeenCalledTimes(2)
427+
expect(activity.end).toHaveBeenCalledTimes(2)
428+
})
429+
430+
it(`during refresh webhook handling`, async () => {
431+
expect.assertions(5)
432+
433+
try {
434+
await sourceNodes(
435+
{
436+
...args,
437+
webhookBody: {
438+
malformattedPayload: true,
439+
},
440+
},
441+
{ baseUrl }
442+
)
443+
} catch (e) {
444+
expect(e).toBeTruthy()
445+
}
446+
447+
expect(reporter.activityTimer).toHaveBeenCalledTimes(1)
448+
expect(reporter.activityTimer).toHaveBeenNthCalledWith(
449+
1,
450+
`loading Drupal content changes`,
451+
expect.anything()
452+
)
453+
454+
expect(activity.start).toHaveBeenCalledTimes(1)
455+
expect(activity.end).toHaveBeenCalledTimes(1)
456+
})
457+
})
458+
})
365459
})

‎packages/gatsby-source-drupal/src/gatsby-node.js

+137-98
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,21 @@ const { handleReferences, handleWebhookUpdate } = require(`./utils`)
77
const asyncPool = require(`tiny-async-pool`)
88
const bodyParser = require(`body-parser`)
99

10+
function gracefullyRethrow(activity, error) {
11+
// activity.panicOnBuild was implemented at some point in gatsby@2
12+
// but plugin can still be used with older version of gatsby core
13+
// so need to do some checking here
14+
if (activity.panicOnBuild) {
15+
activity.panicOnBuild(error)
16+
}
17+
18+
activity.end()
19+
20+
if (!activity.panicOnBuild) {
21+
throw error
22+
}
23+
}
24+
1025
exports.sourceNodes = async (
1126
{
1227
actions,
@@ -44,41 +59,45 @@ exports.sourceNodes = async (
4459
)
4560
changesActivity.start()
4661

47-
const { secret, action, id, data } = webhookBody
48-
if (pluginOptions.secret && pluginOptions.secret !== secret) {
49-
reporter.warn(
50-
`The secret in this request did not match your plugin options secret.`
51-
)
52-
return
53-
}
54-
if (action === `delete`) {
55-
actions.deleteNode({ node: getNode(createNodeId(id)) })
56-
reporter.log(`Deleted node: ${id}`)
57-
return
58-
}
62+
try {
63+
const { secret, action, id, data } = webhookBody
64+
if (pluginOptions.secret && pluginOptions.secret !== secret) {
65+
reporter.warn(
66+
`The secret in this request did not match your plugin options secret.`
67+
)
68+
return
69+
}
70+
if (action === `delete`) {
71+
actions.deleteNode({ node: getNode(createNodeId(id)) })
72+
reporter.log(`Deleted node: ${id}`)
73+
return
74+
}
5975

60-
let nodesToUpdate = data
61-
if (!Array.isArray(data)) {
62-
nodesToUpdate = [data]
63-
}
76+
let nodesToUpdate = data
77+
if (!Array.isArray(data)) {
78+
nodesToUpdate = [data]
79+
}
6480

65-
for (const nodeToUpdate of nodesToUpdate) {
66-
await handleWebhookUpdate(
67-
{
68-
nodeToUpdate,
69-
actions,
70-
cache,
71-
createNodeId,
72-
createContentDigest,
73-
getCache,
74-
getNode,
75-
reporter,
76-
store,
77-
},
78-
pluginOptions
79-
)
81+
for (const nodeToUpdate of nodesToUpdate) {
82+
await handleWebhookUpdate(
83+
{
84+
nodeToUpdate,
85+
actions,
86+
cache,
87+
createNodeId,
88+
createContentDigest,
89+
getCache,
90+
getNode,
91+
reporter,
92+
store,
93+
},
94+
pluginOptions
95+
)
96+
}
97+
} catch (e) {
98+
gracefullyRethrow(changesActivity, e)
99+
return
80100
}
81-
82101
changesActivity.end()
83102
return
84103
}
@@ -118,74 +137,80 @@ exports.sourceNodes = async (
118137

119138
drupalFetchActivity.start()
120139

121-
const data = await axios.get(`${baseUrl}/${apiBase}`, {
122-
auth: basicAuth,
123-
headers,
124-
params,
125-
})
126-
const allData = await Promise.all(
127-
_.map(data.data.links, async (url, type) => {
128-
if (disallowedLinkTypes.includes(type)) return
129-
if (!url) return
130-
if (!type) return
131-
const getNext = async (url, data = []) => {
132-
if (typeof url === `object`) {
133-
// url can be string or object containing href field
134-
url = url.href
135-
136-
// Apply any filters configured in gatsby-config.js. Filters
137-
// can be any valid JSON API filter query string.
138-
// See https://www.drupal.org/docs/8/modules/jsonapi/filtering
139-
if (typeof filters === `object`) {
140-
if (filters.hasOwnProperty(type)) {
141-
url = url + `?${filters[type]}`
140+
let allData
141+
try {
142+
const data = await axios.get(`${baseUrl}/${apiBase}`, {
143+
auth: basicAuth,
144+
headers,
145+
params,
146+
})
147+
allData = await Promise.all(
148+
_.map(data.data.links, async (url, type) => {
149+
if (disallowedLinkTypes.includes(type)) return
150+
if (!url) return
151+
if (!type) return
152+
const getNext = async (url, data = []) => {
153+
if (typeof url === `object`) {
154+
// url can be string or object containing href field
155+
url = url.href
156+
157+
// Apply any filters configured in gatsby-config.js. Filters
158+
// can be any valid JSON API filter query string.
159+
// See https://www.drupal.org/docs/8/modules/jsonapi/filtering
160+
if (typeof filters === `object`) {
161+
if (filters.hasOwnProperty(type)) {
162+
url = url + `?${filters[type]}`
163+
}
142164
}
143165
}
144-
}
145166

146-
let d
147-
try {
148-
d = await axios.get(url, {
149-
auth: basicAuth,
150-
headers,
151-
params,
152-
})
153-
} catch (error) {
154-
if (error.response && error.response.status == 405) {
155-
// The endpoint doesn't support the GET method, so just skip it.
156-
return []
157-
} else {
158-
console.error(`Failed to fetch ${url}`, error.message)
159-
console.log(error.data)
160-
throw error
167+
let d
168+
try {
169+
d = await axios.get(url, {
170+
auth: basicAuth,
171+
headers,
172+
params,
173+
})
174+
} catch (error) {
175+
if (error.response && error.response.status == 405) {
176+
// The endpoint doesn't support the GET method, so just skip it.
177+
return []
178+
} else {
179+
console.error(`Failed to fetch ${url}`, error.message)
180+
console.log(error.data)
181+
throw error
182+
}
183+
}
184+
data = data.concat(d.data.data)
185+
// Add support for includes. Includes allow entity data to be expanded
186+
// based on relationships. The expanded data is exposed as `included`
187+
// in the JSON API response.
188+
// See https://www.drupal.org/docs/8/modules/jsonapi/includes
189+
if (d.data.included) {
190+
data = data.concat(d.data.included)
191+
}
192+
if (d.data.links && d.data.links.next) {
193+
data = await getNext(d.data.links.next, data)
161194
}
162-
}
163-
data = data.concat(d.data.data)
164-
// Add support for includes. Includes allow entity data to be expanded
165-
// based on relationships. The expanded data is exposed as `included`
166-
// in the JSON API response.
167-
// See https://www.drupal.org/docs/8/modules/jsonapi/includes
168-
if (d.data.included) {
169-
data = data.concat(d.data.included)
170-
}
171-
if (d.data.links && d.data.links.next) {
172-
data = await getNext(d.data.links.next, data)
173-
}
174195

175-
return data
176-
}
196+
return data
197+
}
177198

178-
const data = await getNext(url)
199+
const data = await getNext(url)
179200

180-
const result = {
181-
type,
182-
data,
183-
}
201+
const result = {
202+
type,
203+
data,
204+
}
184205

185-
// eslint-disable-next-line consistent-return
186-
return result
187-
})
188-
)
206+
// eslint-disable-next-line consistent-return
207+
return result
208+
})
209+
)
210+
} catch (e) {
211+
gracefullyRethrow(drupalFetchActivity, e)
212+
return
213+
}
189214

190215
drupalFetchActivity.end()
191216

@@ -216,17 +241,31 @@ exports.sourceNodes = async (
216241

217242
// Download all files (await for each pool to complete to fix concurrency issues)
218243
const fileNodes = [...nodes.values()].filter(isFileNode)
244+
219245
if (fileNodes.length) {
220246
const downloadingFilesActivity = reporter.activityTimer(
221247
`Remote file download`
222248
)
223249
downloadingFilesActivity.start()
224-
await asyncPool(concurrentFileRequests, fileNodes, async node => {
225-
await downloadFile(
226-
{ node, store, cache, createNode, createNodeId, getCache, reporter },
227-
pluginOptions
228-
)
229-
})
250+
try {
251+
await asyncPool(concurrentFileRequests, fileNodes, async node => {
252+
await downloadFile(
253+
{
254+
node,
255+
store,
256+
cache,
257+
createNode,
258+
createNodeId,
259+
getCache,
260+
reporter,
261+
},
262+
pluginOptions
263+
)
264+
})
265+
} catch (e) {
266+
gracefullyRethrow(downloadingFilesActivity, e)
267+
return
268+
}
230269
downloadingFilesActivity.end()
231270
}
232271
}

0 commit comments

Comments
 (0)
Please sign in to comment.