Skip to content

Propagate URLSession errors to HTTPBodyOutputStreamBridge#94

Merged
guoye-zhang merged 1 commit into
apple:mainfrom
budde:stream-leak
Jun 19, 2026
Merged

Propagate URLSession errors to HTTPBodyOutputStreamBridge#94
guoye-zhang merged 1 commit into
apple:mainfrom
budde:stream-leak

Conversation

@budde

@budde budde commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

Motivation

Resolves leaks described in swift-openapi-generator#917 by explicitly passing URLSession errors on to HTTPBodyOutputStreamBridge for proper cleanup.

Modifications

Added a cancel(error) method to HTTPBodyOutputStreamBridge which BidirectionalStreamingURLSessionDelegate calls directly when handling a URLSession error.

Result

The outputStream is closed on URLSession errors and the writer Task is cancelled if it was running to avoid keeping a strong reference to self.

Test Plan

  • Additional unit tests added
  • Actual production code no longer exhibits
  • Reproduction code similar to example in swift-openapi-generator#917 does not leak

deinit {
debug("Output stream delegate deinit")
outputStream.delegate = nil
if outputStream.streamStatus != .notOpen && outputStream.streamStatus != .closed {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added this while debugging. Shouldn't really be necessary any more with the other changes but I think it's a good defensive check

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Most NSStream APIs are not thread-safe but deinit can run on any thread, so it is not safe to call its methods here.

@budde budde Jun 19, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Will just remove. Will have to drop the unit test for this too.

deinit {
debug("Output stream delegate deinit")
outputStream.delegate = nil
if outputStream.streamStatus != .notOpen && outputStream.streamStatus != .closed {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Most NSStream APIs are not thread-safe but deinit can run on any thread, so it is not safe to call its methods here.

@budde

budde commented Jun 19, 2026

Copy link
Copy Markdown
Contributor Author

@guoye-zhang Updated per your feedback

mutating func errorOccurred(_ error: any Error) -> Action {
switch self {
case .initial:
self = .closed(error)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Is this change necessary?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I believe it is. In the initializer, the output stream is opened and the state is set to initial. If URLSession receives an error before the state updates then the output stream is never closed.

@guoye-zhang guoye-zhang added the 🔨 semver/patch No public API change. label Jun 19, 2026
@guoye-zhang guoye-zhang merged commit d381557 into apple:main Jun 19, 2026
43 of 45 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🔨 semver/patch No public API change.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants