Skip to content

Fix InterceptorChain modifications, error handling, and hook dispatch - #852

Merged
jonathan343 merged 3 commits into
developfrom
fix-interceptor-chain
Oct 15, 2026
Merged

jonathan343 merged 3 commits into
developfrom
fix-interceptor-chain

Conversation

@jonathan343

Copy link
Copy Markdown
Contributor

Summary

The InterceptorChain currently passes the original context to every interceptor, so a modify_* hook's result gets lost whenever another interceptor runs after it on the same hook. That includes the base Interceptor defaults, which returned the original value. Additionally, modify_before_completion incorrectly calls each interceptor's modify_before_attempt_completion. In 11 of the 12 read hooks, one raising interceptor kept every later interceptor from running. Separately, RequestPipeline serialized and resolved endpoints from call.input, so the request returned by modify_before_serialization never reached the wire.

This PR passes each interceptor's result to the next interceptor and makes modify_before_completion call its own hook. Every read hook now runs all interceptors before raising the last error, as the Smithy interceptor guide specifies for immutable hooks. Earlier errors are logged at debug level, and read_after_attempt passes each error on to later interceptors, as read_after_execution already did. The pipeline now uses the modified request for serialization, endpoint resolution, and the serialization error path. A modify_* hook that returns None now raises TypeError instead of passing None to the next interceptor.

Before / After

from typing import Any

from smithy_core.interceptors import Interceptor, InterceptorChain, RequestContext
from smithy_core.types import TypedProperties


class AddHeader(Interceptor[Any, Any, Any, Any]):
    def __init__(self, name: str) -> None:
        self.name = name

    def modify_before_signing(self, context: RequestContext[Any, Any]) -> Any:
        return {**context.transport_request, self.name: "1"}


class Fails(Interceptor[Any, Any, Any, Any]):
    def read_before_signing(self, context: RequestContext[Any, Any]) -> None:
        raise RuntimeError("first interceptor failed")


class Logs(Interceptor[Any, Any, Any, Any]):
    def read_before_signing(self, context: RequestContext[Any, Any]) -> None:
        print("second interceptor ran")


context = RequestContext(request=None, transport_request={}, properties=TypedProperties())

chain = InterceptorChain([AddHeader("x-a"), AddHeader("x-b"), Interceptor()])
print("headers:", chain.modify_before_signing(context))

try:
    InterceptorChain([Fails(), Logs()]).read_before_signing(context)
except RuntimeError as e:
    print("raised:", e)

Before

headers: {}
raised: first interceptor failed

After

headers: {'x-a': '1', 'x-b': '1'}
second interceptor ran
raised: first interceptor failed

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.

InterceptorChain called every interceptor's modify_* hook with the
original context and returned only the last interceptor's result. Any
interceptor after one that modified the request, response, or output
received the unmodified value, and returning it unchanged, as the base
Interceptor does, discarded the earlier modification. For example, a
user interceptor added through per-call plugins undid changes an
operation's default interceptor made in modify_before_signing.

Each interceptor now receives a context holding the value returned by
the interceptor before it. modify_before_completion also called each
interceptor's modify_before_attempt_completion hook instead of its
modify_before_completion hook.
When an interceptor raised in a read_* hook, InterceptorChain let the
exception propagate immediately, so the interceptors after it never saw
that stage. Read hooks can't change the request or response, so one
failing shouldn't keep the others from running.

Each read hook now runs on every interceptor, then the last exception is
raised and earlier ones are logged and dropped. read_after_execution
already ran every interceptor and now logs the exceptions it drops too.
modify_* hooks still stop at the first exception, since later
interceptors would receive a value that was never produced.
The pipeline serialized and resolved endpoints from call.input, so the
request returned by modify_before_serialization never reached the wire.
It now uses the modified request, including on the serialization error
path.

InterceptorChain now raises TypeError when a modify_* hook returns None,
passes errors to later interceptors in read_after_attempt as
read_after_execution does, logs dropped errors at debug, and reuses the
context when a hook returns its input unchanged. Read-hook docstrings
now describe running every interceptor before raising.
@jonathan343
jonathan343 requested a review from a team as a code owner October 15, 2026 17:11
@jonathan343 jonathan343 changed the title Fix interceptor modifications being discarded and read-hook errors skipping interceptors Fix InterceptorChain modifications, error handling, and hook dispatch Oct 15, 2026
@jonathan343
jonathan343 merged commit ea03527 into develop Oct 15, 2026
11 checks passed
@jonathan343
jonathan343 deleted the fix-interceptor-chain branch October 15, 2026 17:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants