Skip to content
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
16 changes: 14 additions & 2 deletions nodejs/src/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,23 @@ class HttpResponseError extends HackMDError {

class MissingRequiredArgument extends HackMDError {}
class InternalServerError extends HttpResponseError {}

class TooManyRequestsError extends HttpResponseError {
public constructor (
message: string,
readonly code: number,
readonly statusText: string,
readonly userLimit: number,
readonly userRemaining: number,
readonly resetAfter?: number,
) {
super(message, code, statusText)
}
}

export {
HackMDError,
HttpResponseError,
MissingRequiredArgument,
InternalServerError
InternalServerError,
TooManyRequestsError,
}
63 changes: 39 additions & 24 deletions nodejs/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,14 @@ const defaultOption: RequestOptions = {

type OptionReturnType<Opt, T> = Opt extends { unwrapData: false } ? AxiosResponse<T> : Opt extends { unwrapData: true } ? T : T

export type APIClientOptions = {
wrapResponseErrors: boolean
}

export class API {
private axios: AxiosInstance

constructor (readonly accessToken: string, public hackmdAPIEndpointURL: string = "https://api.hackmd.io/v1") {
constructor (readonly accessToken: string, public hackmdAPIEndpointURL: string = "https://api.hackmd.io/v1", public options: APIClientOptions = { wrapResponseErrors: true }) {
if (!accessToken) {
throw new HackMDErrors.MissingRequiredArgument('Missing access token when creating HackMD client')
}
Expand All @@ -37,30 +41,41 @@ export class API {
}
)

this.axios.interceptors.response.use(
(response: AxiosResponse) => {
return response
},
async (err: AxiosError) => {
if (!err.response) {
return Promise.reject(err)
}

if (err.response.status >= 500) {
throw new HackMDErrors.InternalServerError(
`HackMD internal error (${err.response.status} ${err.response.statusText})`,
err.response.status,
err.response.statusText,
)
} else {
throw new HackMDErrors.HttpResponseError(
`Received an error response (${err.response.status} ${err.response.statusText}) from HackMD`,
err.response.status,
err.response.statusText,
)
if (options.wrapResponseErrors) {
this.axios.interceptors.response.use(
(response: AxiosResponse) => {
return response
},
async (err: AxiosError) => {
if (!err.response) {
return Promise.reject(err)
}

if (err.response.status >= 500) {
throw new HackMDErrors.InternalServerError(
`HackMD internal error (${err.response.status} ${err.response.statusText})`,
err.response.status,
err.response.statusText,
)
} else if (err.response.status === 429) {
throw new HackMDErrors.TooManyRequestsError(
`Too many requests (${err.response.status} ${err.response.statusText})`,
err.response.status,
err.response.statusText,
parseInt(err.response.headers['x-ratelimit-userlimit'], 10),
parseInt(err.response.headers['x-ratelimit-userremaining'], 10),
parseInt(err.response.headers['x-ratelimit-userreset'], 10),
)
} else {
throw new HackMDErrors.HttpResponseError(
`Received an error response (${err.response.status} ${err.response.statusText}) from HackMD`,
err.response.status,
err.response.statusText,
)
}
}
}
)
)
}
}

async getMe<Opt extends RequestOptions> (options = defaultOption as Opt): Promise<OptionReturnType<Opt, GetMe>> {
Expand Down
52 changes: 52 additions & 0 deletions nodejs/tests/api.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { server } from './mock'
import { API } from '../src'
import { rest } from 'msw'
import { TooManyRequestsError } from '../src/error'

let client: API

Expand Down Expand Up @@ -34,3 +36,53 @@ test('getMe unwrapped', async () => {
expect(response).toHaveProperty('userPath')
expect(response).toHaveProperty('photo')
})

test('should throw axios error object if set wrapResponseErrors to false', async () => {
const customCilent = new API(process.env.HACKMD_ACCESS_TOKEN!, undefined, {
wrapResponseErrors: false,
})

server.use(
rest.get('https://api.hackmd.io/v1/me', (req, res, ctx) => {
return res(ctx.status(429))
}),
)

try {
await customCilent.getMe()
} catch (error: any) {
expect(error).toHaveProperty('response')
expect(error.response).toHaveProperty('status', 429)
}
})

test.only('should throw HackMD error object', async () => {
server.use(
rest.get('https://api.hackmd.io/v1/me', (req, res, ctx) => {
return res(
ctx.status(429),
ctx.set({
'X-RateLimit-UserLimit': '100',
'x-RateLimit-UserRemaining': '0',
'x-RateLimit-UserReset': String(
new Date().getTime() + 1000 * 60 * 60 * 24,
),
}),
)
}),
)

try {
await client.getMe()
} catch (error: any) {
expect(error).toBeInstanceOf(TooManyRequestsError)

console.log(JSON.stringify(error))

expect(error).toHaveProperty('code', 429)
expect(error).toHaveProperty('statusText', 'Too Many Requests')
expect(error).toHaveProperty('userLimit', 100)
expect(error).toHaveProperty('userRemaining', 0)
expect(error).toHaveProperty('resetAfter')
}
})