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
9 changes: 6 additions & 3 deletions src/http/routes/s3/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,12 @@ export default async function routes(fastify: FastifyInstance) {
const isValid = compiler(data)

if (!isValid) {
const validationError = new Error('Invalid request')
;(validationError as Error & { validation?: unknown }).validation =
compiler.errors
const validationError = new Error('Invalid request') as Error & {
validation?: unknown
statusCode?: number
}
validationError.validation = compiler.errors
validationError.statusCode = 400
throw validationError
}

Expand Down
9 changes: 9 additions & 0 deletions src/internal/database/connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,15 @@ export class TenantConnection {
throw ERRORS.DatabaseTimeout(e)
}

// Handle database connection limit errors
if (
e instanceof DatabaseError &&
((e.code === '08P01' && e.message.includes('no more connections allowed')) ||
e.message.includes('Max client connections reached'))
) {
throw ERRORS.DatabaseConnectionLimit(e)
}

throw e
}
}
Expand Down
55 changes: 51 additions & 4 deletions src/internal/errors/codes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ export enum ErrorCode {
KeyAlreadyExists = 'KeyAlreadyExists',
BucketAlreadyExists = 'BucketAlreadyExists',
DatabaseTimeout = 'DatabaseTimeout',
DatabaseConnectionLimit = 'DatabaseConnectionLimit',
DatabaseReadOnly = 'DatabaseReadOnly',
DatabaseInvalidObjectDefinition = 'DatabaseInvalidObjectDefinition',
DatabaseSchemaMismatch = 'DatabaseSchemaMismatch',
InvalidSignature = 'InvalidSignature',
ExpiredToken = 'ExpiredToken',
SignatureDoesNotMatch = 'SignatureDoesNotMatch',
Expand Down Expand Up @@ -276,12 +280,12 @@ export const ERRORS = {
message: `invalid range provided`,
}),

EntityTooLarge: (e?: Error, entity = 'object') =>
EntityTooLarge: (e?: Error, entity = 'object', limit = 'the maximum allowed size') =>
new StorageBackendError({
error: 'Payload too large',
code: ErrorCode.EntityTooLarge,
httpStatusCode: 413,
message: `The ${entity} exceeded the maximum allowed size`,
message: `The ${entity} exceeded ${limit}`,
originalError: e,
}),

Expand Down Expand Up @@ -372,6 +376,39 @@ export const ERRORS = {
originalError: e,
}),

DatabaseConnectionLimit: (e?: Error) =>
new StorageBackendError({
code: ErrorCode.DatabaseConnectionLimit,
httpStatusCode: 503,
message:
'The database has reached its maximum number of connections. Please try again later.',
originalError: e,
}),

DatabaseReadOnly: (e?: Error) =>
new StorageBackendError({
code: ErrorCode.DatabaseReadOnly,
httpStatusCode: 503,
message: 'The database is currently in read-only mode. Please try again later.',
originalError: e,
}),

InvalidObjectDefinition: (e?: Error) =>
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is internal but maybe Database prefix here as well

new StorageBackendError({
code: ErrorCode.DatabaseInvalidObjectDefinition,
httpStatusCode: 503,
message: 'The database schema is invalid or incompatible.',
originalError: e,
}),

DatabaseSchemaMismatch: (e?: Error) =>
new StorageBackendError({
code: ErrorCode.DatabaseSchemaMismatch,
httpStatusCode: 503,
message: 'The database schema is out of sync. Please run migrations or contact support.',
originalError: e,
}),

ResourceLocked: (e?: Error) =>
new StorageBackendError({
code: ErrorCode.ResourceLocked,
Expand Down Expand Up @@ -517,10 +554,20 @@ export function isStorageError(errorType: ErrorCode, error: any): error is Stora
return error instanceof StorageBackendError && error.code === errorType
}

function hasStatusCode(error: Error): error is Error & { statusCode: number } {
return 'statusCode' in error && typeof (error as any).statusCode === 'number'
}

export function normalizeRawError(error: any) {
if (error instanceof Error) {
const statusCode =
error instanceof StorageBackendError && error.httpStatusCode ? error.httpStatusCode : 0
let statusCode = 0
if (error instanceof StorageBackendError && error.httpStatusCode) {
statusCode = error.httpStatusCode
} else if (hasStatusCode(error)) {
// Fastify validation errors include statusCode we can use
statusCode = error.statusCode
}

return {
raw: JSON.stringify(error),
name: error.name,
Expand Down
28 changes: 28 additions & 0 deletions src/storage/database/knex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1171,6 +1171,34 @@ export class DBError extends StorageBackendError implements RenderableError {
query,
code: pgError.code,
})
case '25006': // read only sql transaction
return ERRORS.DatabaseReadOnly(pgError).withMetadata({
query,
code: pgError.code,
})
case '42P17': // invalid object definition
return ERRORS.InvalidObjectDefinition(pgError).withMetadata({
query,
code: pgError.code,
})
case '22P02': // invalid text representation
return ERRORS.InvalidParameter('value', {
error: pgError,
message: pgError.message || 'Invalid value format or type conversion failed',
}).withMetadata({
query,
code: pgError.code,
})
case '42703': // undefined column
return ERRORS.DatabaseSchemaMismatch(pgError).withMetadata({
query,
code: pgError.code,
})
case '42P01': // relation does not exist
return ERRORS.DatabaseSchemaMismatch(pgError).withMetadata({
query,
code: pgError.code,
})
default:
return ERRORS.DatabaseError(`database error, code: ${pgError.code}`, pgError).withMetadata({
query,
Expand Down
10 changes: 9 additions & 1 deletion src/storage/protocols/s3/signature-v4-stream.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// ChunkSignatureParser.ts

import { ERRORS } from '@internal/errors'
import crypto from 'crypto'
import { Transform, TransformCallback, TransformOptions } from 'stream'

Expand Down Expand Up @@ -118,7 +119,14 @@ export class ChunkSignatureV4Parser extends Transform {

cb()
} catch (err) {
cb(err as Error)
const error = err as Error
// Convert chunk size errors to 400 instead of 500
if (error.message && error.message.includes('Chunk size exceeds')) {
const limit = error.message.replace('Chunk size exceeds', '').trim()
cb(ERRORS.EntityTooLarge(error, 'chunk', limit))
} else {
cb(error)
}
}
}

Expand Down
4 changes: 2 additions & 2 deletions src/test/signature-v4-stream.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ describe('ChunkSignatureV4Parser', () => {
const sig = 'f'.repeat(64)
return new Promise<void>((resolve) => {
parser.on('error', (err) => {
expect(err.message).toMatch(/Chunk size exceeds 1 bytes/)
expect(err.message).toEqual('The chunk exceeded 1 bytes')
resolve()
})
parser.write(`2;chunk-signature=${sig}\r\n`)
Expand Down Expand Up @@ -161,7 +161,7 @@ describe('ChunkSignatureV4Parser', () => {
const parser = makeParser({ maxChunkSize: 1 })
return new Promise<void>((resolve) => {
parser.on('error', (err) => {
expect(err.message).toMatch(/Chunk size exceeds 1 bytes/)
expect(err.message).toEqual('The chunk exceeded 1 bytes')
resolve()
})
parser.write(`2;chunk-signature=${sig}\r\n`)
Expand Down