Visitar URL original
fix(knex): omit queries from all database error messages by marshallswain · Pull Request #3725 · feathersjs/feathers · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
fix(knex): omit queries from all database error messages
Closes #3711
  • Loading branch information
marshallswain committed Oct 26, 2026
commit 46c4f0eebc1a52fefd78f708828849c76aa21882
18 changes: 11 additions & 7 deletions packages/knex/src/error-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,18 @@ import { errors } from '@feathersjs/errors'

export const ERROR = Symbol.for('@feathersjs/knex/error')

/**
* Knex prefixes driver errors with the query and its bound values as "<sql> - <message>",
* so keep only the text after the last " - " to not send them to the client.
*/
const omitQuery = (message: string) => {
const index = message.lastIndexOf(' - ')

return index === -1 ? message : message.slice(index + 3)
}

export function errorHandler(error: any) {
const { message } = error
const message = omitQuery(error.message || '')
let feathersError = error

if (error.sqlState && error.sqlState.length) {
Expand Down Expand Up @@ -64,12 +74,6 @@ export function errorHandler(error: any) {
} else if (typeof error.code === 'string' && error.severity && error.routine) {
// NOTE: Error codes taken from
// https://www.postgresql.org/docs/9.6/static/errcodes-appendix.html
// Omit query information. Knex prefixes the driver message with the SQL
// as "<sql> - <pg message>", so keep only the text after the last " - ".
const rawMessage: string = error.message || ''
const separator = rawMessage.lastIndexOf(' - ')
const message = separator === -1 ? rawMessage : rawMessage.slice(separator + 3) // if no separator, use as is, else move pointer to the right of the separator

switch (error.code.slice(0, 2)) {
case '22':
feathersError = new errors.NotFound(message)
Expand Down
78 changes: 76 additions & 2 deletions packages/knex/test/error-handler.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import assert from 'assert'
import { errorHandler, ERROR } from '../src'
import knex from 'knex'
import { errorHandler, ERROR, KnexService } from '../src'

describe('Knex Error handler', () => {
it('sqlState', () => {
Expand Down Expand Up @@ -101,7 +102,8 @@ describe('Knex Error handler', () => {

// A " - " inside the SQL and a hyphen in the error text are both handled
assert.throws(
() => errorHandler(pgError('42703', 'select "a" - "b" from "t" - column "non-existent" does not exist')),
() =>
errorHandler(pgError('42703', 'select "a" - "b" from "t" - column "non-existent" does not exist')),
{
name: 'Unprocessable',
message: 'column "non-existent" does not exist'
Expand All @@ -124,4 +126,76 @@ describe('Knex Error handler', () => {
assert.strictEqual(error[ERROR].constraint, 'users_email_unique')
}
})

it('sqlState errors omit query information', () => {
assert.throws(
() =>
errorHandler({
sqlState: '#23000',
message:
"insert into `users` (`email`) values ('a@example.com') - ER_DUP_ENTRY: Duplicate entry 'a@example.com' for key 'email'"
}),
{ name: 'BadRequest', message: "ER_DUP_ENTRY: Duplicate entry 'a@example.com' for key 'email'" }
)
})

it('SQLite errors omit query information', () => {
assert.throws(
() =>
errorHandler({
code: 'SQLITE_ERROR',
errno: 1,
message: "select `users`.* from `users` where `nope` = 'x' - SQLITE_ERROR: no such column: nope"
}),
{ name: 'BadRequest', message: 'SQLITE_ERROR: no such column: nope' }
)
})

it('other database errors omit query information', () => {
assert.throws(
() =>
errorHandler({
code: 'SQLITE_CONSTRAINT',
errno: 19,
message:
"insert into `users` (`password`) values ('secret') - SQLITE_CONSTRAINT: UNIQUE constraint failed: users.email"
}),
{ name: 'GeneralError', message: 'SQLITE_CONSTRAINT: UNIQUE constraint failed: users.email' }
)
})

it('does not leak queries or bound values from a real database', async () => {
const db = knex({ client: 'sqlite3', connection: { filename: ':memory:' }, useNullAsDefault: true })

try {
await db.schema.createTable('users', (table) => {
table.increments('id')
table.string('email').unique()
table.string('password')
})
const service = new KnexService({ Model: db, name: 'users' })

await service.create({ email: 'a@example.com', password: 'hash-1' })

await assert.rejects(
() => service.create({ email: 'a@example.com', password: 'secret-hash-2' }),
(error: any) => {
assert.ok(!error.message.includes('insert into'))
assert.ok(!error.message.includes('secret-hash-2'))
assert.ok(!error.toJSON().message.includes('insert into'))
assert.ok(!error.toJSON().message.includes('secret-hash-2'))
return true
}
)
await assert.rejects(
() => service.find({ query: { nope: 'x' } } as any),
(error: any) => {
assert.ok(!error.message.includes('select'))
return true
}
)
} finally {
await db.destroy()
}
})
})
Loading