scrapfly-sdk 0.2.4

Async Rust client for the Scrapfly web scraping, screenshot, extraction and crawler APIs
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><meta name="generator" content="rustdoc"><meta name="description" content="Source of the Rust file `src/client.rs`."><title>client.rs - source</title><script>if(window.location.protocol!=="file:")document.head.insertAdjacentHTML("beforeend","SourceSerif4-Regular-6b053e98.ttf.woff2,FiraSans-Italic-81dc35de.woff2,FiraSans-Regular-0fe48ade.woff2,FiraSans-MediumItalic-ccf7e434.woff2,FiraSans-Medium-e1aa3f0a.woff2,SourceCodePro-Regular-8badfe75.ttf.woff2,SourceCodePro-Semibold-aa29a496.ttf.woff2".split(",").map(f=>`<link rel="preload" as="font" type="font/woff2"href="../../static.files/${f}">`).join(""))</script><link rel="stylesheet" href="../../static.files/normalize-9960930a.css"><link rel="stylesheet" href="../../static.files/rustdoc-b7b9f40b.css"><meta name="rustdoc-vars" data-root-path="../../" data-static-root-path="../../static.files/" data-current-crate="scrapfly_sdk" data-themes="" data-resource-suffix="" data-rustdoc-version="1.95.0 (59807616e 2026-04-14)" data-channel="1.95.0" data-search-js="search-63369b7b.js" data-stringdex-js="stringdex-b897f86f.js" data-settings-js="settings-170eb4bf.js" ><script src="../../static.files/storage-41dd4d93.js"></script><script defer src="../../static.files/src-script-813739b1.js"></script><script defer src="../../src-files.js"></script><script defer src="../../static.files/main-5013f961.js"></script><noscript><link rel="stylesheet" href="../../static.files/noscript-f7c3ffd8.css"></noscript><link rel="alternate icon" type="image/png" href="../../static.files/favicon-32x32-eab170b8.png"><link rel="icon" type="image/svg+xml" href="../../static.files/favicon-044be391.svg"></head><body class="rustdoc src"><a class="skip-main-content" href="#main-content">Skip to main content</a><!--[if lte IE 11]><div class="warning">This old browser is unsupported and will most likely display funky things.</div><![endif]--><nav class="sidebar"><div class="src-sidebar-title"><h2>Files</h2></div></nav><div class="sidebar-resizer" title="Drag to resize sidebar"></div><main><section id="main-content" class="content" tabindex="-1"><div class="main-heading"><h1><div class="sub-heading">scrapfly_sdk/</div>client.rs</h1><rustdoc-toolbar></rustdoc-toolbar></div><div class="example-wrap digits-4"><pre class="rust"><code><a href=#1 id=1 data-nosnippet>1</a><span class="doccomment">//! HTTP client for the Scrapfly API.
<a href=#2 id=2 data-nosnippet>2</a>//!
<a href=#3 id=3 data-nosnippet>3</a>//! Built on `reqwest` with `rustls`. Single shared [`reqwest::Client`]
<a href=#4 id=4 data-nosnippet>4</a>//! re-used across every call.
<a href=#5 id=5 data-nosnippet>5</a>
<a href=#6 id=6 data-nosnippet>6</a></span><span class="kw">use </span>std::collections::HashMap;
<a href=#7 id=7 data-nosnippet>7</a><span class="kw">use </span>std::sync::Arc;
<a href=#8 id=8 data-nosnippet>8</a><span class="kw">use </span>std::time::Duration;
<a href=#9 id=9 data-nosnippet>9</a>
<a href=#10 id=10 data-nosnippet>10</a><span class="kw">use </span>futures_util::stream::{Stream, StreamExt};
<a href=#11 id=11 data-nosnippet>11</a><span class="kw">use </span>reqwest::header::{HeaderMap, HeaderValue, ACCEPT, CONTENT_TYPE, USER_AGENT};
<a href=#12 id=12 data-nosnippet>12</a><span class="kw">use </span>reqwest::{Method, Response, Url};
<a href=#13 id=13 data-nosnippet>13</a>
<a href=#14 id=14 data-nosnippet>14</a><span class="kw">use </span><span class="kw">crate</span>::config::crawler::CrawlerConfig;
<a href=#15 id=15 data-nosnippet>15</a><span class="kw">use </span><span class="kw">crate</span>::config::extraction::ExtractionConfig;
<a href=#16 id=16 data-nosnippet>16</a><span class="kw">use </span><span class="kw">crate</span>::config::scrape::ScrapeConfig;
<a href=#17 id=17 data-nosnippet>17</a><span class="kw">use </span><span class="kw">crate</span>::config::screenshot::ScreenshotConfig;
<a href=#18 id=18 data-nosnippet>18</a><span class="kw">use </span><span class="kw">crate</span>::enums::HttpMethod;
<a href=#19 id=19 data-nosnippet>19</a><span class="kw">use </span><span class="kw">crate</span>::error::{from_response, parse_retry_after, ApiError, ScrapflyError};
<a href=#20 id=20 data-nosnippet>20</a><span class="kw">use </span><span class="kw">crate</span>::monitoring::{
<a href=#21 id=21 data-nosnippet>21</a>    CloudBrowserMonitoringOptions, MonitoringDataFormat, MonitoringMetricsOptions,
<a href=#22 id=22 data-nosnippet>22</a>    MonitoringTargetMetricsOptions,
<a href=#23 id=23 data-nosnippet>23</a>};
<a href=#24 id=24 data-nosnippet>24</a><span class="kw">use </span><span class="kw">crate</span>::result::account::{AccountData, VerifyApiKeyResult};
<a href=#25 id=25 data-nosnippet>25</a><span class="kw">use </span><span class="kw">crate</span>::result::classify::{ClassifyRequest, ClassifyResult};
<a href=#26 id=26 data-nosnippet>26</a><span class="kw">use </span><span class="kw">crate</span>::result::crawler::{
<a href=#27 id=27 data-nosnippet>27</a>    CrawlerArtifact, CrawlerArtifactType, CrawlerContents, CrawlerStartResponse, CrawlerStatus,
<a href=#28 id=28 data-nosnippet>28</a>    CrawlerUrls,
<a href=#29 id=29 data-nosnippet>29</a>};
<a href=#30 id=30 data-nosnippet>30</a><span class="kw">use </span><span class="kw">crate</span>::result::extraction::ExtractionResult;
<a href=#31 id=31 data-nosnippet>31</a><span class="kw">use </span><span class="kw">crate</span>::result::scrape::{ResultData, ScrapeResult};
<a href=#32 id=32 data-nosnippet>32</a><span class="kw">use </span><span class="kw">crate</span>::result::screenshot::{ScreenshotMetadata, ScreenshotResult};
<a href=#33 id=33 data-nosnippet>33</a>
<a href=#34 id=34 data-nosnippet>34</a><span class="kw">const </span>DEFAULT_HOST: <span class="kw-2">&amp;</span>str = <span class="string">"https://api.scrapfly.io"</span>;
<a href=#35 id=35 data-nosnippet>35</a><span class="kw">const </span>DEFAULT_CLOUD_BROWSER_HOST: <span class="kw-2">&amp;</span>str = <span class="string">"https://browser.scrapfly.io"</span>;
<a href=#36 id=36 data-nosnippet>36</a><span class="kw">const </span>SDK_USER_AGENT: <span class="kw-2">&amp;</span>str = <span class="string">"Scrapfly-Rust-SDK"</span>;
<a href=#37 id=37 data-nosnippet>37</a><span class="kw">const </span>DEFAULT_RETRIES: usize = <span class="number">3</span>;
<a href=#38 id=38 data-nosnippet>38</a><span class="kw">const </span>DEFAULT_RETRY_DELAY: Duration = Duration::from_secs(<span class="number">1</span>);
<a href=#39 id=39 data-nosnippet>39</a><span class="kw">const </span>DEFAULT_TIMEOUT: Duration = Duration::from_secs(<span class="number">150</span>);
<a href=#40 id=40 data-nosnippet>40</a>
<a href=#41 id=41 data-nosnippet>41</a><span class="doccomment">/// Request-inspection callback. Fires right before `send()`.
<a href=#42 id=42 data-nosnippet>42</a>///
<a href=#43 id=43 data-nosnippet>43</a>/// Used by the integration harness to record the outgoing method/URL/headers
<a href=#44 id=44 data-nosnippet>44</a>/// without wrapping the `reqwest::Client` in a middleware layer.
<a href=#45 id=45 data-nosnippet>45</a></span><span class="kw">pub type </span>OnRequest = Arc&lt;<span class="kw">dyn </span>Fn(<span class="kw-2">&amp;</span>Method, <span class="kw-2">&amp;</span>Url, <span class="kw-2">&amp;</span>HeaderMap) + Send + Sync&gt;;
<a href=#46 id=46 data-nosnippet>46</a>
<a href=#47 id=47 data-nosnippet>47</a><span class="doccomment">/// Scrapfly API client. Cheap to `Clone` (the inner `reqwest::Client` is
<a href=#48 id=48 data-nosnippet>48</a>/// `Arc`'d so all clones share one connection pool).
<a href=#49 id=49 data-nosnippet>49</a></span><span class="attr">#[derive(Clone)]
<a href=#50 id=50 data-nosnippet>50</a></span><span class="kw">pub struct </span>Client {
<a href=#51 id=51 data-nosnippet>51</a>    http: reqwest::Client,
<a href=#52 id=52 data-nosnippet>52</a>    key: String,
<a href=#53 id=53 data-nosnippet>53</a>    host: String,
<a href=#54 id=54 data-nosnippet>54</a>    cloud_browser_host: String,
<a href=#55 id=55 data-nosnippet>55</a>    on_request: <span class="prelude-ty">Option</span>&lt;OnRequest&gt;,
<a href=#56 id=56 data-nosnippet>56</a>}
<a href=#57 id=57 data-nosnippet>57</a>
<a href=#58 id=58 data-nosnippet>58</a><span class="kw">impl </span>std::fmt::Debug <span class="kw">for </span>Client {
<a href=#59 id=59 data-nosnippet>59</a>    <span class="kw">fn </span>fmt(<span class="kw-2">&amp;</span><span class="self">self</span>, f: <span class="kw-2">&amp;mut </span>std::fmt::Formatter&lt;<span class="lifetime">'_</span>&gt;) -&gt; std::fmt::Result {
<a href=#60 id=60 data-nosnippet>60</a>        f.debug_struct(<span class="string">"Client"</span>)
<a href=#61 id=61 data-nosnippet>61</a>            .field(<span class="string">"host"</span>, <span class="kw-2">&amp;</span><span class="self">self</span>.host)
<a href=#62 id=62 data-nosnippet>62</a>            .field(<span class="string">"cloud_browser_host"</span>, <span class="kw-2">&amp;</span><span class="self">self</span>.cloud_browser_host)
<a href=#63 id=63 data-nosnippet>63</a>            .finish()
<a href=#64 id=64 data-nosnippet>64</a>    }
<a href=#65 id=65 data-nosnippet>65</a>}
<a href=#66 id=66 data-nosnippet>66</a>
<a href=#67 id=67 data-nosnippet>67</a><span class="doccomment">/// Builder for [`Client`].
<a href=#68 id=68 data-nosnippet>68</a></span><span class="attr">#[derive(Default)]
<a href=#69 id=69 data-nosnippet>69</a></span><span class="kw">pub struct </span>ClientBuilder {
<a href=#70 id=70 data-nosnippet>70</a>    api_key: <span class="prelude-ty">Option</span>&lt;String&gt;,
<a href=#71 id=71 data-nosnippet>71</a>    host: <span class="prelude-ty">Option</span>&lt;String&gt;,
<a href=#72 id=72 data-nosnippet>72</a>    cloud_browser_host: <span class="prelude-ty">Option</span>&lt;String&gt;,
<a href=#73 id=73 data-nosnippet>73</a>    timeout: <span class="prelude-ty">Option</span>&lt;Duration&gt;,
<a href=#74 id=74 data-nosnippet>74</a>    danger_accept_invalid_certs: bool,
<a href=#75 id=75 data-nosnippet>75</a>    http_client: <span class="prelude-ty">Option</span>&lt;reqwest::Client&gt;,
<a href=#76 id=76 data-nosnippet>76</a>    on_request: <span class="prelude-ty">Option</span>&lt;OnRequest&gt;,
<a href=#77 id=77 data-nosnippet>77</a>}
<a href=#78 id=78 data-nosnippet>78</a>
<a href=#79 id=79 data-nosnippet>79</a><span class="kw">impl </span>ClientBuilder {
<a href=#80 id=80 data-nosnippet>80</a>    <span class="doccomment">/// Set the API key (required).
<a href=#81 id=81 data-nosnippet>81</a>    </span><span class="kw">pub fn </span>api_key(<span class="kw-2">mut </span><span class="self">self</span>, key: <span class="kw">impl </span>Into&lt;String&gt;) -&gt; <span class="self">Self </span>{
<a href=#82 id=82 data-nosnippet>82</a>        <span class="self">self</span>.api_key = <span class="prelude-val">Some</span>(key.into());
<a href=#83 id=83 data-nosnippet>83</a>        <span class="self">self
<a href=#84 id=84 data-nosnippet>84</a>    </span>}
<a href=#85 id=85 data-nosnippet>85</a>    <span class="doccomment">/// Override the API host.
<a href=#86 id=86 data-nosnippet>86</a>    </span><span class="kw">pub fn </span>host(<span class="kw-2">mut </span><span class="self">self</span>, host: <span class="kw">impl </span>Into&lt;String&gt;) -&gt; <span class="self">Self </span>{
<a href=#87 id=87 data-nosnippet>87</a>        <span class="self">self</span>.host = <span class="prelude-val">Some</span>(host.into());
<a href=#88 id=88 data-nosnippet>88</a>        <span class="self">self
<a href=#89 id=89 data-nosnippet>89</a>    </span>}
<a href=#90 id=90 data-nosnippet>90</a>    <span class="doccomment">/// Override the Cloud Browser host (`https://browser.scrapfly.io`).
<a href=#91 id=91 data-nosnippet>91</a>    </span><span class="kw">pub fn </span>cloud_browser_host(<span class="kw-2">mut </span><span class="self">self</span>, host: <span class="kw">impl </span>Into&lt;String&gt;) -&gt; <span class="self">Self </span>{
<a href=#92 id=92 data-nosnippet>92</a>        <span class="self">self</span>.cloud_browser_host = <span class="prelude-val">Some</span>(host.into());
<a href=#93 id=93 data-nosnippet>93</a>        <span class="self">self
<a href=#94 id=94 data-nosnippet>94</a>    </span>}
<a href=#95 id=95 data-nosnippet>95</a>    <span class="doccomment">/// Override the HTTP timeout (default 150s).
<a href=#96 id=96 data-nosnippet>96</a>    </span><span class="kw">pub fn </span>timeout(<span class="kw-2">mut </span><span class="self">self</span>, t: Duration) -&gt; <span class="self">Self </span>{
<a href=#97 id=97 data-nosnippet>97</a>        <span class="self">self</span>.timeout = <span class="prelude-val">Some</span>(t);
<a href=#98 id=98 data-nosnippet>98</a>        <span class="self">self
<a href=#99 id=99 data-nosnippet>99</a>    </span>}
<a href=#100 id=100 data-nosnippet>100</a>    <span class="doccomment">/// Accept invalid TLS certificates (tests / self-signed dev hosts).
<a href=#101 id=101 data-nosnippet>101</a>    </span><span class="kw">pub fn </span>danger_accept_invalid_certs(<span class="kw-2">mut </span><span class="self">self</span>, v: bool) -&gt; <span class="self">Self </span>{
<a href=#102 id=102 data-nosnippet>102</a>        <span class="self">self</span>.danger_accept_invalid_certs = v;
<a href=#103 id=103 data-nosnippet>103</a>        <span class="self">self
<a href=#104 id=104 data-nosnippet>104</a>    </span>}
<a href=#105 id=105 data-nosnippet>105</a>    <span class="doccomment">/// Inject a pre-built `reqwest::Client`. Bypasses the timeout /
<a href=#106 id=106 data-nosnippet>106</a>    /// TLS-verify options.
<a href=#107 id=107 data-nosnippet>107</a>    </span><span class="kw">pub fn </span>http_client(<span class="kw-2">mut </span><span class="self">self</span>, client: reqwest::Client) -&gt; <span class="self">Self </span>{
<a href=#108 id=108 data-nosnippet>108</a>        <span class="self">self</span>.http_client = <span class="prelude-val">Some</span>(client);
<a href=#109 id=109 data-nosnippet>109</a>        <span class="self">self
<a href=#110 id=110 data-nosnippet>110</a>    </span>}
<a href=#111 id=111 data-nosnippet>111</a>    <span class="doccomment">/// Install a pre-send request callback (used by the integration runner
<a href=#112 id=112 data-nosnippet>112</a>    /// to capture SDK-layer attribution without installing middleware).
<a href=#113 id=113 data-nosnippet>113</a>    </span><span class="kw">pub fn </span>on_request(<span class="kw-2">mut </span><span class="self">self</span>, cb: OnRequest) -&gt; <span class="self">Self </span>{
<a href=#114 id=114 data-nosnippet>114</a>        <span class="self">self</span>.on_request = <span class="prelude-val">Some</span>(cb);
<a href=#115 id=115 data-nosnippet>115</a>        <span class="self">self
<a href=#116 id=116 data-nosnippet>116</a>    </span>}
<a href=#117 id=117 data-nosnippet>117</a>    <span class="doccomment">/// Build the client.
<a href=#118 id=118 data-nosnippet>118</a>    </span><span class="kw">pub fn </span>build(<span class="self">self</span>) -&gt; <span class="prelude-ty">Result</span>&lt;Client, ScrapflyError&gt; {
<a href=#119 id=119 data-nosnippet>119</a>        <span class="kw">let </span>key = <span class="self">self</span>.api_key.ok_or(ScrapflyError::BadApiKey)<span class="question-mark">?</span>;
<a href=#120 id=120 data-nosnippet>120</a>        <span class="kw">if </span>key.is_empty() {
<a href=#121 id=121 data-nosnippet>121</a>            <span class="kw">return </span><span class="prelude-val">Err</span>(ScrapflyError::BadApiKey);
<a href=#122 id=122 data-nosnippet>122</a>        }
<a href=#123 id=123 data-nosnippet>123</a>
<a href=#124 id=124 data-nosnippet>124</a>        <span class="kw">let </span>http = <span class="kw">if let </span><span class="prelude-val">Some</span>(c) = <span class="self">self</span>.http_client {
<a href=#125 id=125 data-nosnippet>125</a>            c
<a href=#126 id=126 data-nosnippet>126</a>        } <span class="kw">else </span>{
<a href=#127 id=127 data-nosnippet>127</a>            <span class="kw">let </span><span class="kw-2">mut </span>builder = reqwest::Client::builder()
<a href=#128 id=128 data-nosnippet>128</a>                .timeout(<span class="self">self</span>.timeout.unwrap_or(DEFAULT_TIMEOUT))
<a href=#129 id=129 data-nosnippet>129</a>                .user_agent(SDK_USER_AGENT);
<a href=#130 id=130 data-nosnippet>130</a>            <span class="kw">if </span><span class="self">self</span>.danger_accept_invalid_certs {
<a href=#131 id=131 data-nosnippet>131</a>                builder = builder.danger_accept_invalid_certs(<span class="bool-val">true</span>);
<a href=#132 id=132 data-nosnippet>132</a>            }
<a href=#133 id=133 data-nosnippet>133</a>            builder.build().map_err(ScrapflyError::Transport)<span class="question-mark">?
<a href=#134 id=134 data-nosnippet>134</a>        </span>};
<a href=#135 id=135 data-nosnippet>135</a>
<a href=#136 id=136 data-nosnippet>136</a>        <span class="prelude-val">Ok</span>(Client {
<a href=#137 id=137 data-nosnippet>137</a>            http,
<a href=#138 id=138 data-nosnippet>138</a>            key,
<a href=#139 id=139 data-nosnippet>139</a>            host: <span class="self">self</span>.host.unwrap_or_else(|| DEFAULT_HOST.to_string()),
<a href=#140 id=140 data-nosnippet>140</a>            cloud_browser_host: <span class="self">self
<a href=#141 id=141 data-nosnippet>141</a>                </span>.cloud_browser_host
<a href=#142 id=142 data-nosnippet>142</a>                .unwrap_or_else(|| DEFAULT_CLOUD_BROWSER_HOST.to_string()),
<a href=#143 id=143 data-nosnippet>143</a>            on_request: <span class="self">self</span>.on_request,
<a href=#144 id=144 data-nosnippet>144</a>        })
<a href=#145 id=145 data-nosnippet>145</a>    }
<a href=#146 id=146 data-nosnippet>146</a>}
<a href=#147 id=147 data-nosnippet>147</a>
<a href=#148 id=148 data-nosnippet>148</a><span class="kw">impl </span>Client {
<a href=#149 id=149 data-nosnippet>149</a>    <span class="doccomment">/// Start a new [`ClientBuilder`].
<a href=#150 id=150 data-nosnippet>150</a>    </span><span class="kw">pub fn </span>builder() -&gt; ClientBuilder {
<a href=#151 id=151 data-nosnippet>151</a>        ClientBuilder::default()
<a href=#152 id=152 data-nosnippet>152</a>    }
<a href=#153 id=153 data-nosnippet>153</a>
<a href=#154 id=154 data-nosnippet>154</a>    <span class="doccomment">/// Return the configured API key.
<a href=#155 id=155 data-nosnippet>155</a>    </span><span class="kw">pub fn </span>api_key(<span class="kw-2">&amp;</span><span class="self">self</span>) -&gt; <span class="kw-2">&amp;</span>str {
<a href=#156 id=156 data-nosnippet>156</a>        <span class="kw-2">&amp;</span><span class="self">self</span>.key
<a href=#157 id=157 data-nosnippet>157</a>    }
<a href=#158 id=158 data-nosnippet>158</a>
<a href=#159 id=159 data-nosnippet>159</a>    <span class="doccomment">/// Return the configured API host.
<a href=#160 id=160 data-nosnippet>160</a>    </span><span class="kw">pub fn </span>host(<span class="kw-2">&amp;</span><span class="self">self</span>) -&gt; <span class="kw-2">&amp;</span>str {
<a href=#161 id=161 data-nosnippet>161</a>        <span class="kw-2">&amp;</span><span class="self">self</span>.host
<a href=#162 id=162 data-nosnippet>162</a>    }
<a href=#163 id=163 data-nosnippet>163</a>
<a href=#164 id=164 data-nosnippet>164</a>    <span class="doccomment">/// Return the configured Cloud Browser host.
<a href=#165 id=165 data-nosnippet>165</a>    </span><span class="kw">pub fn </span>cloud_browser_host(<span class="kw-2">&amp;</span><span class="self">self</span>) -&gt; <span class="kw-2">&amp;</span>str {
<a href=#166 id=166 data-nosnippet>166</a>        <span class="kw-2">&amp;</span><span class="self">self</span>.cloud_browser_host
<a href=#167 id=167 data-nosnippet>167</a>    }
<a href=#168 id=168 data-nosnippet>168</a>
<a href=#169 id=169 data-nosnippet>169</a>    <span class="doccomment">/// Build a URL by joining `path` onto the configured host.
<a href=#170 id=170 data-nosnippet>170</a>    /// Crate-internal shim over `build_url`, used by `schedule.rs` to share
<a href=#171 id=171 data-nosnippet>171</a>    /// the same auth + host wiring as the rest of the SDK without exposing
<a href=#172 id=172 data-nosnippet>172</a>    /// the helper publicly.
<a href=#173 id=173 data-nosnippet>173</a>    </span><span class="kw">pub</span>(<span class="kw">crate</span>) <span class="kw">fn </span>build_url_public(
<a href=#174 id=174 data-nosnippet>174</a>        <span class="kw-2">&amp;</span><span class="self">self</span>,
<a href=#175 id=175 data-nosnippet>175</a>        path: <span class="kw-2">&amp;</span>str,
<a href=#176 id=176 data-nosnippet>176</a>        query: <span class="kw-2">&amp;</span>[(String, String)],
<a href=#177 id=177 data-nosnippet>177</a>    ) -&gt; <span class="prelude-ty">Result</span>&lt;Url, ScrapflyError&gt; {
<a href=#178 id=178 data-nosnippet>178</a>        <span class="self">self</span>.build_url(path, query)
<a href=#179 id=179 data-nosnippet>179</a>    }
<a href=#180 id=180 data-nosnippet>180</a>
<a href=#181 id=181 data-nosnippet>181</a>    <span class="doccomment">/// Crate-internal shim over `send_simple` — same rationale as
<a href=#182 id=182 data-nosnippet>182</a>    /// `build_url_public`.
<a href=#183 id=183 data-nosnippet>183</a>    </span><span class="kw">pub</span>(<span class="kw">crate</span>) <span class="kw">async fn </span>send_simple_public(
<a href=#184 id=184 data-nosnippet>184</a>        <span class="kw-2">&amp;</span><span class="self">self</span>,
<a href=#185 id=185 data-nosnippet>185</a>        method: Method,
<a href=#186 id=186 data-nosnippet>186</a>        url: Url,
<a href=#187 id=187 data-nosnippet>187</a>        headers: <span class="prelude-ty">Option</span>&lt;HeaderMap&gt;,
<a href=#188 id=188 data-nosnippet>188</a>        body: <span class="prelude-ty">Option</span>&lt;Vec&lt;u8&gt;&gt;,
<a href=#189 id=189 data-nosnippet>189</a>    ) -&gt; <span class="prelude-ty">Result</span>&lt;Response, ScrapflyError&gt; {
<a href=#190 id=190 data-nosnippet>190</a>        <span class="self">self</span>.send_simple(method, url, headers, body).<span class="kw">await
<a href=#191 id=191 data-nosnippet>191</a>    </span>}
<a href=#192 id=192 data-nosnippet>192</a>
<a href=#193 id=193 data-nosnippet>193</a>    <span class="kw">fn </span>build_url(<span class="kw-2">&amp;</span><span class="self">self</span>, path: <span class="kw-2">&amp;</span>str, query: <span class="kw-2">&amp;</span>[(String, String)]) -&gt; <span class="prelude-ty">Result</span>&lt;Url, ScrapflyError&gt; {
<a href=#194 id=194 data-nosnippet>194</a>        <span class="kw">let </span><span class="kw-2">mut </span>u = Url::parse(<span class="kw-2">&amp;</span><span class="macro">format!</span>(<span class="string">"{}{}"</span>, <span class="self">self</span>.host, path))
<a href=#195 id=195 data-nosnippet>195</a>            .map_err(|e| ScrapflyError::Config(<span class="macro">format!</span>(<span class="string">"invalid url: {}"</span>, e)))<span class="question-mark">?</span>;
<a href=#196 id=196 data-nosnippet>196</a>        {
<a href=#197 id=197 data-nosnippet>197</a>            <span class="kw">let </span><span class="kw-2">mut </span>pairs = u.query_pairs_mut();
<a href=#198 id=198 data-nosnippet>198</a>            pairs.append_pair(<span class="string">"key"</span>, <span class="kw-2">&amp;</span><span class="self">self</span>.key);
<a href=#199 id=199 data-nosnippet>199</a>            <span class="kw">for </span>(k, v) <span class="kw">in </span>query {
<a href=#200 id=200 data-nosnippet>200</a>                pairs.append_pair(k, v);
<a href=#201 id=201 data-nosnippet>201</a>            }
<a href=#202 id=202 data-nosnippet>202</a>        }
<a href=#203 id=203 data-nosnippet>203</a>        <span class="prelude-val">Ok</span>(u)
<a href=#204 id=204 data-nosnippet>204</a>    }
<a href=#205 id=205 data-nosnippet>205</a>
<a href=#206 id=206 data-nosnippet>206</a>    <span class="doccomment">/// Verify the API key by hitting `/account`.
<a href=#207 id=207 data-nosnippet>207</a>    </span><span class="kw">pub async fn </span>verify_api_key(<span class="kw-2">&amp;</span><span class="self">self</span>) -&gt; <span class="prelude-ty">Result</span>&lt;VerifyApiKeyResult, ScrapflyError&gt; {
<a href=#208 id=208 data-nosnippet>208</a>        <span class="kw">let </span>url = <span class="self">self</span>.build_url(<span class="string">"/account"</span>, <span class="kw-2">&amp;</span>[])<span class="question-mark">?</span>;
<a href=#209 id=209 data-nosnippet>209</a>        <span class="kw">let </span>resp = <span class="self">self</span>.send_simple(Method::GET, url, <span class="prelude-val">None</span>, <span class="prelude-val">None</span>).<span class="kw">await</span><span class="question-mark">?</span>;
<a href=#210 id=210 data-nosnippet>210</a>        <span class="prelude-val">Ok</span>(VerifyApiKeyResult {
<a href=#211 id=211 data-nosnippet>211</a>            valid: resp.status().is_success(),
<a href=#212 id=212 data-nosnippet>212</a>        })
<a href=#213 id=213 data-nosnippet>213</a>    }
<a href=#214 id=214 data-nosnippet>214</a>
<a href=#215 id=215 data-nosnippet>215</a>    <span class="doccomment">/// Fetch account info.
<a href=#216 id=216 data-nosnippet>216</a>    </span><span class="kw">pub async fn </span>account(<span class="kw-2">&amp;</span><span class="self">self</span>) -&gt; <span class="prelude-ty">Result</span>&lt;AccountData, ScrapflyError&gt; {
<a href=#217 id=217 data-nosnippet>217</a>        <span class="kw">let </span>url = <span class="self">self</span>.build_url(<span class="string">"/account"</span>, <span class="kw-2">&amp;</span>[])<span class="question-mark">?</span>;
<a href=#218 id=218 data-nosnippet>218</a>        <span class="kw">let </span>resp = <span class="self">self</span>.send_simple(Method::GET, url, <span class="prelude-val">None</span>, <span class="prelude-val">None</span>).<span class="kw">await</span><span class="question-mark">?</span>;
<a href=#219 id=219 data-nosnippet>219</a>        <span class="kw">let </span>(status, _headers, body) = read_response(resp).<span class="kw">await</span><span class="question-mark">?</span>;
<a href=#220 id=220 data-nosnippet>220</a>        <span class="kw">if </span>status != <span class="number">200 </span>{
<a href=#221 id=221 data-nosnippet>221</a>            <span class="kw">return </span><span class="prelude-val">Err</span>(from_response(status, <span class="kw-2">&amp;</span>body, <span class="number">0</span>, <span class="bool-val">false</span>));
<a href=#222 id=222 data-nosnippet>222</a>        }
<a href=#223 id=223 data-nosnippet>223</a>        <span class="prelude-val">Ok</span>(serde_json::from_slice(<span class="kw-2">&amp;</span>body)<span class="question-mark">?</span>)
<a href=#224 id=224 data-nosnippet>224</a>    }
<a href=#225 id=225 data-nosnippet>225</a>
<a href=#226 id=226 data-nosnippet>226</a>    <span class="doccomment">/// Classify an already-fetched HTTP response for anti-bot blocking.
<a href=#227 id=227 data-nosnippet>227</a>    ///
<a href=#228 id=228 data-nosnippet>228</a>    /// Runs the same detection pipeline used by every live Scrapfly scrape
<a href=#229 id=229 data-nosnippet>229</a>    /// against a response you already have (from your own proxy, cache, etc).
<a href=#230 id=230 data-nosnippet>230</a>    /// 1 API credit per successful call. See
<a href=#231 id=231 data-nosnippet>231</a>    /// &lt;https://scrapfly.io/docs/scrape-api/classify&gt;.
<a href=#232 id=232 data-nosnippet>232</a>    </span><span class="kw">pub async fn </span>classify(<span class="kw-2">&amp;</span><span class="self">self</span>, req: <span class="kw-2">&amp;</span>ClassifyRequest) -&gt; <span class="prelude-ty">Result</span>&lt;ClassifyResult, ScrapflyError&gt; {
<a href=#233 id=233 data-nosnippet>233</a>        <span class="kw">if </span>req.url.is_empty() {
<a href=#234 id=234 data-nosnippet>234</a>            <span class="kw">return </span><span class="prelude-val">Err</span>(ScrapflyError::Config(<span class="string">"classify: url is required"</span>.into()));
<a href=#235 id=235 data-nosnippet>235</a>        }
<a href=#236 id=236 data-nosnippet>236</a>        <span class="kw">if </span>!(<span class="number">100</span>..=<span class="number">599</span>).contains(<span class="kw-2">&amp;</span>req.status_code) {
<a href=#237 id=237 data-nosnippet>237</a>            <span class="kw">return </span><span class="prelude-val">Err</span>(ScrapflyError::Config(
<a href=#238 id=238 data-nosnippet>238</a>                <span class="string">"classify: status_code must be in [100, 599]"</span>.into(),
<a href=#239 id=239 data-nosnippet>239</a>            ));
<a href=#240 id=240 data-nosnippet>240</a>        }
<a href=#241 id=241 data-nosnippet>241</a>
<a href=#242 id=242 data-nosnippet>242</a>        <span class="kw">let </span>url = <span class="self">self</span>.build_url(<span class="string">"/classify"</span>, <span class="kw-2">&amp;</span>[])<span class="question-mark">?</span>;
<a href=#243 id=243 data-nosnippet>243</a>        <span class="kw">let </span>body = serde_json::to_vec(req)
<a href=#244 id=244 data-nosnippet>244</a>            .map_err(|e| ScrapflyError::Config(<span class="macro">format!</span>(<span class="string">"marshal classify request: {}"</span>, e)))<span class="question-mark">?</span>;
<a href=#245 id=245 data-nosnippet>245</a>
<a href=#246 id=246 data-nosnippet>246</a>        <span class="kw">let </span><span class="kw-2">mut </span>headers = HeaderMap::new();
<a href=#247 id=247 data-nosnippet>247</a>        headers.insert(CONTENT_TYPE, HeaderValue::from_static(<span class="string">"application/json"</span>));
<a href=#248 id=248 data-nosnippet>248</a>        headers.insert(ACCEPT, HeaderValue::from_static(<span class="string">"application/json"</span>));
<a href=#249 id=249 data-nosnippet>249</a>
<a href=#250 id=250 data-nosnippet>250</a>        <span class="kw">let </span>resp = <span class="self">self
<a href=#251 id=251 data-nosnippet>251</a>            </span>.send_simple(Method::POST, url, <span class="prelude-val">Some</span>(headers), <span class="prelude-val">Some</span>(body))
<a href=#252 id=252 data-nosnippet>252</a>            .<span class="kw">await</span><span class="question-mark">?</span>;
<a href=#253 id=253 data-nosnippet>253</a>        <span class="kw">let </span>(status, _headers, bytes) = read_response(resp).<span class="kw">await</span><span class="question-mark">?</span>;
<a href=#254 id=254 data-nosnippet>254</a>        <span class="kw">if </span>status &gt;= <span class="number">400 </span>{
<a href=#255 id=255 data-nosnippet>255</a>            <span class="kw">return </span><span class="prelude-val">Err</span>(from_response(status, <span class="kw-2">&amp;</span>bytes, <span class="number">0</span>, <span class="bool-val">false</span>));
<a href=#256 id=256 data-nosnippet>256</a>        }
<a href=#257 id=257 data-nosnippet>257</a>        <span class="kw">let </span>out: ClassifyResult = serde_json::from_slice(<span class="kw-2">&amp;</span>bytes)
<a href=#258 id=258 data-nosnippet>258</a>            .map_err(|e| ScrapflyError::Config(<span class="macro">format!</span>(<span class="string">"decode classify response: {}"</span>, e)))<span class="question-mark">?</span>;
<a href=#259 id=259 data-nosnippet>259</a>        <span class="prelude-val">Ok</span>(out)
<a href=#260 id=260 data-nosnippet>260</a>    }
<a href=#261 id=261 data-nosnippet>261</a>
<a href=#262 id=262 data-nosnippet>262</a>    <span class="comment">// ── Monitoring API (Enterprise+ plan only) ──────────────────────
<a href=#263 id=263 data-nosnippet>263</a>    // The Monitoring API exposes per-product aggregates and per-target
<a href=#264 id=264 data-nosnippet>264</a>    // timeseries. Web Scraping / Screenshot / Extraction / Crawler share
<a href=#265 id=265 data-nosnippet>265</a>    // one shape (request-based) but live under different URL prefixes;
<a href=#266 id=266 data-nosnippet>266</a>    // Cloud Browser is session-based and exposes a distinct shape.
<a href=#267 id=267 data-nosnippet>267</a>    // See &lt;https://scrapfly.io/docs/monitoring#api&gt;.
<a href=#268 id=268 data-nosnippet>268</a>
<a href=#269 id=269 data-nosnippet>269</a>    </span><span class="kw">fn </span>build_metrics_pairs(opts: <span class="kw-2">&amp;</span>MonitoringMetricsOptions) -&gt; Vec&lt;(String, String)&gt; {
<a href=#270 id=270 data-nosnippet>270</a>        <span class="kw">let </span><span class="kw-2">mut </span>pairs: Vec&lt;(String, String)&gt; = Vec::new();
<a href=#271 id=271 data-nosnippet>271</a>        <span class="kw">let </span>format = opts.format.unwrap_or(MonitoringDataFormat::Structured);
<a href=#272 id=272 data-nosnippet>272</a>        pairs.push((<span class="string">"format"</span>.into(), format.as_str().into()));
<a href=#273 id=273 data-nosnippet>273</a>        <span class="kw">if let </span><span class="prelude-val">Some</span>(p) = opts.period {
<a href=#274 id=274 data-nosnippet>274</a>            pairs.push((<span class="string">"period"</span>.into(), p.as_str().into()));
<a href=#275 id=275 data-nosnippet>275</a>        }
<a href=#276 id=276 data-nosnippet>276</a>        <span class="kw">if let </span><span class="prelude-val">Some</span>(<span class="kw-2">ref </span>aggs) = opts.aggregation {
<a href=#277 id=277 data-nosnippet>277</a>            <span class="kw">if </span>!aggs.is_empty() {
<a href=#278 id=278 data-nosnippet>278</a>                <span class="kw">let </span>joined = aggs
<a href=#279 id=279 data-nosnippet>279</a>                    .iter()
<a href=#280 id=280 data-nosnippet>280</a>                    .map(|a| a.as_str())
<a href=#281 id=281 data-nosnippet>281</a>                    .collect::&lt;Vec&lt;<span class="kw">_</span>&gt;&gt;()
<a href=#282 id=282 data-nosnippet>282</a>                    .join(<span class="string">","</span>);
<a href=#283 id=283 data-nosnippet>283</a>                pairs.push((<span class="string">"aggregation"</span>.into(), joined));
<a href=#284 id=284 data-nosnippet>284</a>            }
<a href=#285 id=285 data-nosnippet>285</a>        }
<a href=#286 id=286 data-nosnippet>286</a>        <span class="kw">if </span>opts.include_webhook {
<a href=#287 id=287 data-nosnippet>287</a>            pairs.push((<span class="string">"include_webhook"</span>.into(), <span class="string">"true"</span>.into()));
<a href=#288 id=288 data-nosnippet>288</a>        }
<a href=#289 id=289 data-nosnippet>289</a>        pairs
<a href=#290 id=290 data-nosnippet>290</a>    }
<a href=#291 id=291 data-nosnippet>291</a>
<a href=#292 id=292 data-nosnippet>292</a>    <span class="kw">fn </span>build_target_pairs(
<a href=#293 id=293 data-nosnippet>293</a>        opts: <span class="kw-2">&amp;</span>MonitoringTargetMetricsOptions,
<a href=#294 id=294 data-nosnippet>294</a>    ) -&gt; <span class="prelude-ty">Result</span>&lt;Vec&lt;(String, String)&gt;, ScrapflyError&gt; {
<a href=#295 id=295 data-nosnippet>295</a>        <span class="kw">if </span>opts.domain.is_empty() {
<a href=#296 id=296 data-nosnippet>296</a>            <span class="kw">return </span><span class="prelude-val">Err</span>(ScrapflyError::Config(
<a href=#297 id=297 data-nosnippet>297</a>                <span class="string">"monitoring target metrics: domain is required"</span>.into(),
<a href=#298 id=298 data-nosnippet>298</a>            ));
<a href=#299 id=299 data-nosnippet>299</a>        }
<a href=#300 id=300 data-nosnippet>300</a>        <span class="kw">if </span>opts.start.is_some() != opts.end.is_some() {
<a href=#301 id=301 data-nosnippet>301</a>            <span class="kw">return </span><span class="prelude-val">Err</span>(ScrapflyError::Config(
<a href=#302 id=302 data-nosnippet>302</a>                <span class="string">"monitoring target metrics: start and end must be provided together"</span>.into(),
<a href=#303 id=303 data-nosnippet>303</a>            ));
<a href=#304 id=304 data-nosnippet>304</a>        }
<a href=#305 id=305 data-nosnippet>305</a>        <span class="kw">let </span><span class="kw-2">mut </span>pairs: Vec&lt;(String, String)&gt; = Vec::new();
<a href=#306 id=306 data-nosnippet>306</a>        pairs.push((<span class="string">"domain"</span>.into(), opts.domain.clone()));
<a href=#307 id=307 data-nosnippet>307</a>        pairs.push((<span class="string">"group_subdomain"</span>.into(), opts.group_subdomain.to_string()));
<a href=#308 id=308 data-nosnippet>308</a>        <span class="kw">match </span>(<span class="kw-2">&amp;</span>opts.start, <span class="kw-2">&amp;</span>opts.end) {
<a href=#309 id=309 data-nosnippet>309</a>            (<span class="prelude-val">Some</span>(s), <span class="prelude-val">Some</span>(e)) =&gt; {
<a href=#310 id=310 data-nosnippet>310</a>                pairs.push((<span class="string">"start"</span>.into(), s.clone()));
<a href=#311 id=311 data-nosnippet>311</a>                pairs.push((<span class="string">"end"</span>.into(), e.clone()));
<a href=#312 id=312 data-nosnippet>312</a>            }
<a href=#313 id=313 data-nosnippet>313</a>            <span class="kw">_ </span>=&gt; {
<a href=#314 id=314 data-nosnippet>314</a>                <span class="kw">let </span>period = opts
<a href=#315 id=315 data-nosnippet>315</a>                    .period
<a href=#316 id=316 data-nosnippet>316</a>                    .unwrap_or(<span class="kw">crate</span>::monitoring::MonitoringPeriod::Last24h);
<a href=#317 id=317 data-nosnippet>317</a>                pairs.push((<span class="string">"period"</span>.into(), period.as_str().into()));
<a href=#318 id=318 data-nosnippet>318</a>            }
<a href=#319 id=319 data-nosnippet>319</a>        }
<a href=#320 id=320 data-nosnippet>320</a>        <span class="kw">if </span>opts.include_webhook {
<a href=#321 id=321 data-nosnippet>321</a>            pairs.push((<span class="string">"include_webhook"</span>.into(), <span class="string">"true"</span>.into()));
<a href=#322 id=322 data-nosnippet>322</a>        }
<a href=#323 id=323 data-nosnippet>323</a>        <span class="prelude-val">Ok</span>(pairs)
<a href=#324 id=324 data-nosnippet>324</a>    }
<a href=#325 id=325 data-nosnippet>325</a>
<a href=#326 id=326 data-nosnippet>326</a>    <span class="kw">async fn </span>fetch_monitoring_json(
<a href=#327 id=327 data-nosnippet>327</a>        <span class="kw-2">&amp;</span><span class="self">self</span>,
<a href=#328 id=328 data-nosnippet>328</a>        path: <span class="kw-2">&amp;</span>str,
<a href=#329 id=329 data-nosnippet>329</a>        pairs: <span class="kw-2">&amp;</span>[(String, String)],
<a href=#330 id=330 data-nosnippet>330</a>    ) -&gt; <span class="prelude-ty">Result</span>&lt;serde_json::Value, ScrapflyError&gt; {
<a href=#331 id=331 data-nosnippet>331</a>        <span class="kw">let </span>url = <span class="self">self</span>.build_url(path, pairs)<span class="question-mark">?</span>;
<a href=#332 id=332 data-nosnippet>332</a>        <span class="kw">let </span>resp = <span class="self">self</span>.send_simple(Method::GET, url, <span class="prelude-val">None</span>, <span class="prelude-val">None</span>).<span class="kw">await</span><span class="question-mark">?</span>;
<a href=#333 id=333 data-nosnippet>333</a>        <span class="kw">let </span>(status, _headers, body) = read_response(resp).<span class="kw">await</span><span class="question-mark">?</span>;
<a href=#334 id=334 data-nosnippet>334</a>        <span class="kw">if </span>status != <span class="number">200 </span>{
<a href=#335 id=335 data-nosnippet>335</a>            <span class="kw">return </span><span class="prelude-val">Err</span>(from_response(status, <span class="kw-2">&amp;</span>body, <span class="number">0</span>, <span class="bool-val">false</span>));
<a href=#336 id=336 data-nosnippet>336</a>        }
<a href=#337 id=337 data-nosnippet>337</a>        <span class="prelude-val">Ok</span>(serde_json::from_slice(<span class="kw-2">&amp;</span>body)<span class="question-mark">?</span>)
<a href=#338 id=338 data-nosnippet>338</a>    }
<a href=#339 id=339 data-nosnippet>339</a>
<a href=#340 id=340 data-nosnippet>340</a>    <span class="comment">// ── Web Scraping API ─────────────────────────────────────────────
<a href=#341 id=341 data-nosnippet>341</a>
<a href=#342 id=342 data-nosnippet>342</a>    </span><span class="doccomment">/// Fetch aggregate monitoring metrics for the Web Scraping API.
<a href=#343 id=343 data-nosnippet>343</a>    </span><span class="kw">pub async fn </span>get_monitoring_metrics(
<a href=#344 id=344 data-nosnippet>344</a>        <span class="kw-2">&amp;</span><span class="self">self</span>,
<a href=#345 id=345 data-nosnippet>345</a>        opts: <span class="kw-2">&amp;</span>MonitoringMetricsOptions,
<a href=#346 id=346 data-nosnippet>346</a>    ) -&gt; <span class="prelude-ty">Result</span>&lt;serde_json::Value, ScrapflyError&gt; {
<a href=#347 id=347 data-nosnippet>347</a>        <span class="self">self</span>.fetch_monitoring_json(
<a href=#348 id=348 data-nosnippet>348</a>            <span class="string">"/scrape/monitoring/metrics"</span>,
<a href=#349 id=349 data-nosnippet>349</a>            <span class="kw-2">&amp;</span><span class="self">Self</span>::build_metrics_pairs(opts),
<a href=#350 id=350 data-nosnippet>350</a>        )
<a href=#351 id=351 data-nosnippet>351</a>        .<span class="kw">await
<a href=#352 id=352 data-nosnippet>352</a>    </span>}
<a href=#353 id=353 data-nosnippet>353</a>
<a href=#354 id=354 data-nosnippet>354</a>    <span class="doccomment">/// Fetch per-target monitoring metrics for the Web Scraping API.
<a href=#355 id=355 data-nosnippet>355</a>    </span><span class="kw">pub async fn </span>get_monitoring_target_metrics(
<a href=#356 id=356 data-nosnippet>356</a>        <span class="kw-2">&amp;</span><span class="self">self</span>,
<a href=#357 id=357 data-nosnippet>357</a>        opts: <span class="kw-2">&amp;</span>MonitoringTargetMetricsOptions,
<a href=#358 id=358 data-nosnippet>358</a>    ) -&gt; <span class="prelude-ty">Result</span>&lt;serde_json::Value, ScrapflyError&gt; {
<a href=#359 id=359 data-nosnippet>359</a>        <span class="kw">let </span>pairs = <span class="self">Self</span>::build_target_pairs(opts)<span class="question-mark">?</span>;
<a href=#360 id=360 data-nosnippet>360</a>        <span class="self">self</span>.fetch_monitoring_json(<span class="string">"/scrape/monitoring/metrics/target"</span>, <span class="kw-2">&amp;</span>pairs)
<a href=#361 id=361 data-nosnippet>361</a>            .<span class="kw">await
<a href=#362 id=362 data-nosnippet>362</a>    </span>}
<a href=#363 id=363 data-nosnippet>363</a>
<a href=#364 id=364 data-nosnippet>364</a>    <span class="comment">// ── Screenshot API ───────────────────────────────────────────────
<a href=#365 id=365 data-nosnippet>365</a>
<a href=#366 id=366 data-nosnippet>366</a>    </span><span class="doccomment">/// Fetch aggregate monitoring metrics for the Screenshot API.
<a href=#367 id=367 data-nosnippet>367</a>    </span><span class="kw">pub async fn </span>get_screenshot_monitoring_metrics(
<a href=#368 id=368 data-nosnippet>368</a>        <span class="kw-2">&amp;</span><span class="self">self</span>,
<a href=#369 id=369 data-nosnippet>369</a>        opts: <span class="kw-2">&amp;</span>MonitoringMetricsOptions,
<a href=#370 id=370 data-nosnippet>370</a>    ) -&gt; <span class="prelude-ty">Result</span>&lt;serde_json::Value, ScrapflyError&gt; {
<a href=#371 id=371 data-nosnippet>371</a>        <span class="self">self</span>.fetch_monitoring_json(
<a href=#372 id=372 data-nosnippet>372</a>            <span class="string">"/screenshot/monitoring/metrics"</span>,
<a href=#373 id=373 data-nosnippet>373</a>            <span class="kw-2">&amp;</span><span class="self">Self</span>::build_metrics_pairs(opts),
<a href=#374 id=374 data-nosnippet>374</a>        )
<a href=#375 id=375 data-nosnippet>375</a>        .<span class="kw">await
<a href=#376 id=376 data-nosnippet>376</a>    </span>}
<a href=#377 id=377 data-nosnippet>377</a>
<a href=#378 id=378 data-nosnippet>378</a>    <span class="doccomment">/// Fetch per-target monitoring metrics for the Screenshot API.
<a href=#379 id=379 data-nosnippet>379</a>    </span><span class="kw">pub async fn </span>get_screenshot_monitoring_target_metrics(
<a href=#380 id=380 data-nosnippet>380</a>        <span class="kw-2">&amp;</span><span class="self">self</span>,
<a href=#381 id=381 data-nosnippet>381</a>        opts: <span class="kw-2">&amp;</span>MonitoringTargetMetricsOptions,
<a href=#382 id=382 data-nosnippet>382</a>    ) -&gt; <span class="prelude-ty">Result</span>&lt;serde_json::Value, ScrapflyError&gt; {
<a href=#383 id=383 data-nosnippet>383</a>        <span class="kw">let </span>pairs = <span class="self">Self</span>::build_target_pairs(opts)<span class="question-mark">?</span>;
<a href=#384 id=384 data-nosnippet>384</a>        <span class="self">self</span>.fetch_monitoring_json(<span class="string">"/screenshot/monitoring/metrics/target"</span>, <span class="kw-2">&amp;</span>pairs)
<a href=#385 id=385 data-nosnippet>385</a>            .<span class="kw">await
<a href=#386 id=386 data-nosnippet>386</a>    </span>}
<a href=#387 id=387 data-nosnippet>387</a>
<a href=#388 id=388 data-nosnippet>388</a>    <span class="comment">// ── Extraction API ───────────────────────────────────────────────
<a href=#389 id=389 data-nosnippet>389</a>
<a href=#390 id=390 data-nosnippet>390</a>    </span><span class="doccomment">/// Fetch aggregate monitoring metrics for the Extraction API.
<a href=#391 id=391 data-nosnippet>391</a>    </span><span class="kw">pub async fn </span>get_extraction_monitoring_metrics(
<a href=#392 id=392 data-nosnippet>392</a>        <span class="kw-2">&amp;</span><span class="self">self</span>,
<a href=#393 id=393 data-nosnippet>393</a>        opts: <span class="kw-2">&amp;</span>MonitoringMetricsOptions,
<a href=#394 id=394 data-nosnippet>394</a>    ) -&gt; <span class="prelude-ty">Result</span>&lt;serde_json::Value, ScrapflyError&gt; {
<a href=#395 id=395 data-nosnippet>395</a>        <span class="self">self</span>.fetch_monitoring_json(
<a href=#396 id=396 data-nosnippet>396</a>            <span class="string">"/extraction/monitoring/metrics"</span>,
<a href=#397 id=397 data-nosnippet>397</a>            <span class="kw-2">&amp;</span><span class="self">Self</span>::build_metrics_pairs(opts),
<a href=#398 id=398 data-nosnippet>398</a>        )
<a href=#399 id=399 data-nosnippet>399</a>        .<span class="kw">await
<a href=#400 id=400 data-nosnippet>400</a>    </span>}
<a href=#401 id=401 data-nosnippet>401</a>
<a href=#402 id=402 data-nosnippet>402</a>    <span class="doccomment">/// Fetch per-target monitoring metrics for the Extraction API.
<a href=#403 id=403 data-nosnippet>403</a>    </span><span class="kw">pub async fn </span>get_extraction_monitoring_target_metrics(
<a href=#404 id=404 data-nosnippet>404</a>        <span class="kw-2">&amp;</span><span class="self">self</span>,
<a href=#405 id=405 data-nosnippet>405</a>        opts: <span class="kw-2">&amp;</span>MonitoringTargetMetricsOptions,
<a href=#406 id=406 data-nosnippet>406</a>    ) -&gt; <span class="prelude-ty">Result</span>&lt;serde_json::Value, ScrapflyError&gt; {
<a href=#407 id=407 data-nosnippet>407</a>        <span class="kw">let </span>pairs = <span class="self">Self</span>::build_target_pairs(opts)<span class="question-mark">?</span>;
<a href=#408 id=408 data-nosnippet>408</a>        <span class="self">self</span>.fetch_monitoring_json(<span class="string">"/extraction/monitoring/metrics/target"</span>, <span class="kw-2">&amp;</span>pairs)
<a href=#409 id=409 data-nosnippet>409</a>            .<span class="kw">await
<a href=#410 id=410 data-nosnippet>410</a>    </span>}
<a href=#411 id=411 data-nosnippet>411</a>
<a href=#412 id=412 data-nosnippet>412</a>    <span class="comment">// ── Crawler API ──────────────────────────────────────────────────
<a href=#413 id=413 data-nosnippet>413</a>
<a href=#414 id=414 data-nosnippet>414</a>    </span><span class="doccomment">/// Fetch aggregate monitoring metrics for the Crawler API.
<a href=#415 id=415 data-nosnippet>415</a>    </span><span class="kw">pub async fn </span>get_crawler_monitoring_metrics(
<a href=#416 id=416 data-nosnippet>416</a>        <span class="kw-2">&amp;</span><span class="self">self</span>,
<a href=#417 id=417 data-nosnippet>417</a>        opts: <span class="kw-2">&amp;</span>MonitoringMetricsOptions,
<a href=#418 id=418 data-nosnippet>418</a>    ) -&gt; <span class="prelude-ty">Result</span>&lt;serde_json::Value, ScrapflyError&gt; {
<a href=#419 id=419 data-nosnippet>419</a>        <span class="self">self</span>.fetch_monitoring_json(
<a href=#420 id=420 data-nosnippet>420</a>            <span class="string">"/crawl/monitoring/metrics"</span>,
<a href=#421 id=421 data-nosnippet>421</a>            <span class="kw-2">&amp;</span><span class="self">Self</span>::build_metrics_pairs(opts),
<a href=#422 id=422 data-nosnippet>422</a>        )
<a href=#423 id=423 data-nosnippet>423</a>        .<span class="kw">await
<a href=#424 id=424 data-nosnippet>424</a>    </span>}
<a href=#425 id=425 data-nosnippet>425</a>
<a href=#426 id=426 data-nosnippet>426</a>    <span class="doccomment">/// Fetch per-target monitoring metrics for the Crawler API.
<a href=#427 id=427 data-nosnippet>427</a>    </span><span class="kw">pub async fn </span>get_crawler_monitoring_target_metrics(
<a href=#428 id=428 data-nosnippet>428</a>        <span class="kw-2">&amp;</span><span class="self">self</span>,
<a href=#429 id=429 data-nosnippet>429</a>        opts: <span class="kw-2">&amp;</span>MonitoringTargetMetricsOptions,
<a href=#430 id=430 data-nosnippet>430</a>    ) -&gt; <span class="prelude-ty">Result</span>&lt;serde_json::Value, ScrapflyError&gt; {
<a href=#431 id=431 data-nosnippet>431</a>        <span class="kw">let </span>pairs = <span class="self">Self</span>::build_target_pairs(opts)<span class="question-mark">?</span>;
<a href=#432 id=432 data-nosnippet>432</a>        <span class="self">self</span>.fetch_monitoring_json(<span class="string">"/crawl/monitoring/metrics/target"</span>, <span class="kw-2">&amp;</span>pairs)
<a href=#433 id=433 data-nosnippet>433</a>            .<span class="kw">await
<a href=#434 id=434 data-nosnippet>434</a>    </span>}
<a href=#435 id=435 data-nosnippet>435</a>
<a href=#436 id=436 data-nosnippet>436</a>    <span class="comment">// ── Cloud Browser API (session-based, distinct shape) ────────────
<a href=#437 id=437 data-nosnippet>437</a>
<a href=#438 id=438 data-nosnippet>438</a>    </span><span class="doccomment">/// Fetch aggregate monitoring metrics for the Cloud Browser API.
<a href=#439 id=439 data-nosnippet>439</a>    </span><span class="kw">pub async fn </span>get_browser_monitoring_metrics(
<a href=#440 id=440 data-nosnippet>440</a>        <span class="kw-2">&amp;</span><span class="self">self</span>,
<a href=#441 id=441 data-nosnippet>441</a>        opts: <span class="kw-2">&amp;</span>CloudBrowserMonitoringOptions,
<a href=#442 id=442 data-nosnippet>442</a>    ) -&gt; <span class="prelude-ty">Result</span>&lt;serde_json::Value, ScrapflyError&gt; {
<a href=#443 id=443 data-nosnippet>443</a>        <span class="kw">let </span>pairs = <span class="self">Self</span>::build_browser_pairs(opts)<span class="question-mark">?</span>;
<a href=#444 id=444 data-nosnippet>444</a>        <span class="self">self</span>.fetch_monitoring_json(<span class="string">"/browser/monitoring/metrics"</span>, <span class="kw-2">&amp;</span>pairs)
<a href=#445 id=445 data-nosnippet>445</a>            .<span class="kw">await
<a href=#446 id=446 data-nosnippet>446</a>    </span>}
<a href=#447 id=447 data-nosnippet>447</a>
<a href=#448 id=448 data-nosnippet>448</a>    <span class="doccomment">/// Fetch monitoring time-series for the Cloud Browser API.
<a href=#449 id=449 data-nosnippet>449</a>    </span><span class="kw">pub async fn </span>get_browser_monitoring_timeseries(
<a href=#450 id=450 data-nosnippet>450</a>        <span class="kw-2">&amp;</span><span class="self">self</span>,
<a href=#451 id=451 data-nosnippet>451</a>        opts: <span class="kw-2">&amp;</span>CloudBrowserMonitoringOptions,
<a href=#452 id=452 data-nosnippet>452</a>    ) -&gt; <span class="prelude-ty">Result</span>&lt;serde_json::Value, ScrapflyError&gt; {
<a href=#453 id=453 data-nosnippet>453</a>        <span class="kw">let </span>pairs = <span class="self">Self</span>::build_browser_pairs(opts)<span class="question-mark">?</span>;
<a href=#454 id=454 data-nosnippet>454</a>        <span class="self">self</span>.fetch_monitoring_json(<span class="string">"/browser/monitoring/metrics/timeseries"</span>, <span class="kw-2">&amp;</span>pairs)
<a href=#455 id=455 data-nosnippet>455</a>            .<span class="kw">await
<a href=#456 id=456 data-nosnippet>456</a>    </span>}
<a href=#457 id=457 data-nosnippet>457</a>
<a href=#458 id=458 data-nosnippet>458</a>    <span class="kw">fn </span>build_browser_pairs(
<a href=#459 id=459 data-nosnippet>459</a>        opts: <span class="kw-2">&amp;</span>CloudBrowserMonitoringOptions,
<a href=#460 id=460 data-nosnippet>460</a>    ) -&gt; <span class="prelude-ty">Result</span>&lt;Vec&lt;(String, String)&gt;, ScrapflyError&gt; {
<a href=#461 id=461 data-nosnippet>461</a>        <span class="kw">if </span>opts.start.is_some() != opts.end.is_some() {
<a href=#462 id=462 data-nosnippet>462</a>            <span class="kw">return </span><span class="prelude-val">Err</span>(ScrapflyError::Config(
<a href=#463 id=463 data-nosnippet>463</a>                <span class="string">"cloud browser monitoring: start and end must be provided together"</span>.into(),
<a href=#464 id=464 data-nosnippet>464</a>            ));
<a href=#465 id=465 data-nosnippet>465</a>        }
<a href=#466 id=466 data-nosnippet>466</a>        <span class="kw">let </span><span class="kw-2">mut </span>pairs: Vec&lt;(String, String)&gt; = Vec::new();
<a href=#467 id=467 data-nosnippet>467</a>        <span class="kw">match </span>(<span class="kw-2">&amp;</span>opts.start, <span class="kw-2">&amp;</span>opts.end) {
<a href=#468 id=468 data-nosnippet>468</a>            (<span class="prelude-val">Some</span>(s), <span class="prelude-val">Some</span>(e)) =&gt; {
<a href=#469 id=469 data-nosnippet>469</a>                pairs.push((<span class="string">"start"</span>.into(), s.clone()));
<a href=#470 id=470 data-nosnippet>470</a>                pairs.push((<span class="string">"end"</span>.into(), e.clone()));
<a href=#471 id=471 data-nosnippet>471</a>            }
<a href=#472 id=472 data-nosnippet>472</a>            <span class="kw">_ </span>=&gt; {
<a href=#473 id=473 data-nosnippet>473</a>                <span class="kw">if let </span><span class="prelude-val">Some</span>(p) = opts.period {
<a href=#474 id=474 data-nosnippet>474</a>                    pairs.push((<span class="string">"period"</span>.into(), p.as_str().into()));
<a href=#475 id=475 data-nosnippet>475</a>                }
<a href=#476 id=476 data-nosnippet>476</a>            }
<a href=#477 id=477 data-nosnippet>477</a>        }
<a href=#478 id=478 data-nosnippet>478</a>        <span class="kw">if let </span><span class="prelude-val">Some</span>(<span class="kw-2">ref </span>pool) = opts.proxy_pool {
<a href=#479 id=479 data-nosnippet>479</a>            pairs.push((<span class="string">"proxy_pool"</span>.into(), pool.clone()));
<a href=#480 id=480 data-nosnippet>480</a>        }
<a href=#481 id=481 data-nosnippet>481</a>        <span class="prelude-val">Ok</span>(pairs)
<a href=#482 id=482 data-nosnippet>482</a>    }
<a href=#483 id=483 data-nosnippet>483</a>
<a href=#484 id=484 data-nosnippet>484</a>    <span class="doccomment">/// Scrape a URL.
<a href=#485 id=485 data-nosnippet>485</a>    </span><span class="kw">pub async fn </span>scrape(<span class="kw-2">&amp;</span><span class="self">self</span>, config: <span class="kw-2">&amp;</span>ScrapeConfig) -&gt; <span class="prelude-ty">Result</span>&lt;ScrapeResult, ScrapflyError&gt; {
<a href=#486 id=486 data-nosnippet>486</a>        <span class="kw">let </span>pairs = config.to_query_pairs()<span class="question-mark">?</span>;
<a href=#487 id=487 data-nosnippet>487</a>        <span class="kw">let </span>url = <span class="self">self</span>.build_url(<span class="string">"/scrape"</span>, <span class="kw-2">&amp;</span>pairs)<span class="question-mark">?</span>;
<a href=#488 id=488 data-nosnippet>488</a>        <span class="kw">let </span>method = <span class="kw">match </span>config.method {
<a href=#489 id=489 data-nosnippet>489</a>            <span class="prelude-val">Some</span>(m) =&gt; Method::from_bytes(m.as_str().as_bytes())
<a href=#490 id=490 data-nosnippet>490</a>                .map_err(|e| ScrapflyError::Config(<span class="macro">format!</span>(<span class="string">"invalid method: {}"</span>, e)))<span class="question-mark">?</span>,
<a href=#491 id=491 data-nosnippet>491</a>            <span class="prelude-val">None </span>=&gt; Method::GET,
<a href=#492 id=492 data-nosnippet>492</a>        };
<a href=#493 id=493 data-nosnippet>493</a>        <span class="kw">let </span><span class="kw-2">mut </span>headers = HeaderMap::new();
<a href=#494 id=494 data-nosnippet>494</a>        headers.insert(ACCEPT, HeaderValue::from_static(<span class="string">"application/json"</span>));
<a href=#495 id=495 data-nosnippet>495</a>        <span class="kw">let </span>body = config.body.clone();
<a href=#496 id=496 data-nosnippet>496</a>        <span class="kw">let </span>resp = <span class="self">self
<a href=#497 id=497 data-nosnippet>497</a>            </span>.send_with_retry(method, url, <span class="prelude-val">Some</span>(headers), body.map(|b| b.into_bytes()))
<a href=#498 id=498 data-nosnippet>498</a>            .<span class="kw">await</span><span class="question-mark">?</span>;
<a href=#499 id=499 data-nosnippet>499</a>        <span class="kw">let </span>(status, _h, body_bytes) = read_response(resp).<span class="kw">await</span><span class="question-mark">?</span>;
<a href=#500 id=500 data-nosnippet>500</a>        <span class="kw">if </span>status != <span class="number">200 </span>{
<a href=#501 id=501 data-nosnippet>501</a>            <span class="kw">return </span><span class="prelude-val">Err</span>(from_response(status, <span class="kw-2">&amp;</span>body_bytes, <span class="number">0</span>, <span class="bool-val">false</span>));
<a href=#502 id=502 data-nosnippet>502</a>        }
<a href=#503 id=503 data-nosnippet>503</a>        <span class="comment">// HEAD has no body per HTTP spec, so the Scrapfly API returns a 200
<a href=#504 id=504 data-nosnippet>504</a>        // with an empty body — there's no JSON envelope to parse. Synthesize
<a href=#505 id=505 data-nosnippet>505</a>        // a minimal ScrapeResult so callers still get a typed response with
<a href=#506 id=506 data-nosnippet>506</a>        // status_code=200 and an empty content string. Matches Python SDK
<a href=#507 id=507 data-nosnippet>507</a>        // behavior, which tolerates an empty body_handler read on HEAD.
<a href=#508 id=508 data-nosnippet>508</a>        </span><span class="kw">if </span><span class="macro">matches!</span>(config.method, <span class="prelude-val">Some</span>(HttpMethod::Head)) &amp;&amp; body_bytes.is_empty() {
<a href=#509 id=509 data-nosnippet>509</a>            <span class="kw">return </span><span class="prelude-val">Ok</span>(ScrapeResult {
<a href=#510 id=510 data-nosnippet>510</a>                uuid: String::new(),
<a href=#511 id=511 data-nosnippet>511</a>                config: serde_json::Value::Null,
<a href=#512 id=512 data-nosnippet>512</a>                context: serde_json::Value::Null,
<a href=#513 id=513 data-nosnippet>513</a>                result: ResultData {
<a href=#514 id=514 data-nosnippet>514</a>                    status_code: <span class="number">200</span>,
<a href=#515 id=515 data-nosnippet>515</a>                    success: <span class="bool-val">true</span>,
<a href=#516 id=516 data-nosnippet>516</a>                    ..Default::default()
<a href=#517 id=517 data-nosnippet>517</a>                },
<a href=#518 id=518 data-nosnippet>518</a>            });
<a href=#519 id=519 data-nosnippet>519</a>        }
<a href=#520 id=520 data-nosnippet>520</a>        <span class="kw">let </span><span class="kw-2">mut </span>result: ScrapeResult = serde_json::from_slice(<span class="kw-2">&amp;</span>body_bytes)<span class="question-mark">?</span>;
<a href=#521 id=521 data-nosnippet>521</a>        <span class="comment">// Upstream failure handling: the Scrapfly API call itself may succeed
<a href=#522 id=522 data-nosnippet>522</a>        // (HTTP 200) while the *target* site returned a failure. In that case
<a href=#523 id=523 data-nosnippet>523</a>        // result.result.success is false and we must surface it as an error
<a href=#524 id=524 data-nosnippet>524</a>        // variant so callers can `match` on it. Mirrors the Go SDK behavior
<a href=#525 id=525 data-nosnippet>525</a>        // in `sdk/go/client.go::checkResult` (4xx → UpstreamClient,
<a href=#526 id=526 data-nosnippet>526</a>        // 5xx → UpstreamServer).
<a href=#527 id=527 data-nosnippet>527</a>        </span><span class="kw">if </span>!result.result.success {
<a href=#528 id=528 data-nosnippet>528</a>            <span class="kw">let </span>(err_code, err_message, err_doc) = <span class="kw">match </span><span class="kw-2">&amp;</span>result.result.error {
<a href=#529 id=529 data-nosnippet>529</a>                <span class="prelude-val">Some</span>(e) =&gt; (e.code.clone(), e.message.clone(), e.doc_url.clone()),
<a href=#530 id=530 data-nosnippet>530</a>                <span class="prelude-val">None </span>=&gt; (
<a href=#531 id=531 data-nosnippet>531</a>                    result.result.status.clone(),
<a href=#532 id=532 data-nosnippet>532</a>                    <span class="macro">format!</span>(
<a href=#533 id=533 data-nosnippet>533</a>                        <span class="string">"scrape failed with status_code={}"</span>,
<a href=#534 id=534 data-nosnippet>534</a>                        result.result.status_code
<a href=#535 id=535 data-nosnippet>535</a>                    ),
<a href=#536 id=536 data-nosnippet>536</a>                    String::new(),
<a href=#537 id=537 data-nosnippet>537</a>                ),
<a href=#538 id=538 data-nosnippet>538</a>            };
<a href=#539 id=539 data-nosnippet>539</a>            <span class="kw">let </span>api_err = ApiError {
<a href=#540 id=540 data-nosnippet>540</a>                code: err_code,
<a href=#541 id=541 data-nosnippet>541</a>                message: err_message,
<a href=#542 id=542 data-nosnippet>542</a>                http_status: result.result.status_code,
<a href=#543 id=543 data-nosnippet>543</a>                documentation_url: err_doc,
<a href=#544 id=544 data-nosnippet>544</a>                hint: String::new(),
<a href=#545 id=545 data-nosnippet>545</a>                retry_after_ms: <span class="number">0</span>,
<a href=#546 id=546 data-nosnippet>546</a>            };
<a href=#547 id=547 data-nosnippet>547</a>            <span class="kw">let </span>sc = result.result.status_code;
<a href=#548 id=548 data-nosnippet>548</a>            <span class="kw">if </span>(<span class="number">400</span>..<span class="number">500</span>).contains(<span class="kw-2">&amp;</span>sc) {
<a href=#549 id=549 data-nosnippet>549</a>                <span class="kw">return </span><span class="prelude-val">Err</span>(ScrapflyError::UpstreamClient(api_err));
<a href=#550 id=550 data-nosnippet>550</a>            }
<a href=#551 id=551 data-nosnippet>551</a>            <span class="kw">if </span>(<span class="number">500</span>..<span class="number">600</span>).contains(<span class="kw-2">&amp;</span>sc) {
<a href=#552 id=552 data-nosnippet>552</a>                <span class="kw">return </span><span class="prelude-val">Err</span>(ScrapflyError::UpstreamServer(api_err));
<a href=#553 id=553 data-nosnippet>553</a>            }
<a href=#554 id=554 data-nosnippet>554</a>            <span class="comment">// Unknown status code (e.g. 0, timeouts) — fall through to generic
<a href=#555 id=555 data-nosnippet>555</a>            // Api error rather than silently returning a failed result.
<a href=#556 id=556 data-nosnippet>556</a>            </span><span class="kw">return </span><span class="prelude-val">Err</span>(ScrapflyError::Api(api_err));
<a href=#557 id=557 data-nosnippet>557</a>        }
<a href=#558 id=558 data-nosnippet>558</a>        <span class="comment">// Transparent large-object handling: when a scrape response is too
<a href=#559 id=559 data-nosnippet>559</a>        // large, the engine offloads the body to a signed URL and sets
<a href=#560 id=560 data-nosnippet>560</a>        // `format=clob|blob`, stashing the URL in `content`. The SDK must
<a href=#561 id=561 data-nosnippet>561</a>        // auto-fetch and surface the final bytes + a user-friendly format
<a href=#562 id=562 data-nosnippet>562</a>        // marker (clob→text, blob→binary). Mirrors `sdk/go/client.go::handleLargeObjects`.
<a href=#563 id=563 data-nosnippet>563</a>        </span><span class="kw">if </span>result.result.success &amp;&amp; result.result.status == <span class="string">"DONE" </span>{
<a href=#564 id=564 data-nosnippet>564</a>            <span class="kw">let </span>fmt = result.result.format.as_str();
<a href=#565 id=565 data-nosnippet>565</a>            <span class="kw">if </span>fmt == <span class="string">"clob" </span>|| fmt == <span class="string">"blob" </span>{
<a href=#566 id=566 data-nosnippet>566</a>                <span class="kw">let </span>(new_content, new_format) =
<a href=#567 id=567 data-nosnippet>567</a>                    <span class="self">self</span>.fetch_large_object(<span class="kw-2">&amp;</span>result.result.content, fmt).<span class="kw">await</span><span class="question-mark">?</span>;
<a href=#568 id=568 data-nosnippet>568</a>                result.result.content = new_content;
<a href=#569 id=569 data-nosnippet>569</a>                result.result.format = new_format;
<a href=#570 id=570 data-nosnippet>570</a>            }
<a href=#571 id=571 data-nosnippet>571</a>        }
<a href=#572 id=572 data-nosnippet>572</a>        <span class="prelude-val">Ok</span>(result)
<a href=#573 id=573 data-nosnippet>573</a>    }
<a href=#574 id=574 data-nosnippet>574</a>
<a href=#575 id=575 data-nosnippet>575</a>    <span class="doccomment">/// Fetch an offloaded large-object body from its signed URL, re-attaching
<a href=#576 id=576 data-nosnippet>576</a>    /// the API key as a query param. Returns `(content, format)`:
<a href=#577 id=577 data-nosnippet>577</a>    /// `clob → ("…text…", "text")`, `blob → ("…bytes as lossy utf8…", "binary")`.
<a href=#578 id=578 data-nosnippet>578</a>    </span><span class="kw">async fn </span>fetch_large_object(
<a href=#579 id=579 data-nosnippet>579</a>        <span class="kw-2">&amp;</span><span class="self">self</span>,
<a href=#580 id=580 data-nosnippet>580</a>        content_url: <span class="kw-2">&amp;</span>str,
<a href=#581 id=581 data-nosnippet>581</a>        format: <span class="kw-2">&amp;</span>str,
<a href=#582 id=582 data-nosnippet>582</a>    ) -&gt; <span class="prelude-ty">Result</span>&lt;(String, String), ScrapflyError&gt; {
<a href=#583 id=583 data-nosnippet>583</a>        <span class="kw">let </span><span class="kw-2">mut </span>url = Url::parse(content_url)
<a href=#584 id=584 data-nosnippet>584</a>            .map_err(|e| ScrapflyError::Config(<span class="macro">format!</span>(<span class="string">"invalid large-object url: {}"</span>, e)))<span class="question-mark">?</span>;
<a href=#585 id=585 data-nosnippet>585</a>        <span class="comment">// Append the API key without clobbering existing query params.
<a href=#586 id=586 data-nosnippet>586</a>        </span>{
<a href=#587 id=587 data-nosnippet>587</a>            <span class="kw">let </span>existing: Vec&lt;(String, String)&gt; = url
<a href=#588 id=588 data-nosnippet>588</a>                .query_pairs()
<a href=#589 id=589 data-nosnippet>589</a>                .filter(|(k, <span class="kw">_</span>)| k != <span class="string">"key"</span>)
<a href=#590 id=590 data-nosnippet>590</a>                .map(|(k, v)| (k.into_owned(), v.into_owned()))
<a href=#591 id=591 data-nosnippet>591</a>                .collect();
<a href=#592 id=592 data-nosnippet>592</a>            <span class="kw">let </span><span class="kw-2">mut </span>qs = url.query_pairs_mut();
<a href=#593 id=593 data-nosnippet>593</a>            qs.clear();
<a href=#594 id=594 data-nosnippet>594</a>            <span class="kw">for </span>(k, v) <span class="kw">in </span>existing {
<a href=#595 id=595 data-nosnippet>595</a>                qs.append_pair(<span class="kw-2">&amp;</span>k, <span class="kw-2">&amp;</span>v);
<a href=#596 id=596 data-nosnippet>596</a>            }
<a href=#597 id=597 data-nosnippet>597</a>            qs.append_pair(<span class="string">"key"</span>, <span class="self">self</span>.api_key());
<a href=#598 id=598 data-nosnippet>598</a>        }
<a href=#599 id=599 data-nosnippet>599</a>        <span class="kw">let </span><span class="kw-2">mut </span>headers = HeaderMap::new();
<a href=#600 id=600 data-nosnippet>600</a>        headers.insert(ACCEPT, HeaderValue::from_static(<span class="string">"application/json"</span>));
<a href=#601 id=601 data-nosnippet>601</a>        <span class="kw">let </span>resp = <span class="self">self
<a href=#602 id=602 data-nosnippet>602</a>            </span>.send_with_retry(Method::GET, url, <span class="prelude-val">Some</span>(headers), <span class="prelude-val">None</span>)
<a href=#603 id=603 data-nosnippet>603</a>            .<span class="kw">await</span><span class="question-mark">?</span>;
<a href=#604 id=604 data-nosnippet>604</a>        <span class="kw">let </span>(status, _h, body) = read_response(resp).<span class="kw">await</span><span class="question-mark">?</span>;
<a href=#605 id=605 data-nosnippet>605</a>        <span class="kw">if </span>status != <span class="number">200 </span>{
<a href=#606 id=606 data-nosnippet>606</a>            <span class="kw">return </span><span class="prelude-val">Err</span>(from_response(status, <span class="kw-2">&amp;</span>body, <span class="number">0</span>, <span class="bool-val">false</span>));
<a href=#607 id=607 data-nosnippet>607</a>        }
<a href=#608 id=608 data-nosnippet>608</a>        <span class="kw">let </span>new_format = <span class="kw">match </span>format {
<a href=#609 id=609 data-nosnippet>609</a>            <span class="string">"clob" </span>=&gt; <span class="string">"text"</span>,
<a href=#610 id=610 data-nosnippet>610</a>            <span class="string">"blob" </span>=&gt; <span class="string">"binary"</span>,
<a href=#611 id=611 data-nosnippet>611</a>            <span class="kw">_ </span>=&gt; {
<a href=#612 id=612 data-nosnippet>612</a>                <span class="kw">return </span><span class="prelude-val">Err</span>(ScrapflyError::Config(<span class="macro">format!</span>(
<a href=#613 id=613 data-nosnippet>613</a>                    <span class="string">"unsupported large-object format: {}"</span>,
<a href=#614 id=614 data-nosnippet>614</a>                    format
<a href=#615 id=615 data-nosnippet>615</a>                )))
<a href=#616 id=616 data-nosnippet>616</a>            }
<a href=#617 id=617 data-nosnippet>617</a>        };
<a href=#618 id=618 data-nosnippet>618</a>        <span class="comment">// For blob (binary PDF, image, etc.) we use from_utf8_lossy to
<a href=#619 id=619 data-nosnippet>619</a>        // preserve the raw bytes in the `content` string field, matching
<a href=#620 id=620 data-nosnippet>620</a>        // the Go/Python SDKs' behavior.
<a href=#621 id=621 data-nosnippet>621</a>        </span><span class="kw">let </span>content = String::from_utf8_lossy(<span class="kw-2">&amp;</span>body).into_owned();
<a href=#622 id=622 data-nosnippet>622</a>        <span class="prelude-val">Ok</span>((content, new_format.to_string()))
<a href=#623 id=623 data-nosnippet>623</a>    }
<a href=#624 id=624 data-nosnippet>624</a>
<a href=#625 id=625 data-nosnippet>625</a>    <span class="doccomment">/// Concurrent-scrape stream. Emits results in completion order.
<a href=#626 id=626 data-nosnippet>626</a>    </span><span class="kw">pub fn </span>concurrent_scrape&lt;<span class="lifetime">'a</span>, I&gt;(
<a href=#627 id=627 data-nosnippet>627</a>        <span class="kw-2">&amp;</span><span class="lifetime">'a </span><span class="self">self</span>,
<a href=#628 id=628 data-nosnippet>628</a>        configs: I,
<a href=#629 id=629 data-nosnippet>629</a>        concurrency_limit: usize,
<a href=#630 id=630 data-nosnippet>630</a>    ) -&gt; <span class="kw">impl </span>Stream&lt;Item = <span class="prelude-ty">Result</span>&lt;ScrapeResult, ScrapflyError&gt;&gt; + <span class="lifetime">'a
<a href=#631 id=631 data-nosnippet>631</a>    </span><span class="kw">where
<a href=#632 id=632 data-nosnippet>632</a>        </span>I: IntoIterator&lt;Item = ScrapeConfig&gt; + <span class="lifetime">'a</span>,
<a href=#633 id=633 data-nosnippet>633</a>        &lt;I <span class="kw">as </span>IntoIterator&gt;::IntoIter: <span class="lifetime">'a</span>,
<a href=#634 id=634 data-nosnippet>634</a>    {
<a href=#635 id=635 data-nosnippet>635</a>        <span class="kw">let </span>limit = <span class="kw">if </span>concurrency_limit == <span class="number">0 </span>{
<a href=#636 id=636 data-nosnippet>636</a>            <span class="number">5
<a href=#637 id=637 data-nosnippet>637</a>        </span>} <span class="kw">else </span>{
<a href=#638 id=638 data-nosnippet>638</a>            concurrency_limit
<a href=#639 id=639 data-nosnippet>639</a>        };
<a href=#640 id=640 data-nosnippet>640</a>        futures_util::stream::iter(
<a href=#641 id=641 data-nosnippet>641</a>            configs
<a href=#642 id=642 data-nosnippet>642</a>                .into_iter()
<a href=#643 id=643 data-nosnippet>643</a>                .map(<span class="kw">move </span>|cfg| <span class="kw">async move </span>{ <span class="self">self</span>.scrape(<span class="kw-2">&amp;</span>cfg).<span class="kw">await </span>}),
<a href=#644 id=644 data-nosnippet>644</a>        )
<a href=#645 id=645 data-nosnippet>645</a>        .buffer_unordered(limit)
<a href=#646 id=646 data-nosnippet>646</a>    }
<a href=#647 id=647 data-nosnippet>647</a>
<a href=#648 id=648 data-nosnippet>648</a>    <span class="doccomment">/// POST /scrape/batch: scrape up to 100 URLs and stream results
<a href=#649 id=649 data-nosnippet>649</a>    /// back as each scrape completes. Returns an async stream where
<a href=#650 id=650 data-nosnippet>650</a>    /// each item is `(correlation_id, Result&lt;ScrapeResult, ScrapflyError&gt;)`.
<a href=#651 id=651 data-nosnippet>651</a>    ///
<a href=#652 id=652 data-nosnippet>652</a>    /// Results arrive OUT OF ORDER — whichever scrape finishes first
<a href=#653 id=653 data-nosnippet>653</a>    /// is yielded first. Every `ScrapeConfig` MUST carry a unique
<a href=#654 id=654 data-nosnippet>654</a>    /// `correlation_id`; missing / duplicate values are caught
<a href=#655 id=655 data-nosnippet>655</a>    /// client-side before the request is sent.
<a href=#656 id=656 data-nosnippet>656</a>    ///
<a href=#657 id=657 data-nosnippet>657</a>    /// Batch-level failures (plan gate, insufficient concurrency,
<a href=#658 id=658 data-nosnippet>658</a>    /// validation) surface as the outer `Err(ScrapflyError)` returned
<a href=#659 id=659 data-nosnippet>659</a>    /// from the `await` — the stream is only created after the
<a href=#660 id=660 data-nosnippet>660</a>    /// batch request succeeds.
<a href=#661 id=661 data-nosnippet>661</a>    </span><span class="kw">pub async fn </span>scrape_batch(
<a href=#662 id=662 data-nosnippet>662</a>        <span class="kw-2">&amp;</span><span class="self">self</span>,
<a href=#663 id=663 data-nosnippet>663</a>        configs: <span class="kw-2">&amp;</span>[ScrapeConfig],
<a href=#664 id=664 data-nosnippet>664</a>    ) -&gt; <span class="prelude-ty">Result</span>&lt;<span class="kw">impl </span>Stream&lt;Item = (String, <span class="kw">crate</span>::batch::BatchOutcome)&gt;, ScrapflyError&gt; {
<a href=#665 id=665 data-nosnippet>665</a>        <span class="self">self</span>.scrape_batch_with_options(configs, <span class="kw">crate</span>::batch::BatchOptions::default())
<a href=#666 id=666 data-nosnippet>666</a>            .<span class="kw">await
<a href=#667 id=667 data-nosnippet>667</a>    </span>}
<a href=#668 id=668 data-nosnippet>668</a>
<a href=#669 id=669 data-nosnippet>669</a>    <span class="doccomment">/// Like `scrape_batch` but with explicit `BatchOptions`
<a href=#670 id=670 data-nosnippet>670</a>    /// (msgpack wire format, etc.).
<a href=#671 id=671 data-nosnippet>671</a>    </span><span class="kw">pub async fn </span>scrape_batch_with_options(
<a href=#672 id=672 data-nosnippet>672</a>        <span class="kw-2">&amp;</span><span class="self">self</span>,
<a href=#673 id=673 data-nosnippet>673</a>        configs: <span class="kw-2">&amp;</span>[ScrapeConfig],
<a href=#674 id=674 data-nosnippet>674</a>        opts: <span class="kw">crate</span>::batch::BatchOptions,
<a href=#675 id=675 data-nosnippet>675</a>    ) -&gt; <span class="prelude-ty">Result</span>&lt;<span class="kw">impl </span>Stream&lt;Item = (String, <span class="kw">crate</span>::batch::BatchOutcome)&gt;, ScrapflyError&gt; {
<a href=#676 id=676 data-nosnippet>676</a>        <span class="kw">use </span><span class="kw">crate</span>::batch::{
<a href=#677 id=677 data-nosnippet>677</a>            build_proxified_response, decode_part_body, parts_from_response, BatchOutcome,
<a href=#678 id=678 data-nosnippet>678</a>        };
<a href=#679 id=679 data-nosnippet>679</a>
<a href=#680 id=680 data-nosnippet>680</a>        <span class="kw">if </span>configs.is_empty() {
<a href=#681 id=681 data-nosnippet>681</a>            <span class="kw">return </span><span class="prelude-val">Err</span>(ScrapflyError::Config(
<a href=#682 id=682 data-nosnippet>682</a>                <span class="string">"scrape_batch: configs is empty"</span>.into(),
<a href=#683 id=683 data-nosnippet>683</a>            ));
<a href=#684 id=684 data-nosnippet>684</a>        }
<a href=#685 id=685 data-nosnippet>685</a>
<a href=#686 id=686 data-nosnippet>686</a>        <span class="kw">if </span>configs.len() &gt; <span class="number">100 </span>{
<a href=#687 id=687 data-nosnippet>687</a>            <span class="kw">return </span><span class="prelude-val">Err</span>(ScrapflyError::Config(<span class="macro">format!</span>(
<a href=#688 id=688 data-nosnippet>688</a>                <span class="string">"scrape_batch: max 100 configs per batch (got {})"</span>,
<a href=#689 id=689 data-nosnippet>689</a>                configs.len()
<a href=#690 id=690 data-nosnippet>690</a>            )));
<a href=#691 id=691 data-nosnippet>691</a>        }
<a href=#692 id=692 data-nosnippet>692</a>
<a href=#693 id=693 data-nosnippet>693</a>        <span class="kw">let </span><span class="kw-2">mut </span>seen: HashMap&lt;String, usize&gt; = HashMap::new();
<a href=#694 id=694 data-nosnippet>694</a>        <span class="kw">let </span><span class="kw-2">mut </span>body_configs: Vec&lt;HashMap&lt;String, String&gt;&gt; = Vec::with_capacity(configs.len());
<a href=#695 id=695 data-nosnippet>695</a>
<a href=#696 id=696 data-nosnippet>696</a>        <span class="kw">for </span>(i, cfg) <span class="kw">in </span>configs.iter().enumerate() {
<a href=#697 id=697 data-nosnippet>697</a>            <span class="kw">let </span>correlation_id = cfg.correlation_id.clone().ok_or_else(|| {
<a href=#698 id=698 data-nosnippet>698</a>                ScrapflyError::Config(<span class="macro">format!</span>(
<a href=#699 id=699 data-nosnippet>699</a>                    <span class="string">"scrape_batch: configs[{}] is missing correlation_id (required for matching streamed parts)"</span>,
<a href=#700 id=700 data-nosnippet>700</a>                    i
<a href=#701 id=701 data-nosnippet>701</a>                ))
<a href=#702 id=702 data-nosnippet>702</a>            })<span class="question-mark">?</span>;
<a href=#703 id=703 data-nosnippet>703</a>
<a href=#704 id=704 data-nosnippet>704</a>            <span class="kw">if let </span><span class="prelude-val">Some</span>(prev) = seen.get(<span class="kw-2">&amp;</span>correlation_id) {
<a href=#705 id=705 data-nosnippet>705</a>                <span class="kw">return </span><span class="prelude-val">Err</span>(ScrapflyError::Config(<span class="macro">format!</span>(
<a href=#706 id=706 data-nosnippet>706</a>                    <span class="string">"scrape_batch: correlation_id {:?} reused by configs[{}] and configs[{}]"</span>,
<a href=#707 id=707 data-nosnippet>707</a>                    correlation_id, prev, i
<a href=#708 id=708 data-nosnippet>708</a>                )));
<a href=#709 id=709 data-nosnippet>709</a>            }
<a href=#710 id=710 data-nosnippet>710</a>
<a href=#711 id=711 data-nosnippet>711</a>            seen.insert(correlation_id.clone(), i);
<a href=#712 id=712 data-nosnippet>712</a>
<a href=#713 id=713 data-nosnippet>713</a>            <span class="kw">let </span>pairs = cfg.to_query_pairs()<span class="question-mark">?</span>;
<a href=#714 id=714 data-nosnippet>714</a>            <span class="kw">let </span><span class="kw-2">mut </span>entry: HashMap&lt;String, String&gt; = HashMap::with_capacity(pairs.len());
<a href=#715 id=715 data-nosnippet>715</a>
<a href=#716 id=716 data-nosnippet>716</a>            <span class="kw">for </span>(k, v) <span class="kw">in </span>pairs {
<a href=#717 id=717 data-nosnippet>717</a>                <span class="kw">if </span>k == <span class="string">"key" </span>{
<a href=#718 id=718 data-nosnippet>718</a>                    <span class="kw">continue</span>;
<a href=#719 id=719 data-nosnippet>719</a>                }
<a href=#720 id=720 data-nosnippet>720</a>
<a href=#721 id=721 data-nosnippet>721</a>                entry.insert(k, v);
<a href=#722 id=722 data-nosnippet>722</a>            }
<a href=#723 id=723 data-nosnippet>723</a>
<a href=#724 id=724 data-nosnippet>724</a>            body_configs.push(entry);
<a href=#725 id=725 data-nosnippet>725</a>        }
<a href=#726 id=726 data-nosnippet>726</a>
<a href=#727 id=727 data-nosnippet>727</a>        <span class="kw">let </span>body = <span class="macro">serde_json::json!</span>({ <span class="string">"configs"</span>: body_configs });
<a href=#728 id=728 data-nosnippet>728</a>        <span class="kw">let </span>body_bytes = serde_json::to_vec(<span class="kw-2">&amp;</span>body)<span class="question-mark">?</span>;
<a href=#729 id=729 data-nosnippet>729</a>
<a href=#730 id=730 data-nosnippet>730</a>        <span class="kw">let </span><span class="kw-2">mut </span>url = Url::parse(<span class="kw-2">&amp;</span><span class="self">self</span>.host)
<a href=#731 id=731 data-nosnippet>731</a>            .map_err(|e| ScrapflyError::Config(<span class="macro">format!</span>(<span class="string">"invalid host: {}"</span>, e)))<span class="question-mark">?</span>;
<a href=#732 id=732 data-nosnippet>732</a>        url.set_path(<span class="string">"/scrape/batch"</span>);
<a href=#733 id=733 data-nosnippet>733</a>        url.query_pairs_mut().append_pair(<span class="string">"key"</span>, <span class="kw-2">&amp;</span><span class="self">self</span>.key);
<a href=#734 id=734 data-nosnippet>734</a>
<a href=#735 id=735 data-nosnippet>735</a>        <span class="kw">let </span><span class="kw-2">mut </span>headers = HeaderMap::new();
<a href=#736 id=736 data-nosnippet>736</a>        headers.insert(CONTENT_TYPE, HeaderValue::from_static(<span class="string">"application/json"</span>));
<a href=#737 id=737 data-nosnippet>737</a>        headers.insert(
<a href=#738 id=738 data-nosnippet>738</a>            ACCEPT,
<a href=#739 id=739 data-nosnippet>739</a>            HeaderValue::from_static(opts.format.accept_header()),
<a href=#740 id=740 data-nosnippet>740</a>        );
<a href=#741 id=741 data-nosnippet>741</a>        headers.insert(USER_AGENT, HeaderValue::from_static(SDK_USER_AGENT));
<a href=#742 id=742 data-nosnippet>742</a>
<a href=#743 id=743 data-nosnippet>743</a>        <span class="kw">let </span>method = Method::POST;
<a href=#744 id=744 data-nosnippet>744</a>
<a href=#745 id=745 data-nosnippet>745</a>        <span class="kw">if let </span><span class="prelude-val">Some</span>(cb) = <span class="kw-2">&amp;</span><span class="self">self</span>.on_request {
<a href=#746 id=746 data-nosnippet>746</a>            cb(<span class="kw-2">&amp;</span>method, <span class="kw-2">&amp;</span>url, <span class="kw-2">&amp;</span>headers);
<a href=#747 id=747 data-nosnippet>747</a>        }
<a href=#748 id=748 data-nosnippet>748</a>
<a href=#749 id=749 data-nosnippet>749</a>        <span class="kw">let </span>resp = <span class="self">self
<a href=#750 id=750 data-nosnippet>750</a>            </span>.http
<a href=#751 id=751 data-nosnippet>751</a>            .request(method, url)
<a href=#752 id=752 data-nosnippet>752</a>            .headers(headers)
<a href=#753 id=753 data-nosnippet>753</a>            .body(body_bytes)
<a href=#754 id=754 data-nosnippet>754</a>            .send()
<a href=#755 id=755 data-nosnippet>755</a>            .<span class="kw">await
<a href=#756 id=756 data-nosnippet>756</a>            </span>.map_err(|e| ScrapflyError::Config(<span class="macro">format!</span>(<span class="string">"scrape_batch: send: {}"</span>, e)))<span class="question-mark">?</span>;
<a href=#757 id=757 data-nosnippet>757</a>
<a href=#758 id=758 data-nosnippet>758</a>        <span class="kw">let </span>status = resp.status().as_u16();
<a href=#759 id=759 data-nosnippet>759</a>
<a href=#760 id=760 data-nosnippet>760</a>        <span class="kw">if </span>status != <span class="number">200 </span>{
<a href=#761 id=761 data-nosnippet>761</a>            <span class="kw">let </span>body_bytes = resp.bytes().<span class="kw">await</span>.unwrap_or_default();
<a href=#762 id=762 data-nosnippet>762</a>
<a href=#763 id=763 data-nosnippet>763</a>            <span class="kw">return </span><span class="prelude-val">Err</span>(from_response(status, <span class="kw-2">&amp;</span>body_bytes, <span class="number">0</span>, <span class="bool-val">false</span>));
<a href=#764 id=764 data-nosnippet>764</a>        }
<a href=#765 id=765 data-nosnippet>765</a>
<a href=#766 id=766 data-nosnippet>766</a>        <span class="kw">let </span>parts_stream = parts_from_response(resp)<span class="question-mark">?</span>;
<a href=#767 id=767 data-nosnippet>767</a>
<a href=#768 id=768 data-nosnippet>768</a>        <span class="prelude-val">Ok</span>(parts_stream.map(|part_r| <span class="kw">match </span>part_r {
<a href=#769 id=769 data-nosnippet>769</a>            <span class="prelude-val">Ok</span>(part) =&gt; {
<a href=#770 id=770 data-nosnippet>770</a>                <span class="kw">let </span>correlation_id = part
<a href=#771 id=771 data-nosnippet>771</a>                    .headers
<a href=#772 id=772 data-nosnippet>772</a>                    .get(<span class="string">"x-scrapfly-correlation-id"</span>)
<a href=#773 id=773 data-nosnippet>773</a>                    .cloned()
<a href=#774 id=774 data-nosnippet>774</a>                    .unwrap_or_default();
<a href=#775 id=775 data-nosnippet>775</a>
<a href=#776 id=776 data-nosnippet>776</a>                <span class="comment">// Proxified-response parts: the part body is the raw
<a href=#777 id=777 data-nosnippet>777</a>                // upstream bytes, not a JSON envelope. Surface as a
<a href=#778 id=778 data-nosnippet>778</a>                // BatchProxifiedResponse rather than attempting to
<a href=#779 id=779 data-nosnippet>779</a>                // decode the body as JSON.
<a href=#780 id=780 data-nosnippet>780</a>                </span><span class="kw">if </span>part
<a href=#781 id=781 data-nosnippet>781</a>                    .headers
<a href=#782 id=782 data-nosnippet>782</a>                    .get(<span class="string">"x-scrapfly-proxified"</span>)
<a href=#783 id=783 data-nosnippet>783</a>                    .map(|v| v == <span class="string">"true"</span>)
<a href=#784 id=784 data-nosnippet>784</a>                    .unwrap_or(<span class="bool-val">false</span>)
<a href=#785 id=785 data-nosnippet>785</a>                {
<a href=#786 id=786 data-nosnippet>786</a>                    <span class="kw">let </span>prox = build_proxified_response(part);
<a href=#787 id=787 data-nosnippet>787</a>                    <span class="kw">return </span>(correlation_id, BatchOutcome::Proxified(prox));
<a href=#788 id=788 data-nosnippet>788</a>                }
<a href=#789 id=789 data-nosnippet>789</a>
<a href=#790 id=790 data-nosnippet>790</a>                <span class="kw">match </span>decode_part_body::&lt;ScrapeResult&gt;(<span class="kw-2">&amp;</span>part) {
<a href=#791 id=791 data-nosnippet>791</a>                    <span class="prelude-val">Ok</span>(r) =&gt; (correlation_id, BatchOutcome::Scrape(r)),
<a href=#792 id=792 data-nosnippet>792</a>                    <span class="prelude-val">Err</span>(e) =&gt; (correlation_id, BatchOutcome::Err(e)),
<a href=#793 id=793 data-nosnippet>793</a>                }
<a href=#794 id=794 data-nosnippet>794</a>            }
<a href=#795 id=795 data-nosnippet>795</a>            <span class="prelude-val">Err</span>(e) =&gt; (String::new(), BatchOutcome::Err(e)),
<a href=#796 id=796 data-nosnippet>796</a>        }))
<a href=#797 id=797 data-nosnippet>797</a>    }
<a href=#798 id=798 data-nosnippet>798</a>
<a href=#799 id=799 data-nosnippet>799</a>    <span class="doccomment">/// Scrape a URL with `proxified_response=true`, returning the raw
<a href=#800 id=800 data-nosnippet>800</a>    /// upstream `reqwest::Response` (target's status, headers, body).
<a href=#801 id=801 data-nosnippet>801</a>    ///
<a href=#802 id=802 data-nosnippet>802</a>    /// Unlike [`scrape()`], no JSON parsing occurs — the response body is
<a href=#803 id=803 data-nosnippet>803</a>    /// the target page's raw content. Scrapfly metadata is available on
<a href=#804 id=804 data-nosnippet>804</a>    /// the `X-Scrapfly-*` response headers (`Api-Cost`, `Content-Format`,
<a href=#805 id=805 data-nosnippet>805</a>    /// `Log`, etc.).
<a href=#806 id=806 data-nosnippet>806</a>    ///
<a href=#807 id=807 data-nosnippet>807</a>    /// Automatically forces `proxified_response=true` regardless of the
<a href=#808 id=808 data-nosnippet>808</a>    /// config's field value.
<a href=#809 id=809 data-nosnippet>809</a>    </span><span class="kw">pub async fn </span>scrape_proxified(
<a href=#810 id=810 data-nosnippet>810</a>        <span class="kw-2">&amp;</span><span class="self">self</span>,
<a href=#811 id=811 data-nosnippet>811</a>        config: <span class="kw-2">&amp;</span>ScrapeConfig,
<a href=#812 id=812 data-nosnippet>812</a>    ) -&gt; <span class="prelude-ty">Result</span>&lt;reqwest::Response, ScrapflyError&gt; {
<a href=#813 id=813 data-nosnippet>813</a>        <span class="kw">let </span><span class="kw-2">mut </span>cfg = config.clone();
<a href=#814 id=814 data-nosnippet>814</a>        cfg.proxified_response = <span class="bool-val">true</span>;
<a href=#815 id=815 data-nosnippet>815</a>        <span class="kw">let </span>pairs = cfg.to_query_pairs()<span class="question-mark">?</span>;
<a href=#816 id=816 data-nosnippet>816</a>        <span class="kw">let </span>url = <span class="self">self</span>.build_url(<span class="string">"/scrape"</span>, <span class="kw-2">&amp;</span>pairs)<span class="question-mark">?</span>;
<a href=#817 id=817 data-nosnippet>817</a>        <span class="kw">let </span>method = <span class="kw">match </span>cfg.method {
<a href=#818 id=818 data-nosnippet>818</a>            <span class="prelude-val">Some</span>(m) =&gt; Method::from_bytes(m.as_str().as_bytes())
<a href=#819 id=819 data-nosnippet>819</a>                .map_err(|e| ScrapflyError::Config(<span class="macro">format!</span>(<span class="string">"invalid method: {}"</span>, e)))<span class="question-mark">?</span>,
<a href=#820 id=820 data-nosnippet>820</a>            <span class="prelude-val">None </span>=&gt; Method::GET,
<a href=#821 id=821 data-nosnippet>821</a>        };
<a href=#822 id=822 data-nosnippet>822</a>        <span class="kw">let </span>body = cfg.body.clone();
<a href=#823 id=823 data-nosnippet>823</a>        <span class="kw">let </span>resp = <span class="self">self
<a href=#824 id=824 data-nosnippet>824</a>            </span>.send_with_retry(method, url, <span class="prelude-val">None</span>, body.map(|b| b.into_bytes()))
<a href=#825 id=825 data-nosnippet>825</a>            .<span class="kw">await</span><span class="question-mark">?</span>;
<a href=#826 id=826 data-nosnippet>826</a>        <span class="comment">// Error restoration: if X-Scrapfly-Reject-Code is present, the
<a href=#827 id=827 data-nosnippet>827</a>        // scrape failed. Return a typed error so callers get the same
<a href=#828 id=828 data-nosnippet>828</a>        // interface as non-proxified mode.
<a href=#829 id=829 data-nosnippet>829</a>        </span><span class="kw">if let </span><span class="prelude-val">Some</span>(reject_code) = resp.headers().get(<span class="string">"x-scrapfly-reject-code"</span>) {
<a href=#830 id=830 data-nosnippet>830</a>            <span class="kw">let </span>code = reject_code.to_str().unwrap_or(<span class="string">""</span>).to_string();
<a href=#831 id=831 data-nosnippet>831</a>            <span class="kw">let </span>desc = resp
<a href=#832 id=832 data-nosnippet>832</a>                .headers()
<a href=#833 id=833 data-nosnippet>833</a>                .get(<span class="string">"x-scrapfly-reject-description"</span>)
<a href=#834 id=834 data-nosnippet>834</a>                .and_then(|v| v.to_str().ok())
<a href=#835 id=835 data-nosnippet>835</a>                .unwrap_or(<span class="string">""</span>)
<a href=#836 id=836 data-nosnippet>836</a>                .to_string();
<a href=#837 id=837 data-nosnippet>837</a>            <span class="kw">let </span>retryable = resp
<a href=#838 id=838 data-nosnippet>838</a>                .headers()
<a href=#839 id=839 data-nosnippet>839</a>                .get(<span class="string">"x-scrapfly-reject-retryable"</span>)
<a href=#840 id=840 data-nosnippet>840</a>                .and_then(|v| v.to_str().ok())
<a href=#841 id=841 data-nosnippet>841</a>                .unwrap_or(<span class="string">"false"</span>)
<a href=#842 id=842 data-nosnippet>842</a>                == <span class="string">"true"</span>;
<a href=#843 id=843 data-nosnippet>843</a>            <span class="kw">let </span>retry_after_ms: u64 = <span class="kw">if </span>retryable {
<a href=#844 id=844 data-nosnippet>844</a>                resp.headers()
<a href=#845 id=845 data-nosnippet>845</a>                    .get(<span class="string">"retry-after"</span>)
<a href=#846 id=846 data-nosnippet>846</a>                    .and_then(|v| v.to_str().ok())
<a href=#847 id=847 data-nosnippet>847</a>                    .and_then(|v| v.parse::&lt;u64&gt;().ok())
<a href=#848 id=848 data-nosnippet>848</a>                    .unwrap_or(<span class="number">0</span>)
<a href=#849 id=849 data-nosnippet>849</a>                    * <span class="number">1000 </span><span class="comment">// Retry-After header is in seconds
<a href=#850 id=850 data-nosnippet>850</a>            </span>} <span class="kw">else </span>{
<a href=#851 id=851 data-nosnippet>851</a>                <span class="number">0
<a href=#852 id=852 data-nosnippet>852</a>            </span>};
<a href=#853 id=853 data-nosnippet>853</a>            <span class="kw">let </span>status = resp.status().as_u16();
<a href=#854 id=854 data-nosnippet>854</a>            <span class="kw">let </span>doc = resp
<a href=#855 id=855 data-nosnippet>855</a>                .headers()
<a href=#856 id=856 data-nosnippet>856</a>                .get(<span class="string">"x-scrapfly-reject-doc"</span>)
<a href=#857 id=857 data-nosnippet>857</a>                .and_then(|v| v.to_str().ok())
<a href=#858 id=858 data-nosnippet>858</a>                .unwrap_or(<span class="string">""</span>)
<a href=#859 id=859 data-nosnippet>859</a>                .to_string();
<a href=#860 id=860 data-nosnippet>860</a>            <span class="kw">return </span><span class="prelude-val">Err</span>(ScrapflyError::Api(<span class="kw">crate</span>::error::ApiError {
<a href=#861 id=861 data-nosnippet>861</a>                code,
<a href=#862 id=862 data-nosnippet>862</a>                message: <span class="macro">format!</span>(<span class="string">"Proxified scrape error: {}"</span>, desc),
<a href=#863 id=863 data-nosnippet>863</a>                http_status: status,
<a href=#864 id=864 data-nosnippet>864</a>                documentation_url: doc,
<a href=#865 id=865 data-nosnippet>865</a>                hint: String::new(),
<a href=#866 id=866 data-nosnippet>866</a>                retry_after_ms,
<a href=#867 id=867 data-nosnippet>867</a>            }));
<a href=#868 id=868 data-nosnippet>868</a>        }
<a href=#869 id=869 data-nosnippet>869</a>        <span class="prelude-val">Ok</span>(resp)
<a href=#870 id=870 data-nosnippet>870</a>    }
<a href=#871 id=871 data-nosnippet>871</a>
<a href=#872 id=872 data-nosnippet>872</a>    <span class="doccomment">/// Screenshot a URL.
<a href=#873 id=873 data-nosnippet>873</a>    </span><span class="kw">pub async fn </span>screenshot(
<a href=#874 id=874 data-nosnippet>874</a>        <span class="kw-2">&amp;</span><span class="self">self</span>,
<a href=#875 id=875 data-nosnippet>875</a>        config: <span class="kw-2">&amp;</span>ScreenshotConfig,
<a href=#876 id=876 data-nosnippet>876</a>    ) -&gt; <span class="prelude-ty">Result</span>&lt;ScreenshotResult, ScrapflyError&gt; {
<a href=#877 id=877 data-nosnippet>877</a>        <span class="kw">let </span>pairs = config.to_query_pairs()<span class="question-mark">?</span>;
<a href=#878 id=878 data-nosnippet>878</a>        <span class="kw">let </span>url = <span class="self">self</span>.build_url(<span class="string">"/screenshot"</span>, <span class="kw-2">&amp;</span>pairs)<span class="question-mark">?</span>;
<a href=#879 id=879 data-nosnippet>879</a>        <span class="kw">let </span>resp = <span class="self">self</span>.send_with_retry(Method::GET, url, <span class="prelude-val">None</span>, <span class="prelude-val">None</span>).<span class="kw">await</span><span class="question-mark">?</span>;
<a href=#880 id=880 data-nosnippet>880</a>        <span class="kw">let </span>(status, headers, body) = read_response(resp).<span class="kw">await</span><span class="question-mark">?</span>;
<a href=#881 id=881 data-nosnippet>881</a>        <span class="kw">if </span>status != <span class="number">200 </span>{
<a href=#882 id=882 data-nosnippet>882</a>            <span class="kw">return </span><span class="prelude-val">Err</span>(from_response(status, <span class="kw-2">&amp;</span>body, <span class="number">0</span>, <span class="bool-val">false</span>));
<a href=#883 id=883 data-nosnippet>883</a>        }
<a href=#884 id=884 data-nosnippet>884</a>        <span class="kw">let </span>content_type = headers
<a href=#885 id=885 data-nosnippet>885</a>            .get(CONTENT_TYPE)
<a href=#886 id=886 data-nosnippet>886</a>            .and_then(|v| v.to_str().ok())
<a href=#887 id=887 data-nosnippet>887</a>            .unwrap_or(<span class="string">"application/octet-stream"</span>);
<a href=#888 id=888 data-nosnippet>888</a>        <span class="kw">let </span>ext = content_type
<a href=#889 id=889 data-nosnippet>889</a>            .split(<span class="string">'/'</span>)
<a href=#890 id=890 data-nosnippet>890</a>            .nth(<span class="number">1</span>)
<a href=#891 id=891 data-nosnippet>891</a>            .and_then(|s| s.split(<span class="string">';'</span>).next())
<a href=#892 id=892 data-nosnippet>892</a>            .unwrap_or(<span class="string">"bin"</span>)
<a href=#893 id=893 data-nosnippet>893</a>            .to_string();
<a href=#894 id=894 data-nosnippet>894</a>        <span class="kw">let </span>upstream_status_code: u16 = headers
<a href=#895 id=895 data-nosnippet>895</a>            .get(<span class="string">"x-scrapfly-upstream-http-code"</span>)
<a href=#896 id=896 data-nosnippet>896</a>            .and_then(|v| v.to_str().ok())
<a href=#897 id=897 data-nosnippet>897</a>            .and_then(|s| s.parse().ok())
<a href=#898 id=898 data-nosnippet>898</a>            .unwrap_or(<span class="number">0</span>);
<a href=#899 id=899 data-nosnippet>899</a>        <span class="kw">let </span>upstream_url = headers
<a href=#900 id=900 data-nosnippet>900</a>            .get(<span class="string">"x-scrapfly-upstream-url"</span>)
<a href=#901 id=901 data-nosnippet>901</a>            .and_then(|v| v.to_str().ok())
<a href=#902 id=902 data-nosnippet>902</a>            .unwrap_or(<span class="string">""</span>)
<a href=#903 id=903 data-nosnippet>903</a>            .to_string();
<a href=#904 id=904 data-nosnippet>904</a>        <span class="prelude-val">Ok</span>(ScreenshotResult {
<a href=#905 id=905 data-nosnippet>905</a>            image: body,
<a href=#906 id=906 data-nosnippet>906</a>            metadata: ScreenshotMetadata {
<a href=#907 id=907 data-nosnippet>907</a>                extension_name: ext,
<a href=#908 id=908 data-nosnippet>908</a>                upstream_status_code,
<a href=#909 id=909 data-nosnippet>909</a>                upstream_url,
<a href=#910 id=910 data-nosnippet>910</a>            },
<a href=#911 id=911 data-nosnippet>911</a>        })
<a href=#912 id=912 data-nosnippet>912</a>    }
<a href=#913 id=913 data-nosnippet>913</a>
<a href=#914 id=914 data-nosnippet>914</a>    <span class="doccomment">/// Run AI extraction on a document.
<a href=#915 id=915 data-nosnippet>915</a>    </span><span class="kw">pub async fn </span>extract(
<a href=#916 id=916 data-nosnippet>916</a>        <span class="kw-2">&amp;</span><span class="self">self</span>,
<a href=#917 id=917 data-nosnippet>917</a>        config: <span class="kw-2">&amp;</span>ExtractionConfig,
<a href=#918 id=918 data-nosnippet>918</a>    ) -&gt; <span class="prelude-ty">Result</span>&lt;ExtractionResult, ScrapflyError&gt; {
<a href=#919 id=919 data-nosnippet>919</a>        <span class="kw">let </span>pairs = config.to_query_pairs()<span class="question-mark">?</span>;
<a href=#920 id=920 data-nosnippet>920</a>        <span class="kw">let </span>url = <span class="self">self</span>.build_url(<span class="string">"/extraction"</span>, <span class="kw-2">&amp;</span>pairs)<span class="question-mark">?</span>;
<a href=#921 id=921 data-nosnippet>921</a>        <span class="kw">let </span><span class="kw-2">mut </span>headers = HeaderMap::new();
<a href=#922 id=922 data-nosnippet>922</a>        headers.insert(
<a href=#923 id=923 data-nosnippet>923</a>            CONTENT_TYPE,
<a href=#924 id=924 data-nosnippet>924</a>            HeaderValue::from_str(<span class="kw-2">&amp;</span>config.content_type)
<a href=#925 id=925 data-nosnippet>925</a>                .map_err(|e| ScrapflyError::Config(<span class="macro">format!</span>(<span class="string">"invalid content-type: {}"</span>, e)))<span class="question-mark">?</span>,
<a href=#926 id=926 data-nosnippet>926</a>        );
<a href=#927 id=927 data-nosnippet>927</a>        headers.insert(ACCEPT, HeaderValue::from_static(<span class="string">"application/json"</span>));
<a href=#928 id=928 data-nosnippet>928</a>        <span class="kw">if let </span><span class="prelude-val">Some</span>(fmt) = config.document_compression_format {
<a href=#929 id=929 data-nosnippet>929</a>            headers.insert(
<a href=#930 id=930 data-nosnippet>930</a>                <span class="string">"content-encoding"</span>,
<a href=#931 id=931 data-nosnippet>931</a>                HeaderValue::from_str(fmt.as_str())
<a href=#932 id=932 data-nosnippet>932</a>                    .map_err(|e| ScrapflyError::Config(<span class="macro">format!</span>(<span class="string">"invalid encoding: {}"</span>, e)))<span class="question-mark">?</span>,
<a href=#933 id=933 data-nosnippet>933</a>            );
<a href=#934 id=934 data-nosnippet>934</a>        }
<a href=#935 id=935 data-nosnippet>935</a>        <span class="kw">let </span>resp = <span class="self">self
<a href=#936 id=936 data-nosnippet>936</a>            </span>.send_with_retry(Method::POST, url, <span class="prelude-val">Some</span>(headers), <span class="prelude-val">Some</span>(config.body.clone()))
<a href=#937 id=937 data-nosnippet>937</a>            .<span class="kw">await</span><span class="question-mark">?</span>;
<a href=#938 id=938 data-nosnippet>938</a>        <span class="kw">let </span>(status, _h, body_bytes) = read_response(resp).<span class="kw">await</span><span class="question-mark">?</span>;
<a href=#939 id=939 data-nosnippet>939</a>        <span class="kw">if </span>status != <span class="number">200 </span>{
<a href=#940 id=940 data-nosnippet>940</a>            <span class="kw">return </span><span class="prelude-val">Err</span>(from_response(status, <span class="kw-2">&amp;</span>body_bytes, <span class="number">0</span>, <span class="bool-val">false</span>));
<a href=#941 id=941 data-nosnippet>941</a>        }
<a href=#942 id=942 data-nosnippet>942</a>        <span class="prelude-val">Ok</span>(serde_json::from_slice(<span class="kw-2">&amp;</span>body_bytes)<span class="question-mark">?</span>)
<a href=#943 id=943 data-nosnippet>943</a>    }
<a href=#944 id=944 data-nosnippet>944</a>
<a href=#945 id=945 data-nosnippet>945</a>    <span class="comment">// ==============================================================================
<a href=#946 id=946 data-nosnippet>946</a>    // Crawler methods
<a href=#947 id=947 data-nosnippet>947</a>    // ==============================================================================
<a href=#948 id=948 data-nosnippet>948</a>
<a href=#949 id=949 data-nosnippet>949</a>    </span><span class="doccomment">/// Schedule a new crawler job.
<a href=#950 id=950 data-nosnippet>950</a>    </span><span class="kw">pub async fn </span>start_crawl(
<a href=#951 id=951 data-nosnippet>951</a>        <span class="kw-2">&amp;</span><span class="self">self</span>,
<a href=#952 id=952 data-nosnippet>952</a>        config: <span class="kw-2">&amp;</span>CrawlerConfig,
<a href=#953 id=953 data-nosnippet>953</a>    ) -&gt; <span class="prelude-ty">Result</span>&lt;CrawlerStartResponse, ScrapflyError&gt; {
<a href=#954 id=954 data-nosnippet>954</a>        <span class="kw">let </span>body = config.to_json_body()<span class="question-mark">?</span>;
<a href=#955 id=955 data-nosnippet>955</a>        <span class="kw">let </span>url = <span class="self">self</span>.build_url(<span class="string">"/crawl"</span>, <span class="kw-2">&amp;</span>[])<span class="question-mark">?</span>;
<a href=#956 id=956 data-nosnippet>956</a>        <span class="kw">let </span><span class="kw-2">mut </span>headers = HeaderMap::new();
<a href=#957 id=957 data-nosnippet>957</a>        headers.insert(CONTENT_TYPE, HeaderValue::from_static(<span class="string">"application/json"</span>));
<a href=#958 id=958 data-nosnippet>958</a>        headers.insert(ACCEPT, HeaderValue::from_static(<span class="string">"application/json"</span>));
<a href=#959 id=959 data-nosnippet>959</a>        <span class="kw">let </span>resp = <span class="self">self
<a href=#960 id=960 data-nosnippet>960</a>            </span>.send_with_retry(Method::POST, url, <span class="prelude-val">Some</span>(headers), <span class="prelude-val">Some</span>(body))
<a href=#961 id=961 data-nosnippet>961</a>            .<span class="kw">await</span><span class="question-mark">?</span>;
<a href=#962 id=962 data-nosnippet>962</a>        <span class="kw">let </span>(status, _h, body_bytes) = read_response(resp).<span class="kw">await</span><span class="question-mark">?</span>;
<a href=#963 id=963 data-nosnippet>963</a>        <span class="kw">if </span>status != <span class="number">200 </span>&amp;&amp; status != <span class="number">201 </span>{
<a href=#964 id=964 data-nosnippet>964</a>            <span class="kw">return </span><span class="prelude-val">Err</span>(from_response(status, <span class="kw-2">&amp;</span>body_bytes, <span class="number">0</span>, <span class="bool-val">true</span>));
<a href=#965 id=965 data-nosnippet>965</a>        }
<a href=#966 id=966 data-nosnippet>966</a>        <span class="kw">let </span>parsed: CrawlerStartResponse = serde_json::from_slice(<span class="kw-2">&amp;</span>body_bytes)<span class="question-mark">?</span>;
<a href=#967 id=967 data-nosnippet>967</a>        <span class="kw">if </span>parsed.crawler_uuid.is_empty() {
<a href=#968 id=968 data-nosnippet>968</a>            <span class="kw">return </span><span class="prelude-val">Err</span>(ScrapflyError::UnexpectedResponseFormat(
<a href=#969 id=969 data-nosnippet>969</a>                <span class="string">"crawler start response missing crawler_uuid"</span>.into(),
<a href=#970 id=970 data-nosnippet>970</a>            ));
<a href=#971 id=971 data-nosnippet>971</a>        }
<a href=#972 id=972 data-nosnippet>972</a>        <span class="prelude-val">Ok</span>(parsed)
<a href=#973 id=973 data-nosnippet>973</a>    }
<a href=#974 id=974 data-nosnippet>974</a>
<a href=#975 id=975 data-nosnippet>975</a>    <span class="doccomment">/// Fetch crawler status.
<a href=#976 id=976 data-nosnippet>976</a>    </span><span class="kw">pub async fn </span>crawl_status(<span class="kw-2">&amp;</span><span class="self">self</span>, uuid: <span class="kw-2">&amp;</span>str) -&gt; <span class="prelude-ty">Result</span>&lt;CrawlerStatus, ScrapflyError&gt; {
<a href=#977 id=977 data-nosnippet>977</a>        <span class="kw">if </span>uuid.is_empty() {
<a href=#978 id=978 data-nosnippet>978</a>            <span class="kw">return </span><span class="prelude-val">Err</span>(ScrapflyError::Config(<span class="string">"uuid cannot be empty"</span>.into()));
<a href=#979 id=979 data-nosnippet>979</a>        }
<a href=#980 id=980 data-nosnippet>980</a>        <span class="kw">let </span>url = <span class="self">self</span>.build_url(<span class="kw-2">&amp;</span><span class="macro">format!</span>(<span class="string">"/crawl/{}/status"</span>, uuid), <span class="kw-2">&amp;</span>[])<span class="question-mark">?</span>;
<a href=#981 id=981 data-nosnippet>981</a>        <span class="kw">let </span>resp = <span class="self">self</span>.send_with_retry(Method::GET, url, <span class="prelude-val">None</span>, <span class="prelude-val">None</span>).<span class="kw">await</span><span class="question-mark">?</span>;
<a href=#982 id=982 data-nosnippet>982</a>        <span class="kw">let </span>(status, _h, body) = read_response(resp).<span class="kw">await</span><span class="question-mark">?</span>;
<a href=#983 id=983 data-nosnippet>983</a>        <span class="kw">if </span>status != <span class="number">200 </span>{
<a href=#984 id=984 data-nosnippet>984</a>            <span class="kw">return </span><span class="prelude-val">Err</span>(from_response(status, <span class="kw-2">&amp;</span>body, <span class="number">0</span>, <span class="bool-val">true</span>));
<a href=#985 id=985 data-nosnippet>985</a>        }
<a href=#986 id=986 data-nosnippet>986</a>        <span class="prelude-val">Ok</span>(serde_json::from_slice(<span class="kw-2">&amp;</span>body)<span class="question-mark">?</span>)
<a href=#987 id=987 data-nosnippet>987</a>    }
<a href=#988 id=988 data-nosnippet>988</a>
<a href=#989 id=989 data-nosnippet>989</a>    <span class="doccomment">/// List crawled URLs (streaming text endpoint).
<a href=#990 id=990 data-nosnippet>990</a>    </span><span class="kw">pub async fn </span>crawl_urls(
<a href=#991 id=991 data-nosnippet>991</a>        <span class="kw-2">&amp;</span><span class="self">self</span>,
<a href=#992 id=992 data-nosnippet>992</a>        uuid: <span class="kw-2">&amp;</span>str,
<a href=#993 id=993 data-nosnippet>993</a>        status_filter: <span class="prelude-ty">Option</span>&lt;<span class="kw-2">&amp;</span>str&gt;,
<a href=#994 id=994 data-nosnippet>994</a>        page: u32,
<a href=#995 id=995 data-nosnippet>995</a>        per_page: u32,
<a href=#996 id=996 data-nosnippet>996</a>    ) -&gt; <span class="prelude-ty">Result</span>&lt;CrawlerUrls, ScrapflyError&gt; {
<a href=#997 id=997 data-nosnippet>997</a>        <span class="kw">if </span>uuid.is_empty() {
<a href=#998 id=998 data-nosnippet>998</a>            <span class="kw">return </span><span class="prelude-val">Err</span>(ScrapflyError::Config(<span class="string">"uuid cannot be empty"</span>.into()));
<a href=#999 id=999 data-nosnippet>999</a>        }
<a href=#1000 id=1000 data-nosnippet>1000</a>        <span class="kw">let </span>page = <span class="kw">if </span>page == <span class="number">0 </span>{ <span class="number">1 </span>} <span class="kw">else </span>{ page };
<a href=#1001 id=1001 data-nosnippet>1001</a>        <span class="kw">let </span>per_page = <span class="kw">if </span>per_page == <span class="number">0 </span>{ <span class="number">100 </span>} <span class="kw">else </span>{ per_page };
<a href=#1002 id=1002 data-nosnippet>1002</a>        <span class="kw">let </span>status_hint = status_filter.unwrap_or(<span class="string">"visited"</span>);
<a href=#1003 id=1003 data-nosnippet>1003</a>        <span class="kw">let </span><span class="kw-2">mut </span>pairs: Vec&lt;(String, String)&gt; = <span class="macro">vec!</span>[
<a href=#1004 id=1004 data-nosnippet>1004</a>            (<span class="string">"page"</span>.into(), page.to_string()),
<a href=#1005 id=1005 data-nosnippet>1005</a>            (<span class="string">"per_page"</span>.into(), per_page.to_string()),
<a href=#1006 id=1006 data-nosnippet>1006</a>        ];
<a href=#1007 id=1007 data-nosnippet>1007</a>        <span class="kw">if let </span><span class="prelude-val">Some</span>(s) = status_filter {
<a href=#1008 id=1008 data-nosnippet>1008</a>            pairs.push((<span class="string">"status"</span>.into(), s.to_string()));
<a href=#1009 id=1009 data-nosnippet>1009</a>        }
<a href=#1010 id=1010 data-nosnippet>1010</a>        <span class="kw">let </span>url = <span class="self">self</span>.build_url(<span class="kw-2">&amp;</span><span class="macro">format!</span>(<span class="string">"/crawl/{}/urls"</span>, uuid), <span class="kw-2">&amp;</span>pairs)<span class="question-mark">?</span>;
<a href=#1011 id=1011 data-nosnippet>1011</a>        <span class="kw">let </span><span class="kw-2">mut </span>headers = HeaderMap::new();
<a href=#1012 id=1012 data-nosnippet>1012</a>        headers.insert(
<a href=#1013 id=1013 data-nosnippet>1013</a>            ACCEPT,
<a href=#1014 id=1014 data-nosnippet>1014</a>            HeaderValue::from_static(<span class="string">"text/plain, application/json"</span>),
<a href=#1015 id=1015 data-nosnippet>1015</a>        );
<a href=#1016 id=1016 data-nosnippet>1016</a>        <span class="kw">let </span>resp = <span class="self">self
<a href=#1017 id=1017 data-nosnippet>1017</a>            </span>.send_with_retry(Method::GET, url, <span class="prelude-val">Some</span>(headers), <span class="prelude-val">None</span>)
<a href=#1018 id=1018 data-nosnippet>1018</a>            .<span class="kw">await</span><span class="question-mark">?</span>;
<a href=#1019 id=1019 data-nosnippet>1019</a>        <span class="kw">let </span>(status, resp_headers, body) = read_response(resp).<span class="kw">await</span><span class="question-mark">?</span>;
<a href=#1020 id=1020 data-nosnippet>1020</a>        <span class="kw">if </span>status != <span class="number">200 </span>{
<a href=#1021 id=1021 data-nosnippet>1021</a>            <span class="kw">return </span><span class="prelude-val">Err</span>(from_response(status, <span class="kw-2">&amp;</span>body, <span class="number">0</span>, <span class="bool-val">true</span>));
<a href=#1022 id=1022 data-nosnippet>1022</a>        }
<a href=#1023 id=1023 data-nosnippet>1023</a>        <span class="kw">let </span>ct = resp_headers
<a href=#1024 id=1024 data-nosnippet>1024</a>            .get(CONTENT_TYPE)
<a href=#1025 id=1025 data-nosnippet>1025</a>            .and_then(|v| v.to_str().ok())
<a href=#1026 id=1026 data-nosnippet>1026</a>            .unwrap_or(<span class="string">""</span>);
<a href=#1027 id=1027 data-nosnippet>1027</a>        <span class="kw">if </span>ct.contains(<span class="string">"application/json"</span>) {
<a href=#1028 id=1028 data-nosnippet>1028</a>            <span class="kw">return </span><span class="prelude-val">Err</span>(ScrapflyError::UnexpectedResponseFormat(<span class="macro">format!</span>(
<a href=#1029 id=1029 data-nosnippet>1029</a>                <span class="string">"GET /crawl/{}/urls returned JSON on a 200 response (expected text/plain)"</span>,
<a href=#1030 id=1030 data-nosnippet>1030</a>                uuid
<a href=#1031 id=1031 data-nosnippet>1031</a>            )));
<a href=#1032 id=1032 data-nosnippet>1032</a>        }
<a href=#1033 id=1033 data-nosnippet>1033</a>        <span class="kw">let </span>body_str = std::str::from_utf8(<span class="kw-2">&amp;</span>body)
<a href=#1034 id=1034 data-nosnippet>1034</a>            .map_err(|e| ScrapflyError::UnexpectedResponseFormat(<span class="macro">format!</span>(<span class="string">"invalid utf8: {}"</span>, e)))<span class="question-mark">?</span>;
<a href=#1035 id=1035 data-nosnippet>1035</a>        <span class="prelude-val">Ok</span>(CrawlerUrls::from_text(
<a href=#1036 id=1036 data-nosnippet>1036</a>            body_str,
<a href=#1037 id=1037 data-nosnippet>1037</a>            status_hint,
<a href=#1038 id=1038 data-nosnippet>1038</a>            page,
<a href=#1039 id=1039 data-nosnippet>1039</a>            per_page,
<a href=#1040 id=1040 data-nosnippet>1040</a>        ))
<a href=#1041 id=1041 data-nosnippet>1041</a>    }
<a href=#1042 id=1042 data-nosnippet>1042</a>
<a href=#1043 id=1043 data-nosnippet>1043</a>    <span class="doccomment">/// Bulk `GET /crawl/{uuid}/contents` in JSON mode.
<a href=#1044 id=1044 data-nosnippet>1044</a>    </span><span class="kw">pub async fn </span>crawl_contents_json(
<a href=#1045 id=1045 data-nosnippet>1045</a>        <span class="kw-2">&amp;</span><span class="self">self</span>,
<a href=#1046 id=1046 data-nosnippet>1046</a>        uuid: <span class="kw-2">&amp;</span>str,
<a href=#1047 id=1047 data-nosnippet>1047</a>        format: <span class="kw">crate</span>::enums::CrawlerContentFormat,
<a href=#1048 id=1048 data-nosnippet>1048</a>        limit: <span class="prelude-ty">Option</span>&lt;u32&gt;,
<a href=#1049 id=1049 data-nosnippet>1049</a>        offset: <span class="prelude-ty">Option</span>&lt;u32&gt;,
<a href=#1050 id=1050 data-nosnippet>1050</a>    ) -&gt; <span class="prelude-ty">Result</span>&lt;CrawlerContents, ScrapflyError&gt; {
<a href=#1051 id=1051 data-nosnippet>1051</a>        <span class="kw">if </span>uuid.is_empty() {
<a href=#1052 id=1052 data-nosnippet>1052</a>            <span class="kw">return </span><span class="prelude-val">Err</span>(ScrapflyError::Config(<span class="string">"uuid cannot be empty"</span>.into()));
<a href=#1053 id=1053 data-nosnippet>1053</a>        }
<a href=#1054 id=1054 data-nosnippet>1054</a>        <span class="kw">let </span><span class="kw-2">mut </span>pairs: Vec&lt;(String, String)&gt; = <span class="macro">vec!</span>[(<span class="string">"formats"</span>.into(), format.as_str().into())];
<a href=#1055 id=1055 data-nosnippet>1055</a>        <span class="kw">if let </span><span class="prelude-val">Some</span>(l) = limit {
<a href=#1056 id=1056 data-nosnippet>1056</a>            pairs.push((<span class="string">"limit"</span>.into(), l.to_string()));
<a href=#1057 id=1057 data-nosnippet>1057</a>        }
<a href=#1058 id=1058 data-nosnippet>1058</a>        <span class="kw">if let </span><span class="prelude-val">Some</span>(o) = offset {
<a href=#1059 id=1059 data-nosnippet>1059</a>            pairs.push((<span class="string">"offset"</span>.into(), o.to_string()));
<a href=#1060 id=1060 data-nosnippet>1060</a>        }
<a href=#1061 id=1061 data-nosnippet>1061</a>        <span class="kw">let </span>url = <span class="self">self</span>.build_url(<span class="kw-2">&amp;</span><span class="macro">format!</span>(<span class="string">"/crawl/{}/contents"</span>, uuid), <span class="kw-2">&amp;</span>pairs)<span class="question-mark">?</span>;
<a href=#1062 id=1062 data-nosnippet>1062</a>        <span class="kw">let </span><span class="kw-2">mut </span>headers = HeaderMap::new();
<a href=#1063 id=1063 data-nosnippet>1063</a>        headers.insert(ACCEPT, HeaderValue::from_static(<span class="string">"application/json"</span>));
<a href=#1064 id=1064 data-nosnippet>1064</a>        <span class="kw">let </span>resp = <span class="self">self
<a href=#1065 id=1065 data-nosnippet>1065</a>            </span>.send_with_retry(Method::GET, url, <span class="prelude-val">Some</span>(headers), <span class="prelude-val">None</span>)
<a href=#1066 id=1066 data-nosnippet>1066</a>            .<span class="kw">await</span><span class="question-mark">?</span>;
<a href=#1067 id=1067 data-nosnippet>1067</a>        <span class="kw">let </span>(status, resp_headers, body) = read_response(resp).<span class="kw">await</span><span class="question-mark">?</span>;
<a href=#1068 id=1068 data-nosnippet>1068</a>        <span class="kw">if </span>status != <span class="number">200 </span>{
<a href=#1069 id=1069 data-nosnippet>1069</a>            <span class="kw">return </span><span class="prelude-val">Err</span>(from_response(status, <span class="kw-2">&amp;</span>body, <span class="number">0</span>, <span class="bool-val">true</span>));
<a href=#1070 id=1070 data-nosnippet>1070</a>        }
<a href=#1071 id=1071 data-nosnippet>1071</a>        <span class="kw">let </span>ct = resp_headers
<a href=#1072 id=1072 data-nosnippet>1072</a>            .get(CONTENT_TYPE)
<a href=#1073 id=1073 data-nosnippet>1073</a>            .and_then(|v| v.to_str().ok())
<a href=#1074 id=1074 data-nosnippet>1074</a>            .unwrap_or(<span class="string">""</span>);
<a href=#1075 id=1075 data-nosnippet>1075</a>        <span class="kw">if </span>!ct.contains(<span class="string">"application/json"</span>) {
<a href=#1076 id=1076 data-nosnippet>1076</a>            <span class="kw">return </span><span class="prelude-val">Err</span>(ScrapflyError::UnexpectedResponseFormat(<span class="macro">format!</span>(
<a href=#1077 id=1077 data-nosnippet>1077</a>                <span class="string">"expected JSON, got Content-Type={}"</span>,
<a href=#1078 id=1078 data-nosnippet>1078</a>                ct
<a href=#1079 id=1079 data-nosnippet>1079</a>            )));
<a href=#1080 id=1080 data-nosnippet>1080</a>        }
<a href=#1081 id=1081 data-nosnippet>1081</a>        <span class="prelude-val">Ok</span>(serde_json::from_slice(<span class="kw-2">&amp;</span>body)<span class="question-mark">?</span>)
<a href=#1082 id=1082 data-nosnippet>1082</a>    }
<a href=#1083 id=1083 data-nosnippet>1083</a>
<a href=#1084 id=1084 data-nosnippet>1084</a>    <span class="doccomment">/// Plain single-URL `GET /crawl/{uuid}/contents?plain=true`.
<a href=#1085 id=1085 data-nosnippet>1085</a>    </span><span class="kw">pub async fn </span>crawl_contents_plain(
<a href=#1086 id=1086 data-nosnippet>1086</a>        <span class="kw-2">&amp;</span><span class="self">self</span>,
<a href=#1087 id=1087 data-nosnippet>1087</a>        uuid: <span class="kw-2">&amp;</span>str,
<a href=#1088 id=1088 data-nosnippet>1088</a>        target_url: <span class="kw-2">&amp;</span>str,
<a href=#1089 id=1089 data-nosnippet>1089</a>        format: <span class="kw">crate</span>::enums::CrawlerContentFormat,
<a href=#1090 id=1090 data-nosnippet>1090</a>    ) -&gt; <span class="prelude-ty">Result</span>&lt;String, ScrapflyError&gt; {
<a href=#1091 id=1091 data-nosnippet>1091</a>        <span class="kw">if </span>uuid.is_empty() {
<a href=#1092 id=1092 data-nosnippet>1092</a>            <span class="kw">return </span><span class="prelude-val">Err</span>(ScrapflyError::Config(<span class="string">"uuid cannot be empty"</span>.into()));
<a href=#1093 id=1093 data-nosnippet>1093</a>        }
<a href=#1094 id=1094 data-nosnippet>1094</a>        <span class="kw">if </span>target_url.is_empty() {
<a href=#1095 id=1095 data-nosnippet>1095</a>            <span class="kw">return </span><span class="prelude-val">Err</span>(ScrapflyError::Config(
<a href=#1096 id=1096 data-nosnippet>1096</a>                <span class="string">"plain mode requires a single url argument"</span>.into(),
<a href=#1097 id=1097 data-nosnippet>1097</a>            ));
<a href=#1098 id=1098 data-nosnippet>1098</a>        }
<a href=#1099 id=1099 data-nosnippet>1099</a>        <span class="kw">let </span>pairs: Vec&lt;(String, String)&gt; = <span class="macro">vec!</span>[
<a href=#1100 id=1100 data-nosnippet>1100</a>            (<span class="string">"formats"</span>.into(), format.as_str().into()),
<a href=#1101 id=1101 data-nosnippet>1101</a>            (<span class="string">"url"</span>.into(), target_url.into()),
<a href=#1102 id=1102 data-nosnippet>1102</a>            (<span class="string">"plain"</span>.into(), <span class="string">"true"</span>.into()),
<a href=#1103 id=1103 data-nosnippet>1103</a>        ];
<a href=#1104 id=1104 data-nosnippet>1104</a>        <span class="kw">let </span>url = <span class="self">self</span>.build_url(<span class="kw-2">&amp;</span><span class="macro">format!</span>(<span class="string">"/crawl/{}/contents"</span>, uuid), <span class="kw-2">&amp;</span>pairs)<span class="question-mark">?</span>;
<a href=#1105 id=1105 data-nosnippet>1105</a>        <span class="kw">let </span><span class="kw-2">mut </span>headers = HeaderMap::new();
<a href=#1106 id=1106 data-nosnippet>1106</a>        headers.insert(ACCEPT, HeaderValue::from_static(<span class="string">"*/*"</span>));
<a href=#1107 id=1107 data-nosnippet>1107</a>        <span class="kw">let </span>resp = <span class="self">self
<a href=#1108 id=1108 data-nosnippet>1108</a>            </span>.send_with_retry(Method::GET, url, <span class="prelude-val">Some</span>(headers), <span class="prelude-val">None</span>)
<a href=#1109 id=1109 data-nosnippet>1109</a>            .<span class="kw">await</span><span class="question-mark">?</span>;
<a href=#1110 id=1110 data-nosnippet>1110</a>        <span class="kw">let </span>(status, _h, body) = read_response(resp).<span class="kw">await</span><span class="question-mark">?</span>;
<a href=#1111 id=1111 data-nosnippet>1111</a>        <span class="kw">if </span>status != <span class="number">200 </span>{
<a href=#1112 id=1112 data-nosnippet>1112</a>            <span class="kw">return </span><span class="prelude-val">Err</span>(from_response(status, <span class="kw-2">&amp;</span>body, <span class="number">0</span>, <span class="bool-val">true</span>));
<a href=#1113 id=1113 data-nosnippet>1113</a>        }
<a href=#1114 id=1114 data-nosnippet>1114</a>        <span class="prelude-val">Ok</span>(String::from_utf8_lossy(<span class="kw-2">&amp;</span>body).into_owned())
<a href=#1115 id=1115 data-nosnippet>1115</a>    }
<a href=#1116 id=1116 data-nosnippet>1116</a>
<a href=#1117 id=1117 data-nosnippet>1117</a>    <span class="doccomment">/// Bulk-batch `POST /crawl/{uuid}/contents/batch`.
<a href=#1118 id=1118 data-nosnippet>1118</a>    /// Returns `url → format → content` (multipart/related response).
<a href=#1119 id=1119 data-nosnippet>1119</a>    </span><span class="kw">pub async fn </span>crawl_contents_batch(
<a href=#1120 id=1120 data-nosnippet>1120</a>        <span class="kw-2">&amp;</span><span class="self">self</span>,
<a href=#1121 id=1121 data-nosnippet>1121</a>        uuid: <span class="kw-2">&amp;</span>str,
<a href=#1122 id=1122 data-nosnippet>1122</a>        urls: <span class="kw-2">&amp;</span>[String],
<a href=#1123 id=1123 data-nosnippet>1123</a>        formats: <span class="kw-2">&amp;</span>[<span class="kw">crate</span>::enums::CrawlerContentFormat],
<a href=#1124 id=1124 data-nosnippet>1124</a>    ) -&gt; <span class="prelude-ty">Result</span>&lt;
<a href=#1125 id=1125 data-nosnippet>1125</a>        std::collections::BTreeMap&lt;String, std::collections::BTreeMap&lt;String, String&gt;&gt;,
<a href=#1126 id=1126 data-nosnippet>1126</a>        ScrapflyError,
<a href=#1127 id=1127 data-nosnippet>1127</a>    &gt; {
<a href=#1128 id=1128 data-nosnippet>1128</a>        <span class="kw">if </span>uuid.is_empty() {
<a href=#1129 id=1129 data-nosnippet>1129</a>            <span class="kw">return </span><span class="prelude-val">Err</span>(ScrapflyError::Config(<span class="string">"uuid cannot be empty"</span>.into()));
<a href=#1130 id=1130 data-nosnippet>1130</a>        }
<a href=#1131 id=1131 data-nosnippet>1131</a>        <span class="kw">if </span>urls.is_empty() {
<a href=#1132 id=1132 data-nosnippet>1132</a>            <span class="kw">return </span><span class="prelude-val">Err</span>(ScrapflyError::Config(<span class="string">"at least one URL is required"</span>.into()));
<a href=#1133 id=1133 data-nosnippet>1133</a>        }
<a href=#1134 id=1134 data-nosnippet>1134</a>        <span class="kw">if </span>urls.len() &gt; <span class="number">100 </span>{
<a href=#1135 id=1135 data-nosnippet>1135</a>            <span class="kw">return </span><span class="prelude-val">Err</span>(ScrapflyError::Config(<span class="macro">format!</span>(
<a href=#1136 id=1136 data-nosnippet>1136</a>                <span class="string">"batch is limited to 100 URLs per request, got {}"</span>,
<a href=#1137 id=1137 data-nosnippet>1137</a>                urls.len()
<a href=#1138 id=1138 data-nosnippet>1138</a>            )));
<a href=#1139 id=1139 data-nosnippet>1139</a>        }
<a href=#1140 id=1140 data-nosnippet>1140</a>        <span class="kw">if </span>formats.is_empty() {
<a href=#1141 id=1141 data-nosnippet>1141</a>            <span class="kw">return </span><span class="prelude-val">Err</span>(ScrapflyError::Config(
<a href=#1142 id=1142 data-nosnippet>1142</a>                <span class="string">"at least one format is required"</span>.into(),
<a href=#1143 id=1143 data-nosnippet>1143</a>            ));
<a href=#1144 id=1144 data-nosnippet>1144</a>        }
<a href=#1145 id=1145 data-nosnippet>1145</a>        <span class="kw">let </span>format_strs: Vec&lt;<span class="kw-2">&amp;</span><span class="lifetime">'static </span>str&gt; = formats.iter().map(|f| f.as_str()).collect();
<a href=#1146 id=1146 data-nosnippet>1146</a>        <span class="kw">let </span>pairs: Vec&lt;(String, String)&gt; = <span class="macro">vec!</span>[(<span class="string">"formats"</span>.into(), format_strs.join(<span class="string">","</span>))];
<a href=#1147 id=1147 data-nosnippet>1147</a>        <span class="kw">let </span>url = <span class="self">self</span>.build_url(<span class="kw-2">&amp;</span><span class="macro">format!</span>(<span class="string">"/crawl/{}/contents/batch"</span>, uuid), <span class="kw-2">&amp;</span>pairs)<span class="question-mark">?</span>;
<a href=#1148 id=1148 data-nosnippet>1148</a>        <span class="kw">let </span>body = urls.join(<span class="string">"\n"</span>).into_bytes();
<a href=#1149 id=1149 data-nosnippet>1149</a>        <span class="kw">let </span><span class="kw-2">mut </span>headers = HeaderMap::new();
<a href=#1150 id=1150 data-nosnippet>1150</a>        headers.insert(CONTENT_TYPE, HeaderValue::from_static(<span class="string">"text/plain"</span>));
<a href=#1151 id=1151 data-nosnippet>1151</a>        headers.insert(
<a href=#1152 id=1152 data-nosnippet>1152</a>            ACCEPT,
<a href=#1153 id=1153 data-nosnippet>1153</a>            HeaderValue::from_static(<span class="string">"multipart/related, application/json"</span>),
<a href=#1154 id=1154 data-nosnippet>1154</a>        );
<a href=#1155 id=1155 data-nosnippet>1155</a>        <span class="kw">let </span>resp = <span class="self">self
<a href=#1156 id=1156 data-nosnippet>1156</a>            </span>.send_with_retry(Method::POST, url, <span class="prelude-val">Some</span>(headers), <span class="prelude-val">Some</span>(body))
<a href=#1157 id=1157 data-nosnippet>1157</a>            .<span class="kw">await</span><span class="question-mark">?</span>;
<a href=#1158 id=1158 data-nosnippet>1158</a>        <span class="kw">let </span>(status, resp_headers, body_bytes) = read_response(resp).<span class="kw">await</span><span class="question-mark">?</span>;
<a href=#1159 id=1159 data-nosnippet>1159</a>        <span class="kw">if </span>status != <span class="number">200 </span>{
<a href=#1160 id=1160 data-nosnippet>1160</a>            <span class="kw">return </span><span class="prelude-val">Err</span>(from_response(status, <span class="kw-2">&amp;</span>body_bytes, <span class="number">0</span>, <span class="bool-val">true</span>));
<a href=#1161 id=1161 data-nosnippet>1161</a>        }
<a href=#1162 id=1162 data-nosnippet>1162</a>        <span class="kw">let </span>ct = resp_headers
<a href=#1163 id=1163 data-nosnippet>1163</a>            .get(CONTENT_TYPE)
<a href=#1164 id=1164 data-nosnippet>1164</a>            .and_then(|v| v.to_str().ok())
<a href=#1165 id=1165 data-nosnippet>1165</a>            .unwrap_or(<span class="string">""</span>);
<a href=#1166 id=1166 data-nosnippet>1166</a>        <span class="kw">if </span>ct.contains(<span class="string">"application/json"</span>) {
<a href=#1167 id=1167 data-nosnippet>1167</a>            <span class="kw">return </span><span class="prelude-val">Err</span>(ScrapflyError::UnexpectedResponseFormat(
<a href=#1168 id=1168 data-nosnippet>1168</a>                <span class="string">"CrawlContentsBatch expected multipart/related, got JSON"</span>.into(),
<a href=#1169 id=1169 data-nosnippet>1169</a>            ));
<a href=#1170 id=1170 data-nosnippet>1170</a>        }
<a href=#1171 id=1171 data-nosnippet>1171</a>        parse_multipart_related(
<a href=#1172 id=1172 data-nosnippet>1172</a>            std::str::from_utf8(<span class="kw-2">&amp;</span>body_bytes).unwrap_or(<span class="string">""</span>),
<a href=#1173 id=1173 data-nosnippet>1173</a>            ct,
<a href=#1174 id=1174 data-nosnippet>1174</a>            <span class="kw-2">&amp;</span>format_strs,
<a href=#1175 id=1175 data-nosnippet>1175</a>        )
<a href=#1176 id=1176 data-nosnippet>1176</a>    }
<a href=#1177 id=1177 data-nosnippet>1177</a>
<a href=#1178 id=1178 data-nosnippet>1178</a>    <span class="doccomment">/// Cancel a crawler job.
<a href=#1179 id=1179 data-nosnippet>1179</a>    </span><span class="kw">pub async fn </span>crawl_cancel(<span class="kw-2">&amp;</span><span class="self">self</span>, uuid: <span class="kw-2">&amp;</span>str) -&gt; <span class="prelude-ty">Result</span>&lt;(), ScrapflyError&gt; {
<a href=#1180 id=1180 data-nosnippet>1180</a>        <span class="kw">if </span>uuid.is_empty() {
<a href=#1181 id=1181 data-nosnippet>1181</a>            <span class="kw">return </span><span class="prelude-val">Err</span>(ScrapflyError::Config(<span class="string">"uuid cannot be empty"</span>.into()));
<a href=#1182 id=1182 data-nosnippet>1182</a>        }
<a href=#1183 id=1183 data-nosnippet>1183</a>        <span class="kw">let </span>url = <span class="self">self</span>.build_url(<span class="kw-2">&amp;</span><span class="macro">format!</span>(<span class="string">"/crawl/{}/cancel"</span>, uuid), <span class="kw-2">&amp;</span>[])<span class="question-mark">?</span>;
<a href=#1184 id=1184 data-nosnippet>1184</a>        <span class="kw">let </span><span class="kw-2">mut </span>headers = HeaderMap::new();
<a href=#1185 id=1185 data-nosnippet>1185</a>        headers.insert(ACCEPT, HeaderValue::from_static(<span class="string">"application/json"</span>));
<a href=#1186 id=1186 data-nosnippet>1186</a>        <span class="kw">let </span>resp = <span class="self">self
<a href=#1187 id=1187 data-nosnippet>1187</a>            </span>.send_with_retry(Method::POST, url, <span class="prelude-val">Some</span>(headers), <span class="prelude-val">None</span>)
<a href=#1188 id=1188 data-nosnippet>1188</a>            .<span class="kw">await</span><span class="question-mark">?</span>;
<a href=#1189 id=1189 data-nosnippet>1189</a>        <span class="kw">let </span>(status, _h, body) = read_response(resp).<span class="kw">await</span><span class="question-mark">?</span>;
<a href=#1190 id=1190 data-nosnippet>1190</a>        <span class="kw">if </span>status != <span class="number">200 </span>&amp;&amp; status != <span class="number">202 </span>{
<a href=#1191 id=1191 data-nosnippet>1191</a>            <span class="kw">return </span><span class="prelude-val">Err</span>(from_response(status, <span class="kw-2">&amp;</span>body, <span class="number">0</span>, <span class="bool-val">true</span>));
<a href=#1192 id=1192 data-nosnippet>1192</a>        }
<a href=#1193 id=1193 data-nosnippet>1193</a>        <span class="prelude-val">Ok</span>(())
<a href=#1194 id=1194 data-nosnippet>1194</a>    }
<a href=#1195 id=1195 data-nosnippet>1195</a>
<a href=#1196 id=1196 data-nosnippet>1196</a>    <span class="doccomment">/// Download a crawler artifact (WARC or HAR).
<a href=#1197 id=1197 data-nosnippet>1197</a>    </span><span class="kw">pub async fn </span>crawl_artifact(
<a href=#1198 id=1198 data-nosnippet>1198</a>        <span class="kw-2">&amp;</span><span class="self">self</span>,
<a href=#1199 id=1199 data-nosnippet>1199</a>        uuid: <span class="kw-2">&amp;</span>str,
<a href=#1200 id=1200 data-nosnippet>1200</a>        artifact_type: CrawlerArtifactType,
<a href=#1201 id=1201 data-nosnippet>1201</a>    ) -&gt; <span class="prelude-ty">Result</span>&lt;CrawlerArtifact, ScrapflyError&gt; {
<a href=#1202 id=1202 data-nosnippet>1202</a>        <span class="kw">if </span>uuid.is_empty() {
<a href=#1203 id=1203 data-nosnippet>1203</a>            <span class="kw">return </span><span class="prelude-val">Err</span>(ScrapflyError::Config(<span class="string">"uuid cannot be empty"</span>.into()));
<a href=#1204 id=1204 data-nosnippet>1204</a>        }
<a href=#1205 id=1205 data-nosnippet>1205</a>        <span class="kw">let </span>pairs: Vec&lt;(String, String)&gt; = <span class="macro">vec!</span>[(<span class="string">"type"</span>.into(), artifact_type.as_str().into())];
<a href=#1206 id=1206 data-nosnippet>1206</a>        <span class="kw">let </span>url = <span class="self">self</span>.build_url(<span class="kw-2">&amp;</span><span class="macro">format!</span>(<span class="string">"/crawl/{}/artifact"</span>, uuid), <span class="kw-2">&amp;</span>pairs)<span class="question-mark">?</span>;
<a href=#1207 id=1207 data-nosnippet>1207</a>        <span class="kw">let </span><span class="kw-2">mut </span>headers = HeaderMap::new();
<a href=#1208 id=1208 data-nosnippet>1208</a>        <span class="comment">// HAR is plain JSON — asking for `application/gzip` makes the server
<a href=#1209 id=1209 data-nosnippet>1209</a>        // gzip-wrap it, and reqwest can't auto-decode it without a matching
<a href=#1210 id=1210 data-nosnippet>1210</a>        // `Content-Encoding` header. Match `sdk/go/crawler.go::CrawlArtifact`
<a href=#1211 id=1211 data-nosnippet>1211</a>        // which sends different Accept per artifact type.
<a href=#1212 id=1212 data-nosnippet>1212</a>        </span><span class="kw">let </span>accept = <span class="kw">match </span>artifact_type {
<a href=#1213 id=1213 data-nosnippet>1213</a>            CrawlerArtifactType::Har =&gt; <span class="string">"application/json, application/octet-stream"</span>,
<a href=#1214 id=1214 data-nosnippet>1214</a>            CrawlerArtifactType::Warc =&gt; {
<a href=#1215 id=1215 data-nosnippet>1215</a>                <span class="string">"application/gzip, application/octet-stream, application/json"
<a href=#1216 id=1216 data-nosnippet>1216</a>            </span>}
<a href=#1217 id=1217 data-nosnippet>1217</a>        };
<a href=#1218 id=1218 data-nosnippet>1218</a>        headers.insert(ACCEPT, HeaderValue::from_static(accept));
<a href=#1219 id=1219 data-nosnippet>1219</a>        <span class="kw">let </span>resp = <span class="self">self
<a href=#1220 id=1220 data-nosnippet>1220</a>            </span>.send_with_retry(Method::GET, url, <span class="prelude-val">Some</span>(headers), <span class="prelude-val">None</span>)
<a href=#1221 id=1221 data-nosnippet>1221</a>            .<span class="kw">await</span><span class="question-mark">?</span>;
<a href=#1222 id=1222 data-nosnippet>1222</a>        <span class="kw">let </span>(status, _h, body) = read_response(resp).<span class="kw">await</span><span class="question-mark">?</span>;
<a href=#1223 id=1223 data-nosnippet>1223</a>        <span class="kw">if </span>status != <span class="number">200 </span>{
<a href=#1224 id=1224 data-nosnippet>1224</a>            <span class="kw">return </span><span class="prelude-val">Err</span>(from_response(status, <span class="kw-2">&amp;</span>body, <span class="number">0</span>, <span class="bool-val">true</span>));
<a href=#1225 id=1225 data-nosnippet>1225</a>        }
<a href=#1226 id=1226 data-nosnippet>1226</a>        <span class="prelude-val">Ok</span>(CrawlerArtifact {
<a href=#1227 id=1227 data-nosnippet>1227</a>            artifact_type,
<a href=#1228 id=1228 data-nosnippet>1228</a>            data: body,
<a href=#1229 id=1229 data-nosnippet>1229</a>        })
<a href=#1230 id=1230 data-nosnippet>1230</a>    }
<a href=#1231 id=1231 data-nosnippet>1231</a>
<a href=#1232 id=1232 data-nosnippet>1232</a>    <span class="comment">// ==============================================================================
<a href=#1233 id=1233 data-nosnippet>1233</a>    // Cloud browser methods (implementations in cloud_browser.rs)
<a href=#1234 id=1234 data-nosnippet>1234</a>    // ==============================================================================
<a href=#1235 id=1235 data-nosnippet>1235</a>
<a href=#1236 id=1236 data-nosnippet>1236</a>    </span><span class="doccomment">/// Fire a request through the retry loop.
<a href=#1237 id=1237 data-nosnippet>1237</a>    </span><span class="kw">pub</span>(<span class="kw">crate</span>) <span class="kw">async fn </span>send_with_retry(
<a href=#1238 id=1238 data-nosnippet>1238</a>        <span class="kw-2">&amp;</span><span class="self">self</span>,
<a href=#1239 id=1239 data-nosnippet>1239</a>        method: Method,
<a href=#1240 id=1240 data-nosnippet>1240</a>        url: Url,
<a href=#1241 id=1241 data-nosnippet>1241</a>        headers: <span class="prelude-ty">Option</span>&lt;HeaderMap&gt;,
<a href=#1242 id=1242 data-nosnippet>1242</a>        body: <span class="prelude-ty">Option</span>&lt;Vec&lt;u8&gt;&gt;,
<a href=#1243 id=1243 data-nosnippet>1243</a>    ) -&gt; <span class="prelude-ty">Result</span>&lt;Response, ScrapflyError&gt; {
<a href=#1244 id=1244 data-nosnippet>1244</a>        <span class="kw">let </span><span class="kw-2">mut </span>last_err: <span class="prelude-ty">Option</span>&lt;ScrapflyError&gt; = <span class="prelude-val">None</span>;
<a href=#1245 id=1245 data-nosnippet>1245</a>        <span class="kw">for </span>attempt <span class="kw">in </span><span class="number">0</span>..DEFAULT_RETRIES {
<a href=#1246 id=1246 data-nosnippet>1246</a>            <span class="kw">let </span><span class="kw-2">mut </span>req = <span class="self">self</span>.http.request(method.clone(), url.clone());
<a href=#1247 id=1247 data-nosnippet>1247</a>            <span class="kw">let </span><span class="kw-2">mut </span>hmap = headers.clone().unwrap_or_default();
<a href=#1248 id=1248 data-nosnippet>1248</a>            <span class="kw">if </span>!hmap.contains_key(USER_AGENT) {
<a href=#1249 id=1249 data-nosnippet>1249</a>                hmap.insert(USER_AGENT, HeaderValue::from_static(SDK_USER_AGENT));
<a href=#1250 id=1250 data-nosnippet>1250</a>            }
<a href=#1251 id=1251 data-nosnippet>1251</a>            <span class="kw">if let </span><span class="prelude-val">Some</span>(cb) = <span class="kw-2">&amp;</span><span class="self">self</span>.on_request {
<a href=#1252 id=1252 data-nosnippet>1252</a>                cb(<span class="kw-2">&amp;</span>method, <span class="kw-2">&amp;</span>url, <span class="kw-2">&amp;</span>hmap);
<a href=#1253 id=1253 data-nosnippet>1253</a>            }
<a href=#1254 id=1254 data-nosnippet>1254</a>            req = req.headers(hmap);
<a href=#1255 id=1255 data-nosnippet>1255</a>            <span class="kw">if let </span><span class="prelude-val">Some</span>(b) = <span class="kw-2">&amp;</span>body {
<a href=#1256 id=1256 data-nosnippet>1256</a>                req = req.body(b.clone());
<a href=#1257 id=1257 data-nosnippet>1257</a>            }
<a href=#1258 id=1258 data-nosnippet>1258</a>            <span class="kw">match </span>req.send().<span class="kw">await </span>{
<a href=#1259 id=1259 data-nosnippet>1259</a>                <span class="prelude-val">Ok</span>(resp) =&gt; {
<a href=#1260 id=1260 data-nosnippet>1260</a>                    <span class="kw">let </span>status = resp.status().as_u16();
<a href=#1261 id=1261 data-nosnippet>1261</a>                    <span class="kw">if </span>(<span class="number">500</span>..<span class="number">600</span>).contains(<span class="kw-2">&amp;</span>status) &amp;&amp; attempt + <span class="number">1 </span>&lt; DEFAULT_RETRIES {
<a href=#1262 id=1262 data-nosnippet>1262</a>                        last_err = <span class="prelude-val">Some</span>(ScrapflyError::ApiServer(<span class="kw">crate</span>::error::ApiError {
<a href=#1263 id=1263 data-nosnippet>1263</a>                            message: <span class="string">"server error"</span>.into(),
<a href=#1264 id=1264 data-nosnippet>1264</a>                            http_status: status,
<a href=#1265 id=1265 data-nosnippet>1265</a>                            ..Default::default()
<a href=#1266 id=1266 data-nosnippet>1266</a>                        }));
<a href=#1267 id=1267 data-nosnippet>1267</a>                        tokio::time::sleep(DEFAULT_RETRY_DELAY).<span class="kw">await</span>;
<a href=#1268 id=1268 data-nosnippet>1268</a>                        <span class="kw">continue</span>;
<a href=#1269 id=1269 data-nosnippet>1269</a>                    }
<a href=#1270 id=1270 data-nosnippet>1270</a>                    <span class="kw">return </span><span class="prelude-val">Ok</span>(resp);
<a href=#1271 id=1271 data-nosnippet>1271</a>                }
<a href=#1272 id=1272 data-nosnippet>1272</a>                <span class="prelude-val">Err</span>(e) =&gt; {
<a href=#1273 id=1273 data-nosnippet>1273</a>                    last_err = <span class="prelude-val">Some</span>(ScrapflyError::Transport(e));
<a href=#1274 id=1274 data-nosnippet>1274</a>                    <span class="kw">if </span>attempt + <span class="number">1 </span>&lt; DEFAULT_RETRIES {
<a href=#1275 id=1275 data-nosnippet>1275</a>                        tokio::time::sleep(DEFAULT_RETRY_DELAY).<span class="kw">await</span>;
<a href=#1276 id=1276 data-nosnippet>1276</a>                        <span class="kw">continue</span>;
<a href=#1277 id=1277 data-nosnippet>1277</a>                    }
<a href=#1278 id=1278 data-nosnippet>1278</a>                }
<a href=#1279 id=1279 data-nosnippet>1279</a>            }
<a href=#1280 id=1280 data-nosnippet>1280</a>        }
<a href=#1281 id=1281 data-nosnippet>1281</a>        <span class="prelude-val">Err</span>(last_err.unwrap_or_else(|| ScrapflyError::Config(<span class="string">"retry loop exhausted"</span>.into())))
<a href=#1282 id=1282 data-nosnippet>1282</a>    }
<a href=#1283 id=1283 data-nosnippet>1283</a>
<a href=#1284 id=1284 data-nosnippet>1284</a>    <span class="doccomment">/// Single-shot send, no retry (for `verify_api_key`/`account` style calls).
<a href=#1285 id=1285 data-nosnippet>1285</a>    </span><span class="kw">async fn </span>send_simple(
<a href=#1286 id=1286 data-nosnippet>1286</a>        <span class="kw-2">&amp;</span><span class="self">self</span>,
<a href=#1287 id=1287 data-nosnippet>1287</a>        method: Method,
<a href=#1288 id=1288 data-nosnippet>1288</a>        url: Url,
<a href=#1289 id=1289 data-nosnippet>1289</a>        headers: <span class="prelude-ty">Option</span>&lt;HeaderMap&gt;,
<a href=#1290 id=1290 data-nosnippet>1290</a>        body: <span class="prelude-ty">Option</span>&lt;Vec&lt;u8&gt;&gt;,
<a href=#1291 id=1291 data-nosnippet>1291</a>    ) -&gt; <span class="prelude-ty">Result</span>&lt;Response, ScrapflyError&gt; {
<a href=#1292 id=1292 data-nosnippet>1292</a>        <span class="kw">let </span><span class="kw-2">mut </span>req = <span class="self">self</span>.http.request(method.clone(), url.clone());
<a href=#1293 id=1293 data-nosnippet>1293</a>        <span class="kw">let </span><span class="kw-2">mut </span>hmap = headers.unwrap_or_default();
<a href=#1294 id=1294 data-nosnippet>1294</a>        <span class="kw">if </span>!hmap.contains_key(USER_AGENT) {
<a href=#1295 id=1295 data-nosnippet>1295</a>            hmap.insert(USER_AGENT, HeaderValue::from_static(SDK_USER_AGENT));
<a href=#1296 id=1296 data-nosnippet>1296</a>        }
<a href=#1297 id=1297 data-nosnippet>1297</a>        <span class="kw">if let </span><span class="prelude-val">Some</span>(cb) = <span class="kw-2">&amp;</span><span class="self">self</span>.on_request {
<a href=#1298 id=1298 data-nosnippet>1298</a>            cb(<span class="kw-2">&amp;</span>method, <span class="kw-2">&amp;</span>url, <span class="kw-2">&amp;</span>hmap);
<a href=#1299 id=1299 data-nosnippet>1299</a>        }
<a href=#1300 id=1300 data-nosnippet>1300</a>        req = req.headers(hmap);
<a href=#1301 id=1301 data-nosnippet>1301</a>        <span class="kw">if let </span><span class="prelude-val">Some</span>(b) = body {
<a href=#1302 id=1302 data-nosnippet>1302</a>            req = req.body(b);
<a href=#1303 id=1303 data-nosnippet>1303</a>        }
<a href=#1304 id=1304 data-nosnippet>1304</a>        req.send().<span class="kw">await</span>.map_err(ScrapflyError::Transport)
<a href=#1305 id=1305 data-nosnippet>1305</a>    }
<a href=#1306 id=1306 data-nosnippet>1306</a>}
<a href=#1307 id=1307 data-nosnippet>1307</a>
<a href=#1308 id=1308 data-nosnippet>1308</a><span class="doccomment">/// Drain a response into (status, headers, body bytes) and propagate
<a href=#1309 id=1309 data-nosnippet>1309</a>/// `Retry-After` into the retry-ms field when present.
<a href=#1310 id=1310 data-nosnippet>1310</a></span><span class="kw">async fn </span>read_response(resp: Response) -&gt; <span class="prelude-ty">Result</span>&lt;(u16, HeaderMap, bytes::Bytes), ScrapflyError&gt; {
<a href=#1311 id=1311 data-nosnippet>1311</a>    <span class="kw">let </span>status = resp.status().as_u16();
<a href=#1312 id=1312 data-nosnippet>1312</a>    <span class="kw">let </span>headers = resp.headers().clone();
<a href=#1313 id=1313 data-nosnippet>1313</a>    <span class="kw">let </span>body = resp.bytes().<span class="kw">await</span>.map_err(ScrapflyError::Transport)<span class="question-mark">?</span>;
<a href=#1314 id=1314 data-nosnippet>1314</a>    <span class="kw">let _ </span>= parse_retry_after(headers.get(<span class="string">"retry-after"</span>).and_then(|v| v.to_str().ok()));
<a href=#1315 id=1315 data-nosnippet>1315</a>    <span class="prelude-val">Ok</span>((status, headers, body))
<a href=#1316 id=1316 data-nosnippet>1316</a>}
<a href=#1317 id=1317 data-nosnippet>1317</a>
<a href=#1318 id=1318 data-nosnippet>1318</a><span class="doccomment">/// Minimal RFC 2387 multipart/related parser — ported from
<a href=#1319 id=1319 data-nosnippet>1319</a>/// `sdk/go/crawler.go::parseMultipartRelated`.
<a href=#1320 id=1320 data-nosnippet>1320</a></span><span class="kw">fn </span>parse_multipart_related(
<a href=#1321 id=1321 data-nosnippet>1321</a>    body: <span class="kw-2">&amp;</span>str,
<a href=#1322 id=1322 data-nosnippet>1322</a>    content_type: <span class="kw-2">&amp;</span>str,
<a href=#1323 id=1323 data-nosnippet>1323</a>    formats: <span class="kw-2">&amp;</span>[<span class="kw-2">&amp;</span>str],
<a href=#1324 id=1324 data-nosnippet>1324</a>) -&gt; <span class="prelude-ty">Result</span>&lt;
<a href=#1325 id=1325 data-nosnippet>1325</a>    std::collections::BTreeMap&lt;String, std::collections::BTreeMap&lt;String, String&gt;&gt;,
<a href=#1326 id=1326 data-nosnippet>1326</a>    ScrapflyError,
<a href=#1327 id=1327 data-nosnippet>1327</a>&gt; {
<a href=#1328 id=1328 data-nosnippet>1328</a>    <span class="kw">let </span><span class="kw-2">mut </span>boundary = String::new();
<a href=#1329 id=1329 data-nosnippet>1329</a>    <span class="kw">for </span>part <span class="kw">in </span>content_type.split(<span class="string">';'</span>) {
<a href=#1330 id=1330 data-nosnippet>1330</a>        <span class="kw">let </span>p = part.trim();
<a href=#1331 id=1331 data-nosnippet>1331</a>        <span class="kw">if let </span><span class="prelude-val">Some</span>(stripped) = p.strip_prefix(<span class="string">"boundary="</span>) {
<a href=#1332 id=1332 data-nosnippet>1332</a>            boundary = stripped.trim_matches(<span class="string">'"'</span>).to_string();
<a href=#1333 id=1333 data-nosnippet>1333</a>            <span class="kw">break</span>;
<a href=#1334 id=1334 data-nosnippet>1334</a>        }
<a href=#1335 id=1335 data-nosnippet>1335</a>    }
<a href=#1336 id=1336 data-nosnippet>1336</a>    <span class="kw">if </span>boundary.is_empty() {
<a href=#1337 id=1337 data-nosnippet>1337</a>        <span class="kw">return </span><span class="prelude-val">Err</span>(ScrapflyError::UnexpectedResponseFormat(<span class="macro">format!</span>(
<a href=#1338 id=1338 data-nosnippet>1338</a>            <span class="string">"multipart response has no boundary in Content-Type: {}"</span>,
<a href=#1339 id=1339 data-nosnippet>1339</a>            content_type
<a href=#1340 id=1340 data-nosnippet>1340</a>        )));
<a href=#1341 id=1341 data-nosnippet>1341</a>    }
<a href=#1342 id=1342 data-nosnippet>1342</a>    <span class="kw">let </span>delimiter = <span class="macro">format!</span>(<span class="string">"--{}"</span>, boundary);
<a href=#1343 id=1343 data-nosnippet>1343</a>    <span class="kw">let </span><span class="kw-2">mut </span>result: std::collections::BTreeMap&lt;String, std::collections::BTreeMap&lt;String, String&gt;&gt; =
<a href=#1344 id=1344 data-nosnippet>1344</a>        std::collections::BTreeMap::new();
<a href=#1345 id=1345 data-nosnippet>1345</a>    <span class="kw">let </span>segments: Vec&lt;<span class="kw-2">&amp;</span>str&gt; = body.split(<span class="kw-2">&amp;</span>delimiter <span class="kw">as </span><span class="kw-2">&amp;</span>str).collect();
<a href=#1346 id=1346 data-nosnippet>1346</a>    <span class="kw">for </span>segment <span class="kw">in </span>segments.iter().skip(<span class="number">1</span>) {
<a href=#1347 id=1347 data-nosnippet>1347</a>        <span class="kw">let </span><span class="kw-2">mut </span>seg = <span class="kw-2">*</span>segment;
<a href=#1348 id=1348 data-nosnippet>1348</a>        seg = seg.trim_start_matches(<span class="string">"\r\n"</span>).trim_start_matches(<span class="string">'\n'</span>);
<a href=#1349 id=1349 data-nosnippet>1349</a>        <span class="kw">if </span>seg.starts_with(<span class="string">"--"</span>) {
<a href=#1350 id=1350 data-nosnippet>1350</a>            <span class="kw">break</span>;
<a href=#1351 id=1351 data-nosnippet>1351</a>        }
<a href=#1352 id=1352 data-nosnippet>1352</a>        seg = seg.trim_end_matches(<span class="string">"\r\n"</span>).trim_end_matches(<span class="string">'\n'</span>);
<a href=#1353 id=1353 data-nosnippet>1353</a>        <span class="kw">let </span>(headers_raw, part_body) = <span class="kw">if let </span><span class="prelude-val">Some</span>(idx) = seg.find(<span class="string">"\r\n\r\n"</span>) {
<a href=#1354 id=1354 data-nosnippet>1354</a>            (<span class="kw-2">&amp;</span>seg[..idx], <span class="kw-2">&amp;</span>seg[idx + <span class="number">4</span>..])
<a href=#1355 id=1355 data-nosnippet>1355</a>        } <span class="kw">else if let </span><span class="prelude-val">Some</span>(idx) = seg.find(<span class="string">"\n\n"</span>) {
<a href=#1356 id=1356 data-nosnippet>1356</a>            (<span class="kw-2">&amp;</span>seg[..idx], <span class="kw-2">&amp;</span>seg[idx + <span class="number">2</span>..])
<a href=#1357 id=1357 data-nosnippet>1357</a>        } <span class="kw">else </span>{
<a href=#1358 id=1358 data-nosnippet>1358</a>            <span class="kw">continue</span>;
<a href=#1359 id=1359 data-nosnippet>1359</a>        };
<a href=#1360 id=1360 data-nosnippet>1360</a>        <span class="kw">let </span><span class="kw-2">mut </span>part_url = String::new();
<a href=#1361 id=1361 data-nosnippet>1361</a>        <span class="kw">let </span><span class="kw-2">mut </span>part_format = String::new();
<a href=#1362 id=1362 data-nosnippet>1362</a>        <span class="kw">for </span>line <span class="kw">in </span>headers_raw.split(<span class="string">'\n'</span>) {
<a href=#1363 id=1363 data-nosnippet>1363</a>            <span class="kw">let </span>line = line.trim_end_matches(<span class="string">'\r'</span>);
<a href=#1364 id=1364 data-nosnippet>1364</a>            <span class="kw">if let </span><span class="prelude-val">Some</span>(colon) = line.find(<span class="string">':'</span>) {
<a href=#1365 id=1365 data-nosnippet>1365</a>                <span class="kw">let </span>name = line[..colon].trim().to_ascii_lowercase();
<a href=#1366 id=1366 data-nosnippet>1366</a>                <span class="kw">let </span>value = line[colon + <span class="number">1</span>..].trim().to_string();
<a href=#1367 id=1367 data-nosnippet>1367</a>                <span class="kw">match </span>name.as_str() {
<a href=#1368 id=1368 data-nosnippet>1368</a>                    <span class="string">"content-location" </span>=&gt; part_url = value,
<a href=#1369 id=1369 data-nosnippet>1369</a>                    <span class="string">"content-type" </span>=&gt; part_format = infer_format_from_content_type(<span class="kw-2">&amp;</span>value),
<a href=#1370 id=1370 data-nosnippet>1370</a>                    <span class="kw">_ </span>=&gt; {}
<a href=#1371 id=1371 data-nosnippet>1371</a>                }
<a href=#1372 id=1372 data-nosnippet>1372</a>            }
<a href=#1373 id=1373 data-nosnippet>1373</a>        }
<a href=#1374 id=1374 data-nosnippet>1374</a>        <span class="kw">if </span>part_url.is_empty() {
<a href=#1375 id=1375 data-nosnippet>1375</a>            <span class="kw">continue</span>;
<a href=#1376 id=1376 data-nosnippet>1376</a>        }
<a href=#1377 id=1377 data-nosnippet>1377</a>        <span class="kw">if </span>part_format.is_empty() {
<a href=#1378 id=1378 data-nosnippet>1378</a>            part_format = formats.first().copied().unwrap_or(<span class="string">"html"</span>).to_string();
<a href=#1379 id=1379 data-nosnippet>1379</a>        }
<a href=#1380 id=1380 data-nosnippet>1380</a>        result
<a href=#1381 id=1381 data-nosnippet>1381</a>            .entry(part_url)
<a href=#1382 id=1382 data-nosnippet>1382</a>            .or_default()
<a href=#1383 id=1383 data-nosnippet>1383</a>            .insert(part_format, part_body.to_string());
<a href=#1384 id=1384 data-nosnippet>1384</a>    }
<a href=#1385 id=1385 data-nosnippet>1385</a>    <span class="prelude-val">Ok</span>(result)
<a href=#1386 id=1386 data-nosnippet>1386</a>}
<a href=#1387 id=1387 data-nosnippet>1387</a>
<a href=#1388 id=1388 data-nosnippet>1388</a><span class="kw">fn </span>infer_format_from_content_type(ct: <span class="kw-2">&amp;</span>str) -&gt; String {
<a href=#1389 id=1389 data-nosnippet>1389</a>    <span class="kw">let </span>lc = ct
<a href=#1390 id=1390 data-nosnippet>1390</a>        .split(<span class="string">';'</span>)
<a href=#1391 id=1391 data-nosnippet>1391</a>        .next()
<a href=#1392 id=1392 data-nosnippet>1392</a>        .unwrap_or(<span class="string">""</span>)
<a href=#1393 id=1393 data-nosnippet>1393</a>        .trim()
<a href=#1394 id=1394 data-nosnippet>1394</a>        .to_ascii_lowercase();
<a href=#1395 id=1395 data-nosnippet>1395</a>    <span class="kw">match </span>lc.as_str() {
<a href=#1396 id=1396 data-nosnippet>1396</a>        <span class="string">"text/html" </span>=&gt; <span class="string">"html"</span>.into(),
<a href=#1397 id=1397 data-nosnippet>1397</a>        <span class="string">"text/markdown" </span>=&gt; <span class="string">"markdown"</span>.into(),
<a href=#1398 id=1398 data-nosnippet>1398</a>        <span class="string">"text/plain" </span>=&gt; <span class="string">"text"</span>.into(),
<a href=#1399 id=1399 data-nosnippet>1399</a>        <span class="string">"application/json" </span>=&gt; <span class="string">"json"</span>.into(),
<a href=#1400 id=1400 data-nosnippet>1400</a>        <span class="kw">_ </span>=&gt; String::new(),
<a href=#1401 id=1401 data-nosnippet>1401</a>    }
<a href=#1402 id=1402 data-nosnippet>1402</a>}
</code></pre></div></section></main></body></html>