Skip to content

New perform with protocols and make the default client a concrete type#60

Merged
FranzBusch merged 11 commits intoapple:mainfrom
guoye-zhang:new-perform
Jan 28, 2026
Merged

New perform with protocols and make the default client a concrete type#60
FranzBusch merged 11 commits intoapple:mainfrom
guoye-zhang:new-perform

Conversation

@guoye-zhang
Copy link
Contributor

Update the design to the current proposal

@guoye-zhang guoye-zhang added the ⚠️ semver/major Breaks existing public API. label Jan 22, 2026
Copy link
Member

@FranzBusch FranzBusch left a comment

Choose a reason for hiding this comment

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

Can we add exhaustive public documentation to every method please?

body: consuming HTTPClientRequestBody<RequestWriter>? = nil,
options: RequestOptions = .init(),
responseHandler: (HTTPResponse, consuming ResponseConcludingReader) async throws -> Return,
configureOptions: (inout RequestOptions) -> Void = { _ in }
Copy link
Member

Choose a reason for hiding this comment

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

Do we really need this? Could we keep split adding this closure into a follow up PR to discuss it in isolation?

Copy link
Member

Choose a reason for hiding this comment

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

I'd be fine proposing this in a different PR, but it would be very handy to be able to adopt options inline with perform, and I'm in support of the convenience.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Removed for now

Comment on lines 36 to 37
url: URL,
headerFields: HTTPFields = [:],
Copy link
Member

Choose a reason for hiding this comment

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

This is surprising to me. Why is this not taking an HTTPRequest? AFAIK HTTPRequest has methods to convert from a URL.

Copy link
Member

Choose a reason for hiding this comment

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

Not a deal-breaker, but I could see this as a welcome convenience - I wonder how often people will actually realize they can initialize an HTTPRequest with just a URL if they're less familiar with HTTP.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We want HTTP.get(url) to work. Yes, it could take a request, but then there is not much difference from a regular perform.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Also request already has a method. What if there is a mismatch, which one do we respect?

Copy link
Member

Choose a reason for hiding this comment

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

@FranzBusch what do we do here, if the users set the http method POST on the passed in request? I'm with Guoye, that those convenience methods should bypass creating a request completely.

Copy link
Member

Choose a reason for hiding this comment

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

Can we just subset the URL Conveniences out into a separate PR. In general, there is too much happening at once in this PR.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I removed them

UnsupportedPlatformHTTPClient()
#endif
extension HTTPClientRequestBody where Writer: ~Copyable {
public init(_ data: Data) {
Copy link
Member

Choose a reason for hiding this comment

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

What if we make this take a RawSpan instead of Data? This would make it more generally useful.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We will have to make a copy since HTTPClientRequestBody needs to own the data

Copy link
Member

Choose a reason for hiding this comment

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

I am going to argue it has to anyways and Data is the wrong type right now since it requires a FoundationEssentials dependency. I would start off with Array<UInt8> and copy.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I removed this file for now since it's only used by the conveniences. Foundation team disliked our usage of [UInt8] in the previous iteration and told us that Data will be moved to the stdlib, so we should use Data.

self.writeBody = writeBody
}

public init<OtherWriter: ~Copyable>(
Copy link
Member

Choose a reason for hiding this comment

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

Why do we need this?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It is to convert HTTPRequestBody<ConcreteHTTPClient> to HTTPRequestBody<URLSessionHTTPClient> for our internal backend implementation use. I can make this package instead of public?

Copy link
Member

Choose a reason for hiding this comment

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

You can just write that in the internal backend as well right? This convenience conversion doesn't require to have internal knowledge. You can just wrap it in another closure. package is okay for now but once we split into separate packages you need to implement this with just the public API.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It does require internal knowledge since all the closures are private. I guess I could wrap the entire struct and call produce on it, but it would be less maintainable since it might not be updated when a new case is added.

If we can erase the generic from HTTPRequestBody, this workaround wouldn't be necessary.

Copy link
Member

Choose a reason for hiding this comment

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

Let's make it package and we can sort it out in a follow up

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

#endif

@available(macOS 26.2, iOS 26.2, watchOS 26.2, tvOS 26.2, visionOS 26.2, *)
public struct HTTPRequestOptions: HTTPRequestOptionsRedirectionHandler, HTTPRequestOptionsTLSVersion,
Copy link
Member

Choose a reason for hiding this comment

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

Should we nest this type in HTTPClient or rename it to HTTPClientHTTPRequestOptions?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This will be one of the most commonly used types, while the abstract protocol is only typed by backend implementation developers. I think we should rename the protocol instead?

//===----------------------------------------------------------------------===//

@available(macOS 26.2, iOS 26.2, watchOS 26.2, tvOS 26.2, visionOS 26.2, *)
public protocol HTTPRequestOptionsDeclarativePathSelection: HTTPAPIs.HTTPRequestOptions {
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
public protocol HTTPRequestOptionsDeclarativePathSelection: HTTPAPIs.HTTPRequestOptions {
public protocol DeclarativePathSelectionHTTPRequestOptions: HTTPAPIs.HTTPRequestOptions {

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Flipping the order makes them less discoverable. Currently they all share the same prefix and naturally group together. Maybe we could scope them instead?

Copy link
Contributor

Choose a reason for hiding this comment

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

In Swift it's more common to share the suffix. And yeah, some scoping could make sense here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I put all protocols in a scope HTTPClientCapability which also addressed the HTTPRequestOptions naming conflict

body: consuming HTTPClientRequestBody<RequestWriter>? = nil,
options: RequestOptions = .init(),
responseHandler: (HTTPResponse, consuming ResponseConcludingReader) async throws -> Return,
configureOptions: (inout RequestOptions) -> Void = { _ in }
Copy link
Member

Choose a reason for hiding this comment

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

I'd be fine proposing this in a different PR, but it would be very handy to be able to adopt options inline with perform, and I'm in support of the convenience.

Comment on lines 36 to 37
url: URL,
headerFields: HTTPFields = [:],
Copy link
Member

Choose a reason for hiding this comment

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

Not a deal-breaker, but I could see this as a welcome convenience - I wonder how often people will actually realize they can initialize an HTTPRequest with just a URL if they're less familiar with HTTP.


public func get(
url: URL,
headerFields: HTTPFields = [:],
Copy link
Member

Choose a reason for hiding this comment

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

Any reason we couldn't name this parameter to just headers?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

headers is not proper terminology. A header can contain multiple fields, so "headers" is ambiguous whether it's referring to fields or multiple request headers.

Copy link
Member

Choose a reason for hiding this comment

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

@guoye-zhang While I understand the reasoning here, I wonder if this makes more Catholic than the Pope? I assume for most users headerFields adds more confusion than just pure headers.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Well I actually initially named the property headers on HTTPRequest and HTTPResponse, and it was called out for using improper terminology during management review. We have precedence for using the IETF-recommended terminology even on the old NSURLRequest so we'd like to avoid backsliding.

https://github.com/apple/swift-http-types/blob/main/Sources/HTTPTypes/HTTPRequest.swift#L305
https://developer.apple.com/documentation/foundation/urlrequest/value(forhttpheaderfield:)


/// The namespace for all protocols defining HTTP client capabilities.
@available(macOS 26.2, iOS 26.2, watchOS 26.2, tvOS 26.2, visionOS 26.2, *)
public enum HTTPClientCapability {
Copy link
Member

Choose a reason for hiding this comment

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

Why do we need this namespace?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is from the earlier discussion: #60 (comment)

Copy link
Member

Choose a reason for hiding this comment

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

Hm, let's go ahead with this for now then.

Comment on lines 36 to 37
url: URL,
headerFields: HTTPFields = [:],
Copy link
Member

Choose a reason for hiding this comment

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

Can we just subset the URL Conveniences out into a separate PR. In general, there is too much happening at once in this PR.

UnsupportedPlatformHTTPClient()
#endif
extension HTTPClientRequestBody where Writer: ~Copyable {
public init(_ data: Data) {
Copy link
Member

Choose a reason for hiding this comment

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

I am going to argue it has to anyways and Data is the wrong type right now since it requires a FoundationEssentials dependency. I would start off with Array<UInt8> and copy.

self.writeBody = writeBody
}

public init<OtherWriter: ~Copyable>(
Copy link
Member

Choose a reason for hiding this comment

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

You can just write that in the internal backend as well right? This convenience conversion doesn't require to have internal knowledge. You can just wrap it in another closure. package is okay for now but once we split into separate packages you need to implement this with just the public API.

@FranzBusch FranzBusch merged commit 8e3b18e into apple:main Jan 28, 2026
15 of 21 checks passed
@guoye-zhang guoye-zhang deleted the new-perform branch January 28, 2026 20:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

⚠️ semver/major Breaks existing public API.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants