Skip to content
This repository was archived by the owner on Dec 21, 2021. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 0 additions & 20 deletions src/rest/ErrorCode.ts
Original file line number Diff line number Diff line change
@@ -1,20 +0,0 @@
export enum ErrorCode {
NOT_FOUND = 'NOT_FOUND',
VALIDATION_ERROR = 'VALIDATION_ERROR',
UNKNOWN = 'UNKNOWN'
}

export const parseErrorCode = (body: string) => {
let json
try {
json = JSON.parse(body)
} catch (err) {
return ErrorCode.UNKNOWN
}
const code = json.code
const keys = Object.keys(ErrorCode)
if (keys.includes(code)) {
return code as ErrorCode
}
return ErrorCode.UNKNOWN
}
12 changes: 5 additions & 7 deletions src/rest/StreamEndpoints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,9 @@ import Stream, { StreamOperation, StreamProperties } from '../stream'
import StreamPart from '../stream/StreamPart'
import { isKeyExchangeStream } from '../stream/KeyExchange'

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

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

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

async getOrCreateStream(props: { id?: string, name?: string }) {
async getOrCreateStream(props: { id: string, name?: never } | { id?: never, name: string }) {
this.client.debug('getOrCreateStream %o', {
props,
})
Expand All @@ -151,9 +150,8 @@ export class StreamEndpoints {
}
const stream = await this.getStreamByName(props.name!)
return stream
} catch (err) {
const isNotFoundError = (err instanceof AuthFetchError) && (err.errorCode === ErrorCode.NOT_FOUND)
if (!isNotFoundError) {
} catch (err: any) {
if (err.errorCode !== ErrorCode.NOT_FOUND) {
throw err
}
}
Expand Down
46 changes: 41 additions & 5 deletions src/rest/authFetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,32 +2,66 @@ import fetch, { Response } from 'node-fetch'
import Debug from 'debug'

import { getVersionString } from '../utils'
import { ErrorCode, parseErrorCode } from './ErrorCode'
import Session from '../Session'

export enum ErrorCode {
NOT_FOUND = 'NOT_FOUND',
VALIDATION_ERROR = 'VALIDATION_ERROR',
UNKNOWN = 'UNKNOWN'
}

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

export class AuthFetchError extends Error {
response?: Response
body?: any
errorCode?: ErrorCode
errorCode: ErrorCode

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

if (Error.captureStackTrace) {
Error.captureStackTrace(this, this.constructor)
}
}
}

export class ValidationError extends AuthFetchError {
constructor(message: string, response?: Response, body?: any) {
super(message, response, body, ErrorCode.VALIDATION_ERROR)
}
}

export class NotFoundError extends AuthFetchError {
constructor(message: string, response?: Response, body?: any) {
super(message, response, body, ErrorCode.NOT_FOUND)
}
}

const ERROR_TYPES = new Map<ErrorCode, typeof AuthFetchError>()
ERROR_TYPES.set(ErrorCode.VALIDATION_ERROR, ValidationError)
ERROR_TYPES.set(ErrorCode.NOT_FOUND, NotFoundError)
ERROR_TYPES.set(ErrorCode.UNKNOWN, AuthFetchError)

const parseErrorCode = (body: string) => {
let json
try {
json = JSON.parse(body)
} catch (err) {
return ErrorCode.UNKNOWN
}
const { code } = json
return code in ErrorCode ? code : ErrorCode.UNKNOWN
}

const debug = Debug('StreamrClient:utils:authfetch') // TODO: could use the debug instance from the client? (e.g. client.debug.extend('authFetch'))

let ID = 0
Expand Down Expand Up @@ -78,6 +112,8 @@ export default async function authFetch<T extends object>(url: string, session?:
return authFetch<T>(url, session, options, true)
} else {
debug('%d %s – failed', id, url)
throw new AuthFetchError(`Request ${id} to ${url} returned with error code ${response.status}.`, response, body, parseErrorCode(body))
const errorCode = parseErrorCode(body)
const ErrorClass = ERROR_TYPES.get(errorCode)!
throw new ErrorClass(`Request ${id} to ${url} returned with error code ${response.status}.`, response, body, errorCode)
}
}
3 changes: 3 additions & 0 deletions src/stream/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import StreamrClient from '../StreamrClient'
import { Todo } from '../types'

interface StreamPermisionBase {
id: number
operation: StreamOperation
}

Expand Down Expand Up @@ -72,6 +73,8 @@ export default class Stream {
// TODO add field definitions for all fields
// @ts-expect-error
id: string
// @ts-expect-error
name: string
config: {
fields: Field[];
} = { fields: [] }
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { ethers } from 'ethers'
import { ethers, Wallet } from 'ethers'
import { NotFoundError, ValidationError } from '../../src/rest/authFetch'
import Stream, { StreamOperation } from '../../src/stream'

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

function TestStreamEndpoints(getName) {
let client
let wallet
let createdStream
function TestStreamEndpoints(getName: () => string) {
let client: StreamrClient
let wallet: Wallet
let createdStream: Stream

const createClient = (opts = {}) => new StreamrClient({
...config.clientOptions,
autoConnect: false,
autoDisconnect: false,
...opts,
})
} as any)

beforeAll(() => {
wallet = ethers.Wallet.createRandom()
Expand Down Expand Up @@ -53,7 +55,7 @@ function TestStreamEndpoints(getName) {
})

it('invalid id', () => {
return expect(() => client.createStream({ id: 'invalid.eth/foobar' })).rejects.toThrow()
return expect(() => client.createStream({ id: 'invalid.eth/foobar' })).rejects.toThrow(ValidationError)
})
})

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

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

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

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

Expand Down Expand Up @@ -205,25 +207,25 @@ function TestStreamEndpoints(getName) {
})

it('Stream.hasPermission', async () => {
expect(await createdStream.hasPermission('stream_share', wallet.address)).toBeTruthy()
expect(await createdStream.hasPermission(StreamOperation.STREAM_SHARE, wallet.address)).toBeTruthy()
})

it('Stream.grantPermission', async () => {
await createdStream.grantPermission('stream_subscribe', null) // public read
expect(await createdStream.hasPermission('stream_subscribe', null)).toBeTruthy()
await createdStream.grantPermission(StreamOperation.STREAM_SUBSCRIBE, undefined) // public read
expect(await createdStream.hasPermission(StreamOperation.STREAM_SUBSCRIBE, undefined)).toBeTruthy()
})

it('Stream.revokePermission', async () => {
const publicRead = await createdStream.hasPermission('stream_subscribe', null)
await createdStream.revokePermission(publicRead.id)
expect(!(await createdStream.hasPermission('stream_subscribe', null))).toBeTruthy()
const publicRead = await createdStream.hasPermission(StreamOperation.STREAM_SUBSCRIBE, undefined)
await createdStream.revokePermission(publicRead!.id)
expect(!(await createdStream.hasPermission(StreamOperation.STREAM_SUBSCRIBE, undefined))).toBeTruthy()
})
})

describe('Stream deletion', () => {
it('Stream.delete', async () => {
await createdStream.delete()
return expect(() => client.getStream(createdStream.id)).rejects.toThrow()
return expect(() => client.getStream(createdStream.id)).rejects.toThrow(NotFoundError)
})
})

Expand Down