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

Commit d35841c

Browse files
authored
Error handling of getStream/createStream/etc. (#208)
createStream, getStream, getStreamByName and getOrCreateStream reject the promise if there is an API error. To support this functionality, a new errorCode field was added to AuthFetchError. Currently the ErrorCode enum lists the most typical API error codes: VALIDATION_ERROR and NOT_FOUND. Other codes can be added later, if needed.
1 parent e047453 commit d35841c

File tree

6 files changed

+79
-43
lines changed

6 files changed

+79
-43
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -303,7 +303,7 @@ All the below functions return a Promise which gets resolved with the result.
303303
| getStream(streamId) | Fetches a stream object from the API. |
304304
| listStreams(query) | Fetches an array of stream objects from the API. For the query params, consult the [API docs](https://api-explorer.streamr.com). |
305305
| getStreamByName(name) | Fetches a stream which exactly matches the given name. |
306-
| createStream(properties) | Creates a stream with the given properties. For more information on the stream properties, consult the [API docs](https://api-explorer.streamr.com). |
306+
| createStream(\[properties]) | Creates a stream with the given properties. For more information on the stream properties, consult the [API docs](https://api-explorer.streamr.com). |
307307
| getOrCreateStream(properties) | Gets a stream with the id or name given in `properties`, or creates it if one is not found. |
308308
| publish(streamId, message, timestamp, partitionKey) | Publishes a new message to the given stream. |
309309

src/rest/ErrorCode.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
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: 23 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,10 @@ import Stream, { StreamOperation, StreamProperties } from '../stream'
1010
import StreamPart from '../stream/StreamPart'
1111
import { isKeyExchangeStream } from '../stream/KeyExchange'
1212

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

@@ -97,18 +98,11 @@ export class StreamEndpoints {
9798
}
9899

99100
const url = getEndpointUrl(this.client.options.restUrl, 'streams', streamId)
100-
try {
101-
const json = await authFetch<StreamProperties>(url, this.client.session)
102-
return new Stream(this.client, json)
103-
} catch (e) {
104-
if (e.response && e.response.status === 404) {
105-
return undefined
106-
}
107-
throw e
108-
}
101+
const json = await authFetch<StreamProperties>(url, this.client.session)
102+
return new Stream(this.client, json)
109103
}
110104

111-
async listStreams(query: StreamListQuery = {}) {
105+
async listStreams(query: StreamListQuery = {}): Promise<Stream[]> {
112106
this.client.debug('listStreams %o', {
113107
query,
114108
})
@@ -126,10 +120,10 @@ export class StreamEndpoints {
126120
// @ts-expect-error
127121
public: false,
128122
})
129-
return json[0] ? new Stream(this.client, json[0]) : undefined
123+
return json[0] ? new Stream(this.client, json[0]) : Promise.reject(new AuthFetchError('', undefined, undefined, ErrorCode.NOT_FOUND))
130124
}
131125

132-
async createStream(props: StreamProperties) {
126+
async createStream(props?: StreamProperties) {
133127
this.client.debug('createStream %o', {
134128
props,
135129
})
@@ -142,34 +136,31 @@ export class StreamEndpoints {
142136
body: JSON.stringify(props),
143137
},
144138
)
145-
return json ? new Stream(this.client, json) : undefined
139+
return new Stream(this.client, json)
146140
}
147141

148142
async getOrCreateStream(props: { id?: string, name?: string }) {
149143
this.client.debug('getOrCreateStream %o', {
150144
props,
151145
})
152-
let json: any
153-
154146
// Try looking up the stream by id or name, whichever is defined
155-
if (props.id) {
156-
json = await this.getStream(props.id)
157-
} else if (props.name) {
158-
json = await this.getStreamByName(props.name)
159-
}
160-
161-
// If not found, try creating the stream
162-
if (!json) {
163-
json = await this.createStream(props)
164-
debug('Created stream: %s (%s)', props.name, json.id)
147+
try {
148+
if (props.id) {
149+
const stream = await this.getStream(props.id)
150+
return stream
151+
}
152+
const stream = await this.getStreamByName(props.name!)
153+
return stream
154+
} catch (err) {
155+
const isNotFoundError = (err instanceof AuthFetchError) && (err.errorCode === ErrorCode.NOT_FOUND)
156+
if (!isNotFoundError) {
157+
throw err
158+
}
165159
}
166160

167-
// If still nothing, throw
168-
if (!json) {
169-
throw new Error(`Unable to find or create stream: ${props.name || props.id}`)
170-
} else {
171-
return new Stream(this.client, json)
172-
}
161+
const stream = await this.createStream(props)
162+
debug('Created stream: %s (%s)', props.name, stream.id)
163+
return stream
173164
}
174165

175166
async getStreamPublishers(streamId: string) {

src/rest/authFetch.ts

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

44
import { getVersionString } from '../utils'
5+
import { ErrorCode, parseErrorCode } from './ErrorCode'
56
import Session from '../Session'
67

78
export const DEFAULT_HEADERS = {
89
'Streamr-Client': `streamr-client-javascript/${getVersionString()}`,
910
}
1011

1112
export class AuthFetchError extends Error {
12-
response: Response
13+
response?: Response
1314
body?: any
15+
errorCode?: ErrorCode
1416

15-
constructor(message: string, response: Response, body?: any) {
17+
constructor(message: string, response?: Response, body?: any, errorCode?: ErrorCode) {
1618
// add leading space if there is a body set
1719
const bodyMessage = body ? ` ${(typeof body === 'string' ? body : JSON.stringify(body).slice(0, 1024))}...` : ''
1820
super(message + bodyMessage)
1921
this.response = response
2022
this.body = body
23+
this.errorCode = errorCode
2124

2225
if (Error.captureStackTrace) {
2326
Error.captureStackTrace(this, this.constructor)
@@ -75,6 +78,6 @@ export default async function authFetch<T extends object>(url: string, session?:
7578
return authFetch<T>(url, session, options, true)
7679
} else {
7780
debug('%d %s – failed', id, url)
78-
throw new AuthFetchError(`Request ${id} to ${url} returned with error code ${response.status}.`, response, body)
81+
throw new AuthFetchError(`Request ${id} to ${url} returned with error code ${response.status}.`, response, body, parseErrorCode(body))
7982
}
8083
}

test/integration/StreamEndpoints.test.js

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,10 @@ function TestStreamEndpoints(getName) {
5151
expect(stream.requireSignedData).toBe(true)
5252
expect(stream.requireEncryptedData).toBe(true)
5353
})
54+
55+
it('invalid id', () => {
56+
return expect(() => client.createStream({ id: 'invalid.eth/foobar' })).rejects.toThrow()
57+
})
5458
})
5559

5660
describe('getStream', () => {
@@ -62,26 +66,45 @@ function TestStreamEndpoints(getName) {
6266

6367
it('get a non-existing Stream', async () => {
6468
const id = `${wallet.address}/StreamEndpoints-integration-nonexisting-${Date.now()}`
65-
const stream = await client.getStream(id)
66-
expect(stream).toBe(undefined)
69+
return expect(() => client.getStream(id)).rejects.toThrow()
70+
})
71+
})
72+
73+
describe('getStreamByName', () => {
74+
it('get an existing Stream', async () => {
75+
const stream = await client.createStream()
76+
const existingStream = await client.getStreamByName(stream.name)
77+
expect(existingStream.id).toEqual(stream.id)
78+
})
79+
80+
it('get a non-existing Stream', async () => {
81+
const name = `${wallet.address}/StreamEndpoints-integration-nonexisting-${Date.now()}`
82+
return expect(() => client.getStreamByName(name)).rejects.toThrow()
6783
})
6884
})
6985

7086
describe('getOrCreate', () => {
71-
it('getOrCreate an existing Stream', async () => {
87+
it('getOrCreate an existing Stream by name', async () => {
7288
const existingStream = await client.getOrCreateStream({
7389
name: createdStream.name,
7490
})
7591
expect(existingStream.id).toBe(createdStream.id)
7692
expect(existingStream.name).toBe(createdStream.name)
7793
})
7894

95+
it('getOrCreate an existing Stream by id', async () => {
96+
const existingStream = await client.getOrCreateStream({
97+
id: createdStream.id,
98+
})
99+
expect(existingStream.id).toBe(createdStream.id)
100+
expect(existingStream.name).toBe(createdStream.name)
101+
})
102+
79103
it('getOrCreate a new Stream by name', async () => {
80104
const newName = uid('stream')
81105
const newStream = await client.getOrCreateStream({
82106
name: newName,
83107
})
84-
85108
expect(newStream.name).toEqual(newName)
86109
})
87110

@@ -90,7 +113,6 @@ function TestStreamEndpoints(getName) {
90113
const newStream = await client.getOrCreateStream({
91114
id: newId,
92115
})
93-
94116
expect(newStream.id).toEqual(newId)
95117
})
96118
})
@@ -201,7 +223,7 @@ function TestStreamEndpoints(getName) {
201223
describe('Stream deletion', () => {
202224
it('Stream.delete', async () => {
203225
await createdStream.delete()
204-
expect(await client.getStream(createdStream.id)).toBe(undefined)
226+
return expect(() => client.getStream(createdStream.id)).rejects.toThrow()
205227
})
206228
})
207229

test/utils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,7 @@ export function getPublishTestMessages(client: StreamrClient, defaultOpts = {})
237237

238238
export const createMockAddress = () => '0x000000000000000000000000000' + Date.now()
239239

240-
export const createClient = (providerSidechain: providers.JsonRpcProvider) => {
240+
export const createClient = (providerSidechain?: providers.JsonRpcProvider) => {
241241
const wallet = new Wallet(`0x100000000000000000000000000000000000000012300000001${Date.now()}`, providerSidechain)
242242
return new StreamrClient({
243243
...config.clientOptions,

0 commit comments

Comments
 (0)