This repository has no description
1use std::sync::Arc;
2
3use axum::body::{Body, to_bytes};
4use bobbin_edge_index::{CoverageWatch, EdgeStore, IssueStateKind, PullStatusKind, StateIndex};
5use bobbin_knot_proxy::{KnotHttpConfig, KnotProxy, KnotProxyConfig};
6use bobbin_record_lru::{CacheCapacity, LruRecordStore};
7use bobbin_resolver::RepoIdResolver;
8use bobbin_runtime::{RuntimeHasher, SystemClock};
9use bobbin_search::{DEFAULT_WRITER_HEAP_BYTES, SearchIndex, SearchReader};
10use bobbin_slingshot_client::SlingshotClient;
11use bobbin_types::edges::Edge;
12use bobbin_types::ids::SubjectRef;
13use bobbin_xrpc::{AppState, router};
14use http::{Request, StatusCode};
15use jacquard_common::DefaultStr;
16use jacquard_common::types::did::Did;
17use jacquard_common::types::nsid::Nsid;
18use jacquard_common::types::string::{AtUri, Handle};
19use serde_json::{Value, json};
20use tower::ServiceExt;
21use url::Url;
22use wiremock::matchers::{method, path, query_param};
23use wiremock::{Mock, MockServer, ResponseTemplate};
24
25const CID: &str = "bafyreieqygohnz2zqyvtvktbjpvhutphobcmbsnt4q5lc36ri7vpcmoz4i";
26
27const COUNT: &str = "sh.tangled.query.enrichResponse#count";
28const DISTINCT_AUTHORS: &str = "sh.tangled.query.enrichResponse#distinctAuthors";
29const VIEWER: &str = "sh.tangled.query.enrichResponse#viewer";
30const MINIDOC: &str = "com.bad-example.identity.miniDoc";
31
32fn at(s: &str) -> AtUri<DefaultStr> {
33 AtUri::new_owned(s).unwrap()
34}
35
36fn did(s: &str) -> Did<DefaultStr> {
37 Did::new_owned(s).unwrap()
38}
39
40fn handle(s: &str) -> Handle<DefaultStr> {
41 Handle::new_owned(s).unwrap()
42}
43
44fn nsid(s: &'static str) -> Nsid<DefaultStr> {
45 Nsid::new_static(s).unwrap()
46}
47
48static EDGE_COUNTER: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
49
50fn next_sort_micros() -> u64 {
51 EDGE_COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
52}
53
54struct Harness {
55 server: MockServer,
56 edges: Arc<EdgeStore>,
57 state: AppState,
58}
59
60impl Harness {
61 async fn new() -> Self {
62 let server = MockServer::start().await;
63 let edges = Arc::new(EdgeStore::new(RuntimeHasher::default()));
64 let coverage = Arc::new(CoverageWatch::new());
65 let state = AppState::new(
66 Arc::new(LruRecordStore::new(CacheCapacity::from_bytes(64 * 1024))),
67 SlingshotClient::with_default_http(Url::parse(&server.uri()).unwrap()).unwrap(),
68 edges.clone(),
69 Arc::new(StateIndex::<IssueStateKind>::new(RuntimeHasher::default())),
70 Arc::new(StateIndex::<PullStatusKind>::new(RuntimeHasher::default())),
71 coverage.clone(),
72 Arc::new(
73 KnotProxy::new(
74 KnotProxyConfig::default(),
75 KnotHttpConfig::default(),
76 Arc::new(SystemClock::new()),
77 RuntimeHasher::default(),
78 )
79 .unwrap(),
80 ),
81 Arc::new(
82 SearchIndex::new(DEFAULT_WRITER_HEAP_BYTES, Arc::new(SystemClock::new())).unwrap(),
83 ) as Arc<dyn SearchReader>,
84 Arc::new(RepoIdResolver::detached(RuntimeHasher::default())),
85 Arc::new(bobbin_xrpc::default_directory()),
86 );
87 Self {
88 server,
89 edges,
90 state,
91 }
92 }
93
94 fn add_edge(&self, kind: &'static str, subject: SubjectRef, source: &AtUri<DefaultStr>) {
95 self.edges.add(Edge {
96 kind: nsid(kind),
97 subject,
98 source: source.clone(),
99 sort_micros: next_sort_micros(),
100 });
101 }
102
103 async fn mount(&self, did: &Did<DefaultStr>, collection: &str, rkey: &str, value: Value) {
104 let uri = format!("at://{}/{}/{}", did.as_ref(), collection, rkey);
105 let body = json!({ "uri": uri, "cid": CID, "value": value });
106 Mock::given(method("GET"))
107 .and(path("/xrpc/com.atproto.repo.getRecord"))
108 .and(query_param("repo", did.as_ref()))
109 .and(query_param("collection", collection))
110 .and(query_param("rkey", rkey))
111 .respond_with(ResponseTemplate::new(200).set_body_json(body))
112 .mount(&self.server)
113 .await;
114 }
115}
116
117fn enrich_request(body: Value) -> Request<Body> {
118 Request::builder()
119 .method("POST")
120 .uri("/xrpc/sh.tangled.query.enrichResponse")
121 .header("content-type", "application/json")
122 .body(Body::from(serde_json::to_vec(&body).unwrap()))
123 .unwrap()
124}
125
126async fn json_response(resp: axum::response::Response) -> (StatusCode, Value) {
127 let status = resp.status();
128 let bytes = to_bytes(resp.into_body(), 1 << 20).await.unwrap();
129 let parsed: Value = serde_json::from_slice(&bytes).expect("JSON body");
130 (status, parsed)
131}
132
133fn repo_body(name: &str, repo_did: &Did<DefaultStr>) -> Value {
134 json!({
135 "$type": "sh.tangled.repo",
136 "name": name,
137 "knot": "oyster.cafe",
138 "repoDid": repo_did.as_ref(),
139 "createdAt": "2026-05-01T00:00:00Z"
140 })
141}
142
143fn follow_body(subject: &Did<DefaultStr>) -> Value {
144 json!({
145 "$type": "sh.tangled.graph.follow",
146 "createdAt": "2026-05-01T00:00:00Z",
147 "subject": subject.as_ref()
148 })
149}
150
151/// one repo owned by `owner`, with `stars`/`issues` counts against its repo did
152async fn repo_fixture(h: &Harness, owner: &Did<DefaultStr>, repo_did: &Did<DefaultStr>) {
153 let repo_uri = at(&format!("at://{}/sh.tangled.repo/reef", owner.as_ref()));
154 h.add_edge("sh.tangled.repo", SubjectRef::Did(owner.clone()), &repo_uri);
155 h.mount(
156 owner,
157 "sh.tangled.repo",
158 "reef",
159 repo_body("reef", repo_did),
160 )
161 .await;
162 for (i, stargazer) in ["did:plc:a", "did:plc:b", "did:plc:a"].iter().enumerate() {
163 h.add_edge(
164 "sh.tangled.feed.star",
165 SubjectRef::Did(repo_did.clone()),
166 &at(&format!("at://{stargazer}/sh.tangled.feed.star/s{i}")),
167 );
168 }
169 h.add_edge(
170 "sh.tangled.repo.issue",
171 SubjectRef::Did(repo_did.clone()),
172 &at("at://did:plc:a/sh.tangled.repo.issue/i0"),
173 );
174}
175
176#[tokio::test]
177async fn zero_config_counts_stars_and_issues_for_repo_did() {
178 let h = Harness::new().await;
179 let owner = did("did:plc:nel");
180 let repo_did = did("did:plc:limpet");
181 repo_fixture(&h, &owner, &repo_did).await;
182
183 let app = router(h.state.clone());
184 let (status, body) = json_response(
185 app.oneshot(enrich_request(json!({
186 "xrpc": "sh.tangled.repo.listRepos",
187 "params": { "subject": owner.as_ref() },
188 "enrich": [
189 { "source": "sh.tangled.feed.star:subject", "type": COUNT },
190 { "source": "sh.tangled.feed.star:subject", "type": DISTINCT_AUTHORS },
191 { "source": "sh.tangled.repo.issue:subject", "type": COUNT }
192 ]
193 })))
194 .await
195 .unwrap(),
196 )
197 .await;
198
199 assert_eq!(status, StatusCode::OK, "{body}");
200 assert_eq!(body["output"]["items"].as_array().unwrap().len(), 1);
201 let stats = &body["data"];
202 assert_eq!(
203 stats["did:plc:limpet"]["sh.tangled.feed.star:subject"][COUNT],
204 json!(3)
205 );
206 assert_eq!(
207 stats["did:plc:limpet"]["sh.tangled.feed.star:subject"][DISTINCT_AUTHORS],
208 json!(2)
209 );
210 assert_eq!(
211 stats["did:plc:limpet"]["sh.tangled.repo.issue:subject"][COUNT],
212 json!(1)
213 );
214 assert!(stats["at://did:plc:nel/sh.tangled.repo/reef"].is_null());
215}
216
217#[tokio::test]
218async fn follow_counts_cover_both_directions() {
219 let h = Harness::new().await;
220 let owner = did("did:plc:nel");
221 // followers, edges pointing at owner
222 for (i, fan) in ["did:plc:a", "did:plc:b"].iter().enumerate() {
223 h.add_edge(
224 "sh.tangled.graph.follow",
225 SubjectRef::Did(owner.clone()),
226 &at(&format!("at://{fan}/sh.tangled.graph.follow/f{i}")),
227 );
228 h.mount(
229 &did(fan),
230 "sh.tangled.graph.follow",
231 &format!("f{i}"),
232 follow_body(&owner),
233 )
234 .await;
235 }
236 // following, via the .by mirror edge since owner is the author here
237 h.add_edge(
238 "sh.tangled.graph.follow.by",
239 SubjectRef::Did(owner.clone()),
240 &at("at://did:plc:nel/sh.tangled.graph.follow/f0"),
241 );
242
243 let app = router(h.state.clone());
244 let (status, body) = json_response(
245 app.oneshot(enrich_request(json!({
246 "xrpc": "sh.tangled.graph.listFollows",
247 "params": { "subject": owner.as_ref() },
248 "enrich": [{ "source": "sh.tangled.graph.follow:subject", "type": COUNT }, { "source": "sh.tangled.graph.follow:.repo", "type": COUNT }]
249 })))
250 .await
251 .unwrap(),
252 )
253 .await;
254
255 assert_eq!(status, StatusCode::OK, "{body}");
256 let nel = &body["data"]["did:plc:nel"];
257 assert_eq!(
258 nel["sh.tangled.graph.follow:subject"][COUNT],
259 json!(2),
260 "{body}"
261 );
262 assert_eq!(
263 nel["sh.tangled.graph.follow:.repo"][COUNT],
264 json!(1),
265 "{body}"
266 );
267}
268
269// at-uri authorities join the ref set, so record authors get stats keyed by
270// their bare did without appearing as a value anywhere in the response
271#[tokio::test]
272async fn authorities_of_record_uris_become_refs() {
273 let h = Harness::new().await;
274 let owner = did("did:plc:nel");
275 for (i, fan) in ["did:plc:a", "did:plc:b"].iter().enumerate() {
276 h.add_edge(
277 "sh.tangled.graph.follow",
278 SubjectRef::Did(owner.clone()),
279 &at(&format!("at://{fan}/sh.tangled.graph.follow/f{i}")),
280 );
281 h.mount(
282 &did(fan),
283 "sh.tangled.graph.follow",
284 &format!("f{i}"),
285 follow_body(&owner),
286 )
287 .await;
288 }
289 // each fan also follows one other person
290 for (i, fan) in ["did:plc:a", "did:plc:b"].iter().enumerate() {
291 h.add_edge(
292 "sh.tangled.graph.follow.by",
293 SubjectRef::Did(did(fan)),
294 &at(&format!("at://{fan}/sh.tangled.graph.follow/g{i}")),
295 );
296 }
297
298 let app = router(h.state.clone());
299 let (status, body) = json_response(
300 app.oneshot(enrich_request(json!({
301 "xrpc": "sh.tangled.graph.listFollows",
302 "params": { "subject": owner.as_ref() },
303 "enrich": [
304 { "source": "sh.tangled.graph.follow:subject", "type": COUNT },
305 { "source": "sh.tangled.graph.follow:.repo", "type": COUNT }
306 ]
307 })))
308 .await
309 .unwrap(),
310 )
311 .await;
312
313 assert_eq!(status, StatusCode::OK, "{body}");
314 for fan in ["did:plc:a", "did:plc:b"] {
315 let entry = &body["data"][fan];
316 assert_eq!(
317 entry["sh.tangled.graph.follow:subject"][COUNT],
318 json!(0),
319 "{body}"
320 );
321 assert_eq!(
322 entry["sh.tangled.graph.follow:.repo"][COUNT],
323 json!(1),
324 "{body}"
325 );
326 }
327}
328
329#[tokio::test]
330async fn targets_scope_each_payload_independently() {
331 let h = Harness::new().await;
332 let owner = did("did:plc:nel");
333 let repo_did = did("did:plc:limpet");
334 repo_fixture(&h, &owner, &repo_did).await;
335
336 let app = router(h.state.clone());
337 let (status, body) = json_response(
338 app.oneshot(enrich_request(json!({
339 "xrpc": "sh.tangled.repo.listRepos",
340 "params": { "subject": owner.as_ref() },
341 "enrich": [
342 {
343 "source": "sh.tangled.feed.star:subject",
344 "type": COUNT,
345 "targets": ["items[].value.repoDid"]
346 },
347 {
348 "source": "sh.tangled.repo.issue:subject",
349 "type": COUNT,
350 "targets": ["items[].uri"]
351 }
352 ]
353 })))
354 .await
355 .unwrap(),
356 )
357 .await;
358 assert_eq!(status, StatusCode::OK, "{body}");
359 let stats = &body["data"];
360 assert_eq!(
361 stats["did:plc:limpet"]["sh.tangled.feed.star:subject"][COUNT],
362 json!(3)
363 );
364 assert_eq!(stats.as_object().unwrap().len(), 2, "{body}");
365 assert_eq!(
366 stats[owner.as_str()]["sh.tangled.repo.issue:subject"][COUNT],
367 json!(0)
368 );
369 assert!(
370 stats[repo_did.as_str()]["sh.tangled.repo.issue:subject"].is_null(),
371 "{body}"
372 );
373 assert!(
374 stats[owner.as_str()]["sh.tangled.feed.star:subject"].is_null(),
375 "{body}"
376 );
377
378 // a path matching nothing is empty stats, not an error, since selection is vector-matched
379 let app = router(h.state.clone());
380 let (status, body) = json_response(
381 app.oneshot(enrich_request(json!({
382 "xrpc": "sh.tangled.repo.listRepos",
383 "params": { "subject": owner.as_ref() },
384 "enrich": [{
385 "source": "sh.tangled.feed.star:subject",
386 "type": COUNT,
387 "targets": ["items[].value.nope"]
388 }]
389 })))
390 .await
391 .unwrap(),
392 )
393 .await;
394 assert_eq!(status, StatusCode::OK, "{body}");
395 assert_eq!(body["data"], json!({}));
396}
397
398#[tokio::test]
399async fn inner_record_miss_passes_through_as_404() {
400 let h = Harness::new().await;
401 let app = router(h.state.clone());
402 let (status, body) = json_response(
403 app.oneshot(enrich_request(json!({
404 "xrpc": "sh.tangled.repo.getRepo",
405 "params": { "repo": "at://did:plc:nel/sh.tangled.repo/absent" },
406 "enrich": [{ "source": "sh.tangled.feed.star:subject", "type": COUNT }]
407 })))
408 .await
409 .unwrap(),
410 )
411 .await;
412 assert_eq!(status, StatusCode::NOT_FOUND, "{body}");
413 assert_eq!(body["error"], json!("RecordNotFound"));
414}
415
416#[tokio::test]
417async fn rejects_bad_requests() {
418 let h = Harness::new().await;
419 // semantic rejections: the descriptor parses, the handler refuses it
420 let handler_cases = [
421 json!({ "xrpc": "sh.tangled.nope.nope", "enrich": [] }),
422 json!({
423 "xrpc": "sh.tangled.repo.countRepos",
424 "params": { "subject": "did:plc:nel" },
425 "enrich": [{ "source": "sh.tangled.nope:subject", "type": COUNT }]
426 }),
427 json!({
428 "xrpc": "sh.tangled.repo.countRepos",
429 "params": { "subject": "did:plc:nel" },
430 "enrich": [{ "source": "sh.tangled.feed.star:subject", "type": "sh.tangled.query.enrichResponse#bogus" }]
431 }),
432 json!({
433 "xrpc": "sh.tangled.repo.countRepos",
434 "params": { "subject": "did:plc:nel" },
435 "enrich": [{
436 "source": "sh.tangled.feed.star:subject",
437 "type": COUNT,
438 "targets": ["items["]
439 }]
440 }),
441 ];
442 for case in handler_cases {
443 let app = router(h.state.clone());
444 let (status, body) = json_response(app.oneshot(enrich_request(case)).await.unwrap()).await;
445 assert_eq!(status, StatusCode::BAD_REQUEST, "{body}");
446 assert_eq!(body["error"], json!("InvalidRequest"), "{body}");
447 }
448
449 // structural rejections: serde refuses the descriptor before the handler
450 // sees it, which is plain-text 422 rather than our 400 json body
451 let serde_cases = [
452 json!({
453 "xrpc": "sh.tangled.repo.countRepos",
454 "params": { "subject": "did:plc:nel" },
455 "enrich": [{ "source": "sh.tangled.feed.star:subject" }]
456 }),
457 json!({
458 "xrpc": "sh.tangled.repo.countRepos",
459 "params": { "subject": "did:plc:nel" },
460 "enrich": [{ "source": "sh.tangled.feed.star", "type": COUNT }]
461 }),
462 json!({
463 "xrpc": "sh.tangled.repo.countRepos",
464 "params": { "subject": "did:plc:nel" },
465 "enrich": [{ "source": "sh.tangled.feed.star:.rkey", "type": COUNT }]
466 }),
467 json!({
468 "xrpc": "sh.tangled.repo.countRepos",
469 "params": { "subject": "did:plc:nel" },
470 "enrich": [{ "source": "sh.tangled.feed.star:subject.uri", "type": COUNT }]
471 }),
472 ];
473 for case in serde_cases {
474 let app = router(h.state.clone());
475 let response = app.oneshot(enrich_request(case)).await.unwrap();
476 assert_eq!(response.status(), StatusCode::UNPROCESSABLE_ENTITY);
477 }
478}
479
480#[tokio::test]
481async fn viewer_aggregation_uses_explicit_viewer_param() {
482 let h = Harness::new().await;
483 let owner = did("did:plc:abc");
484 let repo_did = did("did:plc:limpet");
485 repo_fixture(&h, &owner, &repo_did).await;
486
487 // the viewer already starred this repo, for the checks below
488 let subject = SubjectRef::Did(repo_did.clone());
489 h.state.edges.add(Edge {
490 kind: nsid("sh.tangled.feed.star"),
491 subject,
492 source: at("at://did:plc:nel/sh.tangled.feed.star/r99"),
493 sort_micros: 99,
494 });
495
496 let app = router(h.state.clone());
497
498 // missing viewer param is a 400, viewer descriptors require it
499 let no_viewer = json!({
500 "xrpc": "sh.tangled.repo.listRepos",
501 "params": { "subject": owner.as_ref() },
502 "enrich": [{ "source": "sh.tangled.feed.star:subject", "type": VIEWER }]
503 });
504 let (status, _) = json_response(
505 app.clone()
506 .oneshot(enrich_request(no_viewer))
507 .await
508 .unwrap(),
509 )
510 .await;
511 assert_eq!(status, StatusCode::BAD_REQUEST);
512
513 // viewer who starred it gets their own star uri back
514 let starred_viewer = json!({
515 "xrpc": "sh.tangled.repo.listRepos",
516 "params": { "subject": owner.as_ref() },
517 "enrich": [{ "source": "sh.tangled.feed.star:subject", "type": VIEWER }],
518 "viewer": "did:plc:nel"
519 });
520 let (status, resp) = json_response(
521 app.clone()
522 .oneshot(enrich_request(starred_viewer))
523 .await
524 .unwrap(),
525 )
526 .await;
527 assert_eq!(status, StatusCode::OK);
528 let stats = &resp["data"][repo_did.as_str()]["sh.tangled.feed.star:subject"];
529 assert_eq!(
530 stats[VIEWER],
531 json!("at://did:plc:nel/sh.tangled.feed.star/r99")
532 );
533
534 // a viewer who never starred it gets an explicit null, not absent
535 let other_viewer = json!({
536 "xrpc": "sh.tangled.repo.listRepos",
537 "params": { "subject": owner.as_ref() },
538 "enrich": [{ "source": "sh.tangled.feed.star:subject", "type": VIEWER }],
539 "viewer": "did:plc:someoneelse"
540 });
541 let (status, resp) =
542 json_response(app.oneshot(enrich_request(other_viewer)).await.unwrap()).await;
543 assert_eq!(status, StatusCode::OK);
544 let stats = &resp["data"][repo_did.as_str()]["sh.tangled.feed.star:subject"];
545 assert_eq!(stats[VIEWER], Value::Null);
546}
547
548#[tokio::test]
549async fn minidoc_payloads_use_observed_record_authors() {
550 let h = Harness::new().await;
551 let owner = did("did:plc:nel");
552 for (i, fan) in ["did:plc:a", "did:plc:b"].iter().enumerate() {
553 h.add_edge(
554 "sh.tangled.graph.follow",
555 SubjectRef::Did(owner.clone()),
556 &at(&format!("at://{fan}/sh.tangled.graph.follow/f{i}")),
557 );
558 h.mount(
559 &did(fan),
560 "sh.tangled.graph.follow",
561 &format!("f{i}"),
562 follow_body(&owner),
563 )
564 .await;
565 }
566 h.state
567 .identity
568 .observe(did("did:plc:a"), handle("a.example.com"));
569
570 let app = router(h.state.clone());
571 let (status, body) = json_response(
572 app.oneshot(enrich_request(json!({
573 "xrpc": "sh.tangled.graph.listFollows",
574 "params": { "subject": owner.as_ref() },
575 "enrich": [
576 { "source": "sh.tangled.graph.follow:.repo", "type": MINIDOC },
577 { "source": "sh.tangled.graph.follow:.repo", "type": MINIDOC },
578 { "source": "sh.tangled.feed.star:.repo", "type": MINIDOC }
579 ]
580 })))
581 .await
582 .unwrap(),
583 )
584 .await;
585
586 assert_eq!(status, StatusCode::OK, "{body}");
587 assert_eq!(
588 body["data"]["did:plc:a"]["sh.tangled.graph.follow:.repo"][MINIDOC]["handle"],
589 json!("a.example.com")
590 );
591 assert_eq!(
592 body["data"]["did:plc:a"]["sh.tangled.feed.star:.repo"][MINIDOC]["handle"],
593 json!("a.example.com")
594 );
595 // missing observations are dropped without waiting on Slingshot
596 assert!(body["data"]["did:plc:b"].is_null(), "{body}");
597 // the profile owner authored nothing here, so it earns no minidoc
598 assert!(body["data"]["did:plc:nel"].is_null(), "{body}");
599 assert_eq!(h.state.identity.stats().upstream_requests, 0);
600}
601
602#[tokio::test]
603async fn minidoc_repo_sources_skip_the_author_index() {
604 let h = Harness::new().await;
605 let owner = did("did:plc:nel");
606 let repo_did = did("did:plc:limpet");
607 repo_fixture(&h, &owner, &repo_did).await;
608 h.state
609 .identity
610 .observe(owner.clone(), handle("nel.example.com"));
611
612 let app = router(h.state.clone());
613 // sh.tangled.repo has no author mirror; stats would 400, minidocs must not
614 let (status, body) = json_response(
615 app.oneshot(enrich_request(json!({
616 "xrpc": "sh.tangled.repo.listRepos",
617 "params": { "subject": owner.as_ref() },
618 "enrich": [{ "source": "sh.tangled.repo:.repo", "type": MINIDOC }]
619 })))
620 .await
621 .unwrap(),
622 )
623 .await;
624
625 assert_eq!(status, StatusCode::OK, "{body}");
626 assert_eq!(
627 body["data"]["did:plc:nel"]["sh.tangled.repo:.repo"][MINIDOC]["handle"],
628 json!("nel.example.com")
629 );
630
631 let app = router(h.state.clone());
632 let (status, body) = json_response(
633 app.oneshot(enrich_request(json!({
634 "xrpc": "sh.tangled.repo.listRepos",
635 "params": { "subject": owner.as_ref() },
636 "enrich": [{ "source": "sh.tangled.repo:.repo", "type": COUNT }]
637 })))
638 .await
639 .unwrap(),
640 )
641 .await;
642 assert_eq!(status, StatusCode::BAD_REQUEST, "{body}");
643}