Skip to content

Commit 061a083

Browse files
gwynnefabianfett
andauthored
Add PSQLError debugDescription (vapor#372)
Co-authored-by: Fabian Fett <fabianfett@apple.com>
1 parent f1744c8 commit 061a083

File tree

4 files changed

+292
-25
lines changed

4 files changed

+292
-25
lines changed

Sources/PostgresNIO/New/PSQLError.swift

Lines changed: 106 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -190,21 +190,13 @@ public struct PSQLError: Error {
190190

191191
private final class Backing {
192192
fileprivate var code: Code
193-
194193
fileprivate var serverInfo: ServerInfo?
195-
196194
fileprivate var underlying: Error?
197-
198195
fileprivate var file: String?
199-
200196
fileprivate var line: Int?
201-
202197
fileprivate var query: PostgresQuery?
203-
204198
fileprivate var backendMessage: PostgresBackendMessage?
205-
206199
fileprivate var unsupportedAuthScheme: UnsupportedAuthScheme?
207-
208200
fileprivate var invalidCommandTag: String?
209201

210202
init(code: Code) {
@@ -224,10 +216,10 @@ public struct PSQLError: Error {
224216
}
225217

226218
public struct ServerInfo {
227-
public struct Field: Hashable, Sendable {
219+
public struct Field: Hashable, Sendable, CustomStringConvertible {
228220
fileprivate let backing: PostgresBackendMessage.Field
229221

230-
private init(_ backing: PostgresBackendMessage.Field) {
222+
fileprivate init(_ backing: PostgresBackendMessage.Field) {
231223
self.backing = backing
232224
}
233225

@@ -306,6 +298,47 @@ public struct PSQLError: Error {
306298

307299
/// Routine: the name of the source-code routine reporting the error.
308300
public static let routine = Self(.routine)
301+
302+
public var description: String {
303+
switch self.backing {
304+
case .localizedSeverity:
305+
return "localizedSeverity"
306+
case .severity:
307+
return "severity"
308+
case .sqlState:
309+
return "sqlState"
310+
case .message:
311+
return "message"
312+
case .detail:
313+
return "detail"
314+
case .hint:
315+
return "hint"
316+
case .position:
317+
return "position"
318+
case .internalPosition:
319+
return "internalPosition"
320+
case .internalQuery:
321+
return "internalQuery"
322+
case .locationContext:
323+
return "locationContext"
324+
case .schemaName:
325+
return "schemaName"
326+
case .tableName:
327+
return "tableName"
328+
case .columnName:
329+
return "columnName"
330+
case .dataTypeName:
331+
return "dataTypeName"
332+
case .constraintName:
333+
return "constraintName"
334+
case .file:
335+
return "file"
336+
case .line:
337+
return "line"
338+
case .routine:
339+
return "routine"
340+
}
341+
}
309342
}
310343

311344
let underlying: PostgresBackendMessage.ErrorResponse
@@ -397,6 +430,65 @@ public struct PSQLError: Error {
397430
}
398431
}
399432

433+
extension PSQLError: CustomStringConvertible {
434+
public var description: String {
435+
// This may seem very odd... But we are afraid that users might accidentally send the
436+
// unfiltered errors out to end-users. This may leak security relevant information. For this
437+
// reason we overwrite the error description by default to this generic "Database error"
438+
"""
439+
PSQLError – Generic description to prevent accidental leakage of sensitive data. For debugging details, use `String(reflecting: error)`.
440+
"""
441+
}
442+
}
443+
444+
extension PSQLError: CustomDebugStringConvertible {
445+
public var debugDescription: String {
446+
var result = #"PSQLError(code: \#(self.code)"#
447+
448+
if let serverInfo = self.serverInfo?.underlying {
449+
result.append(", serverInfo: [")
450+
result.append(
451+
serverInfo.fields
452+
.sorted(by: { $0.key.rawValue < $1.key.rawValue })
453+
.map { "\(PSQLError.ServerInfo.Field($0.0)): \($0.1)" }
454+
.joined(separator: ", ")
455+
)
456+
result.append("]")
457+
}
458+
459+
if let backendMessage = self.backendMessage {
460+
result.append(", backendMessage: \(String(reflecting: backendMessage))")
461+
}
462+
463+
if let unsupportedAuthScheme = self.unsupportedAuthScheme {
464+
result.append(", unsupportedAuthScheme: \(unsupportedAuthScheme)")
465+
}
466+
467+
if let invalidCommandTag = self.invalidCommandTag {
468+
result.append(", invalidCommandTag: \(invalidCommandTag)")
469+
}
470+
471+
if let underlying = self.underlying {
472+
result.append(", underlying: \(String(reflecting: underlying))")
473+
}
474+
475+
if let file = self.file {
476+
result.append(", triggeredFromRequestInFile: \(file)")
477+
if let line = self.line {
478+
result.append(", line: \(line)")
479+
}
480+
}
481+
482+
if let query = self.query {
483+
result.append(", query: \(String(reflecting: query))")
484+
}
485+
486+
result.append(")")
487+
488+
return result
489+
}
490+
}
491+
400492
/// An error that may happen when a ``PostgresRow`` or ``PostgresCell`` is decoded to native Swift types.
401493
public struct PostgresDecodingError: Error, Equatable {
402494
public struct Code: Hashable, Error, CustomStringConvertible {
@@ -490,7 +582,9 @@ extension PostgresDecodingError: CustomStringConvertible {
490582
// This may seem very odd... But we are afraid that users might accidentally send the
491583
// unfiltered errors out to end-users. This may leak security relevant information. For this
492584
// reason we overwrite the error description by default to this generic "Database error"
493-
"Database error"
585+
"""
586+
PostgresDecodingError – Generic description to prevent accidental leakage of sensitive data. For debugging details, use `String(reflecting: error)`.
587+
"""
494588
}
495589
}
496590

@@ -504,7 +598,7 @@ extension PostgresDecodingError: CustomDebugStringConvertible {
504598
result.append(#", postgresType: \#(self.postgresType)"#)
505599
result.append(#", postgresFormat: \#(self.postgresFormat)"#)
506600
if let postgresData = self.postgresData {
507-
result.append(#", postgresData: \#(postgresData.debugDescription)"#) // https://github.com/apple/swift-nio/pull/2418
601+
result.append(#", postgresData: \#(String(reflecting: postgresData))"#)
508602
}
509603
result.append(#", file: \#(self.file)"#)
510604
result.append(#", line: \#(self.line)"#)

Sources/PostgresNIO/New/PostgresQuery.swift

Lines changed: 129 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,20 @@ extension PostgresQuery {
9595
}
9696
}
9797

98+
extension PostgresQuery: CustomStringConvertible {
99+
/// See ``Swift/CustomStringConvertible/description``.
100+
public var description: String {
101+
"\(self.sql) \(self.binds)"
102+
}
103+
}
104+
105+
extension PostgresQuery: CustomDebugStringConvertible {
106+
/// See ``Swift/CustomDebugStringConvertible/debugDescription``.
107+
public var debugDescription: String {
108+
"PostgresQuery(sql: \(String(describing: self.sql)), binds: \(String(reflecting: self.binds)))"
109+
}
110+
}
111+
98112
struct PSQLExecuteStatement {
99113
/// The statements name
100114
var name: String
@@ -111,16 +125,19 @@ public struct PostgresBindings: Sendable, Hashable {
111125
var dataType: PostgresDataType
112126
@usableFromInline
113127
var format: PostgresFormat
128+
@usableFromInline
129+
var protected: Bool
114130

115131
@inlinable
116-
init(dataType: PostgresDataType, format: PostgresFormat) {
132+
init(dataType: PostgresDataType, format: PostgresFormat, protected: Bool) {
117133
self.dataType = dataType
118134
self.format = format
135+
self.protected = protected
119136
}
120137

121138
@inlinable
122-
init<Value: PostgresEncodable>(value: Value) {
123-
self.init(dataType: Value.psqlType, format: Value.psqlFormat)
139+
init<Value: PostgresEncodable>(value: Value, protected: Bool) {
140+
self.init(dataType: Value.psqlType, format: Value.psqlFormat, protected: protected)
124141
}
125142
}
126143

@@ -147,7 +164,7 @@ public struct PostgresBindings: Sendable, Hashable {
147164

148165
public mutating func appendNull() {
149166
self.bytes.writeInteger(-1, as: Int32.self)
150-
self.metadata.append(.init(dataType: .null, format: .binary))
167+
self.metadata.append(.init(dataType: .null, format: .binary, protected: true))
151168
}
152169

153170
@inlinable
@@ -156,7 +173,7 @@ public struct PostgresBindings: Sendable, Hashable {
156173
context: PostgresEncodingContext<JSONEncoder>
157174
) throws {
158175
try value.encodeRaw(into: &self.bytes, context: context)
159-
self.metadata.append(.init(value: value))
176+
self.metadata.append(.init(value: value, protected: true))
160177
}
161178

162179
@inlinable
@@ -165,7 +182,25 @@ public struct PostgresBindings: Sendable, Hashable {
165182
context: PostgresEncodingContext<JSONEncoder>
166183
) {
167184
value.encodeRaw(into: &self.bytes, context: context)
168-
self.metadata.append(.init(value: value))
185+
self.metadata.append(.init(value: value, protected: true))
186+
}
187+
188+
@inlinable
189+
mutating func appendUnprotected<Value: PostgresEncodable, JSONEncoder: PostgresJSONEncoder>(
190+
_ value: Value,
191+
context: PostgresEncodingContext<JSONEncoder>
192+
) throws {
193+
try value.encodeRaw(into: &self.bytes, context: context)
194+
self.metadata.append(.init(value: value, protected: false))
195+
}
196+
197+
@inlinable
198+
mutating func appendUnprotected<Value: PostgresNonThrowingEncodable, JSONEncoder: PostgresJSONEncoder>(
199+
_ value: Value,
200+
context: PostgresEncodingContext<JSONEncoder>
201+
) {
202+
value.encodeRaw(into: &self.bytes, context: context)
203+
self.metadata.append(.init(value: value, protected: false))
169204
}
170205

171206
public mutating func append(_ postgresData: PostgresData) {
@@ -176,6 +211,93 @@ public struct PostgresBindings: Sendable, Hashable {
176211
self.bytes.writeInteger(Int32(input.readableBytes))
177212
self.bytes.writeBuffer(&input)
178213
}
179-
self.metadata.append(.init(dataType: postgresData.type, format: .binary))
214+
self.metadata.append(.init(dataType: postgresData.type, format: .binary, protected: true))
215+
}
216+
}
217+
218+
extension PostgresBindings: CustomStringConvertible, CustomDebugStringConvertible {
219+
/// See ``Swift/CustomStringConvertible/description``.
220+
public var description: String {
221+
"""
222+
[\(zip(self.metadata, BindingsReader(buffer: self.bytes))
223+
.lazy.map({ Self.makeBindingPrintable(protected: $0.protected, type: $0.dataType, format: $0.format, buffer: $1) })
224+
.joined(separator: ", "))]
225+
"""
226+
}
227+
228+
/// See ``Swift/CustomDebugStringConvertible/description``.
229+
public var debugDescription: String {
230+
"""
231+
[\(zip(self.metadata, BindingsReader(buffer: self.bytes))
232+
.lazy.map({ Self.makeDebugDescription(protected: $0.protected, type: $0.dataType, format: $0.format, buffer: $1) })
233+
.joined(separator: ", "))]
234+
"""
235+
}
236+
237+
private static func makeDebugDescription(protected: Bool, type: PostgresDataType, format: PostgresFormat, buffer: ByteBuffer?) -> String {
238+
"(\(Self.makeBindingPrintable(protected: protected, type: type, format: format, buffer: buffer)); \(type); format: \(format))"
239+
}
240+
241+
private static func makeBindingPrintable(protected: Bool, type: PostgresDataType, format: PostgresFormat, buffer: ByteBuffer?) -> String {
242+
if protected {
243+
return "****"
244+
}
245+
246+
guard var buffer = buffer else {
247+
return "null"
248+
}
249+
250+
do {
251+
switch (type, format) {
252+
case (.int4, _), (.int2, _), (.int8, _):
253+
let number = try Int64.init(from: &buffer, type: type, format: format, context: .default)
254+
return String(describing: number)
255+
256+
case (.bool, _):
257+
let bool = try Bool.init(from: &buffer, type: type, format: format, context: .default)
258+
return String(describing: bool)
259+
260+
case (.varchar, _), (.bpchar, _), (.text, _), (.name, _):
261+
let value = try String.init(from: &buffer, type: type, format: format, context: .default)
262+
return String(reflecting: value) // adds quotes
263+
264+
default:
265+
return "\(buffer.readableBytes) bytes"
266+
}
267+
} catch {
268+
return "\(buffer.readableBytes) bytes"
269+
}
270+
}
271+
}
272+
273+
/// A small helper to inspect encoded bindings
274+
private struct BindingsReader: Sequence {
275+
typealias Element = Optional<ByteBuffer>
276+
277+
var buffer: ByteBuffer
278+
279+
struct Iterator: IteratorProtocol {
280+
typealias Element = Optional<ByteBuffer>
281+
private var buffer: ByteBuffer
282+
283+
init(buffer: ByteBuffer) {
284+
self.buffer = buffer
285+
}
286+
287+
mutating func next() -> Optional<Optional<ByteBuffer>> {
288+
guard let length = self.buffer.readInteger(as: Int32.self) else {
289+
return .none
290+
}
291+
292+
if length < 0 {
293+
return .some(.none)
294+
}
295+
296+
return .some(self.buffer.readSlice(length: Int(length))!)
297+
}
298+
}
299+
300+
func makeIterator() -> Iterator {
301+
Iterator(buffer: self.buffer)
180302
}
181303
}

Tests/IntegrationTests/AsyncTests.swift

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ final class AsyncPostgresConnectionTests: XCTestCase {
7272
var counter = 0
7373

7474
for try await element in rows.decode((Int, String, String, String, String?, Int, Date, Date, String, String).self) {
75-
XCTAssertEqual(element.1, env("POSTGRES_DB") ?? "localhost")
75+
XCTAssertEqual(element.1, env("POSTGRES_DB") ?? "test_database")
7676
XCTAssertEqual(element.2, env("POSTGRES_USER") ?? "test_username")
7777

7878
XCTAssertEqual(element.8, query.sql)
@@ -106,8 +106,6 @@ final class AsyncPostgresConnectionTests: XCTestCase {
106106
} catch {
107107
guard let error = error as? PSQLError else { return XCTFail("Unexpected error type") }
108108

109-
print(error)
110-
111109
XCTAssertEqual(error.code, .server)
112110
XCTAssertEqual(error.serverInfo?[.severity], "ERROR")
113111
}

0 commit comments

Comments
 (0)