Skip to content

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 see Call.Decorator network pinning on Android #8376

Docs

   * The equivalent of an Interceptor for [Call.Factory], but supported directly within [OkHttpClient] newCall.
   *
   * An [Interceptor] forms a chain as part of execution of a Call. Instead, Call.Decorator intercepts
   * [Call.Factory.newCall] with similar flexibility to Application [OkHttpClient.interceptors].
   *
   * That is, 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, instead it should return a Call that will fail on [Call.execute].
   *
   * A Decorator that changes the OkHttpClient should typically retain later decorators in the new client.

@yschimke yschimke requested a review from swankjesse May 26, 2025 12:19
@yschimke yschimke added this to the 5.x milestone Jun 22, 2025
@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.

@yschimke
Copy link
Collaborator Author

yschimke commented Aug 1, 2025

@pyricau I'm thinking I might need access to the OkHttpClient in my decorator to spawn new derived instances.

Will the current API meet your needs?

@pyricau
Copy link
Member

pyricau commented Aug 1, 2025

@pyricau I'm thinking I might need access to the OkHttpClient in my decorator to spawn new derived instances.

Will the current API meet your needs?

It does yes, I only need access to the request.

Technically you can actually access the client, by forwarding newCall() then retrieving the client from RealCall.. not sure it's ideal though.

@yschimke
Copy link
Collaborator Author

yschimke commented Aug 1, 2025

@pyricau in my case, I need to fork off new instances of OkHttpClient, so can't let the chain proceed normally.

https://github.com/square/okhttp/pull/8376/files#diff-24d566d17e597cae3dc5320b45de67927c45576c79e80c069114a4d0969100e5

  fun OkHttpClient.Builder.withNetwork(network: Network?): OkHttpClient.Builder {
    return if (network == null) {
      dns(Dns.ANDROID)
        .socketFactory(SocketFactory.getDefault())
    } else {
      dns(Dns.forNetwork(network))
        .socketFactory(AndroidSocketFactory(network))
    }
  }

@yschimke
Copy link
Collaborator Author

yschimke commented Aug 2, 2025

@pyricau this is the case I want to get working #8376

So I'll introduce a Chain interface that maps more closely to the Interceptor Chain.

@yschimke
Copy link
Collaborator Author

yschimke commented Aug 2, 2025

@harrytmthy I think you were right, so I updated the docs to say

A Decorator that changes the OkHttpClient should typically retain later decorators in the new client.

example from #8376

  private fun OkHttpClient.withNetwork(network: Network): OkHttpClient =
    newBuilder()
      .dns(AndroidDns(network))
      .socketFactory(network.socketFactory)
      .apply {
        // Keep decorators after this one in the new client
        val indexOfThisDecorator = callDecorators.indexOf(this@AndroidNetworkPinning)
        callDecorators.subList(0, indexOfThisDecorator + 1).clear()
      }.build()

@yschimke yschimke force-pushed the factory_decorator branch from 5482806 to f39a193 Compare August 2, 2025 10:57
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.

4 participants