Skip to content

Commit 6fd1122

Browse files
committed
Add missing WorkflowUpdateRPCTimeoutOrCanceledError error type
Adds `WorkflowUpdateRPCTimeoutOrCanceledError` for update poll timeouts, and add the `ApplicationError.category` field.
1 parent 1ae82a6 commit 6fd1122

File tree

5 files changed

+115
-3
lines changed

5 files changed

+115
-3
lines changed

Sources/Temporal/Client/WorkflowService/WorkflowService+Update.swift

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import SwiftProtobuf
1616

1717
public import struct GRPCCore.CallOptions
18+
import struct GRPCCore.RPCError
1819

1920
#if canImport(FoundationEssentials)
2021
public import FoundationEssentials
@@ -178,6 +179,12 @@ extension TemporalClient.WorkflowService {
178179
request: request,
179180
callOptions: callOptions
180181
)
182+
} catch let rpcError as RPCError
183+
where rpcError.code == .deadlineExceeded || rpcError.code == .cancelled
184+
{
185+
throw WorkflowUpdateRPCTimeoutOrCanceledError(cause: rpcError)
186+
} catch is CancellationError {
187+
throw WorkflowUpdateRPCTimeoutOrCanceledError()
181188
} catch {
182189
throw error
183190
}
@@ -287,8 +294,13 @@ extension TemporalClient.WorkflowService {
287294
break
288295
}
289296
}
297+
} catch let rpcError as RPCError
298+
where rpcError.code == .deadlineExceeded || rpcError.code == .cancelled
299+
{
300+
throw WorkflowUpdateRPCTimeoutOrCanceledError(cause: rpcError)
301+
} catch is CancellationError {
302+
throw WorkflowUpdateRPCTimeoutOrCanceledError()
290303
} catch {
291-
// TODO: We need to convert out deadline exceeds and cancels here
292304
throw error
293305
}
294306
}

Sources/Temporal/Converters/DefaultFailureConverter.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,8 @@ public struct DefaultFailureConverter: FailureConverter {
110110
details: application.details.payloads,
111111
type: application.type,
112112
isNonRetryable: application.nonRetryable,
113-
nextRetryDelay: .init(protobufDuration: application.nextRetryDelay)
113+
nextRetryDelay: .init(protobufDuration: application.nextRetryDelay),
114+
category: application.category
114115
)
115116
case .canceledFailureInfo(let cancelled):
116117
return CanceledError(
@@ -206,6 +207,7 @@ public struct DefaultFailureConverter: FailureConverter {
206207
if let nextRetryDelay = applicationError.nextRetryDelay {
207208
$0.nextRetryDelay = .init(duration: nextRetryDelay)
208209
}
210+
$0.category = applicationError.category
209211
}
210212
)
211213
case let cancelledError as CanceledError:

Sources/Temporal/Errors/Failures/ApplicationError.swift

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@ public struct ApplicationError: TemporalFailureError {
4141
/// Delay duration before the next retry attempt.
4242
public var nextRetryDelay: Duration?
4343

44+
/// The error category.
45+
public var category: Api.Enums.V1.ApplicationErrorCategory
46+
4447
/// Initializes a new application error.
4548
///
4649
/// - Parameters:
@@ -51,14 +54,16 @@ public struct ApplicationError: TemporalFailureError {
5154
/// - type: The string type of the error if any. Defaults to `nil`.
5255
/// - isNonRetryable: Boolean indicating wehter the error was set as non-retry. Defaults to `false`.
5356
/// - nextRetryDelay: Delay duration before the next retry attempt. Defaults to `nil`.
57+
/// - category: The error category. Defaults to `.unspecified`.
5458
public init(
5559
message: String,
5660
cause: (any Error)? = nil,
5761
stackTrace: String = "",
5862
details: [Api.Common.V1.Payload] = [],
5963
type: String? = nil,
6064
isNonRetryable: Bool = false,
61-
nextRetryDelay: Duration? = nil
65+
nextRetryDelay: Duration? = nil,
66+
category: Api.Enums.V1.ApplicationErrorCategory = .unspecified
6267
) {
6368
self.message = message
6469
self.cause = cause
@@ -67,5 +72,6 @@ public struct ApplicationError: TemporalFailureError {
6772
self.type = type
6873
self.isNonRetryable = isNonRetryable
6974
self.nextRetryDelay = nextRetryDelay
75+
self.category = category
7076
}
7177
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift Temporal SDK open source project
4+
//
5+
// Copyright (c) 2026 Apple Inc. and the Swift Temporal SDK project authors
6+
// Licensed under MIT License
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of Swift Temporal SDK project authors
10+
//
11+
// SPDX-License-Identifier: MIT
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
/// Error thrown when an update RPC call times out or is canceled.
16+
///
17+
/// This is not to be confused with an update itself timing out or being canceled,
18+
/// this is only related to the client call itself.
19+
public struct WorkflowUpdateRPCTimeoutOrCanceledError: TemporalError {
20+
/// The error's message.
21+
public var message: String
22+
23+
/// The cause of the current error.
24+
public var cause: (any Error)?
25+
26+
/// The stack trace of the current error.
27+
public var stackTrace: String
28+
29+
/// Creates a new workflow update RPC timeout or canceled error.
30+
///
31+
/// - Parameters:
32+
/// - cause: The underlying cause of the timeout or cancellation.
33+
/// - stackTrace: The stack trace at the point of failure.
34+
public init(
35+
cause: (any Error)? = nil,
36+
stackTrace: String = ""
37+
) {
38+
self.message = "Timeout or cancellation waiting for update"
39+
self.cause = cause
40+
self.stackTrace = stackTrace
41+
}
42+
}

Tests/TemporalTests/Converters/DefaultFailureConverterTests.swift

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -444,4 +444,54 @@ struct DefaultFailureConverterTests {
444444
#expect(convertedError.type == "TestError")
445445
#expect(convertedError.isNonRetryable == true)
446446
}
447+
448+
@Test
449+
func applicationErrorCategoryRoundTrips() async throws {
450+
let applicationError = ApplicationError(
451+
message: "Benign error",
452+
type: "SomeType",
453+
category: .benign
454+
)
455+
456+
let failureConverter = DefaultFailureConverter()
457+
let jsonPayloadConverter = JSONPayloadConverter()
458+
459+
let failure = failureConverter.convertError(
460+
applicationError,
461+
payloadConverter: jsonPayloadConverter
462+
)
463+
464+
let error = failureConverter.convertFailure(
465+
failure,
466+
payloadConverter: jsonPayloadConverter
467+
)
468+
469+
let convertedError = try #require(error as? ApplicationError)
470+
#expect(convertedError.message == "Benign error")
471+
#expect(convertedError.type == "SomeType")
472+
#expect(convertedError.category == .benign)
473+
}
474+
475+
@Test
476+
func applicationErrorCategoryDefaultsToUnspecified() async throws {
477+
let applicationError = ApplicationError(
478+
message: "Default category"
479+
)
480+
481+
let failureConverter = DefaultFailureConverter()
482+
let jsonPayloadConverter = JSONPayloadConverter()
483+
484+
let failure = failureConverter.convertError(
485+
applicationError,
486+
payloadConverter: jsonPayloadConverter
487+
)
488+
489+
let error = failureConverter.convertFailure(
490+
failure,
491+
payloadConverter: jsonPayloadConverter
492+
)
493+
494+
let convertedError = try #require(error as? ApplicationError)
495+
#expect(convertedError.category == .unspecified)
496+
}
447497
}

0 commit comments

Comments
 (0)