Skip to content

Add per-operation URLRequest overrides - #98

Open
alexisbronchart wants to merge 6 commits into
apple:mainfrom
alexisbronchart:feat/request-interceptor
Open

alexisbronchart wants to merge 6 commits into
apple:mainfrom
alexisbronchart:feat/request-interceptor

Conversation

@alexisbronchart

@alexisbronchart alexisbronchart commented Oct 8, 2026 •

Copy link
Copy Markdown

URLSessionTransport creates URLRequest values internally, so callers cannot configure Foundation-specific request properties such as assumesHTTP3Capable while retaining the standard transport.

Add URLSessionTransport.URLRequestOverrides with an optional assumesHTTP3Capable property, and an optional Configuration.requestOverrides closure that receives the OpenAPI operation ID and returns the overrides for that request. A nil property leaves the default URLRequest behavior unchanged.

Both buffered and streaming paths apply the overrides when constructing the URLRequest, before creating its task. Platform and OS availability checks stay inside the transport. Existing public initializers are preserved, with a new overload accepting the closure.

Validation: 104 tests executed, 1 skipped, 0 failures on macOS. The 31 affected tests passed again after the final test adjustments. Strict formatting lint and git diff --check passed.

@alexisbronchart alexisbronchart changed the title feat(transport): add optional request interception Add optional request interception Oct 8, 2026
@simonjbeaumont

Copy link
Copy Markdown
Collaborator

What are you trying to achieve here that cannot be achieved using ClientMiddleware from the swift-openapi-runtime library, which is the preferred and transport-independent approach?

@alexisbronchart

Copy link
Copy Markdown
Author

The idea is to configure transport-specific properties on the underlying URLRequest, such as assumesHTTP3Capable or allowsPersistentDNS.

Since ClientMiddleware is intentionally transport-independent, it doesn’t expose these Foundation-specific properties. That’s why I’m proposing an optional hook in URLSessionTransport, where the native request is created.

@simonjbeaumont

Copy link
Copy Markdown
Collaborator

That makes more sense. The example in the patch is about adding headers to requests, which is not transport-specific at all. I can see now in the original PR description you mentioned assumesHTTP3Capable, but it looks like that got removed when you scrubbed the agent cruft?

I'm unsure about this approach. We don't want to either (a) reimplement the middleware, just inside a transport, or (b) reimplement a subset of the URLSession delegate.

OOI, how much of what you're trying to do can be done with a delegate, if you had the ability to provide one? The transport is built using a delegate internally so might be challenging to compose, but I'm curious.

@alexisbronchart

Copy link
Copy Markdown
Author

I agree the example was totally confusing. I also put back the previous description.
I also agree we don't want to reimplement a middleware and the favored approach should stay using ClientMiddleware.

However this implementation is the only way I found to customize these transport-specific properties. The URLSession delegate does not seem to allow per-request modifications.

Another way would be exposing these in the URLSessionTransport.Configuration but that would mean losing per-request granularity.

@simonjbeaumont

Copy link
Copy Markdown
Collaborator

@guoye-zhang do you have an opinion on what would be the right layer to expose these kind of options? IIUC these options are per-request for folks using URLSession APIs directly with no URLSession configuration option exposed for them?

@guoye-zhang

Copy link
Copy Markdown
Contributor

Looks fine. Don’t think there’s anything better we can do.

@guoye-zhang guoye-zhang added the 🆕 semver/minor Adds new public API. label Oct 9, 2026

@simonjbeaumont simonjbeaumont left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Marking this as needs changes so it doesn't get merged as-is. Adding parameters to a public symbol is API breaking.

@alexisbronchart

Copy link
Copy Markdown
Author

Marking this as needs changes so it doesn't get merged as-is. Adding parameters to a public symbol is API breaking.

@simonjbeaumont do I need to make any changes? Or is this just a matter of versioning?

@simonjbeaumont

Copy link
Copy Markdown
Collaborator

Yes. We won't release a semver major version for this feature, so you'll need to make these changes in an API backwards compatible manner.

@simonjbeaumont

Copy link
Copy Markdown
Collaborator

The simplest change you can make is to just not change the public init. Users can then change the new public property after creation. Next up you provide an overload, and you can implement the old one in terms of the new one. The alternative approach, which we didn't do in this library is to not expose inits but expose a public static default: Self and have users start there and modify what they want. This allows for evolving the type without the headaches of more init overloads.

@alexisbronchart

Copy link
Copy Markdown
Author

@simonjbeaumont I went for the simple solution. If we ever need yet another param then I guess public static default: Self will make sense.

@simonjbeaumont

Copy link
Copy Markdown
Collaborator

First off, thanks for taking this on, and the third commit does address the API break.

I hadn't really spent much time reviewing the approach, just stepped in because I didn't want us to accidentally ship a break. Now I've had a closer look.

I'm sympathetic to the problem -- there are a few URLRequest properties that ClientMiddleware can't impact. But there aren't loads, and I'm not sure the inout URLRequest is the right solution for this narrow problem.

I'm concerned this will be pretty hard to use correctly in the proposed shape.

The request the closure sees isn't the same request in the two body processing modes. In .buffering it has httpBody populated; in .streaming it doesn't; and a write to httpBody in streaming mode is discarded, because uploadTask(withStreamedRequest:) ignores httpBody and httpBodyStream on the request it's handed.

Combined with .platformDefault (streaming on Darwin, buffering on Linux), the same closure has three behaviours depending on the platform and whether there's a body. The closure can't tell because Implementation and platformSupportsStreaming are deliberately internal.

Consider someone using this to write a Content-Digest header to a POST. On Linux, it would work, on macOS it would give the digest of the nothing. The tests in the PR don't show this because they are gated behind an expectsBufferedBody helper.

I also don't love presenting adopters with multiple ways of doing the same thing because it hands them the question of which to use, when, and why.

Most URLRequest options have URLSessionConfiguration settings too so can be set on the URLSession the adopter provides: e.g. cachePolicy, timeoutInterval, networkServiceType, allowsCellularAccess, allowsExpensiveNetworkAccess, allowsConstrainedNetworkAccess, allowsUltraConstrainedNetworkAccess, requiresDNSSECValidation, and httpShouldHandleCookies.

There's only a handful of URLRequest options that don't: assumesHTTP3Capable, attribution, allowsPersistentDNS, cookiePartitionIdentifier, and mainDocumentURL.

You mentioned the desire for assumesHTTP3Capable -- do you have a real use case for any of the others?

I'm wondering if we should instead allow adopters to return a value type per operation (aside: the client middleware already takes operation ID), e.g. something like this (name TBD):

public struct URLRequestOverrides: Sendable {
    /// Defers to the session configuration when `nil`.
    public var assumesHTTP3Capable: Bool?
    // ...
}

public var requestOptions: (@Sendable (_ operationID: String) -> URLRequestOverrides)? = nil

We sidestep the aforementioned issues because this closure can't operate on the body at all, and we can do all the platform dances we need (i.e. #if canImport(Darwin)) in the implementation, rather than also forcing this on the adopter.

There's no arbitrary escape-hatch, but I suspect that might be the right trade here.

Curious also to hear what @czechboy0 thinks.

@czechboy0

Copy link
Copy Markdown
Contributor

Fully behind @simonjbeaumont here - this Overrides struct approach feels much cleaner, more testable, and predictable across platforms.

@alexisbronchart

Copy link
Copy Markdown
Author

You mentioned the desire for assumesHTTP3Capable -- do you have a real use case for any of the others?

I don't. I actually wish assumesHTTP3Capable was available on URLSession.

In this new approach, do you mean to only expose properties that cannot be set on URLSession, or all of them?

I'm confused by “Defers to the session configuration when nil”, since, for instance, assumesHTTP3Capable isn't available on the session. I guess nil simply means leaving the default URLRequest behavior unchanged.

@simonjbeaumont

Copy link
Copy Markdown
Collaborator

You mentioned the desire for assumesHTTP3Capable -- do you have a real use case for any of the others?

I don't. I actually wish assumesHTTP3Capable was available on URLSession.

You should file a feedback request for it.

In this new approach, do you mean to only expose properties that cannot be set on URLSession, or all of them?

Only those that cannot be set today. And, actually, just the one you want. This is extensible so we can expand if/when there's a need. Or not, if no need arises.

I'm confused by “Defers to the session configuration when nil”, since, for instance, assumesHTTP3Capable isn't available on the session. I guess nil simply means leaving the default URLRequest behaviour unchanged.

Yeah, probably a better doc. But also calling it "overrides" makes it forward-thinking because if URLSession did this configuration at the session-level, it's clear this is the per-request override, rather than just a duplicate configuration place.

@alexisbronchart alexisbronchart changed the title Add optional request interception Add per-operation URLRequest overrides Oct 15, 2026
@alexisbronchart
alexisbronchart force-pushed the feat/request-interceptor branch from b11dad9 to 728cba1 Compare October 15, 2026 07:54
@alexisbronchart
alexisbronchart force-pushed the feat/request-interceptor branch from 728cba1 to d6d1abe Compare October 15, 2026 07:54
@alexisbronchart

Copy link
Copy Markdown
Author

Updated! Let me know if I got that correctly.
(I also opened FB25196909)

@simonjbeaumont simonjbeaumont left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This is starting to take shape. Thanks for working on it. Some additional feedback. One question that still needs some thinking about w.r.t. platform reachability too.

Comment on lines +163 to +164
/// Creates overrides that leave the default URLRequest behavior unchanged.
public init() {}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

So maintainers aren't tempted to extend this init when adding another property, I'd propose we actually don't have any public init for this type and instead off a public static var empty: Self { ... }. Happy to workshop the empty name but it'll be used like this:

(I'm also bundling in the nesting of this type and renaming it in the example):

var requestOverrides = URLSessionTransport.Configuration.RequestOverrides.empty
requestOverrides.assumesHTTP3Capable = true
// ...

This will be very easy to extend should we need to support other overrides.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I couldn't come up with anything better than empty so here goes.

Comment thread Sources/OpenAPIURLSession/URLSessionTransport.swift Outdated
Comment on lines +158 to +161
/// This override is applied on macOS 11.3, iOS 14.5, tvOS 14.5, watchOS 7.4,
/// and later, including Mac Catalyst and visionOS. It is ignored on other
/// platforms and earlier operating system versions.
public var assumesHTTP3Capable: Bool? = nil

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

These docs are a bit overbearing. Probably worth streamlining to just say that it will be enabled if the platform supports it.

I did check to see if we can do a cute hack here with @available that we've done in some other packages:

  public struct RequestOverrides: Sendable {
      private var _assumesHTTP3Capable: Bool?

      #if canImport(Darwin)
      @available(macOS 11.3, iOS 14.5, tvOS 14.5, watchOS 7.4, *)
      #else
      @available(*, unavailable, message: "This option is not supported on this platform")
      #endif
      public var assumesHTTP3Capable: Bool? {
          get { _assumesHTTP3Capable }
          set { _assumesHTTP3Capable = newValue }
      }
  }

But it means adopters have to start managing that in their projects.

@czechboy0 do you have a lean here? The choice is between a boolean that can be set on all platforms but is inert on some; or a boolean that can only be reached on platforms that support it, but with additional ceremony for adopters. WDYT?

Comment on lines +95 to +101
/// let configuration = URLSessionTransport.Configuration(requestOverrides: { operationID in
/// var overrides = URLSessionTransport.URLRequestOverrides()
/// if operationID == "getGreeting" {
/// overrides.assumesHTTP3Capable = true
/// }
/// return overrides
/// })

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

These docs should be removed or moved to the request overrides type.

If we go with .empty then we'll also want reworking to reflect that.

@@ -102,13 +139,31 @@ public struct URLSessionTransport: ClientTransport {

init(session: URLSession = .shared, implementation: Implementation = .platformDefault) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

What I'd usually expect is for everything to funnel through to the internal init that takes all the parameters so it's easy to audit. That will mean this one takes an optional, defaulting to nil, I think.

public init(
session: URLSession = .shared,
httpBodyProcessingMode: HTTPBodyProcessingMode = .platformDefault,
requestOverrides: @escaping @Sendable (_ operationID: String) -> URLRequestOverrides

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Should this be optional, to match the property it sets? As it stands there's no initialiser in the set that takes an optional, so anyone whose overrides are themselves configuration-driven has to construct and then mutate.

Suggested change
requestOverrides: @escaping @Sendable (_ operationID: String) -> URLRequestOverrides
requestOverrides: (@Sendable (_ operationID: String) -> RequestOverrides)? = nil

@simonjbeaumont simonjbeaumont left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This looks much better to me now. Let me kick off the CI. @czechboy0 did you wanna take a look at this as it's adding API or no?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🆕 semver/minor Adds new public API.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants