Skip to content

Introduce Call.Decorator #8800

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 19 commits into
base: master
Choose a base branch
from
Open

Conversation

yschimke
Copy link
Collaborator

@yschimke yschimke commented May 26, 2025

Demonstrate a possible Call.Decorator API that handles some of the following cases, without forcing clients to deal exclusively with Call.Factory.

  • Intercept Call creation on the callers thread - for example associating trace information
  • Modifying the request - adding tags
  • Platform specific checks, such as Android Main thread or insecure URL checks
  • Switching between clients, such as Network Pinning

Docs

   * The equivalent of an Interceptor for [Call.Factory], but critically supported with an [OkHttpClient].
   *
   * While an [Interceptor] forms a chain as part of execution of a Call. Call.Decorator intercepts
   * [Call.Factory.newCall] with similar flexibility to Application [OkHttpClient.interceptors].
   *
   * More specifically, it may do any of
   * - Modify the request such as adding Tracing Context
   * - Wrap the [Call] returned
   * - Return some [Call] implementation that will immediately fail avoiding network calls based on network or authentication state.
   * - Redirect the [Call], such as using an alternative [Call.Factory].
   * - Defer execution, something not safe in an Interceptor.
   *
   * It should not throw an exception and instead return a Call that will fail on [Call.execute].
   *
   * This flexibility means that the app developer configuring the decorators on [OkHttpClient] must be responsible
   * for how these are composed in a chain.

Todo

  • Agree correct API
  • Add tests

@yschimke yschimke changed the title [WIP] Call.Decorator Call.Decorator Jul 13, 2025
@yschimke yschimke marked this pull request as ready for review July 19, 2025 12:49
@yschimke yschimke changed the title Call.Decorator Introduce Call.Decorator Jul 19, 2025
Copy link

@harrytmthy harrytmthy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @yschimke, I noticed the current API is a bit ambiguous about whether decorators are intended to be "wrappers" or can "short-circuit" the call. If one decorator short-circuits (i.e. returns a call without calling chain.newCall()), it effectively skips all decorators after it, and the rest of the chain is unaware.

Short-circuiting might be intentional in cases like:

if (noNetworkAvailable()) {
  return FailingCall("No network")
}
return chain.newCall(request)

This ambiguity could lead to some issues:

  • Ordering becomes critical and fragile: Decorators that must always run (e.g. security, metrics, tracing) need to be registered first, but this isn't obvious or enforced.
  • Invisible dependencies: One decorator might assume another has run (e.g. metrics before auth).
  • Debugging becomes tricky: "Why isn't tracing working?" → "Oh, the offline checker short-circuited the chain."

Would it be worth documenting this explicitly? Something like:

Decorators can optionally short-circuit the call chain. If so, be aware that any decorators added after them will not be invoked.

Alternatively, if short-circuiting is rare or discouraged, it might make sense to split the interface (e.g. CallWrapper vs CallInterceptor), though that might be overkill for now.

@yschimke
Copy link
Collaborator Author

@harrytmthy yep, definitely worth documenting.

Interceptors document exactly this, so I'll add.

In some ways this is the point, while interceptors surround the execution of the Call.

App interceptor - 0..n executions
Network interceptor - exactly 1 ( or failure)

This Call.Decorator are configured centrally in order to allow to short circuit, modify the original request or even redirect the call to another client instance.

if (index > callDecorators.lastIndex) {
RealCall(this@OkHttpClient, request, forWebSocket = false)
} else {
callDecorators[index++].newCall(this, request)
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This mutability might be an issue if someone implements a decorator that the chain twice.


override fun newCall(request: Request): Call =
if (index > callDecorators.lastIndex) {
RealCall(this@OkHttpClient, request, forWebSocket = false)
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should be some wrapper returned, that utilises the result of the first decorator.

then passes it through the interceptor chain.

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