Skip to content

feat(network-details): Implement header and body extraction#7585

Open
43jay wants to merge 11 commits intomobile-935/new-swizzlingfrom
mobile-935/extract-network-details
Open

feat(network-details): Implement header and body extraction#7585
43jay wants to merge 11 commits intomobile-935/new-swizzlingfrom
mobile-935/extract-network-details

Conversation

@43jay
Copy link
Copy Markdown
Collaborator

@43jay 43jay commented Mar 4, 2026

📜 Description

SentryNetworkBody

Extract request/response bodies that are either JSON, formurlencoded, binary or text.

  • Relies on having a contentType to decide how to interpret the body (NSData).
  • Relies on iOS's UTType to decide whether something is JSON or text.

text:
tries .utf8 , then .isoLatin1 then gives up with BODY_PARSE_ERROR warning.

json:
tries JSONSerialization.jsonObject, gives up with BODY_PARSE_ERROR warning

UTType doesn't know:
extracts a placeholder body E.g. "[Body not captured: contentType=application/octet-stream (10240 bytes)]"

SentryReplayNetworkRequestOrResponse

  • case-insensitive header extraction

💡 Motivation and Context

See first PR in stack.

💚 How did you test it?

Unit tests

make test-ios ONLY_TESTING="SentryReplayNetworkDetailsBodyTests,SentryReplayNetworkDetailsHeaderTests,SentryReplayNetworkDetailsIntegrationTests"

Test Suite 'SentryReplayNetworkDetailsBodyTests' started at 2026-03-19 15:25:09.315.
✔ testInit_withBinaryContentType_shouldCreateArtificialString (0.005 seconds)
✔ testInit_withEmptyData_shouldReturnNil (0.000 seconds)
✔ testInit_withFormURLEncoded_duplicateKeys_shouldPromoteToArray (0.001 seconds)
✔ testInit_withFormURLEncoded_emptyKeys_shouldBeSkipped (0.000 seconds)
✔ testInit_withFormURLEncoded_emptyValue_shouldParseAsEmptyString (0.000 seconds)
Resolving Package Graph
✔ testInit_withFormURLEncoded_equalsInValue_shouldPreserve (0.000 seconds)
✔ testInit_withFormURLEncoded_missingEquals_shouldFallbackToText (0.001 seconds)
✔ testInit_withFormURLEncoded_shouldParseAsForm (0.001 seconds)
✔ testInit_withInvalidJSON_shouldFallbackToString (0.001 seconds)
✔ testInit_withJSONArray_shouldParseCorrectly (0.000 seconds)
✔ testInit_withJSONDictionary_shouldParseCorrectly (0.001 seconds)
✔ testInit_withLargeData_shouldTruncate (0.005 seconds)
✔ testInit_withNilContentType_shouldCreatePlaceholder (0.000 seconds)
✔ testInit_withTextData_shouldStoreAsString (0.000 seconds)
✔ testInit_withUnrecognizedContentType_shouldCreatePlaceholder (0.000 seconds)
✔ testParseMimeAndEncoding_shouldHandleEdgeCases (0.001 seconds)
✔ testSerialize_withJSONArray_shouldReturnArray (0.001 seconds)
✔ testSerialize_withJSONDictionary_shouldReturnDictionary (0.001 seconds)
✔ testSerialize_withNoContentType_shouldCreatePlaceholder (0.001 seconds)
✔ testSerialize_withStringBody_shouldReturnDictionary (0.001 seconds)
Executed 20 tests, with 0 failures (0 unexpected) in 0.021 (0.041) seconds
Test Suite 'SentryTests.xctest' passed at 2026-03-19 15:25:09.356.
Executed 20 tests, with 0 failures (0 unexpected) in 0.021 (0.041) seconds

SentryReplayNetworkDetailsHeaderTests (4 tests):

  • testExtractHeaders_withNilInputs_returnsEmptyDict — nil headers/config returns empty
  • testExtractHeaders_unconfiguredHeadersAreExcluded — only configured headers are extracted
  • testExtractHeaders_caseInsensitiveMatching — header matching is case-insensitive
  • testExtractHeaders_nonStringValues_convertedToStrings — non-string header values are converted

SentryReplayNetworkDetailsIntegrationTests (4 tests):

  • testInit_withMethod_shouldSetMethod — HTTP method is stored
  • testSerialize_withFullData_shouldReturnCompleteDictionary — full detail serializes all fields
  • testSerialize_withPartialData_shouldOnlyIncludeSetFields — partial data omits unset fields
  • testSerialize_withHeaderFiltering_shouldOnlyIncludeConfiguredHeaders — header filtering works end-to-end

📝 Checklist

You have to check all boxes before merging:

  • I added tests to verify the changes.
  • No new PII added or SDK only sends newly added PII if sendDefaultPII is enabled. N/A
  • I updated the docs if needed. future PR
  • I updated the wizard if needed. N/A
  • Review from the native team if needed.
  • No breaking change or entry added to the changelog. #skip-changelog future PR
  • No breaking change for hybrid SDKs or communicated to hybrid SDKs.

Closes #7710

@linear
Copy link
Copy Markdown

linear bot commented Mar 4, 2026

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 4, 2026

Semver Impact of This PR

🟡 Minor (new features)

📋 Changelog Preview

This is how your changes will appear in the changelog.
Entries from this PR are highlighted with a left border (blockquote style).


This PR will not appear in the changelog.


🤖 This preview updates automatically when you update the PR.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 4, 2026

Messages
📖 Do not forget to update Sentry-docs with your feature once the pull request gets approved.

Generated by 🚫 dangerJS against bd139bc

@43jay 43jay marked this pull request as ready for review March 4, 2026 04:31
@codecov
Copy link
Copy Markdown

codecov bot commented Mar 4, 2026

Codecov Report

❌ Patch coverage is 97.00000% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 85.007%. Comparing base (75d4331) to head (bd139bc).
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
...ons/SessionReplay/SentryReplayNetworkDetails.swift 97.000% 3 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@                      Coverage Diff                       @@
##           mobile-935/new-swizzling     #7585       +/-   ##
==============================================================
+ Coverage                    84.798%   85.007%   +0.208%     
==============================================================
  Files                           486       486               
  Lines                         28958     29054       +96     
  Branches                      12564     12618       +54     
==============================================================
+ Hits                          24556     24698      +142     
+ Misses                         4353      4308       -45     
+ Partials                         49        48        -1     
Files with missing lines Coverage Δ
...ons/SessionReplay/SentryReplayNetworkDetails.swift 96.644% <97.000%> (+96.644%) ⬆️

... and 4 files with indirect coverage changes


Continue to review full report in Codecov by Sentry.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 75d4331...bd139bc. Read the comment docs.

@43jay 43jay force-pushed the mobile-935/new-swizzling branch from 9473efb to 5bc5830 Compare March 4, 2026 21:03
@43jay 43jay force-pushed the mobile-935/extract-network-details branch 2 times, most recently from 3547546 to e7c5abb Compare March 4, 2026 22:00
@43jay 43jay force-pushed the mobile-935/new-swizzling branch 2 times, most recently from 7faf4ef to 6e19c0a Compare March 6, 2026 16:33
@43jay 43jay force-pushed the mobile-935/extract-network-details branch from e7c5abb to 2e9607e Compare March 6, 2026 16:33
@43jay 43jay marked this pull request as draft March 6, 2026 18:43
@43jay 43jay force-pushed the mobile-935/new-swizzling branch from 6e19c0a to 9a8d48d Compare March 6, 2026 19:25
@43jay 43jay force-pushed the mobile-935/extract-network-details branch 2 times, most recently from 6e5c5bb to 41cf944 Compare March 9, 2026 18:09
@43jay 43jay force-pushed the mobile-935/new-swizzling branch from 9a8d48d to 8584657 Compare March 9, 2026 18:09
@43jay 43jay force-pushed the mobile-935/extract-network-details branch from 41cf944 to 68fbe88 Compare March 9, 2026 19:02
@43jay 43jay force-pushed the mobile-935/new-swizzling branch 2 times, most recently from c3afd6d to 6e873ec Compare March 9, 2026 19:48
@43jay 43jay force-pushed the mobile-935/extract-network-details branch from 68fbe88 to 6a06365 Compare March 9, 2026 19:48
@43jay 43jay marked this pull request as ready for review March 9, 2026 21:01
@43jay 43jay force-pushed the mobile-935/extract-network-details branch from 499493d to fbd791c Compare March 10, 2026 21:06
@43jay 43jay force-pushed the mobile-935/new-swizzling branch from a691160 to 7db5474 Compare March 10, 2026 21:06
Comment on lines +92 to +100
let utType = mimeType.flatMap { UTType(mimeType: $0) }
if let utType, utType.conforms(to: .json) {
if isTruncated { warnings.append(.jsonTruncated) }
return parseJSON(data, warnings: &warnings)
} else if utType?.conforms(to: .text) == true {
if isTruncated { warnings.append(.textTruncated) }
return parseText(data, warnings: &warnings)
}
return nil
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

l: nitpick, no point in using if-else

Suggested change
let utType = mimeType.flatMap { UTType(mimeType: $0) }
if let utType, utType.conforms(to: .json) {
if isTruncated { warnings.append(.jsonTruncated) }
return parseJSON(data, warnings: &warnings)
} else if utType?.conforms(to: .text) == true {
if isTruncated { warnings.append(.textTruncated) }
return parseText(data, warnings: &warnings)
}
return nil
guard let utType = mimeType.flatMap { UTType(mimeType: $0) } else {
return nil
}
if utType.conforms(to: .json) {
if isTruncated { warnings.append(.jsonTruncated) }
return parseJSON(data, warnings: &warnings)
}
if utType.conforms(to: .text) {
if isTruncated { warnings.append(.textTruncated) }
return parseText(data, warnings: &warnings)
}
return nil

Copy link
Copy Markdown
Collaborator Author

@43jay 43jay Mar 16, 2026

Choose a reason for hiding this comment

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


private static func parseFormEncoded(_ data: Data, warnings: inout [NetworkBodyWarning]) -> Body {
guard let string = String(data: data, encoding: .utf8) ?? String(data: data, encoding: .isoLatin1),
let components = URLComponents(string: "http://x?" + string),
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

h: I am not convinced that this is the state-of-the-art to parse form-url-encoded data. Is this how other libraries do it? We should instead consider using an implementation of the Decoder protocol e.g. FormUrlDecoder which works similarly to the JSONDecoder.

Also you assume that the body type is either UTF-8 or ISO-Latin-1, but http requests should actually have a content-type header which defines the encoding format.

Copy link
Copy Markdown
Collaborator Author

@43jay 43jay Mar 16, 2026

Choose a reason for hiding this comment

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

not convinced that this is the state-of-the-art to parse form-url-encoded data

Good shout. I looked into this more.

AFAICT, there's no easy soln - either 1) ship a Decoder-conforming FormUrlDecoder that is a bunch of code that needs proper testing for this narrow use-case, or 2) do something simple and incomplete inline.

Deferring to your preference, I propose option 3) Don't support extracting x-www-form-urlencoded as structured data: just extract it as text.

It won't show up as structured data (expand/collapsible) on dashboard but all the data will be there to see + no weird edge cases to confuse ppl.


Some findings:

Apple's own engineer (Quinn "The Eskimo!") explicitly stated in Apple Developer Forums #113632 that x-www-form-urlencoded is "not supported by URLComponents"

From Apple Developer Forums #113632:

Creating data in this format is tricky because the specification is so weak. Below you’ll find my take on it.

Known bugs with URLComponents for form data:

  • not decoded as space — in form-encoding, + means space; URLComponents treats it as a literal + (rdar://40751862)
    Semicolons not handled as alternate separators
    No nested key support (user[name]=foo)

option 1) Decoder-conforming impl // first-pass approximation via claude:

/// Decodes application/x-www-form-urlencoded data into Decodable types.
final class FormUrlDecoder {
    func decode<T: Decodable>(_ type: T.Type, from data: Data) throws -> T {
        guard let string = String(data: data, encoding: .utf8) else {
            throw DecodingError.dataCorrupted(.init(codingPath: [], debugDescription: "Not valid UTF-8"))
        }
        let pairs = Self.parse(string)
        let decoder = _FormUrlDecoder(pairs: pairs)
        return try T(from: decoder)
    }

    /// Splits on `&`, then `=`, decodes `+` as space, percent-decodes.
    private static func parse(_ string: String) -> [(String, String)] {
        string.split(separator: "&", omittingEmptySubsequences: true).compactMap { pair in
            let parts = pair.split(separator: "=", maxSplits: 1)
            guard let key = String(parts[0])
                .replacingOccurrences(of: "+", with: " ")
                .removingPercentEncoding, !key.isEmpty else { return nil }
            let value = parts.count > 1
                ? String(parts[1]).replacingOccurrences(of: "+", with: " ").removingPercentEncoding ?? ""
                : ""
            return (key, value)
        }
    }
}

// MARK: - Internal Decoder

private struct _FormUrlDecoder: Decoder {
    let pairs: [(String, String)]
    var codingPath: [CodingKey] = []
    var userInfo: [CodingUserInfoKey: Any] = [:]

    func container<Key: CodingKey>(keyedBy type: Key.Type) throws -> KeyedDecodingContainer<Key> {
        KeyedDecodingContainer(_KeyedContainer<Key>(pairs: pairs, codingPath: codingPath))
    }

    func unkeyedContainer() throws -> UnkeyedDecodingContainer {
        throw DecodingError.typeMismatch([Any].self,
            .init(codingPath: codingPath, debugDescription: "Form data does not support unkeyed containers"))
    }

    func singleValueContainer() throws -> SingleValueDecodingContainer {
        throw DecodingError.typeMismatch(Any.self,
            .init(codingPath: codingPath, debugDescription: "Form data does not support single value containers"))
    }
}

// MARK: - KeyedDecodingContainer

private struct _KeyedContainer<Key: CodingKey>: KeyedDecodingContainerProtocol {
    let pairs: [(String, String)]
    var codingPath: [CodingKey]
    var allKeys: [Key] { pairs.compactMap { Key(stringValue: $0.0) } }

    func contains(_ key: Key) -> Bool {
        pairs.contains { $0.0 == key.stringValue }
    }

    private func value(for key: Key) throws -> String {
        guard let pair = pairs.first(where: { $0.0 == key.stringValue }) else {
            throw DecodingError.keyNotFound(key,
                .init(codingPath: codingPath, debugDescription: "No value for key \(key.stringValue)"))
        }
        return pair.1
    }

    func decodeNil(forKey key: Key) throws -> Bool { !contains(key) }
    func decode(_ type: Bool.Type, forKey key: Key) throws -> Bool { try value(for: key) == "true" }
    func decode(_ type: String.Type, forKey key: Key) throws -> String { try value(for: key) }
    func decode(_ type: Int.Type, forKey key: Key) throws -> Int {
        guard let v = Int(try value(for: key)) else {
            throw DecodingError.typeMismatch(Int.self,
                .init(codingPath: codingPath + [key], debugDescription: "Not an Int"))
        }
        return v
    }
    func decode(_ type: Double.Type, forKey key: Key) throws -> Double {
        guard let v = Double(try value(for: key)) else {
            throw DecodingError.typeMismatch(Double.self,
                .init(codingPath: codingPath + [key], debugDescription: "Not a Double"))
        }
        return v
    }
    // ... Int8, Int16, Int32, Int64, UInt, UInt8, UInt16, UInt32, UInt64, Float
    // Each one follows the same pattern as Int/Double above.
    func decode(_ type: Float.Type, forKey key: Key) throws -> Float {
        guard let v = Float(try value(for: key)) else {
            throw DecodingError.typeMismatch(Float.self,
                .init(codingPath: codingPath + [key], debugDescription: "Not a Float"))
        }
        return v
    }
    func decode(_ type: Int8.Type, forKey key: Key) throws -> Int8 { /* same pattern */ }
    func decode(_ type: Int16.Type, forKey key: Key) throws -> Int16 { /* same pattern */ }
    func decode(_ type: Int32.Type, forKey key: Key) throws -> Int32 { /* same pattern */ }
    func decode(_ type: Int64.Type, forKey key: Key) throws -> Int64 { /* same pattern */ }
    func decode(_ type: UInt.Type, forKey key: Key) throws -> UInt { /* same pattern */ }
    func decode(_ type: UInt8.Type, forKey key: Key) throws -> UInt8 { /* same pattern */ }
    func decode(_ type: UInt16.Type, forKey key: Key) throws -> UInt16 { /* same pattern */ }
    func decode(_ type: UInt32.Type, forKey key: Key) throws -> UInt32 { /* same pattern */ }
    func decode(_ type: UInt64.Type, forKey key: Key) throws -> UInt64 { /* same pattern */ }

    func decode<T: Decodable>(_ type: T.Type, forKey key: Key) throws -> T {
        if type == String.self { return try decode(String.self, forKey: key) as! T }
        throw DecodingError.typeMismatch(type,
            .init(codingPath: codingPath + [key], debugDescription: "Nested decoding not supported"))
    }

    func nestedContainer<NK: CodingKey>(keyedBy type: NK.Type, forKey key: Key) throws -> KeyedDecodingContainer<NK> {
        throw DecodingError.typeMismatch(type,
            .init(codingPath: codingPath, debugDescription: "Nested containers not supported"))
    }
    func nestedUnkeyedContainer(forKey key: Key) throws -> UnkeyedDecodingContainer {
        throw DecodingError.typeMismatch([Any].self,
            .init(codingPath: codingPath, debugDescription: "Nested containers not supported"))
    }
    func superDecoder() throws -> Decoder { _FormUrlDecoder(pairs: pairs, codingPath: codingPath) }
    func superDecoder(forKey key: Key) throws -> Decoder { _FormUrlDecoder(pairs: pairs, codingPath: codingPath + [key]) }
}

option 2) in-line parser looking for '&'s and '='s

private static func parseFormEncoded(_ data: Data, warnings: inout [NetworkBodyWarning]) -> Body {
    guard let string = String(data: data, encoding: .utf8) ?? String(data: data, encoding: .isoLatin1) else {
        warnings.append(.bodyParseError)
        return parseText(data, warnings: &warnings)
    }
    var formData = [String: String]()
    for pair in string.split(separator: "&", omittingEmptySubsequences: true) {
        let parts = pair.split(separator: "=", maxSplits: 1)
        let key = String(parts[0]).removingPercentEncoding?.replacingOccurrences(of: "+", with: " ") ?? String(parts[0])
        let value = parts.count > 1
            ? (String(parts[1]).removingPercentEncoding?.replacingOccurrences(of: "+", with: " ") ?? String(parts[1]))
            : ""
        if !key.isEmpty { formData[key] = value }
    }
    return Body(content: formData, warnings: warnings)
}

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Also you assume that the body type is either UTF-8 or ISO-Latin-1, but http requests should actually have a content-type header which defines the encoding format.

See #7585 (comment)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@43jay thanks for research and the proposed options. Reviewing code implementation in a PR review comments is hard to do, so I can't really tell right now which approach is better.

But I just remembered I had a similar issue in the past and created a Form URL Encoder/Decoder we could vendor in from my networking library and add tests:

https://github.com/kula-app/Postie/tree/main/Sources/URLEncodedFormCoding

It should already be fully functional but not fully tested.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

draft while i address this

Copy link
Copy Markdown
Collaborator Author

@43jay 43jay Mar 19, 2026

Choose a reason for hiding this comment

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

PTAL

@philprime looking at the library you linked, majority of it is for decoding into arbitrary Decodable types.
B/c we'll only ever need to parse [String: String], i opted to just this specific parsing logic from postie instead of the whole shebang.

Delta:

@43jay 43jay force-pushed the mobile-935/new-swizzling branch from 7db5474 to a75e679 Compare March 16, 2026 20:31
43jay added a commit that referenced this pull request Mar 16, 2026
#7585 (comment)

Parse MIME type and charset from Content-Type using
CFStringConvertIANACharSetNameToEncoding instead of
hardcoding UTF-8/ISO-Latin-1 fallbacks.
@43jay 43jay force-pushed the mobile-935/extract-network-details branch from fbd791c to e586a59 Compare March 16, 2026 20:31
Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: JSON fallback to text ignores charset encoding
    • The bug was real and fixed by passing the parsed charset encoding into JSON parsing and its text fallback path so malformed JSON now decodes with the declared charset.

Create PR

Or push these changes by commenting:

@cursor push 318ddc50df
Preview (318ddc50df)
diff --git a/Sources/Swift/Integrations/SessionReplay/SentryReplayNetworkDetails.swift b/Sources/Swift/Integrations/SessionReplay/SentryReplayNetworkDetails.swift
--- a/Sources/Swift/Integrations/SessionReplay/SentryReplayNetworkDetails.swift
+++ b/Sources/Swift/Integrations/SessionReplay/SentryReplayNetworkDetails.swift
@@ -128,7 +128,7 @@
             }
             if utType.conforms(to: .json) {
                 if isTruncated { warnings.append(.jsonTruncated) }
-                return parseJSON(data, warnings: &warnings)
+                return parseJSON(data, encoding: encoding, warnings: &warnings)
             }
             if utType.conforms(to: .text) {
                 if isTruncated { warnings.append(.textTruncated) }
@@ -137,13 +137,13 @@
             return nil
         }
 
-        private static func parseJSON(_ data: Data, warnings: inout [NetworkBodyWarning]) -> Body {
+        private static func parseJSON(_ data: Data, encoding: String.Encoding, warnings: inout [NetworkBodyWarning]) -> Body {
             do {
                 let json = try JSONSerialization.jsonObject(with: data, options: .mutableContainers)
                 return Body(content: json, warnings: warnings)
             } catch {
                 warnings.append(.bodyParseError)
-                return parseText(data, warnings: &warnings)
+                return parseText(data, encoding: encoding, warnings: &warnings)
             }
         }

This Bugbot Autofix run was free. To enable autofix for future PRs, go to the Cursor dashboard.

43jay added 8 commits March 18, 2026 10:32
1) Extracts bodies that are JSON, formurlencoded, or text.

Uses UTType to accurately classify content types as JSON
or text without maintaining a manual list. Falls back
to a string match for application/x-www-form-urlencoded
which has no UTType representation.

!Relies on having a valid `contentType`

2) Populates NetworkBodyWarning's for
 "MAYBE_JSON_TRUNCATED"
 "TEXT_TRUNCATED"
 "BODY_PARSE_ERROR"
 ^when encountered, these show custom dashboard UI.
Uses UTType to classify content types: only content positively identified
as text is decoded. Everything else gets a descriptive placeholder:
 Example - "[Body not captured: contentType=image/png (8 bytes)]"

Known text types (where UTType conforms to .text) are reliably classified
by UTType's type hierarchy. If a content type header is incorrect (e.g.
claims text but contains binary), the resulting decode failure is caught
by the existing bodyParseError warning.
UTType (UniformTypeIdentifiers) requires macOS 11+, but the SDK
targets macOS 10.14. Extract UTType-based MIME detection into a
separate method gated with @available(macOS 11, *) so the code
compiles on all macOS targets. Session Replay is not available on
macOS, so the fallback placeholder is fine.
Casts to lower-case before comparing headers.
ObjC setters now accept raw allHeaders and configuredHeaders instead
of pre-filtered headers, keeping the filtering logic in Swift.
#7585 (comment)

Parse MIME type and charset from Content-Type using
CFStringConvertIANACharSetNameToEncoding instead of
hardcoding UTF-8/ISO-Latin-1 fallbacks.
@43jay 43jay force-pushed the mobile-935/new-swizzling branch from a75e679 to 75d4331 Compare March 18, 2026 19:25
@43jay 43jay force-pushed the mobile-935/extract-network-details branch from e586a59 to 37167c7 Compare March 18, 2026 19:25
@43jay 43jay marked this pull request as draft March 18, 2026 19:38
@43jay 43jay force-pushed the mobile-935/extract-network-details branch from 5114950 to bb1abf8 Compare March 19, 2026 19:46
@43jay 43jay marked this pull request as ready for review March 19, 2026 19:47
Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Copy link
Copy Markdown
Member

@philprime philprime left a comment

Choose a reason for hiding this comment

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

LGTM, I'll approve it as I trust you can apply the final cleanup.

/// UTType requires macOS 11+; so this will not compile there.
@available(macOS 11, *)
private static func parseByMimeType(_ mimeType: String?, data: Data, encoding: String.Encoding, isTruncated: Bool, warnings: inout [NetworkBodyWarning]) -> Body? {
guard let utType = mimeType.flatMap({ UTType(mimeType: $0) }) else {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

l: [nitpick] Using flatMap for nul unwrapping in a guard statement is an anti-pattern.

Suggested change
guard let utType = mimeType.flatMap({ UTType(mimeType: $0) }) else {
guard let mimeType = mimeType, let utType = UTType(mimeType: mimeType) else {


func testInit_withFormURLEncoded_duplicateKeys_shouldPromoteToArray() throws {
// -- Act --
let body = try XCTUnwrap(Body(
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

m: Don't use force-unwrapping, we use XCTUnwrap for that, this needs to be fixed in all tests

Comment on lines +153 to +157
if let bodyString = result?["body"] as? String {
XCTAssertTrue(bodyString.hasPrefix("[Body not captured"))
} else {
XCTFail("Expected body to be a string with binary data prefix")
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

m: XCTFail in an if-statement for null unwrapping should be XTCUnwrap.

Suggested change
if let bodyString = result?["body"] as? String {
XCTAssertTrue(bodyString.hasPrefix("[Body not captured"))
} else {
XCTFail("Expected body to be a string with binary data prefix")
}
let bodyString = try XCTUnwrap(result?["body"] as? String, "Expected body to be a string with binary data prefix")
XCTAssertTrue(bodyString.hasPrefix("[Body not captured"))

This applies to multiple tests

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.

2 participants