Skip to content

Commit ac659eb

Browse files
committed
feat(plugin-oracle): show login handshake phase in connection-dropped diagnostic (#1746)
Claude-Session: https://claude.ai/code/session_01NVomHPErfCr8FWRVywmyVP
1 parent 6b54823 commit ac659eb

6 files changed

Lines changed: 69 additions & 14 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1717
### Changed
1818

1919
- The ER diagram now arranges tables in a compact layout that fills the canvas in both directions, keeps tables linked by foreign keys together, and tints each group of connected tables with its own header color. (#1755)
20+
- When an Oracle server closes the connection during login, the error dialog now shows which handshake phase it stopped at, so dropped-handshake failures (for example on Oracle 11g) are easier to pin down. (#1746)
2021

2122
### Fixed
2223

Plugins/OracleDriverPlugin/OracleConnection.swift

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ struct OracleError: Error {
2727
case protocolError
2828
case authVerifierUnsupported(flag: String)
2929
case authVersionNotSupported
30-
case authConnectionDropped
30+
case authConnectionDropped(phase: String?)
3131
}
3232

3333
let message: String
@@ -191,7 +191,8 @@ final class OracleConnectionWrapper: @unchecked Sendable {
191191
osLogger.debug("Connected to Oracle \(target)")
192192
} catch let sqlError as OracleSQLError {
193193
let detail = Self.connectFailureDetail(sqlError)
194-
osLogger.error("Oracle connection failed: \(detail)")
194+
let phase = sqlError.handshakePhase ?? "unknown"
195+
osLogger.error("Oracle connection failed at phase \(phase, privacy: .public) (\(sqlError.code.description, privacy: .public)): \(detail)")
195196
if let sslError = OracleSSLClassifier.classifySSLError(detail) {
196197
throw sslError
197198
}
@@ -215,16 +216,14 @@ final class OracleConnectionWrapper: @unchecked Sendable {
215216
}
216217

217218
private func classifyConnectError(_ error: OracleSQLError) -> OracleError.Category {
218-
let codeDescription = error.code.description
219-
if codeDescription.hasPrefix("unsupportedVerifierType") {
220-
return .authVerifierUnsupported(flag: codeDescription)
221-
}
222-
switch codeDescription {
223-
case "uncleanShutdown":
224-
return .authConnectionDropped
225-
case "serverVersionNotSupported":
219+
switch OracleConnectErrorClassifier.classify(error.code.description) {
220+
case .verifierUnsupported(let flag):
221+
return .authVerifierUnsupported(flag: flag)
222+
case .versionNotSupported:
226223
return .authVersionNotSupported
227-
default:
224+
case .connectionDropped:
225+
return .authConnectionDropped(phase: error.handshakePhase)
226+
case .connectionFailed:
228227
return .connectionFailed
229228
}
230229
}

Plugins/OracleDriverPlugin/OraclePlugin.swift

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -140,15 +140,17 @@ final class OraclePlugin: NSObject, TableProPlugin, DriverPlugin, PluginDiagnost
140140
],
141141
supportURL: issuesURL
142142
)
143-
case .authConnectionDropped:
143+
case .authConnectionDropped(let phase):
144144
return PluginDiagnostic(
145145
title: String(localized: "Connection Dropped During Handshake"),
146146
message: oracleError.message,
147147
suggestedActions: [
148148
String(localized: "Check for a firewall, VPN, or load balancer between you and the server that closes connections mid-handshake."),
149149
String(localized: "If the listener endpoint is TLS-only (TCPS), set the SSL mode in the connection's SSL settings."),
150-
String(localized: "Confirm the host and port reach the database listener directly, not a proxy that resets unknown traffic.")
150+
String(localized: "Confirm the host and port reach the database listener directly, not a proxy that resets unknown traffic."),
151+
String(localized: "If this is Oracle 11g, open an issue and include the handshake phase shown below.")
151152
],
153+
diagnosticInfo: phase.map { [DiagnosticEntry(label: String(localized: "Handshake phase"), value: $0)] } ?? [],
152154
supportURL: URL(string: "https://github.com/TableProApp/TablePro/issues/483")
153155
)
154156
case .authVersionNotSupported:
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import Foundation
2+
3+
public enum OracleConnectFailure: Sendable, Equatable {
4+
case verifierUnsupported(flag: String)
5+
case versionNotSupported
6+
case connectionDropped
7+
case connectionFailed
8+
}
9+
10+
public enum OracleConnectErrorClassifier {
11+
public static func classify(_ codeDescription: String) -> OracleConnectFailure {
12+
if codeDescription.hasPrefix("unsupportedVerifierType") {
13+
return .verifierUnsupported(flag: codeDescription)
14+
}
15+
switch codeDescription {
16+
case "uncleanShutdown":
17+
return .connectionDropped
18+
case "serverVersionNotSupported":
19+
return .versionNotSupported
20+
default:
21+
return .connectionFailed
22+
}
23+
}
24+
}

TableProTests/Plugins/OracleConnectionErrorTests.swift

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import Testing
21
import TableProPluginKit
2+
import Testing
33

44
@Suite("Oracle channel-fatal error classification")
55
struct OracleConnectionErrorTests {
@@ -17,3 +17,30 @@ struct OracleConnectionErrorTests {
1717
#expect(!OracleChannelFatalCode.isChannelFatal("malformedStatement"))
1818
}
1919
}
20+
21+
@Suite("Oracle connect error classification")
22+
struct OracleConnectErrorClassifierTests {
23+
@Test("An unclean shutdown is a dropped handshake")
24+
func uncleanShutdownIsDropped() {
25+
#expect(OracleConnectErrorClassifier.classify("uncleanShutdown") == .connectionDropped)
26+
}
27+
28+
@Test("An unsupported server version is reported as such")
29+
func serverVersionNotSupported() {
30+
#expect(OracleConnectErrorClassifier.classify("serverVersionNotSupported") == .versionNotSupported)
31+
}
32+
33+
@Test("An unsupported verifier carries its flag through")
34+
func verifierCarriesFlag() {
35+
#expect(
36+
OracleConnectErrorClassifier.classify("unsupportedVerifierType(0x939)")
37+
== .verifierUnsupported(flag: "unsupportedVerifierType(0x939)")
38+
)
39+
}
40+
41+
@Test("Any other code falls back to a generic connection failure")
42+
func unknownIsConnectionFailed() {
43+
#expect(OracleConnectErrorClassifier.classify("connectionError") == .connectionFailed)
44+
#expect(OracleConnectErrorClassifier.classify("server") == .connectionFailed)
45+
}
46+
}

docs/databases/oracle.mdx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,4 +148,6 @@ Columns with types not yet supported render as `<unsupported: type>` rather than
148148

149149
**Instant Client not found**: Download Basic package, extract to `/usr/local/oracle/instantclient`, set `DYLD_LIBRARY_PATH`
150150

151+
**Connection dropped during handshake**: The server closed the connection mid-login. The error dialog shows the handshake phase it stopped at (for example `dataTypeNegotiation` or `authentication`). Check for a firewall, VPN, or proxy that resets traffic, and confirm the host and port reach the listener directly. On Oracle 11g, open an issue and include the handshake phase.
152+
151153
**Limitations**: Username/password only, BFILE shows locator metadata only (content fetch via DBMS_LOB not supported), PL/SQL limited to anonymous blocks.

0 commit comments

Comments
 (0)