Skip to content

Handle truncated protobuf responses#4078

Merged
bjtitus merged 4 commits intotrunkfrom
bjtitus/handle-truncated-protobuf-responses
Mar 19, 2026
Merged

Handle truncated protobuf responses#4078
bjtitus merged 4 commits intotrunkfrom
bjtitus/handle-truncated-protobuf-responses

Conversation

@bjtitus
Copy link
Contributor

@bjtitus bjtitus commented Mar 18, 2026

📘 Part of: Watch + Up Next Sync

There are indications from our Up Next Sentry logging that users are receiving truncated protobuf responses. Theoretically, this could lead to "empty" Up Next responses where the response truncated is exactly between the serverModified date and the list of episodes.

The changes here verify that we received the full response by checking the content-length header and verifying the byte count we received.

Two things limit the scope of these changes:

  • The detectTruncatedBackgroundSyncDownloads Feature Flag
  • The Watch App is the only one using BackgroundSyncManager.performBackgroundRefresh

To test

This is difficult to test because Watch ↔ iPhone syncing doesn't work well in the simulators and I cannot figure out how to proxy the watch connection to rewrite the response.

Overall, this can be smoke tested by running the Watch app in Watch mode for a while and making sure new episodes are synced.

Checklist

  • I have considered if this change warrants user-facing release notes and have added them to CHANGELOG.md if necessary.
  • I have considered adding unit tests for my changes.
  • I have updated (or requested that someone edit) the spreadsheet to reflect any new or changed analytics.

@bjtitus bjtitus added the [Project] Watch + Up Next Sync Improvements for Watch + Up Next Sync issues label Mar 18, 2026
Copilot AI review requested due to automatic review settings March 18, 2026 03:00
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds detection for potentially truncated background-sync protobuf downloads (primarily Watch background refresh) by comparing the downloaded byte count against the response’s expected content length, with unit tests for the completeness check.

Changes:

  • Introduce BackgroundSyncManager.isDownloadComplete(receivedBytes:expectedContentLength:) helper for validating download completeness.
  • Add a feature-flagged check in the URLSession download delegate to drop data when a Content-Length mismatch is detected (and log it).
  • Add unit tests covering common completeness/mismatch scenarios.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 5 comments.

File Description
Modules/Server/Sources/PocketCastsServer/Public/Sync/BackgroundSyncManager.swift Adds the download completeness helper used by background sync.
Modules/Server/Sources/PocketCastsServer/Public/Sync/BackgroundSyncManager+URLSession.swift Applies the completeness check after download completion, behind a feature flag, and logs/drops mismatching payloads.
Modules/Server/Tests/PocketCastsServerTests/BackgroundSyncManagerTests.swift Adds unit tests for the completeness helper across expected/unknown/mismatch cases.

You can also share your feedback on Copilot code review. Take the survey.

if FeatureFlag.detectTruncatedBackgroundSyncDownloads.enabled {
let expectedLength = downloadTask.response?.expectedContentLength ?? -1
if let receivedData = data, !BackgroundSyncManager.isDownloadComplete(receivedBytes: receivedData.count, expectedContentLength: expectedLength) {
FileLog.shared.addMessage("Background sync data truncated for task \(downloadTask.taskDescription ?? "unknown"): received \(receivedData.count) bytes, expected \(expectedLength)")
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don't think this is likely and we have no evidence. In theory, the URLSession background task is downloading an incomplete response (due to disconnect or something). It shouldn't receive a response greater than expected.

NSURLResponseUnknownLength is not available from Swift
@bjtitus bjtitus marked this pull request as ready for review March 19, 2026 03:41
@bjtitus bjtitus requested a review from a team as a code owner March 19, 2026 03:41
@bjtitus bjtitus requested review from SergioEstevao and Copilot and removed request for a team March 19, 2026 03:41
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds detection of potentially truncated protobuf downloads during background sync by comparing received bytes against the HTTP Content-Length, gated behind a feature flag.

Changes:

  • Introduces detectTruncatedBackgroundSyncDownloads feature flag.
  • Adds byte-count vs expectedContentLength validation helper in BackgroundSyncManager.
  • Adds unit tests for the completeness-check helper.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.

File Description
Modules/Utils/Sources/PocketCastsUtils/Feature Flags/FeatureFlag.swift Adds a new feature flag and sets its default enabled behavior.
Modules/Server/Sources/PocketCastsServer/Public/Sync/BackgroundSyncManager.swift Introduces isDownloadComplete and an “unknown length” constant used for validation.
Modules/Server/Sources/PocketCastsServer/Public/Sync/BackgroundSyncManager+URLSession.swift Applies the truncation detection during background sync processing and drops data when mismatched.
Modules/Server/Tests/PocketCastsServerTests/BackgroundSyncManagerTests.swift Adds unit tests for the byte-count validation helper.

You can also share your feedback on Copilot code review. Take the survey.

Comment on lines 503 to +507
true
case .cleanUpTmpFiles:
true
case .detectTruncatedBackgroundSyncDownloads:
true
/// Returns `false` if the received byte count doesn't match the expected Content-Length,
/// indicating the download was truncated or corrupt.
/// Value returned by `URLResponse.expectedContentLength` when the length is unknown.
static let unknownContentLength: Int64 = -1
Copy link
Contributor

Choose a reason for hiding this comment

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

This looks like a good suggestion.

Copy link
Contributor Author

@bjtitus bjtitus Mar 19, 2026

Choose a reason for hiding this comment

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

Unfortunately, NSURLResponseUnknownLength was not available in Swift. It's an Objective-C macro instead of a constant. This commit message mentions it.

Comment on lines +16 to +24
// Verify the download completed fully by comparing received bytes to Content-Length.
// A truncated download can produce valid-but-incomplete protobuf that silently drops fields.
if FeatureFlag.detectTruncatedBackgroundSyncDownloads.enabled {
let expectedLength = downloadTask.response?.expectedContentLength ?? BackgroundSyncManager.unknownContentLength
if let receivedData = data, !BackgroundSyncManager.isDownloadComplete(receivedBytes: receivedData.count, expectedContentLength: expectedLength) {
FileLog.shared.addMessage("Background sync data truncated for task \(downloadTask.taskDescription ?? "unknown"): received \(receivedData.count) bytes, expected \(expectedLength)")
data = nil
}
}
@bjtitus bjtitus added this to the 8.9 milestone Mar 19, 2026
Copy link
Contributor

@SergioEstevao SergioEstevao left a comment

Choose a reason for hiding this comment

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

Looking good, I tested this on my watch and phone and all worked correctly.

There is very small nitpick comment regarding the unknow size constant that I think it's a good suggestion, but up to you.

/// Returns `false` if the received byte count doesn't match the expected Content-Length,
/// indicating the download was truncated or corrupt.
/// Value returned by `URLResponse.expectedContentLength` when the length is unknown.
static let unknownContentLength: Int64 = -1
Copy link
Contributor

Choose a reason for hiding this comment

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

This looks like a good suggestion.

@bjtitus bjtitus merged commit a8fab5c into trunk Mar 19, 2026
10 of 12 checks passed
@bjtitus bjtitus deleted the bjtitus/handle-truncated-protobuf-responses branch March 19, 2026 18:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

[Project] Watch + Up Next Sync Improvements for Watch + Up Next Sync issues

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants