react-native-nitro-fetch 1.0.2 → 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.
@@ -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)
package/README.md ADDED
@@ -0,0 +1,370 @@
1
+ <a href="https://margelo.com">
2
+ <picture>
3
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/margelo/react-native-nitro-fetch/main/assets/banner-dark.png" />
4
+ <source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/margelo/react-native-nitro-fetch/main/assets/banner-light.png" />
5
+ <img alt="Nitro Modules" src="https://raw.githubusercontent.com/margelo/react-native-nitro-fetch/main/assets/banner-light.png" />
6
+ </picture>
7
+ </a>
8
+
9
+ <br />
10
+
11
+
12
+
13
+ **react-native-nitro-fetch** is a general purpose network fetching library for React Native. It can be used as a drop-in replacement for the built-in `fetch(...)` method, as well as provide additional features like prefetching and workletized mappers.
14
+
15
+ <p align="center">
16
+ <a href="https://margelo.github.io/react-native-nitro-fetch/"><b>Documentation</b></a>
17
+ </p>
18
+
19
+ ## Features
20
+
21
+ - 🔧 Drop-in replacement for the built-in `fetch(...)` method
22
+ - ⚡️ Fast HTTP stack using [Cronet](https://chromium.googlesource.com/chromium/src/+/lkgr/components/cronet/README.md) on Android, and [URLSession](https://developer.apple.com/documentation/Foundation/URLSession) on iOS
23
+ - 💪 Supports HTTP/1, HTTP/2 and [HTTP/3](https://en.wikipedia.org/wiki/HTTP/3) over [QUIC](https://www.chromium.org/quic/), [Brotli](https://github.com/google/brotli), and disk cache
24
+ - ⏰ Prefetching on app-startup for even faster initialization
25
+ - 🧵 Worklet support for parallel data mapping without blocking the JS Thread
26
+ - 🔌 Optional **WebSockets** via [`react-native-nitro-websockets`](docs/websockets.md) (see [WebSockets & prewarm](#websockets--prewarm) below)
27
+ - 🔥 Powered by [Nitro Modules](https://github.com/mrousavy/nitro)
28
+
29
+ ## Installation
30
+
31
+ ```sh
32
+ npm i react-native-nitro-fetch react-native-nitro-modules
33
+ ```
34
+
35
+ > [Nitro Modules](https://github.com/mrousavy/nitro) requires react-native 0.75+ or higher
36
+
37
+ **WebSockets (optional)** — add the companion socket package plus **text decoder** (peer dependency of websockets):
38
+
39
+ ```sh
40
+ npm i react-native-nitro-websockets react-native-nitro-text-decoder
41
+ ```
42
+
43
+ Full setup, native hooks, prewarm, and API details: **[docs/websockets.md](docs/websockets.md)** · UI: [`example/src/screens/WebSocketScreen.tsx`](example/src/screens/WebSocketScreen.tsx) · auth + prewarm: [Token refresh](#token-refresh-cold-start) (example block).
44
+
45
+ ## Usage
46
+
47
+ To simply fetch data, import the `fetch(...)` method from `react-native-nitro-fetch`:
48
+
49
+ ```ts
50
+ import { fetch } from 'react-native-nitro-fetch'
51
+
52
+ const res = await fetch('https://httpbin.org/get')
53
+ const json = await res.json()
54
+ ```
55
+
56
+ This can be used as a drop-in-replacement for the built-in `fetch(...)` method.
57
+
58
+ ### Prefetching in JS
59
+
60
+ You can prefetch a URL in JS, which keeps the result cached for the next actual `fetch(...)` call - this can be used shortly before navigating to a new screen to have results hot & ready:
61
+
62
+ ```ts
63
+ import { prefetch } from 'react-native-nitro-fetch'
64
+
65
+ await prefetch('https://httpbin.org/uuid', {
66
+ headers: { prefetchKey: 'uuid' }
67
+ })
68
+ ```
69
+
70
+ Then, on the new screen that was navigated to:
71
+
72
+ ```ts
73
+ import { fetch } from 'react-native-nitro-fetch'
74
+
75
+ const res = await fetch('https://httpbin.org/uuid', {
76
+ headers: { prefetchKey: 'uuid' }
77
+ })
78
+ console.log('prefetched header:', res.headers.get('nitroPrefetched'))
79
+ ```
80
+
81
+ ### Prefetching for the next app launch
82
+
83
+ Prefetching data on app launch (or _process start_) will make it hot & ready once your JS code actually runs. Call `prefetchOnAppStart(...)` to enqueue a prefetch for the **next** app start:
84
+
85
+ ```ts
86
+ import { prefetchOnAppStart } from 'react-native-nitro-fetch'
87
+
88
+ await prefetchOnAppStart('https://httpbin.org/uuid', {
89
+ prefetchKey: 'uuid'
90
+ })
91
+ ```
92
+
93
+ Then, once the app opens the next time, a call to `fetch(...)` might resolve faster since it will contain already cached results:
94
+
95
+ ```ts
96
+ import { fetch } from 'react-native-nitro-fetch'
97
+
98
+ const res = await fetch('https://httpbin.org/uuid', {
99
+ headers: { prefetchKey: 'uuid' }
100
+ })
101
+ console.log('prefetched header:', res.headers.get('nitroPrefetched'))
102
+ ```
103
+
104
+ In our tests, prefetching alone yielded a **~220 ms** faster TTI (time-to-interactive) time! 🤯
105
+
106
+ ### Token refresh (cold start)
107
+
108
+ When you use **auto-prefetch** (`prefetchOnAppStart`) and/or **WebSocket prewarm on app start** (`react-native-nitro-websockets`), native code runs **before** your JS bundle. If those requests need auth headers, you can register a **token refresh** configuration. On each cold start, native code calls your refresh URL, maps the response into HTTP headers, and merges them into auto-prefetches and/or WebSocket prewarms.
109
+
110
+ **1. Register the refresh config** (persisted in encrypted native storage):
111
+
112
+ ```ts
113
+ import { registerTokenRefresh } from 'react-native-nitro-fetch'
114
+
115
+ registerTokenRefresh({
116
+ target: 'fetch', // 'websocket' | 'fetch' | 'all'
117
+ url: 'https://api.example.com/oauth/token',
118
+ method: 'POST',
119
+ headers: { 'Content-Type': 'application/json' },
120
+ body: JSON.stringify({ grant_type: 'client_credentials' }),
121
+ responseType: 'json',
122
+ mappings: [
123
+ { jsonPath: 'access_token', header: 'Authorization', valueTemplate: 'Bearer {{value}}' },
124
+ ],
125
+ // If the refresh request fails:
126
+ // - 'useStoredHeaders' — use last successful headers from the previous run (default)
127
+ // - 'skip' — skip auto-prefetch / prewarm entirely when refresh fails
128
+ onFailure: 'useStoredHeaders',
129
+ })
130
+ ```
131
+
132
+ **Response mapping**
133
+
134
+ - Default `responseType` is `'json'`. Use **`mappings`** to copy fields from the JSON body into header names (dot paths supported, e.g. `data.token`).
135
+ - Use **`compositeHeaders`** to build a header from a template and multiple JSON paths (`{{placeholder}}` in the template).
136
+ - For a plain-text body, set `responseType: 'text'` and use **`textHeader`** / optional **`textTemplate`** (with `{{value}}`).
137
+
138
+
139
+
140
+ **Example: token refresh + WebSocket prewarm**
141
+
142
+ ```ts
143
+ import { registerTokenRefresh } from 'react-native-nitro-fetch'
144
+ import { prewarmOnAppStart, NitroWebSocket } from 'react-native-nitro-websockets'
145
+
146
+ const WSS = 'wss://api.example.com/live'
147
+
148
+ registerTokenRefresh({
149
+ target: 'websocket', // use 'all' if you also use prefetchOnAppStart with the same token flow
150
+ url: 'https://api.example.com/oauth/token',
151
+ method: 'POST',
152
+ headers: { 'Content-Type': 'application/json' },
153
+ body: JSON.stringify({
154
+ grant_type: 'client_credentials',
155
+ client_id: '…',
156
+ client_secret: '…',
157
+ }),
158
+ mappings: [
159
+ { jsonPath: 'access_token', header: 'Authorization', valueTemplate: 'Bearer {{value}}' },
160
+ ],
161
+ })
162
+
163
+ ```
164
+
165
+ **3. Optional JS helpers**
166
+
167
+ ```ts
168
+ import {
169
+ callRefreshEndpoint,
170
+ clearTokenRefresh,
171
+ getStoredTokenRefreshConfig,
172
+ } from 'react-native-nitro-fetch'
173
+
174
+ // Same mapping rules as native; uses global fetch from JS
175
+ const headers = await callRefreshEndpoint(config)
176
+
177
+ // Remove stored config and token caches (scope with 'fetch' | 'websocket' | 'all')
178
+ clearTokenRefresh('fetch')
179
+
180
+ // Read back what was registered (or null)
181
+ const stored = getStoredTokenRefreshConfig('fetch')
182
+ ```
183
+
184
+ The refresh config and header caches are stored with **platform secure storage** (Android Keystore + encrypted values in `SharedPreferences`, iOS Keychain-backed encryption in the same `UserDefaults` suite as other nitro keys).
185
+
186
+ ### AbortController
187
+
188
+ Cancel in-flight requests using the standard `AbortController` API:
189
+
190
+ ```ts
191
+ import { fetch } from 'react-native-nitro-fetch'
192
+
193
+ const controller = new AbortController()
194
+
195
+ // Abort after 500ms
196
+ setTimeout(() => controller.abort(), 500)
197
+
198
+ try {
199
+ const res = await fetch('https://httpbin.org/delay/20', {
200
+ signal: controller.signal,
201
+ })
202
+ } catch (e) {
203
+ if (e.name === 'AbortError') {
204
+ console.log('Request was cancelled')
205
+ }
206
+ }
207
+ ```
208
+
209
+ Pre-aborted signals are also supported — the request will throw immediately without making a network call:
210
+
211
+ ```ts
212
+ const controller = new AbortController()
213
+ controller.abort()
214
+
215
+ await fetch(url, { signal: controller.signal }) // throws AbortError
216
+ ```
217
+
218
+ ### FormData
219
+
220
+ Upload files and form fields using `FormData`:
221
+
222
+ ```ts
223
+ import { fetch } from 'react-native-nitro-fetch'
224
+
225
+ const fd = new FormData()
226
+ fd.append('username', 'nitro_user')
227
+ fd.append('avatar', {
228
+ uri: 'file:///path/to/photo.jpg',
229
+ type: 'image/jpeg',
230
+ name: 'avatar.jpg',
231
+ } as any)
232
+
233
+ const res = await fetch('https://httpbin.org/post', {
234
+ method: 'POST',
235
+ body: fd,
236
+ })
237
+ const json = await res.json()
238
+ ```
239
+
240
+ ### Worklet Mapping
241
+
242
+ Since Nitro Fetch is a [Nitro Module](https://nitro.margelo.com), it can be used from Worklets.
243
+ This can be useful to parse data without blocking the main JS-Thread:
244
+
245
+ ```ts
246
+ import { nitroFetchOnWorklet } from 'react-native-nitro-fetch'
247
+
248
+ const data = await nitroFetchOnWorklet(
249
+ 'https://httpbin.org/get',
250
+ undefined,
251
+ (payload) => {
252
+ 'worklet'
253
+ return JSON.parse(payload.bodyString ?? '{}')
254
+ }
255
+ )
256
+ ```
257
+ Before using worklet mapping, install and configure [react-native-worklets](https://docs.swmansion.com/react-native-worklets/docs/).
258
+
259
+ ### Streaming with `TextDecoder`
260
+
261
+ Nitro Fetch can also expose an streaming mode that returns a `ReadableStream` body.
262
+ Combined with [`react-native-nitro-text-decoder`](https://github.com/margelo/react-native-nitro-fetch/tree/main/packages/react-native-nitro-text-decoder), you can incrementally decode UTF‑8 chunks:
263
+
264
+ ```tsx
265
+ import { useRef, useState } from 'react'
266
+ import { fetch as nitroFetch } from 'react-native-nitro-fetch'
267
+ import { TextDecoder } from 'react-native-nitro-text-decoder'
268
+
269
+ export function StreamingExample() {
270
+ const [output, setOutput] = useState('')
271
+ const decoder = useRef(new TextDecoder())
272
+
273
+ const append = (text: string) => {
274
+ setOutput(prev => prev + text)
275
+ }
276
+
277
+ const runStream = async () => {
278
+ // `stream: true` enables the streaming transport
279
+ const res = await nitroFetch('https://httpbin.org/stream/20', {
280
+ stream: true,
281
+ })
282
+
283
+ const reader = res.body?.getReader()
284
+ if (!reader) {
285
+ append('No readable stream!')
286
+ return
287
+ }
288
+
289
+ let chunks = 0
290
+ while (true) {
291
+ const { done, value } = await reader.read()
292
+ if (done) break
293
+ chunks++
294
+ const text = decoder.current.decode(value, { stream: true })
295
+ append(text)
296
+ }
297
+
298
+ append(`\n\n✅ Done — ${chunks} chunk(s) received`)
299
+ }
300
+
301
+ // Call `runStream()` from a button handler in your UI
302
+ }
303
+ ```
304
+
305
+ ### WebSockets & prewarm
306
+
307
+ Use **[react-native-nitro-websockets](docs/websockets.md)** for `NitroWebSocket` (browser-like API: `onopen`, `onmessage`, `send`, `close`, …). Install **`react-native-nitro-text-decoder`** alongside it — the socket package uses it to decode UTF-8 text frames.
308
+
309
+ **Prewarm on next launch** — queue URLs from JS so native code can start the handshake before React loads:
310
+
311
+ ```ts
312
+ import {
313
+ prewarmOnAppStart,
314
+ removeFromPrewarmQueue,
315
+ clearPrewarmQueue,
316
+ } from 'react-native-nitro-websockets'
317
+
318
+ prewarmOnAppStart('wss://echo.websocket.org')
319
+ // optional: prewarmOnAppStart(url, ['subproto'], { Authorization: 'Bearer …' })
320
+
321
+ clearPrewarmQueue()
322
+ removeFromPrewarmQueue('wss://echo.websocket.org')
323
+ ```
324
+
325
+ On **Android**, call `NitroWebSocketAutoPrewarmer.prewarmOnStart(this)` in `Application.onCreate` (see [example `MainApplication.kt`](example/android/app/src/main/java/nitrofetch/example/MainApplication.kt)). **iOS** picks up the queue via the linked pod.
326
+
327
+ Authenticated prewarms: use **`registerTokenRefresh`** with `target: 'websocket'` or `'all'` — see [Token refresh (cold start)](#token-refresh-cold-start) for a **small `registerTokenRefresh` + `prewarmOnAppStart` + `NitroWebSocket` example**.
328
+
329
+ More detail: **[docs/websockets.md](docs/websockets.md)** · UI sample: **[example/src/screens/WebSocketScreen.tsx](example/src/screens/WebSocketScreen.tsx)**.
330
+
331
+ ## Limitations & Alternatives
332
+
333
+ - **WebSockets** are not part of `react-native-nitro-fetch` itself; use the companion package **[react-native-nitro-websockets](docs/websockets.md)** (with **react-native-nitro-text-decoder**). For other stacks, [react-native-fast-io](https://github.com/callstackincubator/react-native-fast-io) is another option.
334
+
335
+ ## Documentation
336
+
337
+ - [Getting Started](docs/getting-started.md)
338
+ - [API Reference](docs/api.md)
339
+ - [Android Details](docs/android.md)
340
+ - [iOS Details](docs/ios.md)
341
+ - [Prefetch & Auto-Prefetch](docs/prefetch.md)
342
+ - [WebSockets & prewarm](docs/websockets.md)
343
+ - [Worklets](docs/worklets.md)
344
+ - [Troubleshooting](docs/troubleshooting.md)
345
+ - [Cronet (Android) notes](docs/cronet-android.md)
346
+ - [Cronet (iOS) notes](docs/cronet-ios.md)
347
+
348
+ ## Margelo
349
+
350
+ Nitro Fetch is built with ❤️ by Margelo.
351
+ We build fast and beautiful apps. Contact us at [margelo.com](https://margelo.com) for high-end consultancy services.
352
+
353
+ ## Contributing
354
+
355
+ - Development workflow: `CONTRIBUTING.md#development-workflow`
356
+ - Sending a pull request: `CONTRIBUTING.md#sending-a-pull-request`
357
+ - Code of conduct: `CODE_OF_CONDUCT.md`
358
+
359
+ ## Authors
360
+
361
+ - [Szymon Kapala](https://github.com/Szymon20000)
362
+ - [Alex Shumihin](https://github.com/pioner92)
363
+ - [Ronald Goedeke](https://github.com/ronickg)
364
+ - [Marc Rousavy](https://github.com/mrousavy)
365
+ - [Ritesh Shukla](https://github.com/riteshshukla04)
366
+
367
+ ## License
368
+
369
+ MIT
370
+
@@ -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
- builder.addHeader(name, value)
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 -> body.value.toByteArray(Charsets.UTF_8)
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 (data, response) = try await session.data(for: urlRequest, delegate: delegate)
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)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-nitro-fetch",
3
- "version": "1.0.2",
3
+ "version": "1.1.0",
4
4
  "description": "Awesome Fetch :)",
5
5
  "main": "./lib/module/index.js",
6
6
  "module": "./lib/module/index.js",
@@ -47,7 +47,9 @@
47
47
  "prepare": "bob build",
48
48
  "build:plugin": "cd expo/plugins && npx tsc",
49
49
  "nitrogen": "nitrogen",
50
- "release": "npm run build:plugin && release-it --only-version"
50
+ "release": "npm run build:plugin && release-it --only-version",
51
+ "prepack": "cp ../../README.md ./README.md",
52
+ "postpack": "rm -f ./README.md"
51
53
  },
52
54
  "keywords": [
53
55
  "react-native",