react-native-nitro-fetch 1.0.3 → 1.1.1-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NitroFetch.podspec +4 -0
- package/android/src/main/java/com/margelo/nitro/nitrofetch/DevToolsReporter.kt +127 -0
- package/android/src/main/java/com/margelo/nitro/nitrofetch/DevToolsReporterImpl.kt +67 -0
- package/android/src/main/java/com/margelo/nitro/nitrofetch/NitroFetchClient.kt +50 -0
- package/android/src/main/java/com/margelo/nitro/nitrofetch/NitroUrlRequestBuilder.kt +63 -3
- package/ios/HybridUrlRequestBuilder.swift +62 -2
- package/ios/NitroDevToolsReporter.h +42 -0
- package/ios/NitroDevToolsReporter.mm +153 -0
- package/ios/NitroFetchClient.swift +49 -1
- package/package.json +1 -1
package/NitroFetch.podspec
CHANGED
|
@@ -22,6 +22,10 @@ Pod::Spec.new do |s|
|
|
|
22
22
|
|
|
23
23
|
s.dependency 'React-jsi'
|
|
24
24
|
s.dependency 'React-callinvoker'
|
|
25
|
+
s.dependency 'React-RCTNetwork'
|
|
26
|
+
|
|
27
|
+
# Expose the DevTools reporter Obj-C facade to Swift via the pod's umbrella module.
|
|
28
|
+
s.public_header_files = ["ios/NitroDevToolsReporter.h"]
|
|
25
29
|
|
|
26
30
|
load 'nitrogen/generated/ios/NitroFetch+autolinking.rb'
|
|
27
31
|
add_nitrogen_files(s)
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
package com.margelo.nitro.nitrofetch
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Thin facade over React Native's `com.facebook.react.modules.network.InspectorNetworkReporter`.
|
|
5
|
+
*
|
|
6
|
+
* The reporter is `internal` in RN and may be missing entirely on older versions or stripped
|
|
7
|
+
* distributions. To stay both safe (no [NoClassDefFoundError]) and fast (no per-call reflection
|
|
8
|
+
* once the class is known to exist), we use the **isolation-class pattern**:
|
|
9
|
+
*
|
|
10
|
+
* - This facade has *no* compile-time reference to `InspectorNetworkReporter`. It can always
|
|
11
|
+
* be loaded and verified by ART, even when the reporter is absent.
|
|
12
|
+
* - All direct calls live in [DevToolsReporterImpl], which is loaded reflectively *once* via
|
|
13
|
+
* `Class.forName`. If verification fails (missing class), we catch and stay in no-op mode.
|
|
14
|
+
* - On success, we hold an [Impl] interface reference and dispatch through it on every call —
|
|
15
|
+
* a single null check + a virtual call (likely devirtualized by the JIT). No reflection,
|
|
16
|
+
* no boxing, no method-handle lookups on the hot path.
|
|
17
|
+
*/
|
|
18
|
+
internal object DevToolsReporter {
|
|
19
|
+
|
|
20
|
+
/** Stable interface in *our* package — Impl implements it; no foreign types here. */
|
|
21
|
+
internal interface Impl {
|
|
22
|
+
fun isDebuggingEnabled(): Boolean
|
|
23
|
+
fun reportRequestStart(
|
|
24
|
+
requestId: String, url: String, method: String,
|
|
25
|
+
headers: Map<String, String>, body: String, encodedDataLength: Long
|
|
26
|
+
)
|
|
27
|
+
fun reportResponseStart(
|
|
28
|
+
requestId: String, url: String, statusCode: Int,
|
|
29
|
+
headers: Map<String, String>, expectedDataLength: Long
|
|
30
|
+
)
|
|
31
|
+
fun reportDataReceived(requestId: String, length: Int)
|
|
32
|
+
fun reportResponseEnd(requestId: String, encodedDataLength: Long)
|
|
33
|
+
fun reportRequestFailed(requestId: String, cancelled: Boolean)
|
|
34
|
+
fun storeResponseBody(requestId: String, body: String, base64Encoded: Boolean)
|
|
35
|
+
fun storeResponseBodyIncremental(requestId: String, data: String)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
@Volatile private var impl: Impl? = null
|
|
39
|
+
// We deliberately do NOT latch failure: cold-start prefetch can run before
|
|
40
|
+
// RN classes are realized, and we want a later request to recover. The
|
|
41
|
+
// probe is cheap (Class.forName has its own internal cache).
|
|
42
|
+
private fun resolve(): Impl? {
|
|
43
|
+
val cached = impl
|
|
44
|
+
if (cached != null) return cached
|
|
45
|
+
|
|
46
|
+
if (!isSoLoaderInitialized()) return null
|
|
47
|
+
return try {
|
|
48
|
+
val cls = Class.forName("com.margelo.nitro.nitrofetch.DevToolsReporterImpl")
|
|
49
|
+
|
|
50
|
+
val created = cls.getDeclaredConstructor().newInstance() as Impl
|
|
51
|
+
impl = created
|
|
52
|
+
created
|
|
53
|
+
} catch (_: Throwable) {
|
|
54
|
+
null
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
private fun isSoLoaderInitialized(): Boolean = try {
|
|
59
|
+
com.facebook.soloader.SoLoader.isInitialized()
|
|
60
|
+
} catch (_: Throwable) {
|
|
61
|
+
false
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// --- Hot path: one null check + interface call. JIT will devirtualize. ---
|
|
65
|
+
|
|
66
|
+
fun isDebuggingEnabled(): Boolean = resolve()?.isDebuggingEnabled() ?: false
|
|
67
|
+
|
|
68
|
+
fun reportRequestStart(
|
|
69
|
+
requestId: String, url: String, method: String,
|
|
70
|
+
headers: Map<String, String>, body: String, encodedDataLength: Long
|
|
71
|
+
) {
|
|
72
|
+
impl?.reportRequestStart(requestId, url, method, headers, body, encodedDataLength)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
fun reportResponseStart(
|
|
76
|
+
requestId: String, url: String, statusCode: Int,
|
|
77
|
+
headers: Map<String, String>, expectedDataLength: Long
|
|
78
|
+
) {
|
|
79
|
+
impl?.reportResponseStart(requestId, url, statusCode, headers, expectedDataLength)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
fun reportDataReceived(requestId: String, length: Int) {
|
|
83
|
+
impl?.reportDataReceived(requestId, length)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
fun reportResponseEnd(requestId: String, encodedDataLength: Long) {
|
|
87
|
+
impl?.reportResponseEnd(requestId, encodedDataLength)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
fun reportRequestFailed(requestId: String, cancelled: Boolean) {
|
|
91
|
+
impl?.reportRequestFailed(requestId, cancelled)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
fun storeResponseBody(requestId: String, body: String, base64Encoded: Boolean) {
|
|
95
|
+
impl?.storeResponseBody(requestId, body, base64Encoded)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
fun storeResponseBodyIncremental(requestId: String, data: String) {
|
|
99
|
+
impl?.storeResponseBodyIncremental(requestId, data)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// --- Pure helpers, no reporter dependency. ---
|
|
103
|
+
|
|
104
|
+
fun isTextualContentType(contentType: String?): Boolean {
|
|
105
|
+
if (contentType == null) return false
|
|
106
|
+
val ct = contentType.lowercase()
|
|
107
|
+
return ct.startsWith("text/") ||
|
|
108
|
+
ct.contains("application/json") ||
|
|
109
|
+
ct.contains("application/xml") ||
|
|
110
|
+
ct.contains("application/javascript") ||
|
|
111
|
+
ct.contains("+json") ||
|
|
112
|
+
ct.contains("+xml")
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
fun headersArrayToMap(headers: Array<NitroHeader>?): Map<String, String> {
|
|
116
|
+
if (headers == null) return emptyMap()
|
|
117
|
+
val map = LinkedHashMap<String, String>(headers.size)
|
|
118
|
+
for (h in headers) map[h.key] = h.value
|
|
119
|
+
return map
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
fun headersListToMap(entries: List<Map.Entry<String, String>>): Map<String, String> {
|
|
123
|
+
val map = LinkedHashMap<String, String>(entries.size)
|
|
124
|
+
for (e in entries) map[e.key] = e.value
|
|
125
|
+
return map
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
package com.margelo.nitro.nitrofetch
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Direct-dispatch backend for [DevToolsReporter]. Loaded reflectively from the facade
|
|
5
|
+
* via `Class.forName`, so a missing `InspectorNetworkReporter` only prevents *this* class
|
|
6
|
+
* from loading — the facade itself stays intact and degrades to a no-op.
|
|
7
|
+
*
|
|
8
|
+
* Once loaded, every call here is a plain JVM static invoke. No reflection, no boxing,
|
|
9
|
+
* no method-handle lookups. The `@Suppress` annotations bypass RN's `internal` visibility
|
|
10
|
+
* (RN has no public surface here) — this is the documented integration point.
|
|
11
|
+
*/
|
|
12
|
+
@Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
|
|
13
|
+
internal class DevToolsReporterImpl : DevToolsReporter.Impl {
|
|
14
|
+
override fun isDebuggingEnabled(): Boolean =
|
|
15
|
+
com.facebook.react.modules.network.InspectorNetworkReporter.isDebuggingEnabled()
|
|
16
|
+
|
|
17
|
+
override fun reportRequestStart(
|
|
18
|
+
requestId: String, url: String, method: String,
|
|
19
|
+
headers: Map<String, String>, body: String, encodedDataLength: Long
|
|
20
|
+
) {
|
|
21
|
+
com.facebook.react.modules.network.InspectorNetworkReporter.reportRequestStart(
|
|
22
|
+
requestId, url, method, headers, body, encodedDataLength
|
|
23
|
+
)
|
|
24
|
+
com.facebook.react.modules.network.InspectorNetworkReporter.reportConnectionTiming(
|
|
25
|
+
requestId, headers
|
|
26
|
+
)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
override fun reportResponseStart(
|
|
30
|
+
requestId: String, url: String, statusCode: Int,
|
|
31
|
+
headers: Map<String, String>, expectedDataLength: Long
|
|
32
|
+
) {
|
|
33
|
+
com.facebook.react.modules.network.InspectorNetworkReporter.reportResponseStart(
|
|
34
|
+
requestId, url, statusCode, headers, expectedDataLength
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
override fun reportDataReceived(requestId: String, length: Int) {
|
|
39
|
+
com.facebook.react.modules.network.InspectorNetworkReporter.reportDataReceivedImpl(
|
|
40
|
+
requestId, length
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
override fun reportResponseEnd(requestId: String, encodedDataLength: Long) {
|
|
45
|
+
com.facebook.react.modules.network.InspectorNetworkReporter.reportResponseEnd(
|
|
46
|
+
requestId, encodedDataLength
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
override fun reportRequestFailed(requestId: String, cancelled: Boolean) {
|
|
51
|
+
com.facebook.react.modules.network.InspectorNetworkReporter.reportRequestFailed(
|
|
52
|
+
requestId, cancelled
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
override fun storeResponseBody(requestId: String, body: String, base64Encoded: Boolean) {
|
|
57
|
+
com.facebook.react.modules.network.InspectorNetworkReporter.maybeStoreResponseBody(
|
|
58
|
+
requestId, body, base64Encoded
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
override fun storeResponseBodyIncremental(requestId: String, data: String) {
|
|
63
|
+
com.facebook.react.modules.network.InspectorNetworkReporter.maybeStoreResponseBodyIncremental(
|
|
64
|
+
requestId, data
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -80,10 +80,18 @@ class NitroFetchClient(private val engine: CronetEngine, private val executor: E
|
|
|
80
80
|
if (BuildConfig.NITRO_FETCH_TRACING) {
|
|
81
81
|
Trace.beginAsyncSection(traceLabel, traceCookie)
|
|
82
82
|
}
|
|
83
|
+
// BuildConfig.DEBUG short-circuits in release: R8 constant-folds the
|
|
84
|
+
// && so every `if (devToolsEnabled)` block below becomes dead code and
|
|
85
|
+
// the DevToolsReporter classes drop out of the release APK entirely.
|
|
86
|
+
// The UUID generation is gated too so SecureRandom isn't touched in release.
|
|
87
|
+
val devToolsEnabled = BuildConfig.DEBUG && DevToolsReporter.isDebuggingEnabled()
|
|
88
|
+
val devToolsRequestId = if (devToolsEnabled) (req.requestId ?: UUID.randomUUID().toString()) else ""
|
|
83
89
|
val callback = object : UrlRequest.Callback() {
|
|
84
90
|
private val buffer = ByteBuffer.allocateDirect(16 * 1024)
|
|
85
91
|
private val out = java.io.ByteArrayOutputStream()
|
|
86
92
|
private var redirectStopped = false
|
|
93
|
+
private var devToolsBytes = 0
|
|
94
|
+
private var devToolsTextual = false
|
|
87
95
|
|
|
88
96
|
override fun onRedirectReceived(request: UrlRequest, info: UrlResponseInfo, newLocationUrl: String) {
|
|
89
97
|
if (shouldFollowRedirects) {
|
|
@@ -113,6 +121,19 @@ class NitroFetchClient(private val engine: CronetEngine, private val executor: E
|
|
|
113
121
|
}
|
|
114
122
|
|
|
115
123
|
override fun onResponseStarted(request: UrlRequest, info: UrlResponseInfo) {
|
|
124
|
+
if (devToolsEnabled) {
|
|
125
|
+
val headersMap = LinkedHashMap<String, String>()
|
|
126
|
+
info.allHeadersAsList.forEach { headersMap[it.key] = it.value }
|
|
127
|
+
val contentType = headersMap["Content-Type"] ?: headersMap["content-type"]
|
|
128
|
+
devToolsTextual = DevToolsReporter.isTextualContentType(contentType)
|
|
129
|
+
DevToolsReporter.reportResponseStart(
|
|
130
|
+
devToolsRequestId,
|
|
131
|
+
info.url,
|
|
132
|
+
info.httpStatusCode,
|
|
133
|
+
headersMap,
|
|
134
|
+
-1L
|
|
135
|
+
)
|
|
136
|
+
}
|
|
116
137
|
buffer.clear()
|
|
117
138
|
request.read(buffer)
|
|
118
139
|
}
|
|
@@ -122,6 +143,13 @@ class NitroFetchClient(private val engine: CronetEngine, private val executor: E
|
|
|
122
143
|
val bytes = ByteArray(byteBuffer.remaining())
|
|
123
144
|
byteBuffer.get(bytes)
|
|
124
145
|
out.write(bytes)
|
|
146
|
+
if (devToolsEnabled) {
|
|
147
|
+
devToolsBytes += bytes.size
|
|
148
|
+
DevToolsReporter.reportDataReceived(devToolsRequestId, bytes.size)
|
|
149
|
+
if (devToolsTextual) {
|
|
150
|
+
DevToolsReporter.storeResponseBodyIncremental(devToolsRequestId, String(bytes, Charsets.UTF_8))
|
|
151
|
+
}
|
|
152
|
+
}
|
|
125
153
|
byteBuffer.clear()
|
|
126
154
|
request.read(byteBuffer)
|
|
127
155
|
}
|
|
@@ -130,6 +158,9 @@ class NitroFetchClient(private val engine: CronetEngine, private val executor: E
|
|
|
130
158
|
if (BuildConfig.NITRO_FETCH_TRACING) {
|
|
131
159
|
Trace.endAsyncSection(traceLabel, traceCookie)
|
|
132
160
|
}
|
|
161
|
+
if (devToolsEnabled) {
|
|
162
|
+
DevToolsReporter.reportResponseEnd(devToolsRequestId, devToolsBytes.toLong())
|
|
163
|
+
}
|
|
133
164
|
try {
|
|
134
165
|
val headersArr: Array<NitroHeader> =
|
|
135
166
|
info.allHeadersAsList.map { NitroHeader(it.key, it.value) }.toTypedArray()
|
|
@@ -166,6 +197,9 @@ class NitroFetchClient(private val engine: CronetEngine, private val executor: E
|
|
|
166
197
|
if (BuildConfig.NITRO_FETCH_TRACING) {
|
|
167
198
|
Trace.endAsyncSection(traceLabel, traceCookie)
|
|
168
199
|
}
|
|
200
|
+
if (devToolsEnabled) {
|
|
201
|
+
DevToolsReporter.reportRequestFailed(devToolsRequestId, false)
|
|
202
|
+
}
|
|
169
203
|
onFail(RuntimeException("Cronet failed: ${error.message}", error))
|
|
170
204
|
}
|
|
171
205
|
|
|
@@ -173,6 +207,9 @@ class NitroFetchClient(private val engine: CronetEngine, private val executor: E
|
|
|
173
207
|
if (BuildConfig.NITRO_FETCH_TRACING) {
|
|
174
208
|
Trace.endAsyncSection(traceLabel, traceCookie)
|
|
175
209
|
}
|
|
210
|
+
if (devToolsEnabled) {
|
|
211
|
+
DevToolsReporter.reportRequestFailed(devToolsRequestId, true)
|
|
212
|
+
}
|
|
176
213
|
if (!redirectStopped) {
|
|
177
214
|
onFail(RuntimeException("Cronet canceled"))
|
|
178
215
|
}
|
|
@@ -205,6 +242,19 @@ class NitroFetchClient(private val engine: CronetEngine, private val executor: E
|
|
|
205
242
|
}
|
|
206
243
|
|
|
207
244
|
val request = builder.build()
|
|
245
|
+
if (devToolsEnabled) {
|
|
246
|
+
val headersMap = DevToolsReporter.headersArrayToMap(req.headers)
|
|
247
|
+
val body = req.bodyString ?: ""
|
|
248
|
+
val encoded = body.toByteArray(Charsets.UTF_8).size.toLong()
|
|
249
|
+
DevToolsReporter.reportRequestStart(
|
|
250
|
+
devToolsRequestId,
|
|
251
|
+
url,
|
|
252
|
+
method,
|
|
253
|
+
headersMap,
|
|
254
|
+
body,
|
|
255
|
+
encoded
|
|
256
|
+
)
|
|
257
|
+
}
|
|
208
258
|
request.start()
|
|
209
259
|
return request
|
|
210
260
|
}
|
|
@@ -7,6 +7,7 @@ import org.chromium.net.UrlRequest as CronetUrlRequest
|
|
|
7
7
|
import org.chromium.net.UrlResponseInfo as CronetUrlResponseInfo
|
|
8
8
|
import org.chromium.net.CronetException as CronetNativeException
|
|
9
9
|
import java.nio.ByteBuffer
|
|
10
|
+
import java.util.UUID
|
|
10
11
|
import java.util.concurrent.Executor as JavaExecutor
|
|
11
12
|
|
|
12
13
|
@DoNotStrip
|
|
@@ -26,6 +27,16 @@ class NitroUrlRequestBuilder(
|
|
|
26
27
|
|
|
27
28
|
private val builder: CronetUrlRequest.Builder
|
|
28
29
|
private val byteBuffer: ByteBuffer
|
|
30
|
+
// BuildConfig.DEBUG short-circuits in release so R8 strips DevTools paths.
|
|
31
|
+
// UUID generation is gated too so SecureRandom isn't touched in release.
|
|
32
|
+
private val devToolsEnabled: Boolean = BuildConfig.DEBUG && DevToolsReporter.isDebuggingEnabled()
|
|
33
|
+
private val devToolsRequestId: String = if (devToolsEnabled) UUID.randomUUID().toString() else ""
|
|
34
|
+
private var devToolsBytes: Int = 0
|
|
35
|
+
private var devToolsTextual: Boolean = false
|
|
36
|
+
private var httpMethod: String = "GET"
|
|
37
|
+
private val requestHeaders: LinkedHashMap<String, String> = LinkedHashMap()
|
|
38
|
+
private var uploadBodyString: String = ""
|
|
39
|
+
private var uploadBodyLength: Long = 0L
|
|
29
40
|
|
|
30
41
|
init {
|
|
31
42
|
// Allocate ONE reusable owning buffer for all reads (64KB)
|
|
@@ -49,6 +60,19 @@ class NitroUrlRequestBuilder(
|
|
|
49
60
|
request: CronetUrlRequest,
|
|
50
61
|
info: CronetUrlResponseInfo
|
|
51
62
|
) {
|
|
63
|
+
if (devToolsEnabled) {
|
|
64
|
+
val headersMap = LinkedHashMap<String, String>()
|
|
65
|
+
info.allHeadersAsList.forEach { headersMap[it.key] = it.value }
|
|
66
|
+
val ct = headersMap["Content-Type"] ?: headersMap["content-type"]
|
|
67
|
+
devToolsTextual = DevToolsReporter.isTextualContentType(ct)
|
|
68
|
+
DevToolsReporter.reportResponseStart(
|
|
69
|
+
devToolsRequestId,
|
|
70
|
+
info.url,
|
|
71
|
+
info.httpStatusCode,
|
|
72
|
+
headersMap,
|
|
73
|
+
-1L
|
|
74
|
+
)
|
|
75
|
+
}
|
|
52
76
|
onResponseStartedCallback?.let { callback ->
|
|
53
77
|
val nitroInfo = info.toNitro()
|
|
54
78
|
callback(nitroInfo)
|
|
@@ -60,8 +84,19 @@ class NitroUrlRequestBuilder(
|
|
|
60
84
|
info: CronetUrlResponseInfo,
|
|
61
85
|
receivedBuffer: ByteBuffer
|
|
62
86
|
) {
|
|
87
|
+
val bytesRead = receivedBuffer.position()
|
|
88
|
+
if (devToolsEnabled && bytesRead > 0) {
|
|
89
|
+
devToolsBytes += bytesRead
|
|
90
|
+
DevToolsReporter.reportDataReceived(devToolsRequestId, bytesRead)
|
|
91
|
+
if (devToolsTextual) {
|
|
92
|
+
val dup = receivedBuffer.duplicate()
|
|
93
|
+
dup.flip()
|
|
94
|
+
val arr = ByteArray(dup.remaining())
|
|
95
|
+
dup.get(arr)
|
|
96
|
+
DevToolsReporter.storeResponseBodyIncremental(devToolsRequestId, String(arr, Charsets.UTF_8))
|
|
97
|
+
}
|
|
98
|
+
}
|
|
63
99
|
onReadCompletedCallback?.let { callback ->
|
|
64
|
-
val bytesRead = receivedBuffer.position()
|
|
65
100
|
val nitroInfo = info.toNitro()
|
|
66
101
|
callback(nitroInfo, reusableBuffer, bytesRead.toDouble())
|
|
67
102
|
}
|
|
@@ -71,6 +106,9 @@ class NitroUrlRequestBuilder(
|
|
|
71
106
|
request: CronetUrlRequest,
|
|
72
107
|
info: CronetUrlResponseInfo
|
|
73
108
|
) {
|
|
109
|
+
if (devToolsEnabled) {
|
|
110
|
+
DevToolsReporter.reportResponseEnd(devToolsRequestId, devToolsBytes.toLong())
|
|
111
|
+
}
|
|
74
112
|
onSucceededCallback?.let { callback ->
|
|
75
113
|
val nitroInfo = info.toNitro()
|
|
76
114
|
callback(nitroInfo)
|
|
@@ -82,6 +120,9 @@ class NitroUrlRequestBuilder(
|
|
|
82
120
|
info: CronetUrlResponseInfo?,
|
|
83
121
|
error: CronetNativeException
|
|
84
122
|
) {
|
|
123
|
+
if (devToolsEnabled) {
|
|
124
|
+
DevToolsReporter.reportRequestFailed(devToolsRequestId, false)
|
|
125
|
+
}
|
|
85
126
|
onFailedCallback?.let { callback ->
|
|
86
127
|
val nitroInfo = info?.toNitro()
|
|
87
128
|
val nitroError = error.toNitro()
|
|
@@ -93,6 +134,9 @@ class NitroUrlRequestBuilder(
|
|
|
93
134
|
request: CronetUrlRequest,
|
|
94
135
|
info: CronetUrlResponseInfo?
|
|
95
136
|
) {
|
|
137
|
+
if (devToolsEnabled) {
|
|
138
|
+
DevToolsReporter.reportRequestFailed(devToolsRequestId, true)
|
|
139
|
+
}
|
|
96
140
|
onCanceledCallback?.let { callback ->
|
|
97
141
|
val nitroInfo = info?.toNitro()
|
|
98
142
|
callback(nitroInfo)
|
|
@@ -104,11 +148,13 @@ class NitroUrlRequestBuilder(
|
|
|
104
148
|
}
|
|
105
149
|
|
|
106
150
|
override fun setHttpMethod(httpMethod: String) {
|
|
151
|
+
this.httpMethod = httpMethod
|
|
107
152
|
builder.setHttpMethod(httpMethod)
|
|
108
153
|
}
|
|
109
154
|
|
|
110
155
|
override fun addHeader(name: String, value: String) {
|
|
111
|
-
|
|
156
|
+
requestHeaders[name] = value
|
|
157
|
+
builder.addHeader(name, value)
|
|
112
158
|
}
|
|
113
159
|
|
|
114
160
|
override fun setUploadBody(body: Variant_ArrayBuffer_String) {
|
|
@@ -119,8 +165,12 @@ class NitroUrlRequestBuilder(
|
|
|
119
165
|
buffer.get(bytes)
|
|
120
166
|
bytes
|
|
121
167
|
}
|
|
122
|
-
is Variant_ArrayBuffer_String.Second ->
|
|
168
|
+
is Variant_ArrayBuffer_String.Second -> {
|
|
169
|
+
uploadBodyString = body.value
|
|
170
|
+
body.value.toByteArray(Charsets.UTF_8)
|
|
171
|
+
}
|
|
123
172
|
}
|
|
173
|
+
uploadBodyLength = bodyBytes.size.toLong()
|
|
124
174
|
|
|
125
175
|
val provider = object : org.chromium.net.UploadDataProvider() {
|
|
126
176
|
private var position = 0
|
|
@@ -176,6 +226,16 @@ class NitroUrlRequestBuilder(
|
|
|
176
226
|
|
|
177
227
|
override fun build(): HybridUrlRequestSpec {
|
|
178
228
|
val cronetRequest = builder.build()
|
|
229
|
+
if (devToolsEnabled) {
|
|
230
|
+
DevToolsReporter.reportRequestStart(
|
|
231
|
+
devToolsRequestId,
|
|
232
|
+
url,
|
|
233
|
+
httpMethod,
|
|
234
|
+
requestHeaders,
|
|
235
|
+
uploadBodyString,
|
|
236
|
+
uploadBodyLength
|
|
237
|
+
)
|
|
238
|
+
}
|
|
179
239
|
return NitroUrlRequest(cronetRequest, byteBuffer)
|
|
180
240
|
}
|
|
181
241
|
}
|
|
@@ -16,6 +16,13 @@ class HybridUrlRequestBuilder: HybridUrlRequestBuilderSpec {
|
|
|
16
16
|
|
|
17
17
|
private var urlRequest: URLRequest
|
|
18
18
|
private var priority: Float = 0.5
|
|
19
|
+
private let devToolsRequestId: String = {
|
|
20
|
+
#if DEBUG
|
|
21
|
+
return UUID().uuidString
|
|
22
|
+
#else
|
|
23
|
+
return ""
|
|
24
|
+
#endif
|
|
25
|
+
}()
|
|
19
26
|
|
|
20
27
|
init(
|
|
21
28
|
url: String,
|
|
@@ -89,7 +96,8 @@ class HybridUrlRequestBuilder: HybridUrlRequestBuilderSpec {
|
|
|
89
96
|
onFailed: onFailedCallback,
|
|
90
97
|
onCanceled: onCanceledCallback,
|
|
91
98
|
executor: executor,
|
|
92
|
-
hybridRequest: nil
|
|
99
|
+
hybridRequest: nil,
|
|
100
|
+
devToolsRequestId: devToolsRequestId
|
|
93
101
|
)
|
|
94
102
|
|
|
95
103
|
let config = URLSessionConfiguration.default
|
|
@@ -107,6 +115,12 @@ class HybridUrlRequestBuilder: HybridUrlRequestBuilderSpec {
|
|
|
107
115
|
task.priority = priority
|
|
108
116
|
delegate.task = task
|
|
109
117
|
|
|
118
|
+
#if DEBUG
|
|
119
|
+
if NitroDevToolsReporter.isDebuggingEnabled() {
|
|
120
|
+
NitroDevToolsReporter.reportRequestStart(withRequest: devToolsRequestId, request: urlRequest)
|
|
121
|
+
}
|
|
122
|
+
#endif
|
|
123
|
+
|
|
110
124
|
let request = HybridUrlRequest(task: task, delegate: delegate)
|
|
111
125
|
delegate.hybridRequest = request
|
|
112
126
|
|
|
@@ -129,6 +143,9 @@ private class URLSessionDelegateAdapter: NSObject, URLSessionDataDelegate, URLSe
|
|
|
129
143
|
weak var hybridRequest: HybridUrlRequest?
|
|
130
144
|
|
|
131
145
|
private var response: HTTPURLResponse?
|
|
146
|
+
private let devToolsRequestId: String
|
|
147
|
+
private var devToolsBytes: Int = 0
|
|
148
|
+
private var devToolsTextual: Bool = false
|
|
132
149
|
|
|
133
150
|
init(
|
|
134
151
|
onRedirectReceived: ((_ info: UrlResponseInfo, _ newLocationUrl: String) -> Void)?,
|
|
@@ -138,7 +155,8 @@ private class URLSessionDelegateAdapter: NSObject, URLSessionDataDelegate, URLSe
|
|
|
138
155
|
onFailed: ((_ info: UrlResponseInfo?, _ error: RequestException) -> Void)?,
|
|
139
156
|
onCanceled: ((_ info: UrlResponseInfo?) -> Void)?,
|
|
140
157
|
executor: DispatchQueue,
|
|
141
|
-
hybridRequest: HybridUrlRequest
|
|
158
|
+
hybridRequest: HybridUrlRequest?,
|
|
159
|
+
devToolsRequestId: String
|
|
142
160
|
) {
|
|
143
161
|
self.onRedirectReceived = onRedirectReceived
|
|
144
162
|
self.onResponseStarted = onResponseStarted
|
|
@@ -148,6 +166,7 @@ private class URLSessionDelegateAdapter: NSObject, URLSessionDataDelegate, URLSe
|
|
|
148
166
|
self.onCanceled = onCanceled
|
|
149
167
|
self.executor = executor
|
|
150
168
|
self.hybridRequest = hybridRequest
|
|
169
|
+
self.devToolsRequestId = devToolsRequestId
|
|
151
170
|
super.init()
|
|
152
171
|
}
|
|
153
172
|
|
|
@@ -181,11 +200,21 @@ private class URLSessionDelegateAdapter: NSObject, URLSessionDataDelegate, URLSe
|
|
|
181
200
|
if let error = error {
|
|
182
201
|
let nsError = error as NSError
|
|
183
202
|
if nsError.code == NSURLErrorCancelled {
|
|
203
|
+
#if DEBUG
|
|
204
|
+
if NitroDevToolsReporter.isDebuggingEnabled() {
|
|
205
|
+
NitroDevToolsReporter.reportRequestFailed(self.devToolsRequestId, cancelled: true)
|
|
206
|
+
}
|
|
207
|
+
#endif
|
|
184
208
|
if let callback = self.onCanceled {
|
|
185
209
|
let nitroInfo = self.response?.toNitro()
|
|
186
210
|
callback(nitroInfo)
|
|
187
211
|
}
|
|
188
212
|
} else {
|
|
213
|
+
#if DEBUG
|
|
214
|
+
if NitroDevToolsReporter.isDebuggingEnabled() {
|
|
215
|
+
NitroDevToolsReporter.reportRequestFailed(self.devToolsRequestId, cancelled: false)
|
|
216
|
+
}
|
|
217
|
+
#endif
|
|
189
218
|
if let callback = self.onFailed {
|
|
190
219
|
let nitroError = error.toNitro()
|
|
191
220
|
let nitroInfo = self.response?.toNitro()
|
|
@@ -193,6 +222,11 @@ private class URLSessionDelegateAdapter: NSObject, URLSessionDataDelegate, URLSe
|
|
|
193
222
|
}
|
|
194
223
|
}
|
|
195
224
|
} else if let response = self.response {
|
|
225
|
+
#if DEBUG
|
|
226
|
+
if NitroDevToolsReporter.isDebuggingEnabled() {
|
|
227
|
+
NitroDevToolsReporter.reportResponseEnd(self.devToolsRequestId, encodedDataLength: self.devToolsBytes)
|
|
228
|
+
}
|
|
229
|
+
#endif
|
|
196
230
|
if let callback = self.onSucceeded {
|
|
197
231
|
let info = response.toNitro()
|
|
198
232
|
callback(info)
|
|
@@ -216,6 +250,22 @@ private class URLSessionDelegateAdapter: NSObject, URLSessionDataDelegate, URLSe
|
|
|
216
250
|
|
|
217
251
|
executor.sync { [weak self] in
|
|
218
252
|
guard let self = self else { return }
|
|
253
|
+
#if DEBUG
|
|
254
|
+
if NitroDevToolsReporter.isDebuggingEnabled() {
|
|
255
|
+
var headerDict: [String: String] = [:]
|
|
256
|
+
httpResponse.allHeaderFields.forEach { k, v in
|
|
257
|
+
if let key = k as? String { headerDict[key] = String(describing: v) }
|
|
258
|
+
}
|
|
259
|
+
NitroDevToolsReporter.reportResponseStart(
|
|
260
|
+
self.devToolsRequestId,
|
|
261
|
+
url: httpResponse.url?.absoluteString ?? "",
|
|
262
|
+
statusCode: httpResponse.statusCode,
|
|
263
|
+
headers: headerDict
|
|
264
|
+
)
|
|
265
|
+
let ct = headerDict["Content-Type"] ?? headerDict["content-type"]
|
|
266
|
+
self.devToolsTextual = NitroDevToolsReporter.isTextualContentType(ct)
|
|
267
|
+
}
|
|
268
|
+
#endif
|
|
219
269
|
if let callback = self.onResponseStarted {
|
|
220
270
|
let info = httpResponse.toNitro()
|
|
221
271
|
callback(info)
|
|
@@ -234,6 +284,16 @@ private class URLSessionDelegateAdapter: NSObject, URLSessionDataDelegate, URLSe
|
|
|
234
284
|
executor.sync { [weak self] in
|
|
235
285
|
guard let self = self, let response = self.response else { return }
|
|
236
286
|
|
|
287
|
+
#if DEBUG
|
|
288
|
+
if NitroDevToolsReporter.isDebuggingEnabled() {
|
|
289
|
+
self.devToolsBytes += data.count
|
|
290
|
+
NitroDevToolsReporter.reportDataReceived(self.devToolsRequestId, length: data.count)
|
|
291
|
+
if self.devToolsTextual, let text = String(data: data, encoding: .utf8) {
|
|
292
|
+
NitroDevToolsReporter.storeResponseBodyIncremental(self.devToolsRequestId, text: text)
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
#endif
|
|
296
|
+
|
|
237
297
|
let arrayBuffer: ArrayBuffer
|
|
238
298
|
do {
|
|
239
299
|
arrayBuffer = try ArrayBuffer.copy(data: data)
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
#import <Foundation/Foundation.h>
|
|
2
|
+
|
|
3
|
+
NS_ASSUME_NONNULL_BEGIN
|
|
4
|
+
|
|
5
|
+
/// Swift-friendly facade over RCTInspectorNetworkReporter.
|
|
6
|
+
/// All methods are no-ops when the modern CDP debugger is not attached
|
|
7
|
+
/// (checked via -isDebuggingEnabled). Safe to call in release builds.
|
|
8
|
+
@interface NitroDevToolsReporter : NSObject
|
|
9
|
+
|
|
10
|
+
+ (BOOL)isDebuggingEnabled;
|
|
11
|
+
|
|
12
|
+
+ (void)reportRequestStartWithRequest:(NSString *)requestId
|
|
13
|
+
request:(NSURLRequest *)request;
|
|
14
|
+
|
|
15
|
+
+ (void)reportRequestStart:(NSString *)requestId
|
|
16
|
+
url:(NSString *)url
|
|
17
|
+
method:(NSString *)method
|
|
18
|
+
headers:(NSDictionary<NSString *, NSString *> *)headers
|
|
19
|
+
bodyString:(nullable NSString *)bodyString;
|
|
20
|
+
|
|
21
|
+
+ (void)reportResponseStart:(NSString *)requestId
|
|
22
|
+
url:(NSString *)url
|
|
23
|
+
statusCode:(NSInteger)statusCode
|
|
24
|
+
headers:(NSDictionary<NSString *, NSString *> *)headers;
|
|
25
|
+
|
|
26
|
+
+ (void)reportDataReceived:(NSString *)requestId length:(NSInteger)length;
|
|
27
|
+
|
|
28
|
+
+ (void)reportResponseEnd:(NSString *)requestId encodedDataLength:(NSInteger)length;
|
|
29
|
+
|
|
30
|
+
+ (void)reportRequestFailed:(NSString *)requestId cancelled:(BOOL)cancelled;
|
|
31
|
+
|
|
32
|
+
+ (void)storeResponseBody:(NSString *)requestId
|
|
33
|
+
data:(NSData *)data
|
|
34
|
+
base64Encoded:(BOOL)base64Encoded;
|
|
35
|
+
|
|
36
|
+
+ (void)storeResponseBodyIncremental:(NSString *)requestId text:(NSString *)text;
|
|
37
|
+
|
|
38
|
+
+ (BOOL)isTextualContentType:(nullable NSString *)contentType;
|
|
39
|
+
|
|
40
|
+
@end
|
|
41
|
+
|
|
42
|
+
NS_ASSUME_NONNULL_END
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
#import "NitroDevToolsReporter.h"
|
|
2
|
+
|
|
3
|
+
// RCTInspectorNetworkReporter is bundled in React-RCTNetwork. In RN < 0.76
|
|
4
|
+
// or when the header isn't visible (release-optimized module maps, OSS forks),
|
|
5
|
+
// we compile to a no-op so the module still builds.
|
|
6
|
+
#if __has_include(<React/RCTInspectorNetworkReporter.h>)
|
|
7
|
+
#import <React/RCTInspectorNetworkReporter.h>
|
|
8
|
+
#define NITRO_HAS_NETWORK_REPORTER 1
|
|
9
|
+
#else
|
|
10
|
+
#define NITRO_HAS_NETWORK_REPORTER 0
|
|
11
|
+
#endif
|
|
12
|
+
|
|
13
|
+
// During cold-start prefetch, RN's RCTInspectorNetworkReporter class may not
|
|
14
|
+
// be realized yet (its underlying C++ NetworkReporter singleton is brought up
|
|
15
|
+
// when the bridge initializes). Every entry point goes through +reporterClass,
|
|
16
|
+
// which uses NSClassFromString so a missing class becomes a clean no-op
|
|
17
|
+
// instead of crashing on a not-yet-initialized C++ singleton.
|
|
18
|
+
|
|
19
|
+
@implementation NitroDevToolsReporter
|
|
20
|
+
|
|
21
|
+
+ (Class _Nullable)reporterClass {
|
|
22
|
+
#if NITRO_HAS_NETWORK_REPORTER
|
|
23
|
+
static Class cached = Nil;
|
|
24
|
+
if (cached == Nil) {
|
|
25
|
+
cached = NSClassFromString(@"RCTInspectorNetworkReporter");
|
|
26
|
+
}
|
|
27
|
+
return cached;
|
|
28
|
+
#else
|
|
29
|
+
return Nil;
|
|
30
|
+
#endif
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
+ (BOOL)isDebuggingEnabled {
|
|
34
|
+
return [self reporterClass] != Nil;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
+ (void)reportRequestStartWithRequest:(NSString *)requestId
|
|
38
|
+
request:(NSURLRequest *)request {
|
|
39
|
+
#if NITRO_HAS_NETWORK_REPORTER
|
|
40
|
+
if (request == nil) return;
|
|
41
|
+
Class cls = [self reporterClass];
|
|
42
|
+
if (cls == Nil) return;
|
|
43
|
+
int encoded = (int)(request.HTTPBody.length);
|
|
44
|
+
[cls reportRequestStart:requestId request:request encodedDataLength:encoded];
|
|
45
|
+
[cls reportConnectionTiming:requestId request:request];
|
|
46
|
+
#endif
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
+ (void)reportRequestStart:(NSString *)requestId
|
|
50
|
+
url:(NSString *)url
|
|
51
|
+
method:(NSString *)method
|
|
52
|
+
headers:(NSDictionary<NSString *, NSString *> *)headers
|
|
53
|
+
bodyString:(NSString *)bodyString {
|
|
54
|
+
#if NITRO_HAS_NETWORK_REPORTER
|
|
55
|
+
Class cls = [self reporterClass];
|
|
56
|
+
if (cls == Nil) return;
|
|
57
|
+
NSURL *u = [NSURL URLWithString:url];
|
|
58
|
+
if (u == nil) return;
|
|
59
|
+
NSMutableURLRequest *req = [NSMutableURLRequest requestWithURL:u];
|
|
60
|
+
req.HTTPMethod = method ?: @"GET";
|
|
61
|
+
for (NSString *k in headers) {
|
|
62
|
+
[req setValue:headers[k] forHTTPHeaderField:k];
|
|
63
|
+
}
|
|
64
|
+
if (bodyString.length > 0) {
|
|
65
|
+
req.HTTPBody = [bodyString dataUsingEncoding:NSUTF8StringEncoding];
|
|
66
|
+
}
|
|
67
|
+
NSInteger encoded = bodyString ? (NSInteger)[bodyString lengthOfBytesUsingEncoding:NSUTF8StringEncoding] : 0;
|
|
68
|
+
[cls reportRequestStart:requestId request:req encodedDataLength:(int)encoded];
|
|
69
|
+
[cls reportConnectionTiming:requestId request:req];
|
|
70
|
+
#endif
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
+ (void)reportResponseStart:(NSString *)requestId
|
|
74
|
+
url:(NSString *)url
|
|
75
|
+
statusCode:(NSInteger)statusCode
|
|
76
|
+
headers:(NSDictionary<NSString *, NSString *> *)headers {
|
|
77
|
+
#if NITRO_HAS_NETWORK_REPORTER
|
|
78
|
+
Class cls = [self reporterClass];
|
|
79
|
+
if (cls == Nil) return;
|
|
80
|
+
NSURL *u = [NSURL URLWithString:url];
|
|
81
|
+
if (u == nil) return;
|
|
82
|
+
NSHTTPURLResponse *resp = [[NSHTTPURLResponse alloc] initWithURL:u
|
|
83
|
+
statusCode:statusCode
|
|
84
|
+
HTTPVersion:@"HTTP/1.1"
|
|
85
|
+
headerFields:headers];
|
|
86
|
+
[cls reportResponseStart:requestId
|
|
87
|
+
response:resp
|
|
88
|
+
statusCode:(int)statusCode
|
|
89
|
+
headers:headers];
|
|
90
|
+
#endif
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
+ (void)reportDataReceived:(NSString *)requestId length:(NSInteger)length {
|
|
94
|
+
#if NITRO_HAS_NETWORK_REPORTER
|
|
95
|
+
if (length <= 0) return;
|
|
96
|
+
Class cls = [self reporterClass];
|
|
97
|
+
if (cls == Nil) return;
|
|
98
|
+
// Only data.length is read by the underlying reporter. Avoid allocating a
|
|
99
|
+
// zero-filled buffer by handing NSData a non-owned byte pointer and the
|
|
100
|
+
// intended length.
|
|
101
|
+
static uint8_t sentinel;
|
|
102
|
+
NSData *sized = [NSData dataWithBytesNoCopy:&sentinel length:(NSUInteger)length freeWhenDone:NO];
|
|
103
|
+
[cls reportDataReceived:requestId data:sized];
|
|
104
|
+
#endif
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
+ (void)reportResponseEnd:(NSString *)requestId encodedDataLength:(NSInteger)length {
|
|
108
|
+
#if NITRO_HAS_NETWORK_REPORTER
|
|
109
|
+
Class cls = [self reporterClass];
|
|
110
|
+
if (cls == Nil) return;
|
|
111
|
+
[cls reportResponseEnd:requestId encodedDataLength:(int)length];
|
|
112
|
+
#endif
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
+ (void)reportRequestFailed:(NSString *)requestId cancelled:(BOOL)cancelled {
|
|
116
|
+
#if NITRO_HAS_NETWORK_REPORTER
|
|
117
|
+
Class cls = [self reporterClass];
|
|
118
|
+
if (cls == Nil) return;
|
|
119
|
+
[cls reportRequestFailed:requestId cancelled:cancelled];
|
|
120
|
+
#endif
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
+ (void)storeResponseBody:(NSString *)requestId
|
|
124
|
+
data:(NSData *)data
|
|
125
|
+
base64Encoded:(BOOL)base64Encoded {
|
|
126
|
+
#if NITRO_HAS_NETWORK_REPORTER
|
|
127
|
+
Class cls = [self reporterClass];
|
|
128
|
+
if (cls == Nil) return;
|
|
129
|
+
[cls maybeStoreResponseBody:requestId data:data base64Encoded:base64Encoded];
|
|
130
|
+
#endif
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
+ (void)storeResponseBodyIncremental:(NSString *)requestId text:(NSString *)text {
|
|
134
|
+
#if NITRO_HAS_NETWORK_REPORTER
|
|
135
|
+
Class cls = [self reporterClass];
|
|
136
|
+
if (cls == Nil) return;
|
|
137
|
+
[cls maybeStoreResponseBodyIncremental:requestId data:text];
|
|
138
|
+
#endif
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
+ (BOOL)isTextualContentType:(NSString *)contentType {
|
|
142
|
+
if (contentType == nil) return NO;
|
|
143
|
+
NSString *ct = [contentType lowercaseString];
|
|
144
|
+
if ([ct hasPrefix:@"text/"]) return YES;
|
|
145
|
+
if ([ct containsString:@"application/json"]) return YES;
|
|
146
|
+
if ([ct containsString:@"application/xml"]) return YES;
|
|
147
|
+
if ([ct containsString:@"application/javascript"]) return YES;
|
|
148
|
+
if ([ct containsString:@"+json"]) return YES;
|
|
149
|
+
if ([ct containsString:@"+xml"]) return YES;
|
|
150
|
+
return NO;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
@end
|
|
@@ -170,8 +170,34 @@ final class NitroFetchClient: HybridNitroFetchClientSpec {
|
|
|
170
170
|
"%{public}s %{public}s", traceMethod, tracePath)
|
|
171
171
|
#endif
|
|
172
172
|
|
|
173
|
-
|
|
173
|
+
// DevTools/CDP reporting is gated on `#if DEBUG` so the entire block is
|
|
174
|
+
// compiled out of release builds — no runtime cost, no symbol references.
|
|
175
|
+
#if DEBUG
|
|
176
|
+
let devToolsId = req.requestId ?? UUID().uuidString
|
|
177
|
+
if NitroDevToolsReporter.isDebuggingEnabled() {
|
|
178
|
+
NitroDevToolsReporter.reportRequestStart(withRequest: devToolsId, request: urlRequest)
|
|
179
|
+
}
|
|
180
|
+
#endif
|
|
181
|
+
|
|
182
|
+
let data: Data
|
|
183
|
+
let response: URLResponse
|
|
184
|
+
do {
|
|
185
|
+
(data, response) = try await session.data(for: urlRequest, delegate: delegate)
|
|
186
|
+
} catch {
|
|
187
|
+
#if DEBUG
|
|
188
|
+
if NitroDevToolsReporter.isDebuggingEnabled() {
|
|
189
|
+
let cancelled = (error as NSError).code == NSURLErrorCancelled
|
|
190
|
+
NitroDevToolsReporter.reportRequestFailed(devToolsId, cancelled: cancelled)
|
|
191
|
+
}
|
|
192
|
+
#endif
|
|
193
|
+
throw error
|
|
194
|
+
}
|
|
174
195
|
guard let http = response as? HTTPURLResponse else {
|
|
196
|
+
#if DEBUG
|
|
197
|
+
if NitroDevToolsReporter.isDebuggingEnabled() {
|
|
198
|
+
NitroDevToolsReporter.reportRequestFailed(devToolsId, cancelled: false)
|
|
199
|
+
}
|
|
200
|
+
#endif
|
|
175
201
|
throw NSError(domain: "NitroFetch", code: -1, userInfo: [NSLocalizedDescriptionKey: "Invalid response"])
|
|
176
202
|
}
|
|
177
203
|
|
|
@@ -180,6 +206,28 @@ final class NitroFetchClient: HybridNitroFetchClientSpec {
|
|
|
180
206
|
return NitroHeader(key: key, value: String(describing: v))
|
|
181
207
|
}
|
|
182
208
|
|
|
209
|
+
#if DEBUG
|
|
210
|
+
if NitroDevToolsReporter.isDebuggingEnabled() {
|
|
211
|
+
var headerDict: [String: String] = [:]
|
|
212
|
+
for h in headersPairs { headerDict[h.key] = h.value }
|
|
213
|
+
NitroDevToolsReporter.reportResponseStart(
|
|
214
|
+
devToolsId,
|
|
215
|
+
url: finalURL?.absoluteString ?? http.url?.absoluteString ?? req.url,
|
|
216
|
+
statusCode: http.statusCode,
|
|
217
|
+
headers: headerDict
|
|
218
|
+
)
|
|
219
|
+
NitroDevToolsReporter.reportDataReceived(devToolsId, length: data.count)
|
|
220
|
+
if NitroDevToolsReporter.isTextualContentType(headerDict["Content-Type"] ?? headerDict["content-type"]) {
|
|
221
|
+
if let text = String(data: data, encoding: .utf8) {
|
|
222
|
+
NitroDevToolsReporter.storeResponseBody(devToolsId, data: Data(text.utf8), base64Encoded: false)
|
|
223
|
+
}
|
|
224
|
+
} else if data.count > 0 && data.count <= 5 * 1024 * 1024 {
|
|
225
|
+
NitroDevToolsReporter.storeResponseBody(devToolsId, data: data, base64Encoded: true)
|
|
226
|
+
}
|
|
227
|
+
NitroDevToolsReporter.reportResponseEnd(devToolsId, encodedDataLength: data.count)
|
|
228
|
+
}
|
|
229
|
+
#endif
|
|
230
|
+
|
|
183
231
|
// Choose bodyString by default (matching Android’s first pass)
|
|
184
232
|
let charset = NitroFetchClient.detectCharset(from: http) ?? String.Encoding.utf8
|
|
185
233
|
let bodyStr = String(data: data, encoding: charset) ?? String(data: data, encoding: .utf8)
|