fix: use dependency-injected transport in uploadAssetData() (#229)#230
fix: use dependency-injected transport in uploadAssetData() (#229)#230leogdion merged 12 commits into30-upload-assetsfrom
Conversation
Replace direct URLSession.shared usage with dependency-injected ClientTransport to enable mocking in tests and maintain architectural consistency. Changes: - Store transport property in MistKitClient - Refactor uploadAssetData() to use HTTPRequest/HTTPBody and transport.send() - Update test helpers to configure mock responses for uploadAssetData operationID - Make test helper methods async to support ResponseProvider configuration - Update test expectations to match proper error types and mock responses Verified on macOS, iOS Simulator, and Ubuntu (Docker). All 8 upload tests pass on all platforms. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the You can disable this status message by setting the
✨ Finishing touches🧪 Generate unit tests (beta)
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. Comment |
Replace direct ClientTransport storage with AssetUploader protocol abstraction. This provides better separation of concerns and allows custom upload implementations (progress tracking, retry logic, etc.) while maintaining backward compatibility. Addresses PR #230 feedback to use protocol-based abstraction. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
The previous refactor to use ClientTransport for asset uploads caused HTTP/2 connection reuse issues. When uploading to CloudKit's CDN (cvws.icloud-content.com), the transport tried to reuse the connection established for api.apple-cloudkit.com, resulting in 421 "Misdirected Request" errors. This change reverts to using URLSession.shared directly for CDN uploads while maintaining the AssetUploader protocol for dependency injection and testing. Also fixes FieldValue type field encoding - CloudKit API doesn't expect the type field in the request, only the value discriminator. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Refactors asset upload architecture to use URLSession directly instead of ClientTransport, maintaining injectability while avoiding HTTP/2 connection reuse issues between API and CDN hosts. Key changes: - Type field set to nil (CloudKit API requirement) * Experimentally verified that CloudKit rejects non-nil type fields * Updated Components+FieldValue to use type: nil for all conversions * CloudKit infers types from value structure, not explicit field - URLSession-based asset uploads * DefaultAssetUploader now accepts URLSession instead of ClientTransport * MistKitClient creates dedicated URLSession(configuration: .default) * Renamed ClientTransport+AssetUpload to URLSession+AssetUpload * Uses URLSession.data(for:) directly, avoiding transport layer - Benefits * Injectable architecture for testing (URLSession or MockAssetUploader) * Separate connection pools prevent HTTP/2 421 errors * Simpler implementation without OpenAPI transport complications * Maintains type: nil requirement for CloudKit API compliance - Test updates * Updated BasicTypes tests to expect type: nil * Some ComplexTypes, EdgeCases, Lists tests still need updates Verified with end-to-end asset upload test - both asset upload and record creation succeed. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
| @available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) | ||
| extension Components.Schemas.FieldValue { | ||
| /// Initialize from MistKit FieldValue | ||
| /// CloudKit API doesn't expect/accept the type field - it infers type from the value structure |
There was a problem hiding this comment.
shouldn't we have 2 separate types then if the json encoding is different
Replace protocol-based asset upload with flexible closure approach: - Add AssetUploadHandler typealias for closure-based uploads - Move JSON decoding from URLSession to CloudKitService layer - URLSession extension now returns raw HTTP response tuple - Remove assetUploader property from MistKitClient initializers - Add optional uploader parameter to uploadAssetData() and uploadAssets() - Deprecate AssetUploader protocol and implementations - Update tests to use mock uploader closures Benefits: - Per-call upload customization instead of client-level locking - Better separation of concerns (HTTP vs JSON decoding) - Simpler API with default URLSession.shared behavior - More flexible for testing and custom upload scenarios Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Pull Request Review: Asset Upload Dependency Injection RefactorThis PR successfully addresses the architectural issue identified in #229 where ✅ StrengthsArchitecture & Design
Testing
Code Quality
|
| /// - url: CloudKit CDN upload URL | ||
| /// - Returns: Tuple containing optional HTTP status code and response data | ||
| /// - Throws: Any error that occurs during upload | ||
| public typealias AssetUploadHandler = (Data, URL) async throws -> (statusCode: Int?, data: Data) |
| fieldName: String, | ||
| recordName: String? = nil | ||
| recordName: String? = nil, | ||
| uploader: AssetUploadHandler? = nil |
There was a problem hiding this comment.
add the argument name with uploader
| public func uploadAssetData( | ||
| _ data: Data, | ||
| to url: URL, | ||
| uploader: AssetUploadHandler? = nil |
The uploadAssetsAcceptsValidSizes() test was missing the uploader parameter, causing it to fall back to URLSession.shared instead of using the mock uploader. This resulted in HTTP 405 errors when the mock transport didn't recognize the CDN URL operations. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
|
Test comment |
Pull Request Review: Fix Asset Upload Dependency InjectionOverall AssessmentThis PR successfully addresses the issue of using hardcoded URLSession.shared in asset uploads by introducing a flexible closure-based approach. The evolution through multiple commits shows thoughtful iteration on the design. Code Quality & ArchitectureStrengths:
Architecture Concerns:
|
Potential Bugs & Issues
Performance & SecurityPerformance:
Security:
|
Test CoverageStrengths:
Gaps:
RecommendationsHigh Priority (before merge):
Medium Priority (follow-up PR):
Low Priority (nice to have):
|
ConclusionThis is a well-architected solution that successfully addresses the dependency injection issue while solving a subtle HTTP/2 connection reuse bug. The closure-based approach is more flexible than the initial protocol design. Recommendation: Approve with minor changes
The code demonstrates strong engineering judgment through iterative refinement. Testing Verification: Review performed following MistKit's CLAUDE.md conventions and Swift best practices |
CloudKit API has asymmetric behavior where requests omit the type field but responses may include it. This change models that accurately: - Split FieldValue schema into FieldValueRequest and FieldValueResponse - Update all conversion code to use appropriate types for requests/responses - Remove CustomFieldValue type override in favor of generated types - Add FieldValue architecture documentation to CLAUDE.md - Remove unused Swift 6.2 nightly devcontainer configuration Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## 30-upload-assets #230 +/- ##
===================================================
Coverage ? 17.89%
===================================================
Files ? 74
Lines ? 7829
Branches ? 0
===================================================
Hits ? 1401
Misses ? 6428
Partials ? 0
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
Replace #if os(Linux) conditionals with #if canImport(FoundationNetworking) to improve cross-platform compatibility. This change detects module availability rather than assuming OS-specific behavior and works on Linux, WASI, and other non-Darwin platforms. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Code Review: Fix uploadAssetData() to use dependency-injected transportSummaryThis PR successfully refactors the asset upload functionality to use dependency-injected ✅ StrengthsArchitecture & Design
Test Coverage
|
…bels Address PR #230 review feedback to improve API clarity. Rename the closure type to AssetUploader, add 'using' argument labels to upload methods, and remove deprecated protocol-based implementations. Document the URLSession vs ClientTransport design decision to help future maintainers understand the HTTP/2 connection pooling considerations. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Pull Request Review: Dependency-Injected Transport for Asset UploadsSummaryThis PR successfully refactors asset uploads to use dependency injection, enabling better testability while maintaining the architectural separation between CloudKit API and CDN uploads. The implementation is well-documented and thoroughly tested. ✅ Strengths1. Architectural Documentation ⭐The CLAUDE.md additions clearly explain the design rationale:
Location: CLAUDE.md:183-203 2. Clean Dependency Injection PatternThe public typealias AssetUploader = (Data, URL) async throws -> (statusCode: Int?, data: Data)
Location: Sources/MistKit/Core/AssetUploader.swift:20 3. OpenAPI Schema ImprovementsThe separation of
Location: openapi.yaml:867-926 4. Comprehensive Test CoverageTests cover all critical scenarios:
Location: Tests/MistKitTests/Service/CloudKitServiceUploadTests*.swift 5. Error HandlingWell-structured error handling with proper error type mapping:
Location: Sources/MistKit/Service/CloudKitService+WriteOperations.swift:374-445 🔍 Areas for Consideration1. Debug Print Statements in Production CodeThe // Debug: Print the operations being sent
print("\n[DEBUG] Sending \(apiOperations.count) operation(s) to CloudKit:")
for (index, operation) in apiOperations.enumerated() {
print("[DEBUG] Operation \(index):")
// ... more debug prints
}Recommendation: Use the existing #if DEBUG
MistKitLogger.logDebug(
"Sending \(apiOperations.count) operation(s) to CloudKit",
logger: MistKitLogger.api,
shouldRedact: false
)
#endifLocation: Sources/MistKit/Service/CloudKitService+WriteOperations.swift:69-84 2. Force Unwrap in Test HelperThe mock asset uploader uses a force unwrap: return (200, response.data(using: .utf8)\!)Recommendation: While this is test code and the string is guaranteed to be valid UTF-8, consider using a guard for consistency: guard let data = response.data(using: .utf8) else {
fatalError("Test JSON is not valid UTF-8")
}
return (200, data)Location: Tests/MistKitTests/Service/CloudKitServiceUploadTests+Helpers.swift:55 3. HTTP Status Code Type InconsistencyThe guard let httpStatusCode = statusCode, (200...299).contains(httpStatusCode) else {
throw CloudKitError.httpError(statusCode: statusCode ?? 0)
}Consideration: The error case uses guard let httpStatusCode = statusCode, (200...299).contains(httpStatusCode) else {
if let code = statusCode {
throw CloudKitError.httpError(statusCode: code)
} else {
throw CloudKitError.httpErrorWithRawResponse(
statusCode: -1,
rawResponse: "No HTTP status code received from CDN"
)
}
}Location: Sources/MistKit/Service/CloudKitService+WriteOperations.swift:396-398 4. WASI Error Message ClarityThe WASI error message could be more informative: throw CloudKitError.httpErrorWithRawResponse(
statusCode: 501,
rawResponse: "Asset uploads not supported on WASI"
)Recommendation: Add guidance: rawResponse: "Asset uploads require URLSession which is not available on WASI. Consider using a custom AssetUploader implementation for WASI platforms."Location: Sources/MistKit/Service/CloudKitService+WriteOperations.swift:382-386 🛡️ Security & Performance✅ Security
✅ Performance
📊 Test CoverageAll 8 upload tests pass on macOS, iOS Simulator, and Ubuntu:
📝 Code Quality
🎯 RecommendationApprove with minor suggestions. The PR achieves its goals effectively:
Suggested follow-up (non-blocking):
Great work on the architectural documentation and test coverage! The design rationale in CLAUDE.md is particularly valuable for future maintainers. |
Pull Request Review: #230Critical Issues - Do NOT MergeAfter reviewing PR #230 against CLAUDE.md and codebase architecture, I found several critical issues that block merging. ❌ Critical Issue #1: Architectural ContradictionSEVERITY: BLOCKING The CLAUDE.md (lines 183-203) explicitly documents why URLSession.shared should be used directly for CDN uploads to avoid HTTP/2 connection reuse issues between api.apple-cloudkit.com and cvws.icloud-content.com. This PR allows injection of ClientTransport which could share HTTP/2 connections, triggering 421 Misdirected Request errors. Files: Sources/MistKit/Service/CloudKitService+WriteOperations.swift:382-390 ❌ Critical Issue #2: Debug Print StatementsSEVERITY: HIGH Location: Sources/MistKit/Service/CloudKitService+WriteOperations.swift:68-84 Raw print() statements will spam production console. Should use MistKitLogger.logDebug(). ❌ Critical Issue #3: WASI Compilation FailureSEVERITY: BLOCKING URLSession+AssetUpload extension only compiled for non-WASI, but CloudKitService+WriteOperations.swift:388 references URLSession.shared.upload() unconditionally. This will fail to compile on WASI.
|
865248d to
2d0d937
Compare
Pull Request Review: #230🔴 Critical Issue FoundThis PR directly contradicts the documented architectural design in CLAUDE.md (lines 183-203). The documentation explicitly states that asset uploads should use The ProblemFrom CLAUDE.md:
This PR removes this protection. While the default implementation still uses ImpactBefore: // Direct URLSession usage - separate connection pool guaranteed
let (responseData, response) = try await URLSession.shared.data(for: request)After: // Uses AssetUploader abstraction - can accidentally share connection pool
let uploadHandler = uploader ?? { data, url in
return try await URLSession.shared.upload(data, to: url)
}The problems:
Detailed Review✅ Code Quality Strengths
🟡 Issues Found1. Status Code Handling (CloudKitService+WriteOperations.swift:396) guard let httpStatusCode = statusCode, (200...299).contains(httpStatusCode) else {
throw CloudKitError.httpError(statusCode: statusCode ?? 0)
}Issue: If an uploader returns Fix: Handle nil explicitly: guard let httpStatusCode = statusCode else {
throw CloudKitError.invalidResponse
}
guard (200...299).contains(httpStatusCode) else {
throw CloudKitError.httpError(statusCode: httpStatusCode)
}2. WASI Error Path (CloudKitService+WriteOperations.swift:379-388) The platform check creates a closure that won't compile on WASI anyway. Better structure: #if os(WASI)
throw CloudKitError.httpErrorWithRawResponse(
statusCode: 501,
rawResponse: "Asset uploads not supported on WASI"
)
#else
let uploadHandler = uploader ?? { data, url in
return try await URLSession.shared.upload(data, to: url)
}
// ... rest of implementation
#endif3. Missing Test Coverage No tests verify that:
4. Documentation Gap CLAUDE.md should be updated with an explicit WARNING: **⚠️ CRITICAL: Do not use the CloudKit API transport for asset uploads**
When providing a custom uploader:
- Must use a separate URLSession instance, NOT the CloudKit API transport
- Must not share HTTP/2 connections with api.apple-cloudkit.com
- Should only be used for testing or specialized CDN configurationsRecommendationsOption 1: Alternative Implementation (Recommended)Preserve testability while enforcing the architectural constraint: // Force a separate URLSession, not an arbitrary uploader
public static func uploadAssets(
data: Data,
recordType: String,
fieldName: String,
recordName: String? = nil,
uploadSession: URLSession = .shared // Explicit URLSession, not generic transport
) async throws(CloudKitError) -> AssetUploadResultThis maintains testability (inject a mock URLSession) while preserving the separation. Option 2: Accept with Major RevisionsIf proceeding with this approach:
SummaryOverall Quality: 6/10 The code is well-written with good Swift practices, but it conflicts with a critical architectural decision made to prevent production issues. The testing improvements are valuable, but the abstraction creates a risk that outweighs the benefits. Recommendation: Request changes. Consider the alternative implementation that uses explicit URLSession injection rather than a generic uploader closure. This preserves testability while maintaining the architectural constraint that prevents HTTP/2 connection reuse issues. |
Enhance asset upload documentation with prominent warnings about never using ClientTransport for CDN uploads. Includes implementation details, testing guidelines, and improved error handling for nil status codes. Restructures WASI compilation path for better clarity. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Code Review: Asset Upload Dependency InjectionI've completed a comprehensive review of this PR. Overall, this is a well-executed architectural improvement with excellent documentation and test coverage. Here are my findings: ✅ Strengths1. Architectural Design
2. Documentation Excellence
3. Test Coverage
4. Code Quality
|
Move WASI check back inside uploader closure to allow test-provided custom uploaders to work on WASI platform. The previous function-level check prevented tests from injecting mock uploaders, causing all upload tests to fail on WASI with 501 errors. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
PR Review: Asset Upload Dependency Injection RefactorSummaryThis PR successfully refactors asset upload functionality to use a closure-based dependency injection pattern instead of direct ✅ StrengthsArchitecture & Design
Code Quality
FieldValue Type Architecture
🔍 Observations & SuggestionsMinor Issues
Documentation Enhancements
🛡️ Security & PerformanceSecurity
Performance
🧪 Test CoverageCoverage Assessment
Test Quality
Suggested Additional Tests
📋 RecommendationsBefore Merging
Future Enhancements
🎯 VerdictLGTM with minor changes ⭐️ This is a well-executed refactor that significantly improves testability while maintaining the existing API surface. The documentation is thorough, the tests are comprehensive, and the architecture is sound. The closure-based dependency injection pattern is particularly elegant—it avoids the overhead of protocols while providing full flexibility for testing and custom implementations. Recommended action: Approve after removing debug print statements. Checklist
Great work on this refactor! The attention to detail in documenting the HTTP/2 connection pooling issue is especially commendable. 🚀 |
Replace direct URLSession.shared usage with dependency-injected ClientTransport to enable mocking in tests and maintain architectural consistency.
Changes:
Verified on macOS, iOS Simulator, and Ubuntu (Docker). All 8 upload tests pass on all platforms.
Perform an AI-assisted review on