Skip to content

fix(functions): invalidate URLSession in streamed response - #1122

Open
AndroidPoet wants to merge 3 commits into
supabase:mainfrom
AndroidPoet:fix/functions-urlsession-invalidate
Open

fix(functions): invalidate URLSession in streamed response#1122
AndroidPoet wants to merge 3 commits into
supabase:mainfrom
AndroidPoet:fix/functions-urlsession-invalidate

Conversation

@AndroidPoet

Copy link
Copy Markdown
Contributor

What

_invokeWithStreamedResponse creates a per-call, delegate-backed URLSession. A delegate-backed URLSession keeps a strong reference to its delegate until the session is explicitly invalidated (Apple docs: "the session object keeps a strong reference to the delegate until your app exits or explicitly invalidates the session").

Today onTermination only cancels the task and holds _ = delegate, but never invalidates the session:

continuation.onTermination = { _ in
  task.cancel()
  // Hold a strong reference to delegate until continuation terminates.
  _ = delegate
}

So every streamed invocation leaks a URLSession and its StreamResponseDelegate (which retains the stream continuation) permanently — even after the caller stops consuming. Apps that open streaming edge functions repeatedly accumulate them unbounded.

Fix

Invalidate the session when the stream terminates. invalidateAndCancel() cancels the outstanding task and releases the session's reference to the delegate, so task.cancel() / the _ = delegate hack are no longer needed:

continuation.onTermination = { _ in
  session.invalidateAndCancel()
}

This mirrors the fix already merged for the Realtime web-socket session in #1080 (fix(realtime): invalidate URLSession when web socket closes).

Testing

Added testInvokeWithStreamedResponseInvalidatesSession, which weak-references the stream delegate and asserts it deallocates after the stream terminates. It fails on main (delegate leaks — assertion fails after the poll window) and passes with this change (delegate freed almost immediately). To observe the delegate's lifetime the streamed setup is factored into an internal streamResponse(_:options:) that also returns the delegate; the public _invokeWithStreamedResponse signature is unchanged.

  • swift test --filter FunctionsTests — 29 passing
  • ./scripts/format.sh — clean

The streamed-response URLSession is delegate-backed, so it retained its
StreamResponseDelegate (and transitively the stream continuation) until
invalidated. onTermination only cancelled the task and never invalidated
the session, leaking a session + delegate on every streamed invocation.
Invalidate the session on termination instead, which also cancels the
outstanding task.
@AndroidPoet
AndroidPoet requested review from a team and grdsdev as code owners July 13, 2026 11:06
Port the streamed-response regression test to Swift Testing, which main now
requires. Without the @test attribute the merged test would not have run.
Spell FunctionInvokeOptions explicitly at the call site, since contextual
.init() is now ambiguous between the disfavored overloads.
@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 3c5dedb4-a0aa-449f-8e43-663523ada0f1

📥 Commits

Reviewing files that changed from the base of the PR and between b118484 and 36cd42b.

📒 Files selected for processing (2)
  • Sources/Functions/FunctionsClient.swift
  • Tests/FunctionsTests/FunctionsClientTests.swift

📝 Walkthrough

Summary by CodeRabbit

  • Bug Fixes

    • Improved cleanup when streamed function responses finish or terminate.
    • Ensured associated network resources are properly released after streaming completes.
  • Tests

    • Added coverage verifying that streamed response resources are released within the expected timeframe.

Walkthrough

FunctionsClient now creates streamed function responses through a streamResponse helper that returns both the AsyncThrowingStream and its delegate. When streaming terminates, the owning URLSession is invalidated and cancelled instead of directly cancelling the data task. A new async test consumes the stream and verifies that the delegate is subsequently deallocated.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

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.

1 participant