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
38 changes: 29 additions & 9 deletions __tests__/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import SqlString from 'sqlstring'
import { connect, format, hex, DatabaseError, type Cast } from '../dist/index'
import { connect, format, hex, DatabaseError, UnknownError, type Cast } from '../dist/index'
import { fetch, MockAgent, setGlobalDispatcher } from 'undici'
import packageJSON from '../package.json'

Expand Down Expand Up @@ -125,7 +125,7 @@ describe('transaction', () => {
})
mockPool.intercept({ path: EXECUTE_PATH, method: 'POST' }).reply(401, () => {
numRequests++
return mockError
return { error: mockError }
})
mockPool.intercept({ path: EXECUTE_PATH, method: 'POST' }).reply(200, () => {
numRequests++
Expand All @@ -139,7 +139,7 @@ describe('transaction', () => {
})
} catch (err) {
expect(numRequests).toEqual(4)
expect(err).toEqual(new DatabaseError('Unauthorized', 401, mockError))
expect(err).toEqual(new DatabaseError(mockError.message, 401, mockError))
}
})
})
Expand Down Expand Up @@ -382,19 +382,39 @@ describe('execute', () => {
}
})

test('it properly returns network errors when not json', async () => {
const mockError = {
code: 'internal',
message: 'Internal Server Error'
test('it returns UnknownError for non-JSON error responses', async () => {
mockPool
.intercept({ path: EXECUTE_PATH, method: 'POST' })
.reply(500, '<html>Bad Gateway</html>', { headers: { 'content-type': 'text/html' } })

const connection = connect(config)
try {
await connection.execute('SELECT * from foo;')
throw new Error('Expected UnknownError')
} catch (err) {
expect(err).toBeInstanceOf(UnknownError)
expect(err).toBeInstanceOf(DatabaseError)
const upstream = err as InstanceType<typeof UnknownError>
expect(upstream.status).toEqual(500)
expect(upstream.context.body).toEqual('<html>Bad Gateway</html>')
expect(upstream.context.status).toEqual(500)
expect(upstream.message).toMatch(/Expected a JSON response/)
}
})

mockPool.intercept({ path: EXECUTE_PATH, method: 'POST' }).reply(500, mockError)
test('it returns UnknownError for JSON without error field', async () => {
mockPool.intercept({ path: EXECUTE_PATH, method: 'POST' }).reply(500, { code: 'internal', message: 'oops' })

const connection = connect(config)
try {
await connection.execute('SELECT * from foo;')
throw new Error('Expected UnknownError')
} catch (err) {
expect(err).toEqual(new DatabaseError(mockError.message, 500, mockError))
expect(err).toBeInstanceOf(UnknownError)
expect(err).toBeInstanceOf(DatabaseError)
const upstream = err as InstanceType<typeof UnknownError>
expect(upstream.status).toEqual(500)
expect(upstream.context.body).toContain('internal')
}
})

Expand Down
66 changes: 52 additions & 14 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,22 @@ export class DatabaseError extends Error {
}
}

interface ResponseContext {
status: number
statusText: string
body: string
headers: Record<string, string>
}

export class UnknownError extends DatabaseError {
context: ResponseContext
constructor(message: string, context: ResponseContext) {
super(message, context.status, { code: 'UNKNOWN', message })
this.name = 'UnknownError'
this.context = context
}
}

type Types = Record<string, string>

export interface ExecutedQuery<T = Row<'array'> | Row<'object'>> {
Expand Down Expand Up @@ -50,8 +66,8 @@ type Res = {
ok: boolean
status: number
statusText: string
json(): Promise<any>
text(): Promise<string>
headers?: { forEach(fn: (value: string, key: string) => void): void }
}

export type Cast = typeof cast
Expand Down Expand Up @@ -318,21 +334,43 @@ async function postJSON<T>(config: Config, fetch: Fetch, url: string | URL, body
cache: 'no-store'
})

const text = await response.text()

if (response.ok) {
return await response.json()
} else {
let error = null
try {
const e = (await response.json()).error
error = new DatabaseError(e.message, response.status, e)
} catch {
error = new DatabaseError(response.statusText, response.status, {
code: 'internal',
message: response.statusText
})
}
throw error
return JSON.parse(text)
}

let parsed: { error?: VitessError } | undefined
try {
parsed = JSON.parse(text)
} catch {
// ignore
}

if (parsed?.error) {
throw new DatabaseError(parsed.error.message, response.status, parsed.error)
}

const headers: Record<string, string> = {}
try {
response.headers?.forEach((value, key) => {
headers[key] = value
})
} catch {
// ignore
}

const context: ResponseContext = {
status: response.status,
statusText: response.statusText,
body: text.substring(0, 4096),
headers
}

throw new UnknownError(
`Expected a JSON response from the database API but received: HTTP ${response.status} ${response.statusText}`,
context
)
}

export function connect(config: Config): Connection {
Expand Down
Loading