react-native-nitro-fetch 1.0.3 → 1.1.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 +132 -0
- package/android/src/main/java/com/margelo/nitro/nitrofetch/NitroFetchClient.kt +46 -0
- package/android/src/main/java/com/margelo/nitro/nitrofetch/NitroUrlRequestBuilder.kt +61 -3
- package/ios/HybridUrlRequestBuilder.swift +44 -2
- package/ios/NitroDevToolsReporter.h +42 -0
- package/ios/NitroDevToolsReporter.mm +129 -0
- package/ios/NitroFetchClient.swift +39 -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,132 @@
|
|
|
1
|
+
package com.margelo.nitro.nitrofetch
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Thin facade over React Native's [com.facebook.react.modules.network.InspectorNetworkReporter].
|
|
5
|
+
* All methods are no-ops when the modern CDP debugger is not attached (guarded by
|
|
6
|
+
* `isDebuggingEnabled()` inside RN), so they are safe to call in release builds.
|
|
7
|
+
*
|
|
8
|
+
* The underlying reporter is marked `internal` in RN but is the officially documented
|
|
9
|
+
* Kotlin entry point for third-party networking libraries to surface requests in the
|
|
10
|
+
* Fusebox / RN DevTools Network tab. We intentionally bypass the visibility modifier
|
|
11
|
+
* rather than duplicating the JNI bindings, matching the pattern used by other
|
|
12
|
+
* community HTTP clients. If the class isn't present (RN < 0.76 or a trimmed
|
|
13
|
+
* distribution), every call becomes a no-op.
|
|
14
|
+
*/
|
|
15
|
+
@Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
|
|
16
|
+
internal object DevToolsReporter {
|
|
17
|
+
private val available: Boolean = try {
|
|
18
|
+
Class.forName("com.facebook.react.modules.network.InspectorNetworkReporter")
|
|
19
|
+
true
|
|
20
|
+
} catch (_: Throwable) {
|
|
21
|
+
false
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
fun isDebuggingEnabled(): Boolean {
|
|
25
|
+
if (!available) return false
|
|
26
|
+
return try {
|
|
27
|
+
com.facebook.react.modules.network.InspectorNetworkReporter.isDebuggingEnabled()
|
|
28
|
+
} catch (_: Throwable) {
|
|
29
|
+
false
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
fun reportRequestStart(
|
|
34
|
+
requestId: String,
|
|
35
|
+
url: String,
|
|
36
|
+
method: String,
|
|
37
|
+
headers: Map<String, String>,
|
|
38
|
+
body: String,
|
|
39
|
+
encodedDataLength: Long
|
|
40
|
+
) {
|
|
41
|
+
if (!available) return
|
|
42
|
+
try {
|
|
43
|
+
com.facebook.react.modules.network.InspectorNetworkReporter.reportRequestStart(
|
|
44
|
+
requestId, url, method, headers, body, encodedDataLength
|
|
45
|
+
)
|
|
46
|
+
com.facebook.react.modules.network.InspectorNetworkReporter.reportConnectionTiming(requestId, headers)
|
|
47
|
+
} catch (_: Throwable) {
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
fun reportResponseStart(
|
|
52
|
+
requestId: String,
|
|
53
|
+
url: String,
|
|
54
|
+
statusCode: Int,
|
|
55
|
+
headers: Map<String, String>,
|
|
56
|
+
expectedDataLength: Long
|
|
57
|
+
) {
|
|
58
|
+
if (!available) return
|
|
59
|
+
try {
|
|
60
|
+
com.facebook.react.modules.network.InspectorNetworkReporter.reportResponseStart(
|
|
61
|
+
requestId, url, statusCode, headers, expectedDataLength
|
|
62
|
+
)
|
|
63
|
+
} catch (_: Throwable) {
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
fun reportDataReceived(requestId: String, length: Int) {
|
|
68
|
+
if (!available) return
|
|
69
|
+
try {
|
|
70
|
+
com.facebook.react.modules.network.InspectorNetworkReporter.reportDataReceivedImpl(requestId, length)
|
|
71
|
+
} catch (_: Throwable) {
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
fun reportResponseEnd(requestId: String, encodedDataLength: Long) {
|
|
76
|
+
if (!available) return
|
|
77
|
+
try {
|
|
78
|
+
com.facebook.react.modules.network.InspectorNetworkReporter.reportResponseEnd(requestId, encodedDataLength)
|
|
79
|
+
} catch (_: Throwable) {
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
fun reportRequestFailed(requestId: String, cancelled: Boolean) {
|
|
84
|
+
if (!available) return
|
|
85
|
+
try {
|
|
86
|
+
com.facebook.react.modules.network.InspectorNetworkReporter.reportRequestFailed(requestId, cancelled)
|
|
87
|
+
} catch (_: Throwable) {
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
fun storeResponseBody(requestId: String, body: String, base64Encoded: Boolean) {
|
|
92
|
+
if (!available) return
|
|
93
|
+
try {
|
|
94
|
+
com.facebook.react.modules.network.InspectorNetworkReporter.maybeStoreResponseBody(
|
|
95
|
+
requestId, body, base64Encoded
|
|
96
|
+
)
|
|
97
|
+
} catch (_: Throwable) {
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
fun storeResponseBodyIncremental(requestId: String, data: String) {
|
|
102
|
+
if (!available) return
|
|
103
|
+
try {
|
|
104
|
+
com.facebook.react.modules.network.InspectorNetworkReporter.maybeStoreResponseBodyIncremental(requestId, data)
|
|
105
|
+
} catch (_: Throwable) {
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
fun isTextualContentType(contentType: String?): Boolean {
|
|
110
|
+
if (contentType == null) return false
|
|
111
|
+
val ct = contentType.lowercase()
|
|
112
|
+
return ct.startsWith("text/") ||
|
|
113
|
+
ct.contains("application/json") ||
|
|
114
|
+
ct.contains("application/xml") ||
|
|
115
|
+
ct.contains("application/javascript") ||
|
|
116
|
+
ct.contains("+json") ||
|
|
117
|
+
ct.contains("+xml")
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
fun headersArrayToMap(headers: Array<NitroHeader>?): Map<String, String> {
|
|
121
|
+
if (headers == null) return emptyMap()
|
|
122
|
+
val map = LinkedHashMap<String, String>(headers.size)
|
|
123
|
+
for (h in headers) map[h.key] = h.value
|
|
124
|
+
return map
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
fun headersListToMap(entries: List<Map.Entry<String, String>>): Map<String, String> {
|
|
128
|
+
val map = LinkedHashMap<String, String>(entries.size)
|
|
129
|
+
for (e in entries) map[e.key] = e.value
|
|
130
|
+
return map
|
|
131
|
+
}
|
|
132
|
+
}
|
|
@@ -80,10 +80,14 @@ 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
|
+
val devToolsRequestId = req.requestId ?: UUID.randomUUID().toString()
|
|
84
|
+
val devToolsEnabled = DevToolsReporter.isDebuggingEnabled()
|
|
83
85
|
val callback = object : UrlRequest.Callback() {
|
|
84
86
|
private val buffer = ByteBuffer.allocateDirect(16 * 1024)
|
|
85
87
|
private val out = java.io.ByteArrayOutputStream()
|
|
86
88
|
private var redirectStopped = false
|
|
89
|
+
private var devToolsBytes = 0
|
|
90
|
+
private var devToolsTextual = false
|
|
87
91
|
|
|
88
92
|
override fun onRedirectReceived(request: UrlRequest, info: UrlResponseInfo, newLocationUrl: String) {
|
|
89
93
|
if (shouldFollowRedirects) {
|
|
@@ -113,6 +117,19 @@ class NitroFetchClient(private val engine: CronetEngine, private val executor: E
|
|
|
113
117
|
}
|
|
114
118
|
|
|
115
119
|
override fun onResponseStarted(request: UrlRequest, info: UrlResponseInfo) {
|
|
120
|
+
if (devToolsEnabled) {
|
|
121
|
+
val headersMap = LinkedHashMap<String, String>()
|
|
122
|
+
info.allHeadersAsList.forEach { headersMap[it.key] = it.value }
|
|
123
|
+
val contentType = headersMap["Content-Type"] ?: headersMap["content-type"]
|
|
124
|
+
devToolsTextual = DevToolsReporter.isTextualContentType(contentType)
|
|
125
|
+
DevToolsReporter.reportResponseStart(
|
|
126
|
+
devToolsRequestId,
|
|
127
|
+
info.url,
|
|
128
|
+
info.httpStatusCode,
|
|
129
|
+
headersMap,
|
|
130
|
+
-1L
|
|
131
|
+
)
|
|
132
|
+
}
|
|
116
133
|
buffer.clear()
|
|
117
134
|
request.read(buffer)
|
|
118
135
|
}
|
|
@@ -122,6 +139,13 @@ class NitroFetchClient(private val engine: CronetEngine, private val executor: E
|
|
|
122
139
|
val bytes = ByteArray(byteBuffer.remaining())
|
|
123
140
|
byteBuffer.get(bytes)
|
|
124
141
|
out.write(bytes)
|
|
142
|
+
if (devToolsEnabled) {
|
|
143
|
+
devToolsBytes += bytes.size
|
|
144
|
+
DevToolsReporter.reportDataReceived(devToolsRequestId, bytes.size)
|
|
145
|
+
if (devToolsTextual) {
|
|
146
|
+
DevToolsReporter.storeResponseBodyIncremental(devToolsRequestId, String(bytes, Charsets.UTF_8))
|
|
147
|
+
}
|
|
148
|
+
}
|
|
125
149
|
byteBuffer.clear()
|
|
126
150
|
request.read(byteBuffer)
|
|
127
151
|
}
|
|
@@ -130,6 +154,9 @@ class NitroFetchClient(private val engine: CronetEngine, private val executor: E
|
|
|
130
154
|
if (BuildConfig.NITRO_FETCH_TRACING) {
|
|
131
155
|
Trace.endAsyncSection(traceLabel, traceCookie)
|
|
132
156
|
}
|
|
157
|
+
if (devToolsEnabled) {
|
|
158
|
+
DevToolsReporter.reportResponseEnd(devToolsRequestId, devToolsBytes.toLong())
|
|
159
|
+
}
|
|
133
160
|
try {
|
|
134
161
|
val headersArr: Array<NitroHeader> =
|
|
135
162
|
info.allHeadersAsList.map { NitroHeader(it.key, it.value) }.toTypedArray()
|
|
@@ -166,6 +193,9 @@ class NitroFetchClient(private val engine: CronetEngine, private val executor: E
|
|
|
166
193
|
if (BuildConfig.NITRO_FETCH_TRACING) {
|
|
167
194
|
Trace.endAsyncSection(traceLabel, traceCookie)
|
|
168
195
|
}
|
|
196
|
+
if (devToolsEnabled) {
|
|
197
|
+
DevToolsReporter.reportRequestFailed(devToolsRequestId, false)
|
|
198
|
+
}
|
|
169
199
|
onFail(RuntimeException("Cronet failed: ${error.message}", error))
|
|
170
200
|
}
|
|
171
201
|
|
|
@@ -173,6 +203,9 @@ class NitroFetchClient(private val engine: CronetEngine, private val executor: E
|
|
|
173
203
|
if (BuildConfig.NITRO_FETCH_TRACING) {
|
|
174
204
|
Trace.endAsyncSection(traceLabel, traceCookie)
|
|
175
205
|
}
|
|
206
|
+
if (devToolsEnabled) {
|
|
207
|
+
DevToolsReporter.reportRequestFailed(devToolsRequestId, true)
|
|
208
|
+
}
|
|
176
209
|
if (!redirectStopped) {
|
|
177
210
|
onFail(RuntimeException("Cronet canceled"))
|
|
178
211
|
}
|
|
@@ -205,6 +238,19 @@ class NitroFetchClient(private val engine: CronetEngine, private val executor: E
|
|
|
205
238
|
}
|
|
206
239
|
|
|
207
240
|
val request = builder.build()
|
|
241
|
+
if (devToolsEnabled) {
|
|
242
|
+
val headersMap = DevToolsReporter.headersArrayToMap(req.headers)
|
|
243
|
+
val body = req.bodyString ?: ""
|
|
244
|
+
val encoded = body.toByteArray(Charsets.UTF_8).size.toLong()
|
|
245
|
+
DevToolsReporter.reportRequestStart(
|
|
246
|
+
devToolsRequestId,
|
|
247
|
+
url,
|
|
248
|
+
method,
|
|
249
|
+
headersMap,
|
|
250
|
+
body,
|
|
251
|
+
encoded
|
|
252
|
+
)
|
|
253
|
+
}
|
|
208
254
|
request.start()
|
|
209
255
|
return request
|
|
210
256
|
}
|
|
@@ -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,14 @@ class NitroUrlRequestBuilder(
|
|
|
26
27
|
|
|
27
28
|
private val builder: CronetUrlRequest.Builder
|
|
28
29
|
private val byteBuffer: ByteBuffer
|
|
30
|
+
private val devToolsRequestId: String = UUID.randomUUID().toString()
|
|
31
|
+
private val devToolsEnabled: Boolean = DevToolsReporter.isDebuggingEnabled()
|
|
32
|
+
private var devToolsBytes: Int = 0
|
|
33
|
+
private var devToolsTextual: Boolean = false
|
|
34
|
+
private var httpMethod: String = "GET"
|
|
35
|
+
private val requestHeaders: LinkedHashMap<String, String> = LinkedHashMap()
|
|
36
|
+
private var uploadBodyString: String = ""
|
|
37
|
+
private var uploadBodyLength: Long = 0L
|
|
29
38
|
|
|
30
39
|
init {
|
|
31
40
|
// Allocate ONE reusable owning buffer for all reads (64KB)
|
|
@@ -49,6 +58,19 @@ class NitroUrlRequestBuilder(
|
|
|
49
58
|
request: CronetUrlRequest,
|
|
50
59
|
info: CronetUrlResponseInfo
|
|
51
60
|
) {
|
|
61
|
+
if (devToolsEnabled) {
|
|
62
|
+
val headersMap = LinkedHashMap<String, String>()
|
|
63
|
+
info.allHeadersAsList.forEach { headersMap[it.key] = it.value }
|
|
64
|
+
val ct = headersMap["Content-Type"] ?: headersMap["content-type"]
|
|
65
|
+
devToolsTextual = DevToolsReporter.isTextualContentType(ct)
|
|
66
|
+
DevToolsReporter.reportResponseStart(
|
|
67
|
+
devToolsRequestId,
|
|
68
|
+
info.url,
|
|
69
|
+
info.httpStatusCode,
|
|
70
|
+
headersMap,
|
|
71
|
+
-1L
|
|
72
|
+
)
|
|
73
|
+
}
|
|
52
74
|
onResponseStartedCallback?.let { callback ->
|
|
53
75
|
val nitroInfo = info.toNitro()
|
|
54
76
|
callback(nitroInfo)
|
|
@@ -60,8 +82,19 @@ class NitroUrlRequestBuilder(
|
|
|
60
82
|
info: CronetUrlResponseInfo,
|
|
61
83
|
receivedBuffer: ByteBuffer
|
|
62
84
|
) {
|
|
85
|
+
val bytesRead = receivedBuffer.position()
|
|
86
|
+
if (devToolsEnabled && bytesRead > 0) {
|
|
87
|
+
devToolsBytes += bytesRead
|
|
88
|
+
DevToolsReporter.reportDataReceived(devToolsRequestId, bytesRead)
|
|
89
|
+
if (devToolsTextual) {
|
|
90
|
+
val dup = receivedBuffer.duplicate()
|
|
91
|
+
dup.flip()
|
|
92
|
+
val arr = ByteArray(dup.remaining())
|
|
93
|
+
dup.get(arr)
|
|
94
|
+
DevToolsReporter.storeResponseBodyIncremental(devToolsRequestId, String(arr, Charsets.UTF_8))
|
|
95
|
+
}
|
|
96
|
+
}
|
|
63
97
|
onReadCompletedCallback?.let { callback ->
|
|
64
|
-
val bytesRead = receivedBuffer.position()
|
|
65
98
|
val nitroInfo = info.toNitro()
|
|
66
99
|
callback(nitroInfo, reusableBuffer, bytesRead.toDouble())
|
|
67
100
|
}
|
|
@@ -71,6 +104,9 @@ class NitroUrlRequestBuilder(
|
|
|
71
104
|
request: CronetUrlRequest,
|
|
72
105
|
info: CronetUrlResponseInfo
|
|
73
106
|
) {
|
|
107
|
+
if (devToolsEnabled) {
|
|
108
|
+
DevToolsReporter.reportResponseEnd(devToolsRequestId, devToolsBytes.toLong())
|
|
109
|
+
}
|
|
74
110
|
onSucceededCallback?.let { callback ->
|
|
75
111
|
val nitroInfo = info.toNitro()
|
|
76
112
|
callback(nitroInfo)
|
|
@@ -82,6 +118,9 @@ class NitroUrlRequestBuilder(
|
|
|
82
118
|
info: CronetUrlResponseInfo?,
|
|
83
119
|
error: CronetNativeException
|
|
84
120
|
) {
|
|
121
|
+
if (devToolsEnabled) {
|
|
122
|
+
DevToolsReporter.reportRequestFailed(devToolsRequestId, false)
|
|
123
|
+
}
|
|
85
124
|
onFailedCallback?.let { callback ->
|
|
86
125
|
val nitroInfo = info?.toNitro()
|
|
87
126
|
val nitroError = error.toNitro()
|
|
@@ -93,6 +132,9 @@ class NitroUrlRequestBuilder(
|
|
|
93
132
|
request: CronetUrlRequest,
|
|
94
133
|
info: CronetUrlResponseInfo?
|
|
95
134
|
) {
|
|
135
|
+
if (devToolsEnabled) {
|
|
136
|
+
DevToolsReporter.reportRequestFailed(devToolsRequestId, true)
|
|
137
|
+
}
|
|
96
138
|
onCanceledCallback?.let { callback ->
|
|
97
139
|
val nitroInfo = info?.toNitro()
|
|
98
140
|
callback(nitroInfo)
|
|
@@ -104,11 +146,13 @@ class NitroUrlRequestBuilder(
|
|
|
104
146
|
}
|
|
105
147
|
|
|
106
148
|
override fun setHttpMethod(httpMethod: String) {
|
|
149
|
+
this.httpMethod = httpMethod
|
|
107
150
|
builder.setHttpMethod(httpMethod)
|
|
108
151
|
}
|
|
109
152
|
|
|
110
153
|
override fun addHeader(name: String, value: String) {
|
|
111
|
-
|
|
154
|
+
requestHeaders[name] = value
|
|
155
|
+
builder.addHeader(name, value)
|
|
112
156
|
}
|
|
113
157
|
|
|
114
158
|
override fun setUploadBody(body: Variant_ArrayBuffer_String) {
|
|
@@ -119,8 +163,12 @@ class NitroUrlRequestBuilder(
|
|
|
119
163
|
buffer.get(bytes)
|
|
120
164
|
bytes
|
|
121
165
|
}
|
|
122
|
-
is Variant_ArrayBuffer_String.Second ->
|
|
166
|
+
is Variant_ArrayBuffer_String.Second -> {
|
|
167
|
+
uploadBodyString = body.value
|
|
168
|
+
body.value.toByteArray(Charsets.UTF_8)
|
|
169
|
+
}
|
|
123
170
|
}
|
|
171
|
+
uploadBodyLength = bodyBytes.size.toLong()
|
|
124
172
|
|
|
125
173
|
val provider = object : org.chromium.net.UploadDataProvider() {
|
|
126
174
|
private var position = 0
|
|
@@ -176,6 +224,16 @@ class NitroUrlRequestBuilder(
|
|
|
176
224
|
|
|
177
225
|
override fun build(): HybridUrlRequestSpec {
|
|
178
226
|
val cronetRequest = builder.build()
|
|
227
|
+
if (devToolsEnabled) {
|
|
228
|
+
DevToolsReporter.reportRequestStart(
|
|
229
|
+
devToolsRequestId,
|
|
230
|
+
url,
|
|
231
|
+
httpMethod,
|
|
232
|
+
requestHeaders,
|
|
233
|
+
uploadBodyString,
|
|
234
|
+
uploadBodyLength
|
|
235
|
+
)
|
|
236
|
+
}
|
|
179
237
|
return NitroUrlRequest(cronetRequest, byteBuffer)
|
|
180
238
|
}
|
|
181
239
|
}
|
|
@@ -16,6 +16,7 @@ class HybridUrlRequestBuilder: HybridUrlRequestBuilderSpec {
|
|
|
16
16
|
|
|
17
17
|
private var urlRequest: URLRequest
|
|
18
18
|
private var priority: Float = 0.5
|
|
19
|
+
private let devToolsRequestId: String = UUID().uuidString
|
|
19
20
|
|
|
20
21
|
init(
|
|
21
22
|
url: String,
|
|
@@ -89,7 +90,8 @@ class HybridUrlRequestBuilder: HybridUrlRequestBuilderSpec {
|
|
|
89
90
|
onFailed: onFailedCallback,
|
|
90
91
|
onCanceled: onCanceledCallback,
|
|
91
92
|
executor: executor,
|
|
92
|
-
hybridRequest: nil
|
|
93
|
+
hybridRequest: nil,
|
|
94
|
+
devToolsRequestId: devToolsRequestId
|
|
93
95
|
)
|
|
94
96
|
|
|
95
97
|
let config = URLSessionConfiguration.default
|
|
@@ -107,6 +109,10 @@ class HybridUrlRequestBuilder: HybridUrlRequestBuilderSpec {
|
|
|
107
109
|
task.priority = priority
|
|
108
110
|
delegate.task = task
|
|
109
111
|
|
|
112
|
+
if NitroDevToolsReporter.isDebuggingEnabled() {
|
|
113
|
+
NitroDevToolsReporter.reportRequestStart(withRequest: devToolsRequestId, request: urlRequest)
|
|
114
|
+
}
|
|
115
|
+
|
|
110
116
|
let request = HybridUrlRequest(task: task, delegate: delegate)
|
|
111
117
|
delegate.hybridRequest = request
|
|
112
118
|
|
|
@@ -129,6 +135,9 @@ private class URLSessionDelegateAdapter: NSObject, URLSessionDataDelegate, URLSe
|
|
|
129
135
|
weak var hybridRequest: HybridUrlRequest?
|
|
130
136
|
|
|
131
137
|
private var response: HTTPURLResponse?
|
|
138
|
+
private let devToolsRequestId: String
|
|
139
|
+
private var devToolsBytes: Int = 0
|
|
140
|
+
private var devToolsTextual: Bool = false
|
|
132
141
|
|
|
133
142
|
init(
|
|
134
143
|
onRedirectReceived: ((_ info: UrlResponseInfo, _ newLocationUrl: String) -> Void)?,
|
|
@@ -138,7 +147,8 @@ private class URLSessionDelegateAdapter: NSObject, URLSessionDataDelegate, URLSe
|
|
|
138
147
|
onFailed: ((_ info: UrlResponseInfo?, _ error: RequestException) -> Void)?,
|
|
139
148
|
onCanceled: ((_ info: UrlResponseInfo?) -> Void)?,
|
|
140
149
|
executor: DispatchQueue,
|
|
141
|
-
hybridRequest: HybridUrlRequest
|
|
150
|
+
hybridRequest: HybridUrlRequest?,
|
|
151
|
+
devToolsRequestId: String
|
|
142
152
|
) {
|
|
143
153
|
self.onRedirectReceived = onRedirectReceived
|
|
144
154
|
self.onResponseStarted = onResponseStarted
|
|
@@ -148,6 +158,7 @@ private class URLSessionDelegateAdapter: NSObject, URLSessionDataDelegate, URLSe
|
|
|
148
158
|
self.onCanceled = onCanceled
|
|
149
159
|
self.executor = executor
|
|
150
160
|
self.hybridRequest = hybridRequest
|
|
161
|
+
self.devToolsRequestId = devToolsRequestId
|
|
151
162
|
super.init()
|
|
152
163
|
}
|
|
153
164
|
|
|
@@ -181,11 +192,17 @@ private class URLSessionDelegateAdapter: NSObject, URLSessionDataDelegate, URLSe
|
|
|
181
192
|
if let error = error {
|
|
182
193
|
let nsError = error as NSError
|
|
183
194
|
if nsError.code == NSURLErrorCancelled {
|
|
195
|
+
if NitroDevToolsReporter.isDebuggingEnabled() {
|
|
196
|
+
NitroDevToolsReporter.reportRequestFailed(self.devToolsRequestId, cancelled: true)
|
|
197
|
+
}
|
|
184
198
|
if let callback = self.onCanceled {
|
|
185
199
|
let nitroInfo = self.response?.toNitro()
|
|
186
200
|
callback(nitroInfo)
|
|
187
201
|
}
|
|
188
202
|
} else {
|
|
203
|
+
if NitroDevToolsReporter.isDebuggingEnabled() {
|
|
204
|
+
NitroDevToolsReporter.reportRequestFailed(self.devToolsRequestId, cancelled: false)
|
|
205
|
+
}
|
|
189
206
|
if let callback = self.onFailed {
|
|
190
207
|
let nitroError = error.toNitro()
|
|
191
208
|
let nitroInfo = self.response?.toNitro()
|
|
@@ -193,6 +210,9 @@ private class URLSessionDelegateAdapter: NSObject, URLSessionDataDelegate, URLSe
|
|
|
193
210
|
}
|
|
194
211
|
}
|
|
195
212
|
} else if let response = self.response {
|
|
213
|
+
if NitroDevToolsReporter.isDebuggingEnabled() {
|
|
214
|
+
NitroDevToolsReporter.reportResponseEnd(self.devToolsRequestId, encodedDataLength: self.devToolsBytes)
|
|
215
|
+
}
|
|
196
216
|
if let callback = self.onSucceeded {
|
|
197
217
|
let info = response.toNitro()
|
|
198
218
|
callback(info)
|
|
@@ -216,6 +236,20 @@ private class URLSessionDelegateAdapter: NSObject, URLSessionDataDelegate, URLSe
|
|
|
216
236
|
|
|
217
237
|
executor.sync { [weak self] in
|
|
218
238
|
guard let self = self else { return }
|
|
239
|
+
if NitroDevToolsReporter.isDebuggingEnabled() {
|
|
240
|
+
var headerDict: [String: String] = [:]
|
|
241
|
+
httpResponse.allHeaderFields.forEach { k, v in
|
|
242
|
+
if let key = k as? String { headerDict[key] = String(describing: v) }
|
|
243
|
+
}
|
|
244
|
+
NitroDevToolsReporter.reportResponseStart(
|
|
245
|
+
self.devToolsRequestId,
|
|
246
|
+
url: httpResponse.url?.absoluteString ?? "",
|
|
247
|
+
statusCode: httpResponse.statusCode,
|
|
248
|
+
headers: headerDict
|
|
249
|
+
)
|
|
250
|
+
let ct = headerDict["Content-Type"] ?? headerDict["content-type"]
|
|
251
|
+
self.devToolsTextual = NitroDevToolsReporter.isTextualContentType(ct)
|
|
252
|
+
}
|
|
219
253
|
if let callback = self.onResponseStarted {
|
|
220
254
|
let info = httpResponse.toNitro()
|
|
221
255
|
callback(info)
|
|
@@ -234,6 +268,14 @@ private class URLSessionDelegateAdapter: NSObject, URLSessionDataDelegate, URLSe
|
|
|
234
268
|
executor.sync { [weak self] in
|
|
235
269
|
guard let self = self, let response = self.response else { return }
|
|
236
270
|
|
|
271
|
+
if NitroDevToolsReporter.isDebuggingEnabled() {
|
|
272
|
+
self.devToolsBytes += data.count
|
|
273
|
+
NitroDevToolsReporter.reportDataReceived(self.devToolsRequestId, length: data.count)
|
|
274
|
+
if self.devToolsTextual, let text = String(data: data, encoding: .utf8) {
|
|
275
|
+
NitroDevToolsReporter.storeResponseBodyIncremental(self.devToolsRequestId, text: text)
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
237
279
|
let arrayBuffer: ArrayBuffer
|
|
238
280
|
do {
|
|
239
281
|
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,129 @@
|
|
|
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
|
+
// The Objective-C wrapper does not expose -isDebuggingEnabled publicly,
|
|
14
|
+
// but the underlying C++ NetworkReporter does. We avoid depending on the
|
|
15
|
+
// C++ header and instead rely on RN guarding every report call internally.
|
|
16
|
+
// For our own body-capture short-circuit we assume enabled when the class
|
|
17
|
+
// exists; the underlying calls are still no-ops when no debugger attached.
|
|
18
|
+
|
|
19
|
+
@implementation NitroDevToolsReporter
|
|
20
|
+
|
|
21
|
+
+ (BOOL)isDebuggingEnabled {
|
|
22
|
+
#if NITRO_HAS_NETWORK_REPORTER
|
|
23
|
+
return YES;
|
|
24
|
+
#else
|
|
25
|
+
return NO;
|
|
26
|
+
#endif
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
+ (void)reportRequestStartWithRequest:(NSString *)requestId
|
|
30
|
+
request:(NSURLRequest *)request {
|
|
31
|
+
#if NITRO_HAS_NETWORK_REPORTER
|
|
32
|
+
if (request == nil) return;
|
|
33
|
+
int encoded = (int)(request.HTTPBody.length);
|
|
34
|
+
[RCTInspectorNetworkReporter reportRequestStart:requestId request:request encodedDataLength:encoded];
|
|
35
|
+
[RCTInspectorNetworkReporter reportConnectionTiming:requestId request:request];
|
|
36
|
+
#endif
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
+ (void)reportRequestStart:(NSString *)requestId
|
|
40
|
+
url:(NSString *)url
|
|
41
|
+
method:(NSString *)method
|
|
42
|
+
headers:(NSDictionary<NSString *, NSString *> *)headers
|
|
43
|
+
bodyString:(NSString *)bodyString {
|
|
44
|
+
#if NITRO_HAS_NETWORK_REPORTER
|
|
45
|
+
NSURL *u = [NSURL URLWithString:url];
|
|
46
|
+
if (u == nil) return;
|
|
47
|
+
NSMutableURLRequest *req = [NSMutableURLRequest requestWithURL:u];
|
|
48
|
+
req.HTTPMethod = method ?: @"GET";
|
|
49
|
+
for (NSString *k in headers) {
|
|
50
|
+
[req setValue:headers[k] forHTTPHeaderField:k];
|
|
51
|
+
}
|
|
52
|
+
if (bodyString.length > 0) {
|
|
53
|
+
req.HTTPBody = [bodyString dataUsingEncoding:NSUTF8StringEncoding];
|
|
54
|
+
}
|
|
55
|
+
NSInteger encoded = bodyString ? (NSInteger)[bodyString lengthOfBytesUsingEncoding:NSUTF8StringEncoding] : 0;
|
|
56
|
+
[RCTInspectorNetworkReporter reportRequestStart:requestId request:req encodedDataLength:(int)encoded];
|
|
57
|
+
[RCTInspectorNetworkReporter reportConnectionTiming:requestId request:req];
|
|
58
|
+
#endif
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
+ (void)reportResponseStart:(NSString *)requestId
|
|
62
|
+
url:(NSString *)url
|
|
63
|
+
statusCode:(NSInteger)statusCode
|
|
64
|
+
headers:(NSDictionary<NSString *, NSString *> *)headers {
|
|
65
|
+
#if NITRO_HAS_NETWORK_REPORTER
|
|
66
|
+
NSURL *u = [NSURL URLWithString:url];
|
|
67
|
+
if (u == nil) return;
|
|
68
|
+
NSHTTPURLResponse *resp = [[NSHTTPURLResponse alloc] initWithURL:u
|
|
69
|
+
statusCode:statusCode
|
|
70
|
+
HTTPVersion:@"HTTP/1.1"
|
|
71
|
+
headerFields:headers];
|
|
72
|
+
[RCTInspectorNetworkReporter reportResponseStart:requestId
|
|
73
|
+
response:resp
|
|
74
|
+
statusCode:(int)statusCode
|
|
75
|
+
headers:headers];
|
|
76
|
+
#endif
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
+ (void)reportDataReceived:(NSString *)requestId length:(NSInteger)length {
|
|
80
|
+
#if NITRO_HAS_NETWORK_REPORTER
|
|
81
|
+
if (length <= 0) return;
|
|
82
|
+
// Only data.length is read by the underlying reporter. Avoid allocating a
|
|
83
|
+
// zero-filled buffer by handing NSData a non-owned byte pointer and the
|
|
84
|
+
// intended length.
|
|
85
|
+
static uint8_t sentinel;
|
|
86
|
+
NSData *sized = [NSData dataWithBytesNoCopy:&sentinel length:(NSUInteger)length freeWhenDone:NO];
|
|
87
|
+
[RCTInspectorNetworkReporter reportDataReceived:requestId data:sized];
|
|
88
|
+
#endif
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
+ (void)reportResponseEnd:(NSString *)requestId encodedDataLength:(NSInteger)length {
|
|
92
|
+
#if NITRO_HAS_NETWORK_REPORTER
|
|
93
|
+
[RCTInspectorNetworkReporter reportResponseEnd:requestId encodedDataLength:(int)length];
|
|
94
|
+
#endif
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
+ (void)reportRequestFailed:(NSString *)requestId cancelled:(BOOL)cancelled {
|
|
98
|
+
#if NITRO_HAS_NETWORK_REPORTER
|
|
99
|
+
[RCTInspectorNetworkReporter reportRequestFailed:requestId cancelled:cancelled];
|
|
100
|
+
#endif
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
+ (void)storeResponseBody:(NSString *)requestId
|
|
104
|
+
data:(NSData *)data
|
|
105
|
+
base64Encoded:(BOOL)base64Encoded {
|
|
106
|
+
#if NITRO_HAS_NETWORK_REPORTER
|
|
107
|
+
[RCTInspectorNetworkReporter maybeStoreResponseBody:requestId data:data base64Encoded:base64Encoded];
|
|
108
|
+
#endif
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
+ (void)storeResponseBodyIncremental:(NSString *)requestId text:(NSString *)text {
|
|
112
|
+
#if NITRO_HAS_NETWORK_REPORTER
|
|
113
|
+
[RCTInspectorNetworkReporter maybeStoreResponseBodyIncremental:requestId data:text];
|
|
114
|
+
#endif
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
+ (BOOL)isTextualContentType:(NSString *)contentType {
|
|
118
|
+
if (contentType == nil) return NO;
|
|
119
|
+
NSString *ct = [contentType lowercaseString];
|
|
120
|
+
if ([ct hasPrefix:@"text/"]) return YES;
|
|
121
|
+
if ([ct containsString:@"application/json"]) return YES;
|
|
122
|
+
if ([ct containsString:@"application/xml"]) return YES;
|
|
123
|
+
if ([ct containsString:@"application/javascript"]) return YES;
|
|
124
|
+
if ([ct containsString:@"+json"]) return YES;
|
|
125
|
+
if ([ct containsString:@"+xml"]) return YES;
|
|
126
|
+
return NO;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
@end
|
|
@@ -170,8 +170,26 @@ final class NitroFetchClient: HybridNitroFetchClientSpec {
|
|
|
170
170
|
"%{public}s %{public}s", traceMethod, tracePath)
|
|
171
171
|
#endif
|
|
172
172
|
|
|
173
|
-
let
|
|
173
|
+
let devToolsId = req.requestId ?? UUID().uuidString
|
|
174
|
+
if NitroDevToolsReporter.isDebuggingEnabled() {
|
|
175
|
+
NitroDevToolsReporter.reportRequestStart(withRequest: devToolsId, request: urlRequest)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
let data: Data
|
|
179
|
+
let response: URLResponse
|
|
180
|
+
do {
|
|
181
|
+
(data, response) = try await session.data(for: urlRequest, delegate: delegate)
|
|
182
|
+
} catch {
|
|
183
|
+
if NitroDevToolsReporter.isDebuggingEnabled() {
|
|
184
|
+
let cancelled = (error as NSError).code == NSURLErrorCancelled
|
|
185
|
+
NitroDevToolsReporter.reportRequestFailed(devToolsId, cancelled: cancelled)
|
|
186
|
+
}
|
|
187
|
+
throw error
|
|
188
|
+
}
|
|
174
189
|
guard let http = response as? HTTPURLResponse else {
|
|
190
|
+
if NitroDevToolsReporter.isDebuggingEnabled() {
|
|
191
|
+
NitroDevToolsReporter.reportRequestFailed(devToolsId, cancelled: false)
|
|
192
|
+
}
|
|
175
193
|
throw NSError(domain: "NitroFetch", code: -1, userInfo: [NSLocalizedDescriptionKey: "Invalid response"])
|
|
176
194
|
}
|
|
177
195
|
|
|
@@ -180,6 +198,26 @@ final class NitroFetchClient: HybridNitroFetchClientSpec {
|
|
|
180
198
|
return NitroHeader(key: key, value: String(describing: v))
|
|
181
199
|
}
|
|
182
200
|
|
|
201
|
+
if NitroDevToolsReporter.isDebuggingEnabled() {
|
|
202
|
+
var headerDict: [String: String] = [:]
|
|
203
|
+
for h in headersPairs { headerDict[h.key] = h.value }
|
|
204
|
+
NitroDevToolsReporter.reportResponseStart(
|
|
205
|
+
devToolsId,
|
|
206
|
+
url: finalURL?.absoluteString ?? http.url?.absoluteString ?? req.url,
|
|
207
|
+
statusCode: http.statusCode,
|
|
208
|
+
headers: headerDict
|
|
209
|
+
)
|
|
210
|
+
NitroDevToolsReporter.reportDataReceived(devToolsId, length: data.count)
|
|
211
|
+
if NitroDevToolsReporter.isTextualContentType(headerDict["Content-Type"] ?? headerDict["content-type"]) {
|
|
212
|
+
if let text = String(data: data, encoding: .utf8) {
|
|
213
|
+
NitroDevToolsReporter.storeResponseBody(devToolsId, data: Data(text.utf8), base64Encoded: false)
|
|
214
|
+
}
|
|
215
|
+
} else if data.count > 0 && data.count <= 5 * 1024 * 1024 {
|
|
216
|
+
NitroDevToolsReporter.storeResponseBody(devToolsId, data: data, base64Encoded: true)
|
|
217
|
+
}
|
|
218
|
+
NitroDevToolsReporter.reportResponseEnd(devToolsId, encodedDataLength: data.count)
|
|
219
|
+
}
|
|
220
|
+
|
|
183
221
|
// Choose bodyString by default (matching Android’s first pass)
|
|
184
222
|
let charset = NitroFetchClient.detectCharset(from: http) ?? String.Encoding.utf8
|
|
185
223
|
let bodyStr = String(data: data, encoding: charset) ?? String(data: data, encoding: .utf8)
|