Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 98 additions & 0 deletions Scripts/check-upstream.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
#!/usr/bin/env bash
# Compares upstream swift-openapi-runtime and swift-openapi-urlsession files
# against their local counterparts, ignoring indentation-only differences.
#
# Usage: ./Scripts/check-upstream.sh
# Requires: gh (GitHub CLI), base64, diff

set -euo pipefail

TMPDIR_LOCAL=$(mktemp -d)
trap 'rm -rf "$TMPDIR_LOCAL"' EXIT

SOURCES="Sources/HTTPClient"

fetch() {
local repo="$1" remote_path="$2" local_path="$3" label="$4"
local upstream="$TMPDIR_LOCAL/$(basename "$remote_path")"
gh api "repos/$repo/contents/$remote_path" --jq '.content' | base64 -d > "$upstream"

# Strip import lines that differ only due to module name (OpenAPIRuntime vs HTTPTypes)
# and do an ignore-whitespace diff to surface only logic changes
if ! diff --ignore-all-space --ignore-blank-lines "$upstream" "$local_path" > /dev/null 2>&1; then
echo "=== $label ==="
diff --ignore-all-space --ignore-blank-lines "$upstream" "$local_path" || true
echo ""
else
echo "[ok] $label"
fi
}

echo "Checking swift-openapi-runtime files..."
fetch apple/swift-openapi-runtime \
Sources/OpenAPIRuntime/Interface/HTTPBody.swift \
"$SOURCES/Interface/HTTPBody.swift" \
"HTTPBody.swift"

fetch apple/swift-openapi-runtime \
Sources/OpenAPIRuntime/Interface/AsyncSequenceCommon.swift \
"$SOURCES/Interface/AsyncSequenceCommon.swift" \
"AsyncSequenceCommon.swift"

fetch apple/swift-openapi-runtime \
Sources/OpenAPIRuntime/Interface/ClientTransport.swift \
"$SOURCES/Interface/ClientTransport.swift" \
"ClientTransport.swift"

fetch apple/swift-openapi-runtime \
Sources/OpenAPIRuntime/Interface/CurrencyTypes.swift \
"$SOURCES/Interface/CurrencyTypes.swift" \
"CurrencyTypes.swift"

fetch apple/swift-openapi-runtime \
Sources/OpenAPIRuntime/Errors/ClientError.swift \
"$SOURCES/Errors/ClientError.swift" \
"ClientError.swift"

fetch apple/swift-openapi-runtime \
Sources/OpenAPIRuntime/Errors/RuntimeError.swift \
"$SOURCES/Errors/RuntimeError.swift" \
"RuntimeError.swift"

fetch apple/swift-openapi-runtime \
Sources/OpenAPIRuntime/Base/PrettyStringConvertible.swift \
"$SOURCES/Base/PrettyStringConvertible.swift" \
"PrettyStringConvertible.swift"

echo ""
echo "Checking swift-openapi-urlsession files..."

fetch apple/swift-openapi-urlsession \
Sources/OpenAPIURLSession/URLSessionTransport.swift \
"$SOURCES/HTTPClientFoundation/URLSessionTransport.swift" \
"URLSessionTransport.swift"

fetch apple/swift-openapi-urlsession \
Sources/OpenAPIURLSession/BufferedStream/BufferedStream.swift \
"$SOURCES/HTTPClientFoundation/BufferedStream/BufferedStream.swift" \
"BufferedStream.swift"

fetch apple/swift-openapi-urlsession \
Sources/OpenAPIURLSession/BufferedStream/Lock.swift \
"$SOURCES/HTTPClientFoundation/BufferedStream/Lock.swift" \
"Lock.swift"

fetch apple/swift-openapi-urlsession \
Sources/OpenAPIURLSession/URLSessionBidirectionalStreaming/BidirectionalStreamingURLSessionDelegate.swift \
"$SOURCES/HTTPClientFoundation/URLSessionBidirectionalStreaming/BidirectionalStreamingURLSessionDelegate.swift" \
"BidirectionalStreamingURLSessionDelegate.swift"

fetch apple/swift-openapi-urlsession \
Sources/OpenAPIURLSession/URLSessionBidirectionalStreaming/HTTPBodyOutputStreamBridge.swift \
"$SOURCES/HTTPClientFoundation/URLSessionBidirectionalStreaming/HTTPBodyOutputStreamBridge.swift" \
"HTTPBodyOutputStreamBridge.swift"

fetch apple/swift-openapi-urlsession \
Sources/OpenAPIURLSession/URLSessionBidirectionalStreaming/URLSession+Extensions.swift \
"$SOURCES/HTTPClientFoundation/URLSessionBidirectionalStreaming/URLSession+Extensions.swift" \
"URLSession+Extensions.swift"
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,10 @@ import HTTPTypes
responseContinuation?.resume(throwing: error)
responseContinuation = nil
}

// Propagate the error to the stream bridge.
requestStream?.cancel(error: error)
requestStream = nil
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,17 @@ import HTTPTypes
break
}
}

func cancel(error: (any Error)? = nil) {
debug("Output stream received cancellation request.")
Self.streamQueue.async { [self] in
switch state {
case .initial, .waitingForBytes, .haveBytes, .needBytes:
self.performAction(state.errorOccurred(error ?? CancellationError()))
case .closed: break
}
}
}
}

extension HTTPBodyOutputStreamBridge {
Expand Down Expand Up @@ -206,10 +217,7 @@ import HTTPTypes

mutating func errorOccurred(_ error: any Error) -> Action {
switch self {
case .initial:
self = .closed(error)
return .none
case .waitingForBytes(_):
case .initial, .waitingForBytes(_):
self = .closed(error)
return .closeStream
case .haveBytes(_, _, let producerContinuation):
Expand All @@ -218,7 +226,9 @@ import HTTPTypes
case .needBytes(_, let producerContinuation):
self = .closed(error)
return .cancelProducerAndCloseStream(producerContinuation)
case .closed: preconditionFailure("\(#function) called in invalid state: \(self)")
case .closed:
debug("Ignoring \(#function) event in closed state")
return .none
}
}

Expand All @@ -227,7 +237,10 @@ import HTTPTypes
case .waitingForBytes(_):
self = .closed(nil)
return .closeStream
case .initial, .haveBytes, .needBytes, .closed:
case .closed:
debug("Ignoring \(#function) event in closed state")
return .none
case .initial, .haveBytes, .needBytes:
preconditionFailure("\(#function) called in invalid state: \(self)")
}
}
Expand All @@ -243,7 +256,10 @@ import HTTPTypes
case .needBytes(_, let producerContinuation):
self = .closed(nil)
return .cancelProducerAndCloseStream(producerContinuation)
case .initial, .closed: preconditionFailure("\(#function) called in invalid state: \(self)")
case .closed:
debug("Ignoring \(#function) event in closed state")
return .none
case .initial: preconditionFailure("\(#function) called in invalid state: \(self)")
}
}

Expand Down
7 changes: 1 addition & 6 deletions Sources/HTTPClient/Interface/HTTPBody.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@

import struct Foundation.Data // only for convenience initializers
import protocol Foundation.LocalizedError
import class Foundation.NSLock

/// A body of an HTTP request or HTTP response.
///
Expand Down Expand Up @@ -142,11 +141,7 @@ public final class HTTPBody: @unchecked Sendable {
private let sequence: AnySequence<ByteChunk>

/// A lock for shared mutable state.
private let lock: NSLock = {
let lock = NSLock()
lock.name = "com.apple.swift-openapi-generator.runtime.body"
return lock
}()
private let lock = Lock()

/// A flag indicating whether an iterator has already been created.
private var locked_iteratorCreated: Bool = false
Expand Down
26 changes: 16 additions & 10 deletions Tests/HTTPClientTests/LoggingMiddlewareTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@ struct LoggingMiddlewareTests {
func clear() {
entries.removeAll()
}

func waitForEntries(count: Int) async {
while entries.count < count {
await Task.yield()
}
}
}

let collector: LogCollector
Expand Down Expand Up @@ -98,7 +104,7 @@ struct LoggingMiddlewareTests {
_ = try await client.send(request)

// Wait a bit for async logging to complete
try await Task.sleep(for: .milliseconds(100))
await collector.waitForEntries(count: 2)

let entries = await collector.getEntries()

Expand Down Expand Up @@ -135,7 +141,7 @@ struct LoggingMiddlewareTests {
let request = HTTPRequest(method: .post, url: serverURL.appending(path: "users"))
_ = try await client.send(request)

try await Task.sleep(for: .milliseconds(100))
await collector.waitForEntries(count: 2)

let entries = await collector.getEntries()
#expect(entries.count == 2)
Expand All @@ -162,7 +168,7 @@ struct LoggingMiddlewareTests {
let request = HTTPRequest(method: .get, url: serverURL.appending(path: "test"))
_ = try await client.send(request)

try await Task.sleep(for: .milliseconds(100))
await collector.waitForEntries(count: 2)

let entries = await collector.getEntries()
#expect(entries.count == 2)
Expand Down Expand Up @@ -190,7 +196,7 @@ struct LoggingMiddlewareTests {
let request = HTTPRequest(method: .get, url: serverURL.appending(path: "test"))
_ = try await client.send(request)

try await Task.sleep(for: .milliseconds(100))
await collector.waitForEntries(count: 2)

let entries = await collector.getEntries()
#expect(entries.count == 2)
Expand All @@ -215,7 +221,7 @@ struct LoggingMiddlewareTests {
let request = HTTPRequest(method: .get, url: serverURL.appending(path: "test"))
_ = try await client.send(request)

try await Task.sleep(for: .milliseconds(100))
await collector.waitForEntries(count: 2)

// Logs should be created
let entries = await collector.getEntries()
Expand Down Expand Up @@ -245,12 +251,12 @@ struct LoggingMiddlewareTests {
let request = HTTPRequest(method: method, url: serverURL.appending(path: "test"))
_ = try await client.send(request)

try await Task.sleep(for: .milliseconds(100))
await collector.waitForEntries(count: 2)

let entries = await collector.getEntries()
#expect(entries.count == 2)

let requestLog = entries[0]
let requestLog = try #require(entries.first(where: { $0.message.contains("⬆️") }))
#expect(requestLog.message.contains(method.rawValue))
}
}
Expand Down Expand Up @@ -278,7 +284,7 @@ struct LoggingMiddlewareTests {
let request = HTTPRequest(method: .get, url: serverURL.appending(path: "test"))
_ = try await client.send(request)

try await Task.sleep(for: .milliseconds(100))
await collector.waitForEntries(count: 2)

let entries = await collector.getEntries()
#expect(entries.count == 2)
Expand Down Expand Up @@ -320,7 +326,7 @@ struct LoggingMiddlewareTests {
let request = HTTPRequest(method: .get, url: serverURL.appending(path: "test"))
_ = try await client.send(request)

try await Task.sleep(for: .milliseconds(100))
await collector.waitForEntries(count: 2)

// Logging middleware should have logged
let entries = await collector.getEntries()
Expand Down Expand Up @@ -363,7 +369,7 @@ struct LoggingMiddlewareTests {
// Expected to throw
}

try await Task.sleep(for: .milliseconds(100))
await collector.waitForEntries(count: 1)

// Request should still be logged
let entries = await collector.getEntries()
Expand Down
Loading