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 39308fc
Show file tree
Hide file tree
Showing 15 changed files with 157 additions and 27 deletions.
7 changes: 6 additions & 1 deletion packages/client/src/runtime/RequestHandler.ts
Expand Up @@ -208,7 +208,12 @@ 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) {
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')
})
})
11 changes: 5 additions & 6 deletions packages/engine-core/src/binary/BinaryEngine.ts
Expand Up @@ -1203,12 +1203,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
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,17 @@ 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)
}
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
11 changes: 5 additions & 6 deletions packages/engine-core/src/library/LibraryEngine.ts
Expand Up @@ -146,12 +146,11 @@ export class LibraryEngine extends Engine {
const response = this.parseEngineResponse<{ [K: string]: unknown }>(result)

if (response.error_code) {
throw new PrismaClientKnownRequestError(
response.message as string,
response.error_code as string,
this.config.clientVersion as string,
response.meta,
)
throw new PrismaClientKnownRequestError(response.message as string, {
code: response.error_code as string,
clientVersion: this.config.clientVersion as string,
meta: response.meta as Record<string, unknown>,
})
}

return response as Tx.Info<undefined> | undefined
Expand Down

0 comments on commit 39308fc

Please sign in to comment.