Skip to content

Commit 2ebf6e2

Browse files
authored
fix(gotrue): ignore 401 and 404 errors on sign out (#179)
* fi: ignore 401 and 404 errors on sign out * test(gotrue): mock APIClient * Move session check inside do/catch block * Fix code check
1 parent 8c7c257 commit 2ebf6e2

File tree

7 files changed

+289
-78
lines changed

7 files changed

+289
-78
lines changed
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<Scheme
3+
LastUpgradeVersion = "1510"
4+
version = "1.7">
5+
<BuildAction
6+
parallelizeBuildables = "YES"
7+
buildImplicitDependencies = "YES">
8+
<BuildActionEntries>
9+
<BuildActionEntry
10+
buildForTesting = "YES"
11+
buildForRunning = "YES"
12+
buildForProfiling = "YES"
13+
buildForArchiving = "YES"
14+
buildForAnalyzing = "YES">
15+
<BuildableReference
16+
BuildableIdentifier = "primary"
17+
BlueprintIdentifier = "79FEFFAB2B07873600D36347"
18+
BuildableName = "UserManagement.app"
19+
BlueprintName = "UserManagement"
20+
ReferencedContainer = "container:Examples.xcodeproj">
21+
</BuildableReference>
22+
</BuildActionEntry>
23+
</BuildActionEntries>
24+
</BuildAction>
25+
<TestAction
26+
buildConfiguration = "Debug"
27+
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
28+
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
29+
shouldUseLaunchSchemeArgsEnv = "YES"
30+
shouldAutocreateTestPlan = "YES">
31+
</TestAction>
32+
<LaunchAction
33+
buildConfiguration = "Debug"
34+
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
35+
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
36+
launchStyle = "0"
37+
useCustomWorkingDirectory = "NO"
38+
ignoresPersistentStateOnLaunch = "NO"
39+
debugDocumentVersioning = "YES"
40+
debugServiceExtension = "internal"
41+
allowLocationSimulation = "YES">
42+
<BuildableProductRunnable
43+
runnableDebuggingMode = "0">
44+
<BuildableReference
45+
BuildableIdentifier = "primary"
46+
BlueprintIdentifier = "79FEFFAB2B07873600D36347"
47+
BuildableName = "UserManagement.app"
48+
BlueprintName = "UserManagement"
49+
ReferencedContainer = "container:Examples.xcodeproj">
50+
</BuildableReference>
51+
</BuildableProductRunnable>
52+
</LaunchAction>
53+
<ProfileAction
54+
buildConfiguration = "Release"
55+
shouldUseLaunchSchemeArgsEnv = "YES"
56+
savedToolIdentifier = ""
57+
useCustomWorkingDirectory = "NO"
58+
debugDocumentVersioning = "YES">
59+
<BuildableProductRunnable
60+
runnableDebuggingMode = "0">
61+
<BuildableReference
62+
BuildableIdentifier = "primary"
63+
BlueprintIdentifier = "79FEFFAB2B07873600D36347"
64+
BuildableName = "UserManagement.app"
65+
BlueprintName = "UserManagement"
66+
ReferencedContainer = "container:Examples.xcodeproj">
67+
</BuildableReference>
68+
</BuildableProductRunnable>
69+
</ProfileAction>
70+
<AnalyzeAction
71+
buildConfiguration = "Debug">
72+
</AnalyzeAction>
73+
<ArchiveAction
74+
buildConfiguration = "Release"
75+
revealArchiveInOrganizer = "YES">
76+
</ArchiveAction>
77+
</Scheme>

Sources/GoTrue/GoTrueClient.swift

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ public actor GoTrueClient {
130130
/// - Parameters:
131131
/// - configuration: The client configuration.
132132
public init(configuration: Configuration) {
133-
let api = APIClient(http: HTTPClient(fetchHandler: configuration.fetch))
133+
let api = APIClient.live(http: HTTPClient(fetchHandler: configuration.fetch))
134134

135135
self.init(
136136
configuration: configuration,
@@ -347,7 +347,7 @@ public actor GoTrueClient {
347347

348348
let (codeChallenge, codeChallengeMethod) = prepareForPKCE()
349349

350-
try await api.execute(
350+
_ = try await api.execute(
351351
.init(
352352
path: "/otp",
353353
method: .post,
@@ -382,7 +382,7 @@ public actor GoTrueClient {
382382
captchaToken: String? = nil
383383
) async throws {
384384
await sessionManager.remove()
385-
try await api.execute(
385+
_ = try await api.execute(
386386
.init(
387387
path: "/otp",
388388
method: .post,
@@ -587,25 +587,36 @@ public actor GoTrueClient {
587587
}
588588

589589
/// Signs out the current user, if there is a logged in user.
590-
/// If using `SignOutScope.others` scope, no `AuthChangeEvent.signedOut` event is fired.
590+
///
591+
/// If using ``SignOutScope/others`` scope, no ``AuthChangeEvent/signedOut`` event is fired.
591592
/// - Parameter scope: Specifies which sessions should be logged out.
592-
public func signOut(scope: SignOutScope = .global) async throws {
593+
public func signOut(scope: SignOutScope = .global) async throws {
593594
do {
595+
// Make sure we have a valid session.
594596
_ = try await sessionManager.session()
597+
595598
try await api.authorizedExecute(
596599
.init(
597600
path: "/logout",
598601
method: .post,
599602
query: [URLQueryItem(name: "scope", value: scope.rawValue)]
600603
)
601604
)
602-
if scope != .others {
603-
await sessionManager.remove()
604-
eventEmitter.emit(.signedOut, session: nil)
605-
}
606605
} catch {
606+
// ignore 404s since user might not exist anymore
607+
// ignore 401s since an invalid or expired JWT should sign out the current session
608+
let ignoredCodes = Set([404, 401])
609+
610+
if case let GoTrueError.api(apiError) = error, let code = apiError.code,
611+
!ignoredCodes.contains(code)
612+
{
613+
throw error
614+
}
615+
}
616+
617+
if scope != .others {
618+
await sessionManager.remove()
607619
eventEmitter.emit(.signedOut, session: nil)
608-
throw error
609620
}
610621
}
611622

@@ -734,7 +745,7 @@ public actor GoTrueClient {
734745
) async throws {
735746
let (codeChallenge, codeChallengeMethod) = prepareForPKCE()
736747

737-
try await api.execute(
748+
_ = try await api.execute(
738749
.init(
739750
path: "/recover",
740751
method: .post,
Lines changed: 28 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,45 @@
11
import Foundation
22
@_spi(Internal) import _Helpers
33

4-
actor APIClient {
5-
private var configuration: GoTrueClient.Configuration {
6-
Dependencies.current.value!.configuration
7-
}
4+
struct APIClient: Sendable {
5+
var execute: @Sendable (_ request: Request) async throws -> Response
6+
}
87

9-
private var sessionManager: SessionManager {
10-
Dependencies.current.value!.sessionManager
11-
}
8+
extension APIClient {
9+
static func live(http: HTTPClient) -> Self {
10+
var configuration: GoTrueClient.Configuration {
11+
Dependencies.current.value!.configuration
12+
}
1213

13-
let http: HTTPClient
14+
return APIClient(
15+
execute: { request in
16+
var request = request
17+
request.headers.merge(configuration.headers) { r, _ in r }
1418

15-
init(http: HTTPClient) {
16-
self.http = http
19+
let response = try await http.fetch(request, baseURL: configuration.url)
20+
21+
guard (200 ..< 300).contains(response.statusCode) else {
22+
let apiError = try configuration.decoder.decode(
23+
GoTrueError.APIError.self,
24+
from: response.data
25+
)
26+
throw GoTrueError.api(apiError)
27+
}
28+
29+
return response
30+
}
31+
)
1732
}
33+
}
1834

35+
extension APIClient {
1936
@discardableResult
2037
func authorizedExecute(_ request: Request) async throws -> Response {
21-
let session = try await sessionManager.session()
38+
let session = try await Dependencies.current.value!.sessionManager.session()
2239

2340
var request = request
2441
request.headers["Authorization"] = "Bearer \(session.accessToken)"
2542

2643
return try await execute(request)
2744
}
28-
29-
@discardableResult
30-
func execute(_ request: Request) async throws -> Response {
31-
var request = request
32-
request.headers.merge(configuration.headers) { r, _ in r }
33-
34-
let response = try await http.fetch(request, baseURL: configuration.url)
35-
36-
guard (200 ..< 300).contains(response.statusCode) else {
37-
let apiError = try configuration.decoder.decode(
38-
GoTrueError.APIError.self,
39-
from: response.data
40-
)
41-
throw GoTrueError.api(apiError)
42-
}
43-
44-
return response
45-
}
4645
}

Sources/GoTrue/Types.swift

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -576,13 +576,15 @@ public struct AuthMFAGetAuthenticatorAssuranceLevelResponse: Decodable, Hashable
576576
public let currentAuthenticationMethods: [AMREntry]
577577
}
578578

579-
public enum SignOutScope: String {
580-
/// All sessions by this account will be signed out.
581-
case global
582-
/// Only this session will be signed out.
583-
case local
584-
/// All other sessions except the current one will be signed out. When using `others`, there is no `AuthChangeEvent.signedOut` event fired on the current session.
585-
case others
579+
public enum SignOutScope: String, Sendable {
580+
/// All sessions by this account will be signed out.
581+
case global
582+
/// Only this session will be signed out.
583+
case local
584+
/// All other sessions except the current one will be signed out. When using
585+
/// ``SignOutScope/others``, there is no ``AuthChangeEvent/signedOut`` event fired on the current
586+
/// session.
587+
case others
586588
}
587589

588590
// MARK: - Encodable & Decodable

0 commit comments

Comments
 (0)