Skip to content

Commit eef4fe8

Browse files
Merge pull request #460 from long2mao1/fix/serde-json-recursion-limit
fix(client): bypass serde_json's 128-level recursion limit
2 parents 25d33a3 + d56480a commit eef4fe8

3 files changed

Lines changed: 73 additions & 6 deletions

File tree

Cargo.lock

Lines changed: 53 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,8 @@ tokio = { version = "1", default-features = false }
7777

7878
# Serialization
7979
serde = { version = "1", features = ["derive"] }
80-
serde_json = { version = "1", features = ["preserve_order"] }
80+
serde_json = { version = "1", features = ["preserve_order", "unbounded_depth"] }
81+
serde_stacker = "0.1"
8182
serde_norway = "0.9"
8283

8384
# HTTP — stream feature omitted on wasm32 (wasm-streams pulls in wasm-bindgen,

src/client.rs

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -427,6 +427,19 @@ static UNSTABLE_OPS: &[&str] = &[
427427

428428
use crate::useragent;
429429

430+
// Parse a reqwest response body as JSON without serde_json's default 128-level
431+
// recursion cap. Some Datadog endpoints (e.g. /profiling/api/v1/aggregate)
432+
// return deeply-nested flame-graph trees that exceed it. serde_stacker grows
433+
// the thread stack on demand so disabling the limit can't blow it.
434+
async fn parse_response_json(resp: reqwest::Response) -> anyhow::Result<serde_json::Value> {
435+
use serde::Deserialize;
436+
let bytes = resp.bytes().await?;
437+
let mut de = serde_json::Deserializer::from_slice(&bytes);
438+
de.disable_recursion_limit();
439+
let de = serde_stacker::Deserializer::new(&mut de);
440+
Ok(serde_json::Value::deserialize(de)?)
441+
}
442+
430443
#[allow(dead_code)]
431444
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
432445
pub enum AuthType {
@@ -838,7 +851,7 @@ pub async fn raw_get(
838851
}
839852
.into());
840853
}
841-
Ok(resp.json().await?)
854+
parse_response_json(resp).await
842855
}
843856

844857
/// Makes an authenticated PATCH request directly via reqwest.
@@ -873,7 +886,7 @@ pub async fn raw_patch(
873886
}
874887
.into());
875888
}
876-
Ok(resp.json().await?)
889+
parse_response_json(resp).await
877890
}
878891

879892
/// Makes an authenticated POST request directly via reqwest.
@@ -928,7 +941,7 @@ async fn raw_post_impl(
928941
}
929942
.into());
930943
}
931-
Ok(resp.json().await?)
944+
parse_response_json(resp).await
932945
}
933946

934947
fn apply_auth(
@@ -993,7 +1006,7 @@ pub async fn raw_post_jsonapi(
9931006
let body = resp.text().await.unwrap_or_default();
9941007
anyhow::bail!("POST {url} failed (HTTP {status}): {body}");
9951008
}
996-
Ok(resp.json().await?)
1009+
parse_response_json(resp).await
9971010
}
9981011

9991012
/// Like `raw_post`, but returns the parsed JSON body even on non-2xx responses.
@@ -1024,7 +1037,7 @@ pub async fn raw_post_lenient(
10241037
.json(&body)
10251038
.send()
10261039
.await?;
1027-
Ok(resp.json().await?)
1040+
parse_response_json(resp).await
10281041
}
10291042

10301043
/// Makes an authenticated DELETE request directly via reqwest.

0 commit comments

Comments
 (0)