Skip to content
This repository was archived by the owner on Dec 21, 2021. It is now read-only.

Commit 9ccd610

Browse files
authored
Stream reject on error refactor (#212)
Better error handling of authFetch errors: added classes ValidationError and NotFoundError. Also smaller changes: - added missing id field to StreamPermission - strict type check of parameter in getOrCreateStream()
1 parent 9b2805f commit 9ccd610

File tree

5 files changed

+67
-48
lines changed

5 files changed

+67
-48
lines changed

src/rest/ErrorCode.ts

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +0,0 @@
1-
export enum ErrorCode {
2-
NOT_FOUND = 'NOT_FOUND',
3-
VALIDATION_ERROR = 'VALIDATION_ERROR',
4-
UNKNOWN = 'UNKNOWN'
5-
}
6-
7-
export const parseErrorCode = (body: string) => {
8-
let json
9-
try {
10-
json = JSON.parse(body)
11-
} catch (err) {
12-
return ErrorCode.UNKNOWN
13-
}
14-
const code = json.code
15-
const keys = Object.keys(ErrorCode)
16-
if (keys.includes(code)) {
17-
return code as ErrorCode
18-
}
19-
return ErrorCode.UNKNOWN
20-
}

src/rest/StreamEndpoints.ts

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,9 @@ import Stream, { StreamOperation, StreamProperties } from '../stream'
1010
import StreamPart from '../stream/StreamPart'
1111
import { isKeyExchangeStream } from '../stream/KeyExchange'
1212

13-
import authFetch, { AuthFetchError } from './authFetch'
13+
import authFetch, { ErrorCode, NotFoundError } from './authFetch'
1414
import { Todo } from '../types'
1515
import StreamrClient from '../StreamrClient'
16-
import { ErrorCode } from './ErrorCode'
1716
// TODO change this import when streamr-client-protocol exports StreamMessage type or the enums types directly
1817
import { ContentType, EncryptionType, SignatureType, StreamMessageType } from 'streamr-client-protocol/dist/src/protocol/message_layer/StreamMessage'
1918

@@ -120,7 +119,7 @@ export class StreamEndpoints {
120119
// @ts-expect-error
121120
public: false,
122121
})
123-
return json[0] ? new Stream(this.client, json[0]) : Promise.reject(new AuthFetchError('', undefined, undefined, ErrorCode.NOT_FOUND))
122+
return json[0] ? new Stream(this.client, json[0]) : Promise.reject(new NotFoundError('Stream: name=' + name))
124123
}
125124

126125
async createStream(props?: StreamProperties) {
@@ -139,7 +138,7 @@ export class StreamEndpoints {
139138
return new Stream(this.client, json)
140139
}
141140

142-
async getOrCreateStream(props: { id?: string, name?: string }) {
141+
async getOrCreateStream(props: { id: string, name?: never } | { id?: never, name: string }) {
143142
this.client.debug('getOrCreateStream %o', {
144143
props,
145144
})
@@ -151,9 +150,8 @@ export class StreamEndpoints {
151150
}
152151
const stream = await this.getStreamByName(props.name!)
153152
return stream
154-
} catch (err) {
155-
const isNotFoundError = (err instanceof AuthFetchError) && (err.errorCode === ErrorCode.NOT_FOUND)
156-
if (!isNotFoundError) {
153+
} catch (err: any) {
154+
if (err.errorCode !== ErrorCode.NOT_FOUND) {
157155
throw err
158156
}
159157
}

src/rest/authFetch.ts

Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,32 +2,66 @@ import fetch, { Response } from 'node-fetch'
22
import Debug from 'debug'
33

44
import { getVersionString } from '../utils'
5-
import { ErrorCode, parseErrorCode } from './ErrorCode'
65
import Session from '../Session'
76

7+
export enum ErrorCode {
8+
NOT_FOUND = 'NOT_FOUND',
9+
VALIDATION_ERROR = 'VALIDATION_ERROR',
10+
UNKNOWN = 'UNKNOWN'
11+
}
12+
813
export const DEFAULT_HEADERS = {
914
'Streamr-Client': `streamr-client-javascript/${getVersionString()}`,
1015
}
1116

1217
export class AuthFetchError extends Error {
1318
response?: Response
1419
body?: any
15-
errorCode?: ErrorCode
20+
errorCode: ErrorCode
1621

1722
constructor(message: string, response?: Response, body?: any, errorCode?: ErrorCode) {
23+
const typePrefix = errorCode ? errorCode + ': ' : ''
1824
// add leading space if there is a body set
1925
const bodyMessage = body ? ` ${(typeof body === 'string' ? body : JSON.stringify(body).slice(0, 1024))}...` : ''
20-
super(message + bodyMessage)
26+
super(typePrefix + message + bodyMessage)
2127
this.response = response
2228
this.body = body
23-
this.errorCode = errorCode
29+
this.errorCode = errorCode || ErrorCode.UNKNOWN
2430

2531
if (Error.captureStackTrace) {
2632
Error.captureStackTrace(this, this.constructor)
2733
}
2834
}
2935
}
3036

37+
export class ValidationError extends AuthFetchError {
38+
constructor(message: string, response?: Response, body?: any) {
39+
super(message, response, body, ErrorCode.VALIDATION_ERROR)
40+
}
41+
}
42+
43+
export class NotFoundError extends AuthFetchError {
44+
constructor(message: string, response?: Response, body?: any) {
45+
super(message, response, body, ErrorCode.NOT_FOUND)
46+
}
47+
}
48+
49+
const ERROR_TYPES = new Map<ErrorCode, typeof AuthFetchError>()
50+
ERROR_TYPES.set(ErrorCode.VALIDATION_ERROR, ValidationError)
51+
ERROR_TYPES.set(ErrorCode.NOT_FOUND, NotFoundError)
52+
ERROR_TYPES.set(ErrorCode.UNKNOWN, AuthFetchError)
53+
54+
const parseErrorCode = (body: string) => {
55+
let json
56+
try {
57+
json = JSON.parse(body)
58+
} catch (err) {
59+
return ErrorCode.UNKNOWN
60+
}
61+
const { code } = json
62+
return code in ErrorCode ? code : ErrorCode.UNKNOWN
63+
}
64+
3165
const debug = Debug('StreamrClient:utils:authfetch') // TODO: could use the debug instance from the client? (e.g. client.debug.extend('authFetch'))
3266

3367
let ID = 0
@@ -78,6 +112,8 @@ export default async function authFetch<T extends object>(url: string, session?:
78112
return authFetch<T>(url, session, options, true)
79113
} else {
80114
debug('%d %s – failed', id, url)
81-
throw new AuthFetchError(`Request ${id} to ${url} returned with error code ${response.status}.`, response, body, parseErrorCode(body))
115+
const errorCode = parseErrorCode(body)
116+
const ErrorClass = ERROR_TYPES.get(errorCode)!
117+
throw new ErrorClass(`Request ${id} to ${url} returned with error code ${response.status}.`, response, body, errorCode)
82118
}
83119
}

src/stream/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import StreamrClient from '../StreamrClient'
66
import { Todo } from '../types'
77

88
interface StreamPermisionBase {
9+
id: number
910
operation: StreamOperation
1011
}
1112

@@ -72,6 +73,8 @@ export default class Stream {
7273
// TODO add field definitions for all fields
7374
// @ts-expect-error
7475
id: string
76+
// @ts-expect-error
77+
name: string
7578
config: {
7679
fields: Field[];
7780
} = { fields: [] }

test/integration/StreamEndpoints.test.js renamed to test/integration/StreamEndpoints.test.ts

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
import { ethers } from 'ethers'
1+
import { ethers, Wallet } from 'ethers'
2+
import { NotFoundError, ValidationError } from '../../src/rest/authFetch'
3+
import Stream, { StreamOperation } from '../../src/stream'
24

35
import StreamrClient from '../../src/StreamrClient'
46
import { uid } from '../utils'
@@ -9,17 +11,17 @@ import config from './config'
911
* These tests should be run in sequential order!
1012
*/
1113

12-
function TestStreamEndpoints(getName) {
13-
let client
14-
let wallet
15-
let createdStream
14+
function TestStreamEndpoints(getName: () => string) {
15+
let client: StreamrClient
16+
let wallet: Wallet
17+
let createdStream: Stream
1618

1719
const createClient = (opts = {}) => new StreamrClient({
1820
...config.clientOptions,
1921
autoConnect: false,
2022
autoDisconnect: false,
2123
...opts,
22-
})
24+
} as any)
2325

2426
beforeAll(() => {
2527
wallet = ethers.Wallet.createRandom()
@@ -53,7 +55,7 @@ function TestStreamEndpoints(getName) {
5355
})
5456

5557
it('invalid id', () => {
56-
return expect(() => client.createStream({ id: 'invalid.eth/foobar' })).rejects.toThrow()
58+
return expect(() => client.createStream({ id: 'invalid.eth/foobar' })).rejects.toThrow(ValidationError)
5759
})
5860
})
5961

@@ -66,7 +68,7 @@ function TestStreamEndpoints(getName) {
6668

6769
it('get a non-existing Stream', async () => {
6870
const id = `${wallet.address}/StreamEndpoints-integration-nonexisting-${Date.now()}`
69-
return expect(() => client.getStream(id)).rejects.toThrow()
71+
return expect(() => client.getStream(id)).rejects.toThrow(NotFoundError)
7072
})
7173
})
7274

@@ -79,7 +81,7 @@ function TestStreamEndpoints(getName) {
7981

8082
it('get a non-existing Stream', async () => {
8183
const name = `${wallet.address}/StreamEndpoints-integration-nonexisting-${Date.now()}`
82-
return expect(() => client.getStreamByName(name)).rejects.toThrow()
84+
return expect(() => client.getStreamByName(name)).rejects.toThrow(NotFoundError)
8385
})
8486
})
8587

@@ -205,25 +207,25 @@ function TestStreamEndpoints(getName) {
205207
})
206208

207209
it('Stream.hasPermission', async () => {
208-
expect(await createdStream.hasPermission('stream_share', wallet.address)).toBeTruthy()
210+
expect(await createdStream.hasPermission(StreamOperation.STREAM_SHARE, wallet.address)).toBeTruthy()
209211
})
210212

211213
it('Stream.grantPermission', async () => {
212-
await createdStream.grantPermission('stream_subscribe', null) // public read
213-
expect(await createdStream.hasPermission('stream_subscribe', null)).toBeTruthy()
214+
await createdStream.grantPermission(StreamOperation.STREAM_SUBSCRIBE, undefined) // public read
215+
expect(await createdStream.hasPermission(StreamOperation.STREAM_SUBSCRIBE, undefined)).toBeTruthy()
214216
})
215217

216218
it('Stream.revokePermission', async () => {
217-
const publicRead = await createdStream.hasPermission('stream_subscribe', null)
218-
await createdStream.revokePermission(publicRead.id)
219-
expect(!(await createdStream.hasPermission('stream_subscribe', null))).toBeTruthy()
219+
const publicRead = await createdStream.hasPermission(StreamOperation.STREAM_SUBSCRIBE, undefined)
220+
await createdStream.revokePermission(publicRead!.id)
221+
expect(!(await createdStream.hasPermission(StreamOperation.STREAM_SUBSCRIBE, undefined))).toBeTruthy()
220222
})
221223
})
222224

223225
describe('Stream deletion', () => {
224226
it('Stream.delete', async () => {
225227
await createdStream.delete()
226-
return expect(() => client.getStream(createdStream.id)).rejects.toThrow()
228+
return expect(() => client.getStream(createdStream.id)).rejects.toThrow(NotFoundError)
227229
})
228230
})
229231

0 commit comments

Comments
 (0)