Skip to content

Commit b53e3fe

Browse files
Add client guidance for interceptors
1 parent 6e6a7a3 commit b53e3fe

4 files changed

Lines changed: 310 additions & 0 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"type": "documentation",
3+
"description": "Added client guidance for interceptors.",
4+
"pull_requests": []
5+
}

docs/source-2.0/guides/client-guidance/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,4 +61,5 @@ application-protocols/index
6161
context
6262
retries
6363
endpoints
64+
interceptors
6465
```
Lines changed: 303 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,303 @@
1+
(client-guidance-interceptors)=
2+
# Interceptors
3+
4+
An **interceptor** is a general-purpose extension point that allows code to
5+
observe or modify *specific* stages of a request execution: serialization,
6+
signing, transmission, deserialization, and so on. Interceptors are registered
7+
at client creation time or per operation invocation.
8+
9+
The other sections of this guide describe specific extension points for known
10+
use cases: retries, endpoint resolution, authentication, and transport. Those
11+
interfaces are designed to handle tasks that every client implementation will
12+
take on and which may need to be replaced or extended. In addition to those
13+
specific extension points, it is recommended to provide interceptors as a more
14+
general-purpose mechanism that allows users to implement a broader array of
15+
extensions.
16+
17+
## Hooks
18+
19+
Interceptors inject logic through methods that are called at specific points in
20+
the execution pipeline. These methods are called **hooks**.
21+
22+
Each hook provides either an **immutable** view of the current state or a
23+
**mutable** view that allows modifications. It is important to maintain this
24+
distinction, even in languages where immutability may not be enforceable. Hooks
25+
often come in pairs: the mutable hook is called first so that all interceptors
26+
can make their changes, then the immutable hook is called so that interceptors
27+
can see the finalized state after all mutations have been applied.
28+
29+
Where possible, it is important to limit what can be mutated in a mutable hook
30+
to only the properties that are relevant to that hook. This makes each hook
31+
easier to reason about, and helps to ensure that a change in execution order
32+
doesn't result in different behavior.
33+
34+
### Hook sequence
35+
36+
The following is an ordered list of recommended hooks. It is also recommended to
37+
make the list of hooks modifiable, so that new hooks may be added later.
38+
39+
:::{important}
40+
41+
In the following list, an **execution** is one entire end-to-end invocation of
42+
an operation. An **attempt** is a single try within that execution. There may be
43+
multiple attempts if the request needs to be retried.
44+
45+
The **transport request** and **transport response** represent the serialized
46+
requests and responses that are sent to and received from the service. For HTTP
47+
protocols, these are HTTP requests and HTTP responses.
48+
:::
49+
50+
1. **readBeforeExecution** *(immutable)* — The first thing called during an
51+
execution. This inspects the client state and the unmodified inputs to the
52+
operation.
53+
2. **modifyBeforeSerialization** *(mutable)* — Modifies the input before it is
54+
serialized.
55+
3. **readBeforeSerialization** *(immutable)* — Called immediately before the
56+
input is serialized.
57+
4. **readAfterSerialization** *(immutable)* — Called immediately after the input
58+
is serialized.
59+
5. **modifyBeforeRetryLoop** *(mutable)* — Modifies the transport request before
60+
the retry loop begins.
61+
6. *(retry loop)*
62+
1. **readBeforeAttempt** *(immutable)* — The first thing called inside the
63+
retry loop.
64+
2. **modifyBeforeSigning** *(mutable)* — Can modify the transport request
65+
before signing.
66+
3. **readBeforeSigning** *(immutable)* — Called immediately before signing.
67+
4. **readAfterSigning** *(immutable)* — Called immediately after signing.
68+
5. **modifyBeforeTransmit** *(mutable)* — Can modify the transport request
69+
before it is sent.
70+
6. **readBeforeTransmit** *(immutable)* — Called immediately before the
71+
request is sent.
72+
7. **readAfterTransmit** *(immutable)* — Called immediately after the
73+
transport response is received.
74+
8. **modifyBeforeDeserialization** *(mutable)* — Can modify the transport
75+
response before deserialization.
76+
9. **readBeforeDeserialization** *(immutable)* — Called immediately before
77+
deserialization.
78+
10. **readAfterDeserialization** *(immutable)* — Called immediately after
79+
deserialization.
80+
11. **modifyBeforeAttemptCompletion** *(mutable)* — Can modify the output or
81+
error before the attempt ends.
82+
12. **readAfterAttempt** *(immutable)* — The last thing called inside the
83+
retry loop.
84+
7. **modifyBeforeCompletion** *(mutable)* — Can modify the output or error
85+
before the execution ends.
86+
8. **readAfterExecution** *(immutable)* — The last thing called during an
87+
execution.
88+
89+
### Error behavior
90+
91+
Errors raised in hooks are handled consistently. The behavior depends on where
92+
in the pipeline the hook is called:
93+
94+
- **Hooks called once per execution** (`readBeforeExecution`,
95+
`readAfterExecution`): Errors are collected across all interceptors before any
96+
further action is taken. If multiple interceptors raise errors, the last error
97+
wins and earlier ones are logged and dropped.
98+
99+
- **Most other hooks**: An error immediately jumps execution to
100+
`modifyBeforeAttemptCompletion` (if inside the retry loop) or
101+
`modifyBeforeCompletion` (if outside), with the error set as the result.
102+
103+
- **`readAfterAttempt`**: Errors are collected the same way as
104+
`readBeforeExecution`. After all interceptors have been called, the
105+
[retry strategy](#client-guidance-retries) decides whether to retry or proceed
106+
to `modifyBeforeCompletion`.
107+
108+
## Interfaces
109+
110+
### Hook input types
111+
112+
Each hook receives a typed input object that contains only the data available at
113+
that stage of the pipeline, along with a [context object](#typed-context). Using
114+
typed inputs prevents interceptors from accidentally accessing data that doesn't
115+
exist yet (for example, trying to read the transport response before a request
116+
has been sent).
117+
118+
```java
119+
// Available from readBeforeExecution and modifyBeforeSerialization onward.
120+
// Always contains the operation input.
121+
public class InputHook<I, O> {
122+
public I input() { ... }
123+
public Context context() { ... }
124+
}
125+
126+
// Available from readAfterSerialization and modifyBeforeRetryLoop onward.
127+
// Adds the protocol-specific request.
128+
public class RequestHook<I, O, RequestT> extends InputHook<I, O> {
129+
public RequestT request() { ... }
130+
}
131+
132+
// Available from readAfterTransmit and modifyBeforeDeserialization onward.
133+
// Adds the protocol-specific response.
134+
public class ResponseHook<I, O, RequestT, ResponseT> extends RequestHook<I, O, RequestT> {
135+
public ResponseT response() { ... }
136+
}
137+
138+
// Available from readAfterDeserialization and modifyBeforeAttemptCompletion onward.
139+
// Adds the deserialized output (may be null if the attempt failed).
140+
public class OutputHook<I, O, RequestT, ResponseT> extends ResponseHook<I, O, RequestT, ResponseT> {
141+
public O output() { ... }
142+
}
143+
```
144+
145+
The context object on each hook is the same instance for the entire execution,
146+
so data stored in it by one hook is available to all subsequent hooks. See the
147+
[context guide](#typed-context) for details.
148+
149+
### Interceptor interface
150+
151+
It is highly recommended to design the interceptor interface so that
152+
implementations only need to override the hooks they care about. In Java, this
153+
means providing default no-op implementations for every method. Clients in other
154+
languages may prefer to use abstract classes or similar features.
155+
156+
Mutable hooks should always return the message, whether or not it was modified.
157+
If no modification is needed, they return the original value unchanged.
158+
159+
```java
160+
public interface Interceptor {
161+
162+
default void readBeforeExecution(InputHook<?, ?> hook) {}
163+
164+
default <I> I modifyBeforeSerialization(InputHook<I, ?> hook) {
165+
return hook.input();
166+
}
167+
168+
default void readBeforeSerialization(InputHook<?, ?> hook) {}
169+
170+
default void readAfterSerialization(RequestHook<?, ?, ?> hook) {}
171+
172+
default <RequestT> RequestT modifyBeforeRetryLoop(RequestHook<?, ?, RequestT> hook) {
173+
return hook.request();
174+
}
175+
176+
default void readBeforeAttempt(RequestHook<?, ?, ?> hook) {}
177+
178+
default <RequestT> RequestT modifyBeforeSigning(RequestHook<?, ?, RequestT> hook) {
179+
return hook.request();
180+
}
181+
182+
default void readBeforeSigning(RequestHook<?, ?, ?> hook) {}
183+
184+
default void readAfterSigning(RequestHook<?, ?, ?> hook) {}
185+
186+
default <RequestT> RequestT modifyBeforeTransmit(RequestHook<?, ?, RequestT> hook) {
187+
return hook.request();
188+
}
189+
190+
default void readBeforeTransmit(RequestHook<?, ?, ?> hook) {}
191+
192+
default void readAfterTransmit(ResponseHook<?, ?, ?, ?> hook) {}
193+
194+
default <ResponseT> ResponseT modifyBeforeDeserialization(ResponseHook<?, ?, ?, ResponseT> hook) {
195+
return hook.response();
196+
}
197+
198+
default void readBeforeDeserialization(ResponseHook<?, ?, ?, ?> hook) {}
199+
200+
default void readAfterDeserialization(OutputHook<?, ?, ?, ?> hook, RuntimeException error) {}
201+
202+
default <O> O modifyBeforeAttemptCompletion(OutputHook<?, O, ?, ?> hook, RuntimeException error) {
203+
return hook.forward(error);
204+
}
205+
206+
default void readAfterAttempt(OutputHook<?, ?, ?, ?> hook, RuntimeException error) {}
207+
208+
default <O> O modifyBeforeCompletion(OutputHook<?, O, ?, ?> hook, RuntimeException error) {
209+
return hook.forward(error);
210+
}
211+
212+
default void readAfterExecution(OutputHook<?, ?, ?, ?> hook, RuntimeException error) {}
213+
}
214+
```
215+
216+
## Example
217+
218+
The following interceptor adds a tracing header to HTTP requests when running
219+
inside AWS Lambda. It uses `modifyBeforeTransmit` because the header needs to be
220+
added to the transport request after signing. Adding it before signing would
221+
cause the signature to include the header. That would be fine, but this
222+
particular header is added after signing in practice.
223+
224+
```java
225+
public class AddTraceHeader implements ClientInterceptor {
226+
private final String traceId;
227+
228+
public AddTraceHeader(String traceId) {
229+
this.traceId = traceId;
230+
}
231+
232+
@Override
233+
public <RequestT> RequestT modifyBeforeTransmit(RequestHook<?, ?, RequestT> hook) {
234+
return hook.mapRequest(HttpRequest.class, h -> {
235+
if (h.request().headers().hasHeader("x-amzn-trace-id")) {
236+
return h.request();
237+
}
238+
return h.request().toBuilder()
239+
.withReplacedHeader("x-amzn-trace-id", List.of(traceId))
240+
.build();
241+
});
242+
}
243+
}
244+
```
245+
246+
`mapRequest` is a convenience method on `RequestHook` that applies a mapping
247+
function only if the request is of the expected type. This keeps the interceptor
248+
from failing if it is used with a non-HTTP protocol.
249+
250+
## Configuring interceptors
251+
252+
Interceptors should be configurable for the whole client, in which case they
253+
apply to every operation invocation made by that client. Interceptors configured
254+
this way can determine which operation is being executed based on the input if
255+
they need to apply to only a subset of operations.
256+
257+
```java
258+
MyServiceClient client = MyServiceClient.builder()
259+
.addInterceptor(new AddTraceHeader(traceId))
260+
.build();
261+
```
262+
263+
Interceptors should also be configurable for a single operation execution. This
264+
is particularly important for things like debugging or profiling specific parts
265+
of a code base.
266+
267+
```java
268+
client.getObject(GetObjectInput.builder()
269+
.bucket("example")
270+
.key("my-object")
271+
.addInterceptor(new AddTraceHeader(traceId))
272+
.build());
273+
```
274+
275+
### Execution order
276+
277+
When multiple interceptors are configured, they should be called in a
278+
deterministic order because the order they are called in can impact the
279+
execution of the operation.
280+
281+
The recommended ordering is:
282+
283+
1. Interceptors that are configured on the client by default. This includes
284+
interceptors that are added during code generation.
285+
2. Interceptors that are configured on the client which are not applied by
286+
default.
287+
3. Interceptors configured for a single operation execution.
288+
289+
## Why interceptors instead of middleware?
290+
291+
Middleware is a common pattern for building request pipelines, and it works well
292+
as an internal implementation strategy. As a public extension point, however, it
293+
has a significant drawback: middleware can modify control flow. A middleware
294+
component can wrap the next stage in its own retry loop, short-circuit the
295+
pipeline entirely, or call the next stage multiple times. This makes it
296+
impossible to reason about the behavior of the pipeline as a whole when
297+
third-party middleware is present.
298+
299+
Interceptors deliberately cannot modify control flow. They can observe and
300+
modify messages, but the pipeline itself always executes in the same order. This
301+
makes the behavior of the client predictable and easier to reason about. It also
302+
makes it safe to add new behavior to the pipeline without risking unexpected
303+
interactions with existing interceptors.

docs/source-2.0/guides/client-guidance/retries.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
(client-guidance-retries)=
12
# Retrying Requests
23

34
Operation requests might fail for a number of reasons that are unrelated to the

0 commit comments

Comments
 (0)