Skip to content

Socket.IO Client for Swift 17: native URLSession transport, Swift 6.4, JavaScript client parity - #1527

Open
kaeferfreund wants to merge 401 commits into
socketio:masterfrom
kaeferfreund:master
Open

kaeferfreund wants to merge 401 commits into
socketio:masterfrom
kaeferfreund:master

Conversation

@kaeferfreund

Copy link
Copy Markdown

Summary

This PR proposes the work from kaeferfreund/socket.io-client-swift (released there as 17.0.0 → 17.1.0) for upstream. It is a major, breaking release: 401 commits on top of master at 42da871, developed in 28 reviewed PRs on the fork.

The aim was to fix production problems we hit with 16.x and to make the Swift client behave like the JavaScript client (socket.io-client / engine.io-client). Behavior is checked against a pinned JS reference. Remaining API and platform differences are documented.

Highlights

Transport and toolchain

  • Starscream is replaced by Apple's URLSessionWebSocketTask plus URLSession long-polling. There are no third-party Swift dependencies.
  • Swift 6.4 toolchain and Swift 6 language mode. Minimum platforms: iOS 15, macOS 12, tvOS 15, watchOS 9.
  • Socket.IO 4 / Engine.IO 4 only.
  • Swift Package Manager is the only installation method. Carthage, CocoaPods and the Xcode project are removed.
  • Transport lists, tryAllTransports and rememberUpgrade. When a transport fails to open, the client can try the next configured one.

Client behavior aligned with JavaScript

  • Reconnection:
    • The client waits for the backoff before each attempt.
    • JS defaults: 1 s delay, 5 s max, factor 2, two-sided jitter.
    • JS event semantics for reconnect, reconnect_attempt, reconnect_error and reconnect_failed.
    • The manager closes when its last active socket disconnects.
  • Acknowledgements:
    • Default ack timeout and error-first acks.
    • Ordered retries across the callback, async and legacy ack APIs.
    • Cancellable async acks and per-socket timeout/retry overrides.
    • Pending acks are settled when the transport drops.
  • A connect_error client event and a per-socket connection timeout.
  • Buffering:
    • Emits made while disconnected are buffered instead of dropped.
    • Events received before the namespace connects are buffered in receive order.
  • Connection state recovery and per-connection dynamic auth.
  • Engine fixes:
    • The polling transport is paused before the WebSocket upgrade.
    • Polling POSTs stay under the server's maxPayload.
    • A t= cache-buster is added to polling requests.
    • Undecodable data closes the engine instead of being dropped silently.
  • Encoding:
    • Unencodable payloads throw a typed SocketPacketError instead of sending a different packet (2[]).
    • Date encodes like toISOString().
    • NaN and Infinity encode as null.

Upstream issues addressed

Compatibility and documentation

This is a breaking change for 16.x users:

  • Removed APIs:
    • SocketIOVersion / .version(...)
    • .compress, .selfSigned(...), .enableSOCKSProxy(...) and .useCustomEngine(...)
    • Obsolete dictionary keys now fail validation instead of being ignored.
  • Reconnect event payloads and timing changed, as described above.
  • Automatic cookie replay requires withCredentials(true).
  • Manager and sockets are not Sendable. Use them on their handleQueue.
  • A query string in the server URL is now sent, unless .connectParams replaces it (as in JS).

Migration and reference docs:

Deliberately unsupported:

  • WebTransport and its stream codec
  • Custom per-transport constructors
  • JS compression switches and Deflate thresholds
  • Documented deviations: JSON object keys are written in sorted order (a Swift Dictionary has no insertion order), and slashes are escaped in JSON output.

Before merging upstream, some fork-specific content needs changes:

  • The README describes this repository as a community fork and links to the fork's CI and badges.
  • The upstream triage report under Documentation/ is written in German.

I'm happy to adapt or drop these, or to split this PR into smaller pieces if the maintainers prefer that.

Validation

Everything runs on macOS CI (xcode-27 runner). The latest run on this exact head (2af783e) passed: https://github.com/kaeferfreund/socket.io-client-swift/actions/runs/35651443320

The CI pipeline covers:

  • swift build without third-party dependencies, plus an independent Swift Package consumer.
  • The full native test suite. It had 844 tests at 17.0.0; 17.1.0 added regression tests on top.
  • Real-server E2E tests against pinned Node Socket.IO fixtures, including TLS/mTLS with ephemeral private PKI and timeout fixtures.
  • The strict parity checker, run against the actual test log. All 195 applicable upstream JS runtime test declarations map to passing native assertions. The other 102 have documented API, platform or unsupported-feature reasons.
  • Thread Sanitizer, strict concurrency, package builds for the four Apple SDKs, and parser differential and wire-format checks.

Not covered: physical iOS/watchOS runtime checks (background/foreground, suspend, network changes). SDK compilation does not certify device behavior.

Preservation

  • Removed historical migration guides and generated 16.x API pages remain available in upstream history at 42da871.
  • Superseded review and audit notes are replaced by PARITY.md and the gated inventory. Evidence is kept under Documentation/ReviewEvidence/.
  • The MIT license is unchanged.

howardsun-tw and others added 30 commits April 24, 2026 17:27
…oot nsp

Same shape as the phase-6 fix: SocketTimedEmitterE2ETest connects to
the default '/' namespace, so the ping echo + never_ack handlers must
live there.
JS reference (socket.io-client/lib/socket.ts) calls destroy() — which
clears `this.subs` (=> active = false) — from three paths:
  1. user `socket.disconnect()`        (already handled, line 446)
  2. server-initiated DISCONNECT       (was missing — added)
  3. CONNECT_ERROR packet receipt      (was missing — added)

Swift only flipped `active = false` in case (1), leaving server-driven
termination paths with `active == true` indefinitely. The CHANGELOG
framing ("Survives engine-close + reconnect cycles") accurately covered
engine-close but glossed over the two server-driven destroy() paths.

Add the flips at the SocketPacket dispatch site, BEFORE invoking
didDisconnect / handleEvent, so user listeners observe `active = false`
when their .disconnect / .error callback fires (matches JS where destroy()
runs before super.emit).

Update the property docstring to enumerate all three termination paths.
Add two regression tests asserting active == false after handlePacket
of a .disconnect packet and a .error (CONNECT_ERROR) packet respectively.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The `fired: Bool` field was initialized false and never assigned true;
five comments across SocketAckManager.swift and SocketTimedEmitter.swift
described an atomic "checks fired -> flips fired = true -> removes
entry -> invokes callback" sequence that did not match the code.
Atomicity was actually upheld by `timedAcks[id] = nil` on the serial
handleQueue — only the first path to remove the entry sees a non-nil
result, every other path's lookup returns nil and short-circuits.

Replace the field + truthy-guard pattern with `removeValue(forKey:)`
on the four call sites (addTimedAck timer body, executeTimedAck,
cancelTimedAck, clearTimedAcks). Rewrite the comments to describe the
actual mechanism. No behavioral change — all 272 tests still pass,
including the Phase 9 race-stress suite (200 iters cancel-vs-timer /
cancel-vs-ack / timer-vs-ack).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Document v3-only gating, the socket.recovered flag, pid/offset capture
(including the 256-byte cap and oversized-offset drop), CONNECT payload
merge precedence, pre-ack replay buffering, and clearRecoveryState()
semantics (in-memory-only scope, identity-swap usage, Swift-only timed-ack
.disconnected side effect, subclass disconnect() ordering).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This fork has no CI history at all: the upstream workflow exists but never
ran, so nothing here has ever been built or tested outside the author's
machine. Before taking changes on top of that base we need a baseline run
of the untouched code, and a way to re-run it on demand.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
v3 runs on a Node runtime GitHub has already retired; runners warn about it
and will eventually stop scheduling it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Engine.IO requires the current transport to be paused before the upgrade
packet is sent. This client only waited for the outstanding long-poll and
ignored an in-flight POST, so the upgrade packet could overtake it. The
server then answers that late POST with HTTP 400 (TRANSPORT_MISMATCH),
the response is discarded because `polling` is already false, and the
packet is never resent because `postWait` is cleared when the POST is
created. The payload is lost without any error surfacing.

An app that emits a request with an ack right after connecting sees this
as a request that never gets acknowledged; if the lost packet is the
namespace CONNECT, no `connect`/auth event ever arrives.

The reference implementation waits for both the poll and the write to
settle before upgrading (`pause()` in engine.io-client's polling
transport, which resolves on `pollComplete` and `drain`). Mirror that:
`canSendUpgradePacket` requires a pending upgrade with no outstanding
poll and no outstanding POST, and whichever callback settles last
carries out the deferred upgrade.
`upgradeTransport()` sets `fastUpgrade` and only then enqueues its noop, so
`flushWaitingForPost()` marked the transport as writing while `doRequest`
refused to start that request (its guard excludes an upgrading transport).
Nothing could clear the flag, so the pause introduced in the previous commit
never resolved: neither callback upgraded or restarted the poll, leaving the
engine connected without an active transport.

Stop attempting polling writes once an upgrade is pending, which is what the
existing comment in `upgradeTransport()` already states. The queued packets
still leave over the WebSocket via `doFastUpgrade`.

Found by an automated correctness review of the previous commit.
A protocol-level script that drives a real Socket.IO 4.8 server the way the
engine does, once with the upgrade packet overtaking an in-flight POST and
once with the POST drained first. It fails unless the first ordering loses
the event (HTTP 400, no ack) and the second delivers it.

Node only, so it also runs where Starscream cannot be built, which is why it
is a script rather than another XCTest case.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The Swift job needs macOS because Starscream does not build elsewhere, so it
cannot cover the server side of this fix. The proof script is plain Node and
runs on Linux in seconds, which keeps the wire-level contract honest even if
the engine.io server changes its behaviour.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The macOS job spawns the fixture server from PATH, so its E2E results silently
depended on whichever Node the runner image shipped. Pin it, and move the proof
job from 22 to the current release so we learn early if the fixture stops
working on it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…efore-upgrade

fix(engine): pause the polling transport before upgrading to WebSocket
engine.io v4 servers advertise a `maxPayload` in the handshake and enforce it:
a POST above the limit is answered with HTTP 413 and every packet it carried is
discarded. The session stays open, so from the application's side the events
simply never happened.

This client batched the whole queue into one POST regardless. The reference
implementation cuts the batch at the limit and leaves the rest for the next
POST (`getWritablePackets()` in engine.io-client); mirror that. A packet larger
than the limit on its own is still sent, because it cannot be split and that is
what the reference client does too. Byte length is what counts, not character
count. engine.io v3 is untouched: those servers advertise no limit and use a
different payload format.

`resetEngine()` now drops whatever is still queued. Until now a POST always
drained the queue completely, so a new session could not inherit packets from
the old one; capping the batch can leave a remainder, and sending it under a
fresh sid would carry ack ids that server never issued.

Evidence: `Fixtures/max-payload-proof.mjs` shows a real Socket.IO 4.8 server
answering the oversized batch with 413 and delivering nothing, while the split
batch gets through and both events are acked. The batching rule itself is
pinned by eight SocketEngineTest cases, and an E2E test asserts the advertised
limit actually reaches the engine over a real handshake.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`SocketIOClient.manager` is weak, so the manager created inside the helper was
deallocated the moment it returned: the engine could not be reached and no emit
ever left. Both failures in the previous run came from that, not from the
batching change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
fix(engine): keep polling POSTs under the server's maxPayload
… them

JS holds an emit made while the socket is not connected in `sendBuffer` and
writes it on the next CONNECT. This client dropped it and reported "Tried
emitting when not connected" — the event was gone, and the only way to not
lose data was for every caller to implement its own queue.

Three details decide whether the buffer is coherent rather than merely present:

- The outgoing any-listeners fire when the packet is actually written, not when
  it is queued. That is where JS fires them (`emitBuffered()`), and it keeps the
  existing promise that they only ever see real sends.
- An ack whose packet is still buffered survives the disconnect. JS `_clearAcks`
  skips exactly those: the packet never reached the server, so the ack is still
  owed once the socket is back. Failing it with `.disconnected` while the packet
  is still queued for delivery would be a lie.
- A timed-out ack drops its packet from the buffer, as JS does in
  `_registerAckCallback`, so a reconnect does not deliver an emit whose caller
  already saw it fail.

Ack responses (`emitAck`) are left as they were: JS does not buffer them either,
and replaying one into a session the server no longer knows is meaningless.

This is a breaking change for callers that used the `.error` as a signal, and
it is in the CHANGELOG as one. The buffer has no upper bound, matching JS.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…veryState

The previous commit edited the wrong one of the two identical `clearTimedAcks`
call sites, so a disconnect still failed the acks of packets that had not been
sent, and CI caught it.

The two paths want opposite things. A disconnect keeps buffered emits, so their
acks are still owed. `clearRecoveryState()` is the identity-swap path: there the
buffer is dropped, because delivering the previous user's queued events into the
successor session would leak them across identities — and their acks are failed
along with them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
fix(client): buffer emits made while disconnected instead of dropping them
Nine scenarios taken verbatim from socket.io-client 4.8.3's test suite, run
against a fixture server extended to match `test/support/server.ts` (the `/no`
and `/with-data` middleware namespaces, `/foo` and `/asd`, and the `echo`
handler).

The point is to stop deciding what "parity" means by reading the source. These
tests encode the reference client's own expectations, so where this client
differs, CI says so instead of us guessing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A reconnect fires .connect again on a handler that is still registered, and
XCTest treats the second fulfill as an API violation — which aborts the whole
test process, so the run reported one failure and skipped the rest.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`_engineDidOpen` re-sent CONNECT for every socket still in `.connecting`. A
socket the server rejected stays in that state, so the client asked again on
every engine open — a server refusing an expired token was re-asked forever.

The rejoin now requires `socket.active`. That flag already goes false on
CONNECT_ERROR and on a user disconnect, with a comment stating the manager
would not auto-rejoin; nothing was reading it. JS destroys the socket in both
cases and waits for an explicit `connect()`.

Found by porting the JS client's own scenario, not by reading the source. That
is the point of PARITY.md, added here: all 116 scenarios of
socket.io-client 4.8.3's test suite with an honest status each, including the
42 that are still unassessed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The manager now only rejoins active sockets, and this test reaches .connecting
through setTestStatus instead of connect(), which is what sets the flag. A
setup artifact, not a behaviour change: production has no path to .connecting
that skips connect().

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
test: measure parity against the JS client's own test suite
`testSocketIdChangesOnReconnection` went red on master right after it was green
on the branch. The cause is the test, not the client: it dropped the engine with
`engine.disconnect(reason:)`, which is a clean client-initiated shutdown and
does not reliably lead to a reconnect. The JS tests use
`socket.io.engine.close()`, but the drop they mean to simulate is an unexpected
one, which here is `/admin/kill-transport` — what StateRecoveryE2ETest already
uses.

`testNoRejoinAfterAMiddlewareFailure` had the worse version of the same problem:
it asserts that something does *not* happen, so an engine that never came back
would have made it pass for the wrong reason. It now connects a second socket on
an accepted namespace, kills that transport, and asserts that socket reconnected
before checking the refused one was not rejoined.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…nnects

test(e2e): force reconnects from the server side, not the client
Nine more scenarios from socket.io-client 4.8.3: namespace joins after the
connection is up and after it was closed, the CONNECT-frame count when the same
namespace is fetched twice, socket ids on a custom namespace, a falsy payload
surviving the round trip, an IPv6 host surviving URL construction, and the two
that assert no error is reported — for a disconnect during open, and for a
transport dropping under an established connection.

The last one matters beyond parity: reporting a dropped transport as an error is
what makes an app tell the user the server is unreachable while it is merely
reconnecting.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Unassessed drops from 42 to 23. Nine scenarios ported, five reclassified as n/a
with a reason (the url.ts cases resolve against the browser's `location`, which
Swift has no equivalent of), and five turned out to be gaps worth naming: no
per-emit compress chain, no manager-level events, and Manager.socket() not
reopening a cached inactive socket.

What is left is almost entirely reconnection semantics.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
test: second batch of ported JS scenarios (42 → 23 unassessed)
Thirteen scenarios from socket.io-client 4.8.3, the reconnection group: manual
and automatic reconnects, a disconnect immediately followed by a connect,
stopping the loop on one namespace while another keeps going, attempt budgets
against a port nothing listens on, query-parameter encoding, binary ordering,
and the v2-server detection (a CONNECT without a payload).

The fixture server gains `/valid`, `/abc` (emits the handshake) and the
`abuff1`/`abuff2` ordering handlers, mirroring the JS support server.

Two of these are written to the JS contract knowing this client may not meet
it: the v2 detection, and exactly one `.disconnect` for a
`disconnect(); connect()` bounce. CI decides; a failure there is a finding.

Implementation by Muse Spark 1.3 via opencode, from a written brief; review,
verification and merge decisions by the orchestrating agent.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
On a `.three` manager a CONNECT packet whose payload carries no `sid` used to
connect the socket. The JS client recognises that shape as a Socket.IO v2
server, which a v3 client cannot talk to, and fires `connect_error` with a
message pointing at the migration guide. Now this client fires `.error` with
the same message and does not connect. `.two` managers are untouched: the v2
protocol carries no sid, and a test pins that exemption.

Found by porting the JS client's own scenario. Nothing else in the suite
injected a bare CONNECT, so no existing expectation changes.

Also corrects the one ported test that had encoded a wrong assumption from the
brief: Swift's `.reconnect` fires when reconnection *starts*
(`setReconnecting`), not on success as JS's `reconnect` does. A successful
reconnect here is another `.connect`, and the test now waits for that.

Implementation by Muse Spark 1.3 via opencode, from a written brief; review,
verification and merge decisions by the orchestrating agent.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
MonkeyMed and others added 25 commits September 19, 2026 13:12
Move release/parity internals one level down into Documentation/,
lead with 30s -> 5min -> deep-dive structure, document modern
ack/auth/volatile/recovery/transport APIs, drop Carthage/CocoaPods
install paths from primary README.
Release 17.0.0: finalize documentation and validate SPM installation
- Remove Cartfile, podspec, Xcode project and workspace metadata
- Rename CI framework-distribution job to native package platform builds
- Point docs, changelog and parity notes at SPM as the only install method
- Delete completed review-audit notes superseded by PARITY.md
Split all 24 substantive README sections into focused application/developer guides and retain the 43 original README anchors. Add documentation, API-source, scripts, contributor, release and evidence indexes. Preserve old Usage Docs byte-for-byte in a clearly marked archive and link already removed reports/plans to their original commit.

Correct stale release/parity references, add issue/PR templates and EditorConfig, and ignore local Python outputs. Run existing Swift jobs on feature-branch pushes without removing any checks; add an offline documentation checker with 17 regressions and compile the marked README example as an independent Swift Package consumer.

Source, Tests, Package.swift, LICENSE, CHANGELOG, contract/inventory data, existing scripts and all retained review evidence are unchanged. Verified the complete Git tree against local file hashes plus preserved baseline tree/blob IDs. No retired package manager or framework metadata restored.
Add a focused project header, navigation and branch-labeled badges.
Document concrete differences from the inspected official Swift client,
including migration trade-offs and the distinction from JavaScript parity.
Align the getting-started guide and retain existing README anchors and
its CI-compiled quick-start example. No library, test or license changes.
Recenter README and WhyThisFork on production bugs, the observed upstream
and Starscream maintenance gaps, JavaScript behavior alignment, missing
client features, and AI-assisted development backed by reproducible checks.

Record the 2026-09-19 maintenance review with pinned commits and releases,
include the reconnect semantics example, and preserve migration guidance,
README anchors, the compiled quick start, and all code and license files.
Add the prepared swallow SVG as a local documentation asset and display it
below the README title. Name and link daltoniam/Starscream consistently.
Preserve all existing examples, anchors, source code and license files.
Restore FinalParityAssertions doc, clarify physical-device scope
and deferred council review in Release17, drop stale
RemainingClientParity references from backlog CSV, fix native
distribution script macOS destination and scheme listing.
Put socketio/socket.io-client-swift and daltoniam/Starscream in matching
left-hand README table cells with separate linked names and descriptions.

Retain the locally restored final assertion report at its stable path,
label its historical scope without changing its body, and reconcile the
evidence and archive indexes. Preserve all changes from bf62914,
including the release clarifications, backlog cleanup and native build fix.
No source, test, script, image or license changes.
Pin all checkout, setup-node and upload-artifact v7 references to verified
upstream commit SHAs without changing job settings or permissions.

Exclude inline-code HTML from anchor collection while retaining heading
text and source line numbers. Remove exponential backtracking from the
Markdown destination pattern, preserving existing link and title handling.

Add seven focused regressions, including a subprocess deadline for long
unterminated destinations, and document all Python validator/test functions.
All 24 documentation tests pass locally. Library code, README, assets,
license, parity evidence and the existing native-build fix are unchanged.
Encode connect parameter keys and values with encodeURIComponent semantics
so Foundation accepts special characters in percentEncodedQuery.
Cancel per-socket connection deadlines after success, disconnection or
replacement so stale timers cannot interrupt reconnection.
Derive defaultSocket's namespace from the manager URL path while retaining
an independently configured transport endpoint.
Support client-certificate credentials for polling, WebSocket and upgrades,
scoped to the configured TLS origin and retaining server trust validation.

Add regression tests, mutual TLS fixtures and usage documentation.
Linux support is outside this change.

Validation: Swift syntax parsing, strict parity-contract checks, 132 query
encoding/round-trip cases and Node mTLS fixture checks passed locally.
Swift XCTest execution remains pending on macOS; the local Linux build is
blocked by existing Apple platform dependencies.

Fixes socketio#1421
Fixes socketio#887
Fixes socketio#1297
Fixes socketio#857
Fixes socketio#936
Fixes socketio#1157
Add requestTimeout in seconds for the initial handshake, subsequent polling
GETs, POSTs and retiring-session close requests. Apply explicit values to
both request idle and session resource timeouts so a slow response cannot
extend the configured total deadline by trickling bytes.

Reject nonpositive/nonfinite values and invalid dictionary types before
opening a transport. Preserve native defaults when omitted and leave
WebSocket requests unchanged. Document seconds versus JavaScript's
milliseconds and the native-default/disabled-timeout differences.

Add request propagation and validation tests plus real HTTP fixtures for
stalled handshake/GET/POST and trickling bounded/unbounded responses.

Validation: Swift syntax parse, strict parity contracts, diff checks and
isolated actual-source Foundation request configuration/timeout smoke test
passed. Full XCTest execution is pending on macOS; Linux compilation is
blocked by existing Apple dependencies.

Fixes socketio#681
Align legacy URL assertions with encodeURIComponent, include the configuration-error prefix and model WSS client authentication as an HTTPS protection space. Production code and security checks remain unchanged.

Refs socketio#1421, socketio#681, socketio#857, socketio#936, socketio#1157
docs: preserve upstream issue triage report and evaluation evidence
- Canonicalize client-cert challenge hosts via IDNA (case, Unicode/Punycode, trailing dot, IPv6 scopes)
- Reset clientCertificate and requestTimeout when a reused engine gets new config
- Emit disconnect on namespace connect timeout, keep buffered acks, let listener reconnects supersede the deadline
…timeout

fix: harden mTLS credential host matching and per-socket connect timeout cleanup
@kaeferfreund

Copy link
Copy Markdown
Author

@nuclearace

@nuclearace

Copy link
Copy Markdown
Member

Thank you! I'll review over the week

@thekhubaib

Copy link
Copy Markdown

Hi @kaeferfreund @nuclearace, thanks for this work! I really needed for an iOS app talking to a Socket.IO 4.8 server.

One request before this lands upstream: could Package.swift declare swift-tools-version:6.2 instead of 6.4?
Today the 6.4 manifest means only Xcode 27 can resolve the package. That locks out teams still supporting Xcode 26.x, including anyone on an Intel Mac, since Xcode 27 is Apple-silicon only. For the official client, that would cut off
every 26.x user at once, including me.
As far as I can tell, the language features the PR relies on are already in Swift 6.2: Swift 6 language mode, nonisolated(nonsending) (SE-0461) and sending results.
Happy to test a branch on Xcode 26.3 if that helps.

@kaeferfreund

Copy link
Copy Markdown
Author

Hi @thekhubaib, thanks for trying it out and for the clear request. That is a fair point about Xcode 26.x and Intel Macs.

I pushed a test branch that declares swift-tools-version:6.2:

.package(url: "https://github.com/kaeferfreund/socket.io-client-swift", branch: "compat/swift-tools-6.2")

Only the tools version changed (Package.swift plus the two consumer manifests our CI scripts generate); the sources are identical to 17.1.0. The full CI suite is green on that branch, but our CI only runs Xcode 27 / Swift 6.4, so that does not prove anything about a 6.2 compiler yet.

It would be great if you could take you up on the offer and check on Xcode 26.3:

@thekhubaib

Copy link
Copy Markdown

Thanks @kaeferfreund Tested compat/swift-tools-6.2 @ 803cc69 on Xcode 26.3 (17C529), Swift 6.2.4, Intel Mac (macOS 15):
swift build: ✅ clean
xcodebuild build -scheme SocketIO -destination 'generic/platform=iOS Simulator': ✅ clean
swift test: ✅ 884 tests, 0 failures, 0 skipped
cripts/check-strict-concurrency.sh: ✅ 0 warnings, baseline 0

So the sources compile and pass unchanged on 6.2. It would be great to see the 6.2 manifest in the upstream PR. That way the official client keeps working for everyone on Xcode 26.x. Happy to re-test anything else.

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.

5 participants