@@ -40,6 +40,56 @@ impl std::fmt::Debug for HttpClient {
4040 }
4141}
4242
43+ /// Largest response body any hosted engine may return.
44+ ///
45+ /// The endpoint is operator-supplied (`SupermemoryMemory::api`,
46+ /// `Mem0Memory::new`, `CogneeMemory::self_hosted` all take an arbitrary URL),
47+ /// so a broken or hostile server must not be able to exhaust the host's
48+ /// memory. 64 MiB is far above any real memory payload -- the largest thing
49+ /// these APIs return is a page of records -- and far below a size that
50+ /// threatens a process.
51+ const MAX_RESPONSE_BYTES : u64 = 64 * 1024 * 1024 ;
52+
53+ /// Read a response body, failing once it exceeds [`MAX_RESPONSE_BYTES`].
54+ ///
55+ /// `Response::json()`/`text()` buffer the whole body before any size check, so
56+ /// a server that omits or understates `Content-Length` (a chunked response,
57+ /// say) could OOM the process despite a declared limit. Reading incrementally
58+ /// enforces the cap while the bytes arrive. Same argument, and same shape, as
59+ /// `tinymemory-sources`' `read_body_capped` -- that guard was written for the
60+ /// web-page reader and simply had not been applied on this path.
61+ async fn read_capped ( response : reqwest:: Response , path : & str ) -> anyhow:: Result < Vec < u8 > > {
62+ use futures:: StreamExt ;
63+ if let Some ( len) = response. content_length ( ) {
64+ if len > MAX_RESPONSE_BYTES {
65+ anyhow:: bail!(
66+ "memory API {path} response exceeds {MAX_RESPONSE_BYTES}-byte limit \
67+ (Content-Length={len})"
68+ ) ;
69+ }
70+ }
71+ let mut body = Vec :: new ( ) ;
72+ let mut stream = response. bytes_stream ( ) ;
73+ while let Some ( chunk) = stream. next ( ) . await {
74+ let chunk = chunk. with_context ( || format ! ( "memory API {path} body read failed" ) ) ?;
75+ // Check BEFORE appending: one oversized chunk would otherwise be
76+ // allocated in full before the limit is noticed, which is the
77+ // allocation this cap exists to prevent.
78+ let next_len = body
79+ . len ( )
80+ . checked_add ( chunk. len ( ) )
81+ . context ( "memory API response length overflowed" ) ?;
82+ if next_len as u64 > MAX_RESPONSE_BYTES {
83+ anyhow:: bail!(
84+ "memory API {path} response exceeds {MAX_RESPONSE_BYTES}-byte limit \
85+ (would reach {next_len} bytes)"
86+ ) ;
87+ }
88+ body. extend_from_slice ( & chunk) ;
89+ }
90+ Ok ( body)
91+ }
92+
4393impl HttpClient {
4494 /// Builds a client that optionally authenticates with a bearer token.
4595 pub ( crate ) fn bearer ( endpoint : & str , credential : Option < & str > ) -> anyhow:: Result < Self > {
@@ -168,9 +218,8 @@ impl HttpClient {
168218 if !status. is_success ( ) {
169219 return Err ( self . status_error ( path, status) ) ;
170220 }
171- response
172- . json ( )
173- . await
221+ let body = read_capped ( response, path) . await ?;
222+ serde_json:: from_slice ( & body)
174223 . with_context ( || format ! ( "memory API {path} returned invalid JSON" ) )
175224 }
176225
@@ -185,10 +234,8 @@ impl HttpClient {
185234 if !status. is_success ( ) {
186235 return Err ( self . status_error ( path, status) ) ;
187236 }
188- response
189- . text ( )
190- . await
191- . context ( "memory API response was unreadable" )
237+ let body = read_capped ( response, path) . await ?;
238+ String :: from_utf8 ( body) . context ( "memory API response was not valid UTF-8" )
192239 }
193240
194241 /// Sends a request whose successful response body is not needed.
0 commit comments