@@ -3,6 +3,7 @@ type JsonObject = Record<string, unknown>;
33type TelegramBotApiOptions = {
44 baseUrl ?: string ;
55 fetchImpl ?: ( url : string , init : RequestInit ) => Promise < Response > ;
6+ maxBodyBytes ?: number ;
67 timeoutMs ?: number ;
78} ;
89
@@ -12,6 +13,10 @@ const DEFAULT_TIMEOUT_MS = readPositiveInt(
1213 process . env . OPENCLAW_TELEGRAM_USER_BOT_API_TIMEOUT_MS ,
1314 30000 ,
1415) ;
16+ const DEFAULT_BODY_MAX_BYTES = readPositiveInt (
17+ process . env . OPENCLAW_TELEGRAM_USER_BOT_API_BODY_MAX_BYTES ,
18+ 1024 * 1024 ,
19+ ) ;
1520
1621function readPositiveInt ( raw : string | undefined , fallback : number ) {
1722 const parsed = Number . parseInt ( raw ?? "" , 10 ) ;
@@ -23,6 +28,58 @@ function optionalString(source: JsonObject, key: string) {
2328 return typeof value === "string" && value . trim ( ) ? value . trim ( ) : undefined ;
2429}
2530
31+ function taggedError ( message : string , code : string ) {
32+ return Object . assign ( new Error ( message ) , { code } ) ;
33+ }
34+
35+ async function readBoundedResponseText (
36+ response : Response ,
37+ label : string ,
38+ byteLimit : number ,
39+ timeoutPromise : Promise < never > ,
40+ ) {
41+ const contentLength = response . headers . get ( "content-length" ) ;
42+ if ( contentLength ) {
43+ const parsedLength = Number ( contentLength ) ;
44+ if ( Number . isSafeInteger ( parsedLength ) && parsedLength > byteLimit ) {
45+ await response . body ?. cancel ( ) . catch ( ( ) => { } ) ;
46+ throw taggedError ( `${ label } response body exceeded ${ byteLimit } bytes` , "ETOOBIG" ) ;
47+ }
48+ }
49+ if ( ! response . body ) {
50+ return "" ;
51+ }
52+
53+ const reader = response . body . getReader ( ) ;
54+ const decoder = new TextDecoder ( ) ;
55+ let byteCount = 0 ;
56+ let text = "" ;
57+ try {
58+ while ( true ) {
59+ const { done, value } = await Promise . race ( [ reader . read ( ) , timeoutPromise ] ) ;
60+ if ( done ) {
61+ return text + decoder . decode ( ) ;
62+ }
63+ byteCount += value . byteLength ;
64+ if ( byteCount > byteLimit ) {
65+ await reader . cancel ( ) . catch ( ( ) => { } ) ;
66+ throw taggedError ( `${ label } response body exceeded ${ byteLimit } bytes` , "ETOOBIG" ) ;
67+ }
68+ text += decoder . decode ( value , { stream : true } ) ;
69+ }
70+ } finally {
71+ reader . releaseLock ( ) ;
72+ }
73+ }
74+
75+ function parseJsonPayload ( rawPayload : string , label : string ) {
76+ try {
77+ return JSON . parse ( rawPayload ) as JsonObject ;
78+ } catch ( error ) {
79+ throw new Error ( `${ label } returned invalid JSON` , { cause : error } ) ;
80+ }
81+ }
82+
2683export async function telegramBotApi (
2784 token : string ,
2885 method : string ,
@@ -31,10 +88,9 @@ export async function telegramBotApi(
3188) {
3289 const baseUrl = options . baseUrl ?? DEFAULT_BASE_URL ;
3390 const timeoutMs = Math . max ( 1 , options . timeoutMs ?? DEFAULT_TIMEOUT_MS ) ;
34- const timeoutError = Object . assign (
35- new Error ( `Telegram Bot API ${ method } timed out after ${ timeoutMs } ms` ) ,
36- { code : "ETIMEDOUT" } ,
37- ) ;
91+ const maxBodyBytes = Math . max ( 1 , options . maxBodyBytes ?? DEFAULT_BODY_MAX_BYTES ) ;
92+ const label = `Telegram Bot API ${ method } ` ;
93+ const timeoutError = taggedError ( `${ label } timed out after ${ timeoutMs } ms` , "ETIMEDOUT" ) ;
3894 const controller = new AbortController ( ) ;
3995 let timeout : NodeJS . Timeout | undefined ;
4096 const timeoutPromise = new Promise < never > ( ( _ , reject ) => {
@@ -55,7 +111,8 @@ export async function telegramBotApi(
55111 } ) ,
56112 timeoutPromise ,
57113 ] ) ;
58- const payload = ( await Promise . race ( [ response . json ( ) , timeoutPromise ] ) ) as JsonObject ;
114+ const rawPayload = await readBoundedResponseText ( response , label , maxBodyBytes , timeoutPromise ) ;
115+ const payload = parseJsonPayload ( rawPayload , label ) ;
59116 if ( ! response . ok || payload . ok !== true ) {
60117 throw new Error (
61118 optionalString ( payload , "description" ) ?? `${ method } failed with HTTP ${ response . status } ` ,
0 commit comments