Skip to content

Commit

Permalink
Update Swift interceptor docs
Browse files Browse the repository at this point in the history
  • Loading branch information
rebello95 committed Oct 31, 2023
1 parent 5fe280b commit a00df5b
Showing 1 changed file with 65 additions and 65 deletions.
130 changes: 65 additions & 65 deletions docs/swift/interceptors.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,36 +4,40 @@ sidebar_position: 5
---

Interceptors are a powerful way to observe and mutate outbound and inbound
headers, data, trailers, and errors both for unary APIs and streams.
headers, data, trailers, typed messages, and errors both for unary APIs and streams.

Each interceptor is instantiated **once per request or stream** and
provides a set of closures that are invoked by the client during the lifecycle
of that call. Each closure allows the interceptor to observe and store
state, as well as to mutate outbound or inbound content.
provides a set of functions that are invoked by the client during the lifecycle
of that call. Each function allows the interceptor to observe and store
state, as well as to mutate outbound or inbound content. Interceptors have the ability to
interact with both typed messages (request messages prior to serialization and response
messages after deserialization) and raw data.

Every interceptor has the opportunity to perform asynchronous work before passing a potentially
altered value to the next interceptor in the chain. When the end of the chain is reached, the
final value is passed to the networking client, where it is sent to the server (outbound request)
or to the caller (inbound response).
final value is passed to the networking client, where it is sent to the server
(outbound request) or to the caller (inbound response).

Interceptors may also fail outbound requests before they are sent; subsequent
interceptors in the chain will not be invoked, and the error will be returned to the original caller.
interceptors in the chain will not be invoked, and the error will be returned to the
original caller.

Interceptors are closure-based and receive both the current value and a closure that
Interceptors receive both the current value and a closure that
should be called to resume the interceptor chain. Propagation will not continue until
this closure is invoked. Additional values may still be passed to a given interceptor even
though it has not yet continued the chain with a previous value. For example:

1. A request is sent
2. Response headers are received, and an interceptor pauses the chain while processing them
3. The first chunk of streamed response data is received, and the interceptor is invoked with this value
4. The interceptor is expected to resume with headers first, and then with data after
1. A request is sent.
2. Response headers are received, and an interceptor pauses the chain while processing them.
3. The first chunk of streamed response data is received, and the interceptor is invoked with
this value.
4. The interceptor is expected to resume with headers first, and then with data after.

Implementations should be thread-safe (hence the `Sendable` requirement on interceptor
closures), as closures can be invoked from different threads during the span of a request or
Implementations should be thread-safe (hence the `Sendable` requirements),
as functions can be invoked from different threads during the span of a request or
stream due to the asynchronous nature of other interceptors which may be present in the chain.

For example, here is an interceptor that adds an `Authorization` header to
As an example, here is an interceptor that adds an `Authorization` header to
all outbound requests that are destined for the `demo.connectrpc.com` host:

```swift
Expand All @@ -42,54 +46,50 @@ import Connect
/// Interceptor that asynchronously fetches an auth token and then adds an `Authorization`
/// header to outbound requests to `demo.connectrpc.com`. If the token fetch fails, it rejects
/// the outbound request and returns an error to the original caller.
final class ExampleAuthInterceptor: Interceptor {
init(config: ProtocolClientConfig) {}

func unaryFunction() -> UnaryFunction {
return UnaryFunction(
requestFunction: { request, proceed in
guard request.url.host == "demo.connectrpc.com" else {
// Allow the request to be sent as-is.
proceed(.success(request))
return
}

fetchUserToken(forPath: request.url.path) { token in
if let token = token {
// Alter the request's headers and pass the request on to other interceptors
// before eventually sending it to the server.
var headers = request.headers
headers["Authorization"] = ["Bearer \(token)"]
proceed(.success(HTTPRequest(
url: request.url,
contentType: request.contentType,
headers: headers,
message: request.message,
trailers: request.trailers
)))
} else {
// Reject the request since no valid token was available, and
// return an error to the caller.
proceed(.failure(ConnectError(
code: .unknown, message: "auth token fetch failed",
exception: nil, details: [], metadata: [:]
)))
}
}
},
responseFunction: { response, proceed in
// Can be used to read and/or alter the response.
proceed(response)
},
responseMetricsFunction: { metrics, proceed in
// Can be used to observe/track metrics.
proceed(metrics)
final class ExampleAuthInterceptor: UnaryInterceptor {
init(config: ProtocolClientConfig) { /* Optional setup */ }

@Sendable
func handleUnaryRequest<Message: ProtobufMessage>(
_ request: HTTPRequest<Message>,
proceed: @escaping @Sendable (Result<HTTPRequest<Message>, ConnectError>) -> Void)
{
guard request.url.host == "demo.connectrpc.com" else {
// Allow the request to be sent as-is.
proceed(.success(request))
return
}

fetchUserToken(forPath: request.url.path) { token in
if let token = token {
// Alter the request's headers and pass the request on to other interceptors
// before eventually sending it to the server.
var headers = request.headers
headers["Authorization"] = ["Bearer \(token)"]
proceed(.success(HTTPRequest(
url: request.url,
headers: headers,
message: request.message,
trailers: request.trailers
)))
} else {
// No valid token was available - reject the request and return
// an error to the caller.
proceed(.failure(ConnectError(
code: .unknown, message: "auth token fetch failed",
exception: nil, details: [], metadata: [:]
)))
}
)
}
}

func streamFunction() -> StreamFunction {
return StreamFunction(...)
@Sendable
func handleUnaryResponse<Message: ProtobufMessage>(
_ response: ResponseMessage<Message>,
proceed: @escaping @Sendable (ResponseMessage<Message>) -> Void
) {
// Can be used to observe/alter the response.
proceed(response)
}
}
```
Expand All @@ -104,7 +104,7 @@ let client = ProtocolClient(
networkProtocol: .connect,
codec: ProtoCodec(),
//highlight-next-line
interceptors: [{ ExampleAuthInterceptor(config: $0) }]
interceptors: [InterceptorFactory { ExampleAuthInterceptor(config: $0) }]
)
)
```
Expand All @@ -116,10 +116,10 @@ For example, if the following interceptors are registered:

```swift
interceptors: [
{ A(config: $0) },
{ B(config: $0) },
{ C(config: $0) },
{ D(config: $0) },
InterceptorFactory { A(config: $0) },
InterceptorFactory { B(config: $0) },
InterceptorFactory { C(config: $0) },
InterceptorFactory { D(config: $0) },
]
```

Expand Down

0 comments on commit a00df5b

Please sign in to comment.