@@ -8,6 +8,7 @@ export const PREVIEW_MAX_SEARXNG_RESPONSE_BYTES = 64 * 1024;
88
99const INVALID_RESPONSE_BODY_MESSAGE = "Invalid SearXNG response body" ;
1010const RESPONSE_TOO_LARGE_MESSAGE = "SearXNG response exceeds configured byte limit" ;
11+ const responseLimitErrors = new WeakSet < Error > ( ) ;
1112let warnedInvalidConfigurationServers = new WeakSet < object > ( ) ;
1213
1314export interface SearxngResponseReadOptions {
@@ -88,28 +89,29 @@ function abortReason(signal: AbortSignal): unknown {
8889 return error ;
8990}
9091
91- export async function readSearxngResponseBody (
92- response : Response ,
93- maxBytes : number ,
94- options : SearxngResponseReadOptions = { } ,
95- ) : Promise < SearxngResponseReadResult > {
96- const body = responseBodyOrThrow ( response ) ;
97- if ( body === null ) {
98- return { text : "" , bytesRead : 0 , truncated : false } ;
99- }
92+ function responseTooLargeError ( ) : Error {
93+ const error = new Error ( RESPONSE_TOO_LARGE_MESSAGE ) ;
94+ responseLimitErrors . add ( error ) ;
95+ return error ;
96+ }
10097
101- const preview = options . preview === true ;
102- const previewLimit = options . previewMaxBytes ?? PREVIEW_MAX_SEARXNG_RESPONSE_BYTES ;
103- const effectiveLimit = preview ? Math . min ( maxBytes , previewLimit ) : maxBytes ;
104- const reader = body . getReader ( ) ;
105- const chunks : Uint8Array [ ] = [ ] ;
106- let bytesRetained = 0 ;
107- let bytesRead = 0 ;
98+ interface ResponseAccumulator {
99+ chunks : Uint8Array [ ] ;
100+ bytesRead : number ;
101+ bytesRetained : number ;
102+ }
103+
104+ interface AbortReadRace {
105+ dispose ( ) : void ;
106+ read ( reader : ReadableStreamDefaultReader < Uint8Array > ) : Promise < ReadableStreamReadResult < Uint8Array > > ;
107+ throwIfAborted ( reader : ReadableStreamDefaultReader < Uint8Array > ) : Promise < void > ;
108+ }
109+
110+ function createAbortReadRace ( signal : AbortSignal | undefined ) : AbortReadRace {
108111 let abortRequested = false ;
109112 let abortedWith : unknown ;
110113 let abortHandler : ( ( ) => void ) | undefined ;
111114 let abortPromise : Promise < never > | undefined ;
112- const signal = options . signal ;
113115
114116 if ( signal !== undefined ) {
115117 abortPromise = new Promise < never > ( ( _resolve , reject ) => {
@@ -118,66 +120,106 @@ export async function readSearxngResponseBody(
118120 abortedWith = abortReason ( signal ) ;
119121 reject ( abortedWith ) ;
120122 } ;
121- if ( signal . aborted ) {
122- abortHandler ( ) ;
123- } else {
124- signal . addEventListener ( "abort" , abortHandler , { once : true } ) ;
125- }
123+ if ( signal . aborted ) abortHandler ( ) ;
124+ else signal . addEventListener ( "abort" , abortHandler , { once : true } ) ;
126125 } ) ;
127126 }
128127
129- try {
130- while ( true ) {
131- const readPromise = reader . read ( ) ;
132- const { done , value } = abortPromise === undefined
133- ? await readPromise
134- : await Promise . race ( [ readPromise , abortPromise ] ) ;
135- if ( done ) {
136- break ;
137- }
138- if ( ! value ) {
139- continue ;
128+ return {
129+ dispose : ( ) => {
130+ if ( signal !== undefined && abortHandler !== undefined ) signal . removeEventListener ( "abort" , abortHandler ) ;
131+ } ,
132+ read : ( reader ) => abortPromise === undefined
133+ ? reader . read ( )
134+ : Promise . race ( [ reader . read ( ) , abortPromise ] ) ,
135+ throwIfAborted : async ( reader ) => {
136+ if ( abortRequested ) {
137+ await bestEffortCancel ( reader , abortedWith ) ;
138+ throw abortedWith ;
140139 }
140+ } ,
141+ } ;
142+ }
141143
142- bytesRead += value . byteLength ;
143- const remaining = effectiveLimit - bytesRetained ;
144- if ( value . byteLength > remaining ) {
145- if ( preview && remaining > 0 ) {
146- chunks . push ( value . subarray ( 0 , remaining ) ) ;
147- bytesRetained += remaining ;
148- }
149- await bestEffortCancel ( reader ) ;
150- if ( preview ) {
151- return {
152- text : new TextDecoder ( "utf-8" ) . decode ( concatenateChunks ( chunks , bytesRetained ) ) ,
153- bytesRead,
154- truncated : true ,
155- } ;
156- }
157- throw new Error ( RESPONSE_TOO_LARGE_MESSAGE ) ;
158- }
144+ function buildReadResult ( accumulator : ResponseAccumulator , truncated : boolean ) : SearxngResponseReadResult {
145+ return {
146+ text : new TextDecoder ( "utf-8" ) . decode ( concatenateChunks ( accumulator . chunks , accumulator . bytesRetained ) ) ,
147+ bytesRead : accumulator . bytesRead ,
148+ truncated,
149+ } ;
150+ }
151+
152+ async function retainResponseChunk (
153+ reader : ReadableStreamDefaultReader < Uint8Array > ,
154+ value : Uint8Array | undefined ,
155+ effectiveLimit : number ,
156+ preview : boolean ,
157+ accumulator : ResponseAccumulator ,
158+ ) : Promise < SearxngResponseReadResult | undefined > {
159+ if ( ! value ) return undefined ;
160+
161+ accumulator . bytesRead += value . byteLength ;
162+ const remaining = effectiveLimit - accumulator . bytesRetained ;
163+ if ( value . byteLength <= remaining ) {
164+ accumulator . chunks . push ( value ) ;
165+ accumulator . bytesRetained += value . byteLength ;
166+ return undefined ;
167+ }
159168
160- chunks . push ( value ) ;
161- bytesRetained += value . byteLength ;
169+ if ( preview && remaining > 0 ) {
170+ accumulator . chunks . push ( value . subarray ( 0 , remaining ) ) ;
171+ accumulator . bytesRetained += remaining ;
172+ }
173+ await bestEffortCancel ( reader ) ;
174+ if ( preview ) return buildReadResult ( accumulator , true ) ;
175+ throw responseTooLargeError ( ) ;
176+ }
177+
178+ async function consumeResponseBody (
179+ reader : ReadableStreamDefaultReader < Uint8Array > ,
180+ effectiveLimit : number ,
181+ preview : boolean ,
182+ signal : AbortSignal | undefined ,
183+ ) : Promise < SearxngResponseReadResult > {
184+ const accumulator : ResponseAccumulator = { chunks : [ ] , bytesRead : 0 , bytesRetained : 0 } ;
185+ const abortRace = createAbortReadRace ( signal ) ;
186+
187+ try {
188+ while ( true ) {
189+ const { done, value } = await abortRace . read ( reader ) ;
190+ if ( done ) return buildReadResult ( accumulator , false ) ;
191+ const truncated = await retainResponseChunk ( reader , value , effectiveLimit , preview , accumulator ) ;
192+ if ( truncated !== undefined ) return truncated ;
162193 }
163194 } catch ( error ) {
164- if ( abortRequested ) {
165- await bestEffortCancel ( reader , abortedWith ) ;
166- throw abortedWith ;
167- }
195+ if ( error instanceof Error && responseLimitErrors . has ( error ) ) throw error ;
196+ await abortRace . throwIfAborted ( reader ) ;
168197 throw error ;
169198 } finally {
170- if ( signal !== undefined && abortHandler !== undefined ) {
171- signal . removeEventListener ( "abort" , abortHandler ) ;
172- }
173- reader . releaseLock ( ) ;
199+ abortRace . dispose ( ) ;
174200 }
201+ }
175202
176- return {
177- text : new TextDecoder ( "utf-8" ) . decode ( concatenateChunks ( chunks , bytesRetained ) ) ,
178- bytesRead,
179- truncated : false ,
180- } ;
203+ export async function readSearxngResponseBody (
204+ response : Response ,
205+ maxBytes : number ,
206+ options : SearxngResponseReadOptions = { } ,
207+ ) : Promise < SearxngResponseReadResult > {
208+ const body = responseBodyOrThrow ( response ) ;
209+ if ( body === null ) {
210+ return { text : "" , bytesRead : 0 , truncated : false } ;
211+ }
212+
213+ const preview = options . preview === true ;
214+ const previewLimit = options . previewMaxBytes ?? PREVIEW_MAX_SEARXNG_RESPONSE_BYTES ;
215+ const effectiveLimit = preview ? Math . min ( maxBytes , previewLimit ) : maxBytes ;
216+ const reader = body . getReader ( ) ;
217+
218+ try {
219+ return await consumeResponseBody ( reader , effectiveLimit , preview , options . signal ) ;
220+ } finally {
221+ reader . releaseLock ( ) ;
222+ }
181223}
182224
183225export async function cancelAuxiliaryResponseBody ( response : Response ) : Promise < void > {
0 commit comments