Skip to content
1 change: 1 addition & 0 deletions .changes/validation-logic
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
patch type="changed" "Minor validation logic improvements"
19 changes: 12 additions & 7 deletions Sources/LiveKit/Core/SignalClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -170,29 +170,34 @@ actor SignalClient: Loggable {
// Skip validation if user cancelled
if error is CancellationError {
await cleanUp(withError: error)
throw error
throw LiveKitError(.cancelled, internalError: error)
}

// Skip validation if reconnect mode
if reconnectMode != nil {
await cleanUp(withError: error)
throw error
throw LiveKitError(.network, internalError: error)
}

await cleanUp(withError: error)

// Validate...
// Attempt to validate with server
let validateUrl = try Utils.buildUrl(url,
connectOptions: connectOptions,
participantSid: participantSid,
adaptiveStream: adaptiveStream,
validate: true)

log("Validating with url: \(validateUrl)...")
let validationResponse = try await HTTP.requestValidation(from: validateUrl, token: token)
let validationResponse = await HTTP.requestValidation(from: validateUrl, token: token)
log("Validate response: \(validationResponse)")
// re-throw with validation response
throw LiveKitError(.network, message: "Validation response: \"\(validationResponse)\"")

if case let .invalid(message) = validationResponse {
// Re-throw with validation response
throw LiveKitError(.validation, message: message)
} else {
// Re-throw original error
throw LiveKitError(.network, internalError: error)
}
}
}

Expand Down
16 changes: 11 additions & 5 deletions Sources/LiveKit/Errors.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ public enum LiveKitErrorType: Int, Sendable {
case webRTC = 201

case network // Network issue
case validation // Network issue
Copy link
Contributor

@pblazej pblazej Oct 20, 2025

Choose a reason for hiding this comment

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

Nit: I'd rename that to authorization (or better?), as this is very generic (not sure about other SDKs).

Otherwise LGTM^2

Copy link
Member Author

Choose a reason for hiding this comment

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

I think we used to call this "validation", the endpoint (path is /validate)

pathSegments.append("validate")

So maybe validation is better ?

Copy link
Contributor

Choose a reason for hiding this comment

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

If that matches backend, then why not


// Server
case duplicateIdentity = 500
Expand Down Expand Up @@ -76,6 +77,8 @@ extension LiveKitErrorType: CustomStringConvertible {
"WebRTC error"
case .network:
"Network error"
case .validation:
"Validation error"
case .duplicateIdentity:
"Duplicate Participant identity"
case .serverShutdown:
Expand Down Expand Up @@ -111,26 +114,29 @@ extension LiveKitErrorType: CustomStringConvertible {
public class LiveKitError: NSError, @unchecked Sendable {
public let type: LiveKitErrorType
public let message: String?
public let underlyingError: Error?
public let internalError: Error?

override public var underlyingErrors: [Error] {
[underlyingError].compactMap { $0 }
[internalError].compactMap { $0 }
}

public init(_ type: LiveKitErrorType,
message: String? = nil,
internalError: Error? = nil)
{
func _computeDescription() -> String {
var suffix = ""
if let message {
return "\(String(describing: type))(\(message))"
suffix = "(\(message))"
} else if let internalError {
suffix = "(\(internalError.localizedDescription))"
}
return String(describing: type)
return String(describing: type) + suffix
}

self.type = type
self.message = message
underlyingError = internalError
self.internalError = internalError
super.init(domain: "io.livekit.swift-sdk",
code: type.rawValue,
userInfo: [NSLocalizedDescriptionKey: _computeDescription()])
Expand Down
52 changes: 38 additions & 14 deletions Sources/LiveKit/Support/HTTP.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,27 +16,51 @@

import Foundation

enum ServerValidationResponse {
case valid
case invalid(message: String)
// Network error etc.
case unknown(error: Error)
Copy link
Contributor

Choose a reason for hiding this comment

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

IMO either throws/Result/Error where throws should be prioritized

}

class HTTP: NSObject {
static let statusCodeOK = 200

private static let operationQueue = OperationQueue()

private static let session: URLSession = .init(configuration: .default,
delegate: nil,
delegateQueue: operationQueue)

static func requestValidation(from url: URL, token: String) async throws -> String {
// let data = try await requestData(from: url, token: token)
var request = URLRequest(url: url,
cachePolicy: .reloadIgnoringLocalAndRemoteCacheData,
timeoutInterval: .defaultHTTPConnect)
// Attach token to header
request.addValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
// Make the data request
let (data, _) = try await session.data(for: request)
// Convert to string
guard let string = String(data: data, encoding: .utf8) else {
throw LiveKitError(.failedToConvertData, message: "Failed to convert string")
}
static func requestValidation(from url: URL, token: String) async -> ServerValidationResponse {
do {
var request = URLRequest(url: url,
cachePolicy: .reloadIgnoringLocalAndRemoteCacheData,
timeoutInterval: .defaultHTTPConnect)
// Attach token to header
request.addValue("Bearer \(token)", forHTTPHeaderField: "Authorization")

// Make the data request
let (data, response) = try await session.data(for: request)

return string
// Print HTTP status code
guard let httpResponse = response as? HTTPURLResponse else {
throw URLError(.badServerResponse)
}

// Valid if 200
if httpResponse.statusCode == statusCodeOK {
return .valid
}

guard let string = String(data: data, encoding: .utf8) else {
throw URLError(.badServerResponse)
}

// Consider anything other than 200 invalid
return .invalid(message: string)
} catch {
return .unknown(error: error)
}
}
}
Loading