@@ -13,6 +13,7 @@ const controlTimeoutMs = readPositiveInt(
1313 Math . min ( fetchTimeoutMs , 30000 ) ,
1414) ;
1515const chatTimeoutMs = readPositiveInt ( "OPENWEBUI_CHAT_TIMEOUT_MS" , fetchTimeoutMs ) ;
16+ const responseBodyMaxBytes = readPositiveInt ( "OPENWEBUI_RESPONSE_BODY_MAX_BYTES" , 1024 * 1024 ) ;
1617const smokeMode =
1718 process . env . OPENWEBUI_SMOKE_MODE ?? process . env . OPENCLAW_OPENWEBUI_SMOKE_MODE ?? "chat" ;
1819
@@ -68,6 +69,12 @@ function createTimeoutError(label, timeoutMs) {
6869 return error ;
6970}
7071
72+ function createBodyTooLargeError ( label , byteLimit ) {
73+ const error = new Error ( `${ label } response body exceeded ${ byteLimit } bytes` ) ;
74+ error . code = "ETOOBIG" ;
75+ return error ;
76+ }
77+
7178async function withRequestTimeout ( label , timeoutMs , run ) {
7279 const controller = new AbortController ( ) ;
7380 const timeoutError = createTimeoutError ( label , timeoutMs ) ;
@@ -87,6 +94,51 @@ async function withRequestTimeout(label, timeoutMs, run) {
8794 }
8895}
8996
97+ async function readBoundedResponseText ( response , label , byteLimit = responseBodyMaxBytes ) {
98+ const contentLength = response . headers . get ( "content-length" ) ;
99+ if ( contentLength ) {
100+ const parsedLength = Number ( contentLength ) ;
101+ if ( Number . isSafeInteger ( parsedLength ) && parsedLength > byteLimit ) {
102+ await response . body ?. cancel ( ) . catch ( ( ) => { } ) ;
103+ throw createBodyTooLargeError ( label , byteLimit ) ;
104+ }
105+ }
106+ if ( ! response . body ) {
107+ return "" ;
108+ }
109+
110+ const reader = response . body . getReader ( ) ;
111+ const decoder = new TextDecoder ( ) ;
112+ let byteCount = 0 ;
113+ let text = "" ;
114+ try {
115+ while ( true ) {
116+ const { done, value } = await reader . read ( ) ;
117+ if ( done ) {
118+ return text + decoder . decode ( ) ;
119+ }
120+ byteCount += value . byteLength ;
121+ if ( byteCount > byteLimit ) {
122+ await reader . cancel ( ) . catch ( ( ) => { } ) ;
123+ throw createBodyTooLargeError ( label , byteLimit ) ;
124+ }
125+ text += decoder . decode ( value , { stream : true } ) ;
126+ }
127+ } finally {
128+ reader . releaseLock ( ) ;
129+ }
130+ }
131+
132+ async function readBoundedResponseJson ( response , label ) {
133+ const body = await readBoundedResponseText ( response , label ) ;
134+ try {
135+ return JSON . parse ( body ) ;
136+ } catch ( error ) {
137+ const message = error instanceof Error ? error . message : String ( error ) ;
138+ throw new Error ( `${ label } returned invalid JSON: ${ message } ` , { cause : error } ) ;
139+ }
140+ }
141+
90142function getCookieHeader ( res ) {
91143 const raw = res . headers . get ( "set-cookie" ) ;
92144 if ( ! raw ) {
@@ -125,12 +177,12 @@ async function fetchSignin() {
125177 signal,
126178 } ) ;
127179 if ( ! response . ok ) {
128- const body = await response . text ( ) ;
180+ const body = await readBoundedResponseText ( response , "Open WebUI signin" ) ;
129181 throw new Error ( `signin failed: HTTP ${ response . status } ${ body } ` ) ;
130182 }
131183 return {
132184 cookie : getCookieHeader ( response ) ,
133- json : await response . json ( ) ,
185+ json : await readBoundedResponseJson ( response , "Open WebUI signin" ) ,
134186 } ;
135187 } ) ;
136188}
@@ -145,11 +197,14 @@ async function fetchModels(authHeaders, attempt) {
145197 return {
146198 ok : false ,
147199 status : response . status ,
148- text : await response . text ( ) ,
200+ text : await readBoundedResponseText (
201+ response ,
202+ `Open WebUI models attempt ${ attempt } ` ,
203+ ) ,
149204 } ;
150205 }
151206 return {
152- json : await response . json ( ) ,
207+ json : await readBoundedResponseJson ( response , `Open WebUI models attempt ${ attempt } ` ) ,
153208 ok : true ,
154209 } ;
155210 } ,
@@ -171,11 +226,12 @@ async function fetchChatCompletion(authHeaders, targetModel) {
171226 signal,
172227 } ) ;
173228 if ( ! response . ok ) {
229+ const body = await readBoundedResponseText ( response , "Open WebUI chat completion" ) ;
174230 throw new Error (
175- `/api/chat/completions failed: HTTP ${ response . status } ${ await response . text ( ) } ` ,
231+ `/api/chat/completions failed: HTTP ${ response . status } ${ body } ` ,
176232 ) ;
177233 }
178- return await response . json ( ) ;
234+ return await readBoundedResponseJson ( response , "Open WebUI chat completion" ) ;
179235 } ) ;
180236}
181237
0 commit comments