Skip to content

Commit b3315a0

Browse files
GatsbyJS Botaxe312ger
GatsbyJS Bot
andauthoredApr 2, 2021
fix(gatsby-source-contentful): Improve network error handling (#30257) (#30617)
Co-authored-by: Ward Peeters <ward@coding-tech.com> (cherry picked from commit c1ac5e4) Co-authored-by: Benedikt Rötsch <axe312ger@users.noreply.github.com>

File tree

6 files changed

+361
-59
lines changed

6 files changed

+361
-59
lines changed
 

‎packages/gatsby-source-contentful/README.md

+6
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,12 @@ Number of entries to retrieve from Contentful at a time. Due to some technical l
181181

182182
Number of workers to use when downloading Contentful assets. Due to technical limitations, opening too many concurrent requests can cause stalled downloads. If you encounter this issue you can set this param to a lower number than 50, e.g 25.
183183

184+
**`contentfulClientConfig`** [object][optional] [default: `{}`]
185+
186+
Additional config which will get passed to [Contentfuls JS SDK](https://github.com/contentful/contentful.js#configuration).
187+
188+
Use this with caution, you might override values this plugin does set for you to connect to Contentful.
189+
184190
## Notes on Contentful Content Models
185191

186192
There are currently some things to keep in mind when building your content models at Contentful.

‎packages/gatsby-source-contentful/package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@
3131
"@babel/cli": "^7.12.1",
3232
"@babel/core": "^7.12.3",
3333
"babel-preset-gatsby-package": "^0.12.0",
34-
"cross-env": "^7.0.3"
34+
"cross-env": "^7.0.3",
35+
"nock": "^13.0.6"
3536
},
3637
"homepage": "https://github.com/gatsbyjs/gatsby/tree/master/packages/gatsby-source-contentful#readme",
3738
"keywords": [
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
/**
2+
* @jest-environment node
3+
*/
4+
5+
import nock from "nock"
6+
import fetchData from "../fetch"
7+
import { createPluginConfig } from "../plugin-options"
8+
9+
nock.disableNetConnect()
10+
11+
const host = `localhost`
12+
const options = {
13+
spaceId: `12345`,
14+
accessToken: `67890`,
15+
host,
16+
contentfulClientConfig: {
17+
retryLimit: 2,
18+
},
19+
}
20+
const baseURI = `https://${host}`
21+
22+
const start = jest.fn()
23+
const end = jest.fn()
24+
const mockActivity = {
25+
start,
26+
end,
27+
tick: jest.fn(),
28+
done: end,
29+
}
30+
31+
const reporter = {
32+
info: jest.fn(),
33+
verbose: jest.fn(),
34+
panic: jest.fn(e => {
35+
throw e
36+
}),
37+
activityTimer: jest.fn(() => mockActivity),
38+
createProgress: jest.fn(() => mockActivity),
39+
}
40+
41+
const pluginConfig = createPluginConfig(options)
42+
43+
describe(`fetch-retry`, () => {
44+
afterEach(() => {
45+
nock.cleanAll()
46+
reporter.verbose.mockClear()
47+
reporter.panic.mockClear()
48+
})
49+
50+
test(`request retries when network timeout happens`, async () => {
51+
const scope = nock(baseURI)
52+
// Space
53+
.get(`/spaces/${options.spaceId}/`)
54+
.reply(200, { items: [] })
55+
// Locales
56+
.get(`/spaces/${options.spaceId}/environments/master/locales`)
57+
.reply(200, { items: [{ code: `en`, default: true }] })
58+
// Sync
59+
.get(
60+
`/spaces/${options.spaceId}/environments/master/sync?initial=true&limit=100`
61+
)
62+
.times(1)
63+
.replyWithError({ code: `ETIMEDOUT` })
64+
.get(
65+
`/spaces/${options.spaceId}/environments/master/sync?initial=true&limit=100`
66+
)
67+
.reply(200, { items: [] })
68+
// Content types
69+
.get(
70+
`/spaces/${options.spaceId}/environments/master/content_types?skip=0&limit=100&order=sys.createdAt`
71+
)
72+
.reply(200, { items: [] })
73+
74+
await fetchData({ pluginConfig, reporter })
75+
76+
expect(reporter.panic).not.toBeCalled()
77+
expect(scope.isDone()).toBeTruthy()
78+
})
79+
80+
test(`request should fail after to many retries`, async () => {
81+
// Due to the retries, this can take up to 10 seconds
82+
jest.setTimeout(10000)
83+
84+
const scope = nock(baseURI)
85+
// Space
86+
.get(`/spaces/${options.spaceId}/`)
87+
.reply(200, { items: [] })
88+
// Locales
89+
.get(`/spaces/${options.spaceId}/environments/master/locales`)
90+
.reply(200, { items: [{ code: `en`, default: true }] })
91+
// Sync
92+
.get(
93+
`/spaces/${options.spaceId}/environments/master/sync?initial=true&limit=100`
94+
)
95+
.times(3)
96+
.reply(
97+
500,
98+
{
99+
sys: {
100+
type: `Error`,
101+
id: `MockedContentfulError`,
102+
},
103+
message: `Mocked message of Contentful error`,
104+
},
105+
{ [`x-contentful-request-id`]: `123abc` }
106+
)
107+
108+
try {
109+
await fetchData({ pluginConfig, reporter })
110+
jest.fail()
111+
} catch (e) {
112+
const msg = expect(e.context.sourceMessage)
113+
msg.toEqual(
114+
expect.stringContaining(
115+
`Fetching contentful data failed: 500 MockedContentfulError`
116+
)
117+
)
118+
msg.toEqual(expect.stringContaining(`Request ID: 123abc`))
119+
msg.toEqual(
120+
expect.stringContaining(`The request was sent with 3 attempts`)
121+
)
122+
}
123+
expect(reporter.panic).toBeCalled()
124+
expect(scope.isDone()).toBeTruthy()
125+
})
126+
})
127+
128+
describe(`fetch-network-errors`, () => {
129+
test(`catches plain network error`, async () => {
130+
const scope = nock(baseURI)
131+
// Space
132+
.get(`/spaces/${options.spaceId}/`)
133+
.replyWithError({ code: `ECONNRESET` })
134+
try {
135+
await fetchData({
136+
pluginConfig: createPluginConfig({
137+
...options,
138+
contentfulClientConfig: { retryOnError: false },
139+
}),
140+
reporter,
141+
})
142+
jest.fail()
143+
} catch (e) {
144+
expect(e.context.sourceMessage).toEqual(
145+
expect.stringContaining(
146+
`Accessing your Contentful space failed: ECONNRESET`
147+
)
148+
)
149+
}
150+
151+
expect(reporter.panic).toBeCalled()
152+
expect(scope.isDone()).toBeTruthy()
153+
})
154+
155+
test(`catches error with response string`, async () => {
156+
const scope = nock(baseURI)
157+
// Space
158+
.get(`/spaces/${options.spaceId}/`)
159+
.reply(502, `Bad Gateway`)
160+
161+
try {
162+
await fetchData({
163+
pluginConfig: createPluginConfig({
164+
...options,
165+
contentfulClientConfig: { retryOnError: false },
166+
}),
167+
reporter,
168+
})
169+
jest.fail()
170+
} catch (e) {
171+
expect(e.context.sourceMessage).toEqual(
172+
expect.stringContaining(
173+
`Accessing your Contentful space failed: Bad Gateway`
174+
)
175+
)
176+
}
177+
178+
expect(reporter.panic).toBeCalled()
179+
expect(scope.isDone()).toBeTruthy()
180+
})
181+
182+
test(`catches error with response object`, async () => {
183+
const scope = nock(baseURI)
184+
// Space
185+
.get(`/spaces/${options.spaceId}/`)
186+
.reply(429, {
187+
sys: {
188+
type: `Error`,
189+
id: `MockedContentfulError`,
190+
},
191+
message: `Mocked message of Contentful error`,
192+
requestId: `123abc`,
193+
})
194+
195+
try {
196+
await fetchData({
197+
pluginConfig: createPluginConfig({
198+
...options,
199+
contentfulClientConfig: { retryOnError: false },
200+
}),
201+
reporter,
202+
})
203+
jest.fail()
204+
} catch (e) {
205+
const msg = expect(e.context.sourceMessage)
206+
207+
msg.toEqual(
208+
expect.stringContaining(
209+
`Accessing your Contentful space failed: MockedContentfulError`
210+
)
211+
)
212+
msg.toEqual(expect.stringContaining(`Mocked message of Contentful error`))
213+
msg.toEqual(expect.stringContaining(`Request ID: 123abc`))
214+
}
215+
216+
expect(reporter.panic).toBeCalled()
217+
expect(scope.isDone()).toBeTruthy()
218+
})
219+
})

0 commit comments

Comments
 (0)
Please sign in to comment.