mirror of
https://github.com/LemmyNet/lemmy.git
synced 2024-11-22 12:21:18 +00:00
Avoid stack overflow when fetching nested comments, reduce max comment depth to 50 (#5009)
* Avoid stack overflow when fetching deeply nested comments * add test case * reduce comment depth, add docs * decrease * reduce max comment depth to 50 * fmt * clippy * cleanup
This commit is contained in:
parent
9b5a9ee7be
commit
1ba848f99d
5 changed files with 40 additions and 21 deletions
14
Cargo.lock
generated
14
Cargo.lock
generated
|
@ -1072,20 +1072,6 @@ dependencies = [
|
||||||
"url",
|
"url",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "clearurls"
|
|
||||||
version = "0.0.4"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "e291c00af89ac0a5b400d9ba46a682e38015ae3cd8926dbbe85b3b864d550be3"
|
|
||||||
dependencies = [
|
|
||||||
"linkify",
|
|
||||||
"percent-encoding",
|
|
||||||
"regex",
|
|
||||||
"serde",
|
|
||||||
"serde_json",
|
|
||||||
"url",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clokwerk"
|
name = "clokwerk"
|
||||||
version = "0.4.0"
|
version = "0.4.0"
|
||||||
|
|
|
@ -858,3 +858,26 @@ test("Dont send a comment reply to a blocked community", async () => {
|
||||||
blockRes = await blockCommunity(beta, newCommunityId, false);
|
blockRes = await blockCommunity(beta, newCommunityId, false);
|
||||||
expect(blockRes.blocked).toBe(false);
|
expect(blockRes.blocked).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/// Fetching a deeply nested comment can lead to stack overflow as all parent comments are also
|
||||||
|
/// fetched recursively. Ensure that it works properly.
|
||||||
|
test("Fetch a deeply nested comment", async () => {
|
||||||
|
let lastComment;
|
||||||
|
for (let i = 0; i < 50; i++) {
|
||||||
|
let commentRes = await createComment(
|
||||||
|
alpha,
|
||||||
|
postOnAlphaRes.post_view.post.id,
|
||||||
|
lastComment?.comment_view.comment.id,
|
||||||
|
);
|
||||||
|
expect(commentRes.comment_view.comment).toBeDefined();
|
||||||
|
lastComment = commentRes;
|
||||||
|
}
|
||||||
|
|
||||||
|
let betaComment = await resolveComment(
|
||||||
|
beta,
|
||||||
|
lastComment!.comment_view.comment,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(betaComment!.comment!.comment).toBeDefined();
|
||||||
|
expect(betaComment?.comment?.post).toBeDefined();
|
||||||
|
});
|
||||||
|
|
|
@ -30,10 +30,9 @@ use lemmy_db_views::structs::{LocalUserView, PostView};
|
||||||
use lemmy_utils::{
|
use lemmy_utils::{
|
||||||
error::{LemmyErrorExt, LemmyErrorType, LemmyResult},
|
error::{LemmyErrorExt, LemmyErrorType, LemmyResult},
|
||||||
utils::{mention::scrape_text_for_mentions, validation::is_valid_body_field},
|
utils::{mention::scrape_text_for_mentions, validation::is_valid_body_field},
|
||||||
|
MAX_COMMENT_DEPTH_LIMIT,
|
||||||
};
|
};
|
||||||
|
|
||||||
const MAX_COMMENT_DEPTH_LIMIT: usize = 100;
|
|
||||||
|
|
||||||
#[tracing::instrument(skip(context))]
|
#[tracing::instrument(skip(context))]
|
||||||
pub async fn create_comment(
|
pub async fn create_comment(
|
||||||
data: Json<CreateComment>,
|
data: Json<CreateComment>,
|
||||||
|
|
|
@ -20,10 +20,9 @@ use lemmy_db_schema::{
|
||||||
source::{community::Community, post::Post},
|
source::{community::Community, post::Post},
|
||||||
traits::Crud,
|
traits::Crud,
|
||||||
};
|
};
|
||||||
use lemmy_utils::{error::LemmyResult, LemmyErrorType};
|
use lemmy_utils::{error::LemmyResult, LemmyErrorType, MAX_COMMENT_DEPTH_LIMIT};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_with::skip_serializing_none;
|
use serde_with::skip_serializing_none;
|
||||||
use std::ops::Deref;
|
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
#[skip_serializing_none]
|
#[skip_serializing_none]
|
||||||
|
@ -58,9 +57,19 @@ impl Note {
|
||||||
&self,
|
&self,
|
||||||
context: &Data<LemmyContext>,
|
context: &Data<LemmyContext>,
|
||||||
) -> LemmyResult<(ApubPost, Option<ApubComment>)> {
|
) -> LemmyResult<(ApubPost, Option<ApubComment>)> {
|
||||||
// Fetch parent comment chain in a box, otherwise it can cause a stack overflow.
|
// We use recursion here to fetch the entire comment chain up to the top-level parent. This is
|
||||||
let parent = Box::pin(self.in_reply_to.dereference(context).await?);
|
// necessary because we need to know the post and parent comment in order to insert a new
|
||||||
match parent.deref() {
|
// comment. However it can also lead to stack overflow when fetching many comments recursively.
|
||||||
|
// To avoid this we check the request count against max comment depth, which based on testing
|
||||||
|
// can be handled without risking stack overflow. This is not a perfect solution, because in
|
||||||
|
// some cases we have to fetch user profiles too, and reach the limit after only 25 comments
|
||||||
|
// or so.
|
||||||
|
// A cleaner solution would be converting the recursion into a loop, but that is tricky.
|
||||||
|
if context.request_count() > MAX_COMMENT_DEPTH_LIMIT as u32 {
|
||||||
|
Err(LemmyErrorType::MaxCommentDepthReached)?;
|
||||||
|
}
|
||||||
|
let parent = self.in_reply_to.dereference(context).await?;
|
||||||
|
match parent {
|
||||||
PostOrComment::Post(p) => Ok((p.clone(), None)),
|
PostOrComment::Post(p) => Ok((p.clone(), None)),
|
||||||
PostOrComment::Comment(c) => {
|
PostOrComment::Comment(c) => {
|
||||||
let post_id = c.post_id;
|
let post_id = c.post_id;
|
||||||
|
|
|
@ -29,6 +29,8 @@ pub const CACHE_DURATION_FEDERATION: Duration = Duration::from_secs(60);
|
||||||
|
|
||||||
pub const CACHE_DURATION_API: Duration = Duration::from_secs(1);
|
pub const CACHE_DURATION_API: Duration = Duration::from_secs(1);
|
||||||
|
|
||||||
|
pub const MAX_COMMENT_DEPTH_LIMIT: usize = 50;
|
||||||
|
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! location_info {
|
macro_rules! location_info {
|
||||||
() => {
|
() => {
|
||||||
|
|
Loading…
Reference in a new issue