Commit 90c6e350 authored by Travis Fischer's avatar Travis Fischer

feat: add ChatGPTError with more info on HTTP errors

parent 8cc642e9
import test from 'ava' import test from 'ava'
import dotenv from 'dotenv-safe' import dotenv from 'dotenv-safe'
import * as types from './types'
import { ChatGPTAPI } from './chatgpt-api' import { ChatGPTAPI } from './chatgpt-api'
dotenv.config() dotenv.config()
...@@ -20,6 +21,7 @@ test('ChatGPTAPI invalid session token', async (t) => { ...@@ -20,6 +21,7 @@ test('ChatGPTAPI invalid session token', async (t) => {
await chatgpt.ensureAuth() await chatgpt.ensureAuth()
}, },
{ {
instanceOf: types.ChatGPTError,
message: 'ChatGPT failed to refresh auth token. Error: Unauthorized' message: 'ChatGPT failed to refresh auth token. Error: Unauthorized'
} }
) )
...@@ -64,6 +66,7 @@ if (!isCI) { ...@@ -64,6 +66,7 @@ if (!isCI) {
await chatgpt.ensureAuth() await chatgpt.ensureAuth()
}, },
{ {
instanceOf: types.ChatGPTError,
message: message:
'ChatGPT failed to refresh auth token. Error: session token may have expired' 'ChatGPT failed to refresh auth token. Error: session token may have expired'
} }
......
import ExpiryMap from 'expiry-map' import ExpiryMap from 'expiry-map'
import pTimeout, { TimeoutError } from 'p-timeout' import pTimeout from 'p-timeout'
import { v4 as uuidv4 } from 'uuid' import { v4 as uuidv4 } from 'uuid'
import * as types from './types' import * as types from './types'
...@@ -68,7 +68,7 @@ export class ChatGPTAPI { ...@@ -68,7 +68,7 @@ export class ChatGPTAPI {
this._accessTokenCache = new ExpiryMap<string, string>(accessTokenTTL) this._accessTokenCache = new ExpiryMap<string, string>(accessTokenTTL)
if (!this._sessionToken) { if (!this._sessionToken) {
throw new Error('ChatGPT invalid session token') throw new types.ChatGPTError('ChatGPT invalid session token')
} }
} }
...@@ -238,6 +238,7 @@ export class ChatGPTAPI { ...@@ -238,6 +238,7 @@ export class ChatGPTAPI {
return cachedAccessToken return cachedAccessToken
} }
let response: Response
try { try {
const res = await fetch('https://chat.openai.com/api/auth/session', { const res = await fetch('https://chat.openai.com/api/auth/session', {
headers: { headers: {
...@@ -245,8 +246,14 @@ export class ChatGPTAPI { ...@@ -245,8 +246,14 @@ export class ChatGPTAPI {
'user-agent': this._userAgent 'user-agent': this._userAgent
} }
}).then((r) => { }).then((r) => {
response = r
if (!r.ok) { if (!r.ok) {
throw new Error(`${r.status} ${r.statusText}`) const error = new types.ChatGPTError(`${r.status} ${r.statusText}`)
error.response = r
error.statusCode = r.status
error.statusText = r.statusText
throw error
} }
return r.json() as any as types.SessionResult return r.json() as any as types.SessionResult
...@@ -255,22 +262,41 @@ export class ChatGPTAPI { ...@@ -255,22 +262,41 @@ export class ChatGPTAPI {
const accessToken = res?.accessToken const accessToken = res?.accessToken
if (!accessToken) { if (!accessToken) {
throw new Error('Unauthorized') const error = new types.ChatGPTError('Unauthorized')
error.response = response
error.statusCode = response?.status
error.statusText = response?.statusText
throw error
} }
const error = res?.error const appError = res?.error
if (error) { if (appError) {
if (error === 'RefreshAccessTokenError') { if (appError === 'RefreshAccessTokenError') {
throw new Error('session token may have expired') const error = new types.ChatGPTError('session token may have expired')
error.response = response
error.statusCode = response?.status
error.statusText = response?.statusText
throw error
} else { } else {
throw new Error(error) const error = new types.ChatGPTError(appError)
error.response = response
error.statusCode = response?.status
error.statusText = response?.statusText
throw error
} }
} }
this._accessTokenCache.set(KEY_ACCESS_TOKEN, accessToken) this._accessTokenCache.set(KEY_ACCESS_TOKEN, accessToken)
return accessToken return accessToken
} catch (err: any) { } catch (err: any) {
throw new Error(`ChatGPT failed to refresh auth token. ${err.toString()}`) const error = new types.ChatGPTError(
`ChatGPT failed to refresh auth token. ${err.toString()}`
)
error.response = response
error.statusCode = response?.status
error.statusText = response?.statusText
error.originalError = err
throw error
} }
} }
......
import { createParser } from 'eventsource-parser' import { createParser } from 'eventsource-parser'
import * as types from './types'
import { fetch } from './fetch' import { fetch } from './fetch'
import { streamAsyncIterable } from './stream-async-iterable' import { streamAsyncIterable } from './stream-async-iterable'
...@@ -10,7 +11,12 @@ export async function fetchSSE( ...@@ -10,7 +11,12 @@ export async function fetchSSE(
const { onMessage, ...fetchOptions } = options const { onMessage, ...fetchOptions } = options
const res = await fetch(url, fetchOptions) const res = await fetch(url, fetchOptions)
if (!res.ok) { if (!res.ok) {
throw new Error(`ChatGPTAPI error ${res.status || res.statusText}`) const msg = `ChatGPTAPI error ${res.status || res.statusText}`
const error = new types.ChatGPTError(msg)
error.statusCode = res.status
error.statusText = res.statusText
error.response = res
throw error
} }
const parser = createParser((event) => { const parser = createParser((event) => {
...@@ -25,7 +31,7 @@ export async function fetchSSE( ...@@ -25,7 +31,7 @@ export async function fetchSSE(
const body: NodeJS.ReadableStream = res.body as any const body: NodeJS.ReadableStream = res.body as any
if (!body.on || !body.read) { if (!body.on || !body.read) {
throw new Error('unsupported "fetch" implementation') throw new types.ChatGPTError('unsupported "fetch" implementation')
} }
body.on('readable', () => { body.on('readable', () => {
......
...@@ -287,3 +287,10 @@ export type SendConversationMessageOptions = Omit< ...@@ -287,3 +287,10 @@ export type SendConversationMessageOptions = Omit<
SendMessageOptions, SendMessageOptions,
'conversationId' | 'parentMessageId' 'conversationId' | 'parentMessageId'
> >
export class ChatGPTError extends Error {
statusCode?: number
statusText?: string
response?: Response
originalError?: Error
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment