Skip to content

Commit 58aa02a

Browse files
authored
Fallback to a text response if JSON fails (#5)
* Fallback to a text response if JSON fails * If instead of try-catch * fetchJSON -> fetchData * [CI] Add the target branch * Separate test commands * Break a test on purpose * Restore test * Bump the version
1 parent 7e72127 commit 58aa02a

File tree

9 files changed

+134
-96
lines changed

9 files changed

+134
-96
lines changed

.github/workflows/lint.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,4 @@ jobs:
66
runs-on: ubuntu-latest
77
steps:
88
- uses: actions/checkout@v2
9-
- uses: reviewdog/action-eslint
9+
- uses: reviewdog/action-eslint@master

.github/workflows/test.yml

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,11 @@ jobs:
66
runs-on: ubuntu-latest
77
steps:
88
- uses: actions/checkout@v2
9-
- uses: mattallty/jest-github-action
9+
- run: npm install
10+
- run: npm test
11+
- uses: mattallty/jest-github-action@master
12+
env:
13+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
14+
CI: true
15+
with:
16+
test-command: 'echo done'

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,4 @@ yalc.lock
1313
tsconfig.tsbuildinfo
1414
/openapi
1515
.#*
16+
jest.results.json

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@gnosis.pm/safe-react-gateway-sdk",
3-
"version": "1.2.0",
3+
"version": "1.2.1",
44
"main": "dist/index.min.js",
55
"types": "dist/index.d.ts",
66
"files": [
@@ -39,7 +39,8 @@
3939
"build": "rm -rf dist && webpack --mode production",
4040
"prepare": "yarn build",
4141
"prettier": "prettier -w",
42-
"test": "jest tests/"
42+
"test:watch": "jest tests/ --watch",
43+
"test": "jest --ci --coverage --json --outputFile='jest.results.json' ./tests"
4344
},
4445
"husky": {
4546
"hooks": {

src/endpoint.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { fetchJson, insertParams, stringifyQuery } from './utils'
1+
import { fetchData, insertParams, stringifyQuery } from './utils'
22
import { paths } from './types/gateway'
33

44
type Primitive = string | number | boolean | null
@@ -25,5 +25,5 @@ export function callEndpoint<T extends keyof paths>(
2525
body = params?.body
2626
}
2727

28-
return fetchJson(url, body)
28+
return fetchData(url, body)
2929
}

src/index.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -49,12 +49,12 @@ export function getTransactionQueue(baseUrl: string, address: string, pageUrl?:
4949
)
5050
}
5151

52-
export function postTransaction(baseUrl: string, address: string, body: operations['post_transaction']['parameters']['body']) {
53-
return callEndpoint(
54-
baseUrl,
55-
'/transactions/{safe_address}/propose',
56-
{ path: { safe_address: address }, body },
57-
)
52+
export function postTransaction(
53+
baseUrl: string,
54+
address: string,
55+
body: operations['post_transaction']['parameters']['body'],
56+
) {
57+
return callEndpoint(baseUrl, '/transactions/{safe_address}/propose', { path: { safe_address: address }, body })
5858
}
5959

6060
/* eslint-enable @typescript-eslint/explicit-module-boundary-types */

src/utils.ts

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -29,24 +29,33 @@ export function stringifyQuery(query?: Params): string {
2929
return searchString ? `?${searchString}` : ''
3030
}
3131

32-
export function fetchJson<T>(url: string, body?: unknown): Promise<T> {
33-
let options: {
34-
method: 'POST',
35-
headers: Record<string, string>,
36-
body: string
37-
} | undefined
32+
export async function fetchData<T>(url: string, body?: unknown): Promise<T> {
33+
let options:
34+
| {
35+
method: 'POST'
36+
headers: Record<string, string>
37+
body: string
38+
}
39+
| undefined
3840
if (body != null) {
3941
options = {
4042
method: 'POST',
4143
body: typeof body === 'string' ? body : JSON.stringify(body),
42-
headers: { 'Content-Type': 'application/json' }
44+
headers: { 'Content-Type': 'application/json' },
4345
}
4446
}
4547

46-
return fetch(url, options).then((resp) => {
47-
if (!resp.ok) {
48-
throw Error(resp.statusText)
49-
}
50-
return resp.json()
51-
})
48+
const resp = await fetch(url, options)
49+
50+
if (!resp.ok) {
51+
throw Error(resp.statusText)
52+
}
53+
54+
// If the reponse is empty, don't try to parse it
55+
const text = await resp.text()
56+
if (!text) {
57+
return text as unknown as T
58+
}
59+
60+
return resp.json()
5261
}

tests/endpoint.test.js

Lines changed: 59 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,75 +1,89 @@
1-
import fetch from 'unfetch'
1+
import { fetchData } from '../src/utils'
22
import { callEndpoint } from '../src/endpoint'
33

4-
jest.mock('unfetch', () => jest.fn(() => {
5-
return Promise.resolve({
6-
ok: true,
7-
json: () => Promise.resolve({ success: true })
8-
})
9-
}))
4+
jest.mock('../src/utils', () => {
5+
const originalModule = jest.requireActual('../src/utils')
6+
7+
return {
8+
__esModule: true,
9+
...originalModule,
10+
fetchData: jest.fn(() => Promise.resolve({ success: true })),
11+
}
12+
})
1013

1114
describe('callEndpoint', () => {
12-
it('should accept just a path', () => {
13-
expect(
14-
callEndpoint('https://safe-client.xdai.staging.gnosisdev.com/v1', '/balances/supported-fiat-codes')
15+
it('should accept just a path', async () => {
16+
await expect(
17+
callEndpoint('https://safe-client.xdai.staging.gnosisdev.com/v1', '/balances/supported-fiat-codes'),
1518
).resolves.toEqual({ success: true })
1619

17-
expect(fetch).toHaveBeenCalledWith('https://safe-client.xdai.staging.gnosisdev.com/v1/balances/supported-fiat-codes', undefined)
20+
expect(fetchData).toHaveBeenCalledWith(
21+
'https://safe-client.xdai.staging.gnosisdev.com/v1/balances/supported-fiat-codes',
22+
undefined,
23+
)
1824
})
1925

20-
it('should accept a path param', () => {
21-
expect(
22-
callEndpoint('https://safe-client.rinkeby.staging.gnosisdev.com/v1', '/safe/{address}', { path: { address: '0x123' } })
26+
it('should accept a path param', async () => {
27+
await expect(
28+
callEndpoint('https://safe-client.rinkeby.staging.gnosisdev.com/v1', '/safe/{address}', {
29+
path: { address: '0x123' },
30+
}),
2331
).resolves.toEqual({ success: true })
2432

25-
expect(fetch).toHaveBeenCalledWith('https://safe-client.rinkeby.staging.gnosisdev.com/v1/safe/0x123', undefined)
33+
expect(fetchData).toHaveBeenCalledWith('https://safe-client.rinkeby.staging.gnosisdev.com/v1/safe/0x123', undefined)
2634
})
2735

28-
it('should accept several path params', () => {
29-
expect(
30-
callEndpoint('https://safe-client.rinkeby.staging.gnosisdev.com/v1', '/balances/{address}/{currency}', { path: { address: '0x123', currency: 'usd' } })
36+
it('should accept several path params', async () => {
37+
await expect(
38+
callEndpoint('https://safe-client.rinkeby.staging.gnosisdev.com/v1', '/balances/{address}/{currency}', {
39+
path: { address: '0x123', currency: 'usd' },
40+
}),
3141
).resolves.toEqual({ success: true })
3242

33-
expect(fetch).toHaveBeenCalledWith('https://safe-client.rinkeby.staging.gnosisdev.com/v1/balances/0x123/usd', undefined)
43+
expect(fetchData).toHaveBeenCalledWith(
44+
'https://safe-client.rinkeby.staging.gnosisdev.com/v1/balances/0x123/usd',
45+
undefined,
46+
)
3447
})
3548

36-
it('should accept query params', () => {
37-
expect(
38-
callEndpoint('https://safe-client.rinkeby.staging.gnosisdev.com/v1', '/balances/{address}/{currency}', { path: { address: '0x123', currency: 'usd' }, query: { exclude_spam: true } })
49+
it('should accept query params', async () => {
50+
await expect(
51+
callEndpoint('https://safe-client.rinkeby.staging.gnosisdev.com/v1', '/balances/{address}/{currency}', {
52+
path: { address: '0x123', currency: 'usd' },
53+
query: { exclude_spam: true },
54+
}),
3955
).resolves.toEqual({ success: true })
4056

41-
expect(fetch).toHaveBeenCalledWith('https://safe-client.rinkeby.staging.gnosisdev.com/v1/balances/0x123/usd?exclude_spam=true', undefined)
57+
expect(fetchData).toHaveBeenCalledWith(
58+
'https://safe-client.rinkeby.staging.gnosisdev.com/v1/balances/0x123/usd?exclude_spam=true',
59+
undefined,
60+
)
4261
})
4362

44-
it('should accept body', () => {
45-
expect(
46-
callEndpoint(
47-
'https://safe-client.rinkeby.staging.gnosisdev.com/v1',
48-
'/transactions/{safe_address}/propose',
49-
{
50-
path: { safe_address: '0x123' },
51-
body: { test: 'test' }
52-
}
53-
)
63+
it('should accept body', async () => {
64+
await expect(
65+
callEndpoint('https://safe-client.rinkeby.staging.gnosisdev.com/v1', '/transactions/{safe_address}/propose', {
66+
path: { safe_address: '0x123' },
67+
body: { test: 'test' },
68+
}),
5469
).resolves.toEqual({ success: true })
5570

56-
expect(fetch).toHaveBeenCalledWith(
71+
expect(fetchData).toHaveBeenCalledWith(
5772
'https://safe-client.rinkeby.staging.gnosisdev.com/v1/transactions/0x123/propose',
58-
{
59-
method: 'POST',
60-
body: '{"test":"test"}',
61-
headers: {
62-
'Content-Type': 'application/json'
63-
}
64-
}
73+
{ test: 'test' },
6574
)
6675
})
6776

68-
it('should accept a raw URL', () => {
69-
expect(
70-
callEndpoint('https://safe-client.rinkeby.staging.gnosisdev.com/v1', '/balances/{address}/{currency}', { path: { address: '0x123', currency: 'usd' }, query: { exclude_spam: true } }, '/test-url?raw=true')
77+
it('should accept a raw URL', async () => {
78+
await expect(
79+
callEndpoint(
80+
'https://safe-client.rinkeby.staging.gnosisdev.com/v1',
81+
'/balances/{address}/{currency}',
82+
{ path: { address: '0x123', currency: 'usd' }, query: { exclude_spam: true } },
83+
'/test-url?raw=true',
84+
),
7185
).resolves.toEqual({ success: true })
7286

73-
expect(fetch).toHaveBeenCalledWith('/test-url?raw=true', undefined)
87+
expect(fetchData).toHaveBeenCalledWith('/test-url?raw=true', undefined)
7488
})
7589
})

tests/utils.test.js

Lines changed: 32 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,25 @@
11
import fetch from 'unfetch'
2-
import { fetchJson, insertParams, stringifyQuery } from '../src/utils'
2+
import { fetchData, insertParams, stringifyQuery } from '../src/utils'
33

44
jest.mock('unfetch')
55

66
describe('utils', () => {
77
describe('insertParams', () => {
88
it('should insert a param into a string', () => {
9-
expect(
10-
insertParams('/{network}/safe/{address}', { address: '0x0' })
11-
).toBe(
12-
'/{network}/safe/0x0'
13-
)
9+
expect(insertParams('/{network}/safe/{address}', { address: '0x0' })).toBe('/{network}/safe/0x0')
1410
})
1511

1612
it('should insert several params into a string', () => {
17-
expect(
18-
insertParams('/{network}/safe/{address}', { address: '0x0', network: 'rinkeby' })
19-
).toBe(
20-
'/rinkeby/safe/0x0'
13+
expect(insertParams('/{network}/safe/{address}', { address: '0x0', network: 'rinkeby' })).toBe(
14+
'/rinkeby/safe/0x0',
2115
)
2216
})
2317
})
2418

2519
describe('stringifyQuery', () => {
2620
it('should stringify query params', () => {
27-
expect(
28-
stringifyQuery({ spam: true, page: 11, name: 'token', exclude: null })
29-
).toBe(
30-
'?spam=true&page=11&name=token'
21+
expect(stringifyQuery({ spam: true, page: 11, name: 'token', exclude: null })).toBe(
22+
'?spam=true&page=11&name=token',
3123
)
3224
})
3325

@@ -38,48 +30,62 @@ describe('utils', () => {
3830
})
3931
})
4032

41-
describe('fetchJson', () => {
42-
it('should fetch a simple url', () => {
33+
describe('fetchData', () => {
34+
it('should fetch a simple url', async () => {
4335
fetch.mockImplementation(() => {
4436
return Promise.resolve({
4537
ok: true,
46-
json: () => Promise.resolve({ success: true })
38+
text: () => Promise.resolve('{"success": "true"}'),
39+
json: () => Promise.resolve({ success: true }),
4740
})
4841
})
4942

50-
expect(fetchJson('/test/safe?q=123')).resolves.toEqual({ success: true })
43+
await expect(fetchData('/test/safe?q=123')).resolves.toEqual({ success: true })
5144
expect(fetch).toHaveBeenCalledWith('/test/safe?q=123', undefined)
5245
})
5346

54-
it('should make a post request', () => {
47+
it('should make a post request', async () => {
5548
fetch.mockImplementation(() => {
5649
return Promise.resolve({
5750
ok: true,
58-
json: () => Promise.resolve({ success: true })
51+
text: () => Promise.resolve('{"success": "true"}'),
52+
json: () => Promise.resolve({ success: true }),
5953
})
6054
})
6155

62-
expect(fetchJson('/test/safe', '123')).resolves.toEqual({ success: true })
56+
await expect(fetchData('/test/safe', '123')).resolves.toEqual({ success: true })
6357

6458
expect(fetch).toHaveBeenCalledWith('/test/safe', {
6559
method: 'POST',
6660
body: '123',
6761
headers: {
68-
'Content-Type': 'application/json'
69-
}
62+
'Content-Type': 'application/json',
63+
},
7064
})
7165
})
7266

73-
it('should throw if response is not OK', () => {
67+
it('should throw if response is not OK', async () => {
7468
fetch.mockImplementation(() => {
7569
return Promise.resolve({
7670
ok: false,
77-
statusText: 'Failed'
71+
statusText: 'Failed',
7872
})
7973
})
8074

81-
expect(fetchJson('/test/safe?q=123')).rejects.toThrow('Failed')
75+
await expect(fetchData('/test/safe?q=123')).rejects.toThrow('Failed')
8276
expect(fetch).toHaveBeenCalledWith('/test/safe?q=123', undefined)
8377
})
78+
79+
it('should fallback to raw text if no JSON in response', async () => {
80+
fetch.mockImplementation(() => {
81+
return Promise.resolve({
82+
ok: true,
83+
text: () => Promise.resolve(''),
84+
json: () => Promise.reject('Unexpected end of JSON input'),
85+
})
86+
})
87+
88+
await expect(fetchData('/propose', 123)).resolves.toEqual('')
89+
})
8490
})
8591
})

0 commit comments

Comments
 (0)