Skip to content

Commit

Permalink
fix(client): Correctly report error location in batch transaction
Browse files Browse the repository at this point in the history
Engine PR: prisma/prisma-engines#3384

Uses newly added `batch_request_idx` property of an errors to identify
and correctly report error location within a batch.

Fix #15433
Fix #14373
  • Loading branch information
SevInf committed Nov 11, 2022
1 parent f041d96 commit af2774f
Show file tree
Hide file tree
Showing 17 changed files with 199 additions and 46 deletions.
12 changes: 10 additions & 2 deletions packages/client/src/runtime/RequestHandler.ts
Expand Up @@ -208,11 +208,19 @@ export class RequestHandler {
message = this.sanitizeMessage(message)
// TODO: Do request with callsite instead, so we don't need to rethrow
if (error.code) {
throw new PrismaClientKnownRequestError(message, error.code, this.client._clientVersion, error.meta)
throw new PrismaClientKnownRequestError(message, {
code: error.code,
clientVersion: this.client._clientVersion,
meta: error.meta,
batchRequestIdx: error.batchRequestIdx,
})
} else if (error.isPanic) {
throw new PrismaClientRustPanicError(message, this.client._clientVersion)
} else if (error instanceof PrismaClientUnknownRequestError) {
throw new PrismaClientUnknownRequestError(message, this.client._clientVersion)
throw new PrismaClientUnknownRequestError(message, {
clientVersion: this.client._clientVersion,
batchRequestIdx: error.batchRequestIdx,
})
} else if (error instanceof PrismaClientInitializationError) {
throw new PrismaClientInitializationError(message, this.client._clientVersion)
} else if (error instanceof PrismaClientRustPanicError) {
Expand Down
3 changes: 2 additions & 1 deletion packages/client/src/runtime/getPrismaClient.ts
Expand Up @@ -56,6 +56,7 @@ import type { InstanceRejectOnNotFound, RejectOnNotFound } from './utils/rejectO
import { getRejectOnNotFound } from './utils/rejectOnNotFound'
import { serializeRawParameters } from './utils/serializeRawParameters'
import { validatePrismaClientOptions } from './utils/validatePrismaClientOptions'
import { waitForBatch } from './utils/waitForBatch'

const debug = Debug('prisma:client')
const ALTER_RE = /^(\s*alter\s)/i
Expand Down Expand Up @@ -957,7 +958,7 @@ new PrismaClient({
return request.requestTransaction?.({ id: txId, isolationLevel: options?.isolationLevel }, lock)
})

return Promise.all(requests)
return waitForBatch(requests)
}

/**
Expand Down
2 changes: 1 addition & 1 deletion packages/client/src/runtime/utils/rejectOnNotFound.ts
Expand Up @@ -17,7 +17,7 @@ export type InstanceRejectOnNotFound =
*/
export class NotFoundError extends PrismaClientKnownRequestError {
constructor(message: string) {
super(message, 'P2025', clientVersion)
super(message, { code: 'P2025', clientVersion })
this.name = 'NotFoundError'
}
}
Expand Down
36 changes: 36 additions & 0 deletions packages/client/src/runtime/utils/waitForBatch.ts
@@ -0,0 +1,36 @@
import { hasBatchIndex } from '@prisma/engine-core'

/**
* Waits for result of batch $transaction and picks the best possible error to report if any
* of the request fails. Best error is determined as follows:
*
* - if engine have reported and index of failed request in the batch, the best error is the one
* who's index matches that
* - otherwise, first error is used (like Promise.all)
*
* @param promises
* @returns
*/
export async function waitForBatch<T extends unknown[]>(promises: T): Promise<{ [K in keyof T]: Awaited<T[K]> }> {
const results = await Promise.allSettled(promises)

let bestError: unknown = null
const successfulResults = [] as { [K in keyof T]: Awaited<T[K]> }
for (const [i, result] of results.entries()) {
if (result.status === 'rejected') {
const reason = result.reason
if (hasBatchIndex(reason) && reason.batchRequestIdx === i) {
bestError = reason
} else if (!bestError) {
bestError = reason
}
} else {
successfulResults.push(result.value)
}
}

if (bestError) {
throw bestError
}
return successfulResults
}
@@ -0,0 +1,24 @@
import { defineMatrix } from '../../_utils/defineMatrix'

export default defineMatrix(() => [
[
{
provider: 'sqlite',
},
{
provider: 'postgresql',
},
{
provider: 'mysql',
},
{
provider: 'mongodb',
},
{
provider: 'cockroachdb',
},
{
provider: 'sqlserver',
},
],
])
@@ -0,0 +1,22 @@
import { idForProvider } from '../../../_utils/idForProvider'
import testMatrix from '../_matrix'

export default testMatrix.setupSchema(({ provider }) => {
return /* Prisma */ `
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "${provider}"
url = env("DATABASE_URI_${provider}")
}
model User {
id ${idForProvider(provider)}
email String @unique
memo String?
createdAt DateTime @default(now())
}
`
})
@@ -0,0 +1,24 @@
// @ts-ignore
import type { PrismaClient } from '@prisma/client'

import testMatrix from './_matrix'

declare let prisma: PrismaClient

testMatrix.setupTestSuite(() => {
test('correctly reports location of a batch error', async () => {
const result = prisma.$transaction([
prisma.user.findMany({}),
prisma.user.update({
where: {
email: 'notthere@example.com',
},
data: {
memo: 'id is 1',
},
}),
])

await expect(result).rejects.toThrowError('Invalid `prisma.user.update()` invocation')
})
})
29 changes: 13 additions & 16 deletions packages/engine-core/src/binary/BinaryEngine.ts
Expand Up @@ -493,10 +493,9 @@ ${chalk.dim("In case we're mistaken, please report this to us 🙏.")}`)
await this.startPromise

if (!this.child && !this.engineEndpoint) {
throw new PrismaClientUnknownRequestError(
`Can't perform request, as the Engine has already been stopped`,
this.clientVersion!,
)
throw new PrismaClientUnknownRequestError(`Can't perform request, as the Engine has already been stopped`, {
clientVersion: this.clientVersion!,
})
}
}

Expand Down Expand Up @@ -951,7 +950,7 @@ You very likely have the wrong "binaryTarget" defined in the schema.prisma file.
throw prismaGraphQLToJSError(data.errors[0], this.clientVersion!)
}
// this case should not happen, as the query engine only returns one error
throw new PrismaClientUnknownRequestError(JSON.stringify(data.errors), this.clientVersion!)
throw new PrismaClientUnknownRequestError(JSON.stringify(data.errors), { clientVersion: this.clientVersion! })
}

// Rust engine returns time in microseconds and we want it in milliseconds
Expand Down Expand Up @@ -1095,10 +1094,9 @@ You very likely have the wrong "binaryTarget" defined in the schema.prisma file.
}

if (this.lastErrorLog && isRustErrorLog(this.lastErrorLog)) {
const err = new PrismaClientUnknownRequestError(
this.getErrorMessageWithLink(getMessage(this.lastErrorLog)),
this.clientVersion!,
)
const err = new PrismaClientUnknownRequestError(this.getErrorMessageWithLink(getMessage(this.lastErrorLog)), {
clientVersion: this.clientVersion!,
})

if (this.lastErrorLog?.fields?.message === 'PANIC') {
this.lastPanic = err
Expand Down Expand Up @@ -1157,7 +1155,7 @@ and your request can't be processed.
You probably have some open handle that prevents your process from exiting.
It could be an open http server or stream that didn't close yet.
We recommend using the \`wtfnode\` package to debug open handles.`,
this.clientVersion!,
{ clientVersion: this.clientVersion! },
)
}

Expand Down Expand Up @@ -1203,12 +1201,11 @@ Please look into the logs or turn on the env var DEBUG=* to debug the constantly
*/
transactionHttpErrorHandler<R>(result: Result<R>): never {
const response = result.data as { [K: string]: unknown }
throw new PrismaClientKnownRequestError(
response.message as string,
response.error_code as string,
this.clientVersion as string,
response.meta,
)
throw new PrismaClientKnownRequestError(response.message as string, {
code: response.error_code as string,
clientVersion: this.clientVersion as string,
meta: response.meta as Record<string, unknown>,
})
}
}

Expand Down
7 changes: 7 additions & 0 deletions packages/engine-core/src/common/errors/ErrorWithBatchIndex.ts
@@ -0,0 +1,7 @@
export interface ErrorWithBatchIndex {
batchRequestIdx?: number
}

export function hasBatchIndex(value: object): value is Required<ErrorWithBatchIndex> {
return typeof value['batchRequestIdx'] === 'number'
}
@@ -1,14 +1,25 @@
export class PrismaClientKnownRequestError extends Error {
import { ErrorWithBatchIndex } from './ErrorWithBatchIndex'

type KnownErrorParams = {
code: string
clientVersion: string
meta?: Record<string, unknown>
batchRequestIdx?: number
}

export class PrismaClientKnownRequestError extends Error implements ErrorWithBatchIndex {
code: string
meta?: Record<string, unknown>
clientVersion: string
batchRequestIdx?: number

constructor(message: string, code: string, clientVersion: string, meta?: any) {
constructor(message: string, { code, clientVersion, meta, batchRequestIdx }: KnownErrorParams) {
super(message)

this.code = code
this.clientVersion = clientVersion
this.meta = meta
this.batchRequestIdx = batchRequestIdx
}
get [Symbol.toStringTag]() {
return 'PrismaClientKnownRequestError'
Expand Down
@@ -1,10 +1,19 @@
export class PrismaClientUnknownRequestError extends Error {
import { ErrorWithBatchIndex } from './ErrorWithBatchIndex'

type UnknownErrorParams = {
clientVersion: string
batchRequestIdx?: number
}

export class PrismaClientUnknownRequestError extends Error implements ErrorWithBatchIndex {
clientVersion: string
batchRequestIdx?: number

constructor(message: string, clientVersion: string) {
constructor(message: string, { clientVersion, batchRequestIdx }: UnknownErrorParams) {
super(message)

this.clientVersion = clientVersion
this.batchRequestIdx = batchRequestIdx
}
get [Symbol.toStringTag]() {
return 'PrismaClientUnknownRequestError'
Expand Down
3 changes: 2 additions & 1 deletion packages/engine-core/src/common/errors/types/RequestError.ts
Expand Up @@ -3,7 +3,8 @@ export interface RequestError {
user_facing_error: {
is_panic: boolean
message: string
meta?: object
meta?: Record<string, unknown>
error_code?: string
batch_request_idx?: number
}
}
Expand Up @@ -3,17 +3,20 @@ import { PrismaClientUnknownRequestError } from '../PrismaClientUnknownRequestEr
import type { RequestError } from '../types/RequestError'

export function prismaGraphQLToJSError(
error: RequestError,
{ error, user_facing_error }: RequestError,
clientVersion: string,
): PrismaClientKnownRequestError | PrismaClientUnknownRequestError {
if (error.user_facing_error.error_code) {
return new PrismaClientKnownRequestError(
error.user_facing_error.message,
error.user_facing_error.error_code,
if (user_facing_error.error_code) {
return new PrismaClientKnownRequestError(user_facing_error.message, {
code: user_facing_error.error_code,
clientVersion,
error.user_facing_error.meta,
)
meta: user_facing_error.meta,
batchRequestIdx: user_facing_error.batch_request_idx,
})
}

return new PrismaClientUnknownRequestError(error.error, clientVersion)
return new PrismaClientUnknownRequestError(error, {
clientVersion,
batchRequestIdx: user_facing_error.batch_request_idx,
})
}
2 changes: 1 addition & 1 deletion packages/engine-core/src/data-proxy/DataProxyEngine.ts
Expand Up @@ -208,7 +208,7 @@ export class DataProxyEngine extends Engine {
if (data.errors.length === 1) {
throw prismaGraphQLToJSError(data.errors[0], this.config.clientVersion!)
} else {
throw new PrismaClientUnknownRequestError(data.errors, this.config.clientVersion!)
throw new PrismaClientUnknownRequestError(data.errors, { clientVersion: this.config.clientVersion! })
}
}

Expand Down
Expand Up @@ -95,7 +95,7 @@ export async function responseToError(
const error = await getResponseErrorBody(response)

if (error.type === 'QueryEngineError') {
throw new PrismaClientKnownRequestError(error.body.message, error.body.error_code, clientVersion)
throw new PrismaClientKnownRequestError(error.body.message, { code: error.body.error_code, clientVersion })
}

if (error.type === 'DataProxyError') {
Expand Down
1 change: 1 addition & 0 deletions packages/engine-core/src/index.ts
Expand Up @@ -4,6 +4,7 @@ export type { EngineEventType } from './common/Engine'
export type { DatasourceOverwrite } from './common/Engine'
export type { BatchTransactionOptions } from './common/Engine'
export { Engine } from './common/Engine'
export { hasBatchIndex } from './common/errors/ErrorWithBatchIndex'
export { PrismaClientInitializationError } from './common/errors/PrismaClientInitializationError'
export { PrismaClientKnownRequestError } from './common/errors/PrismaClientKnownRequestError'
export { PrismaClientRustPanicError } from './common/errors/PrismaClientRustPanicError'
Expand Down

0 comments on commit af2774f

Please sign in to comment.