@@ -16,37 +16,39 @@ import { createTestResults, exitWithResults, printTestSummary, testFunction } fr
1616const results = createTestResults ( ) ;
1717const encoder = new TextEncoder ( ) ;
1818
19- function responseWithChunks ( chunks : Uint8Array [ ] , options : { cancelRejects ?: boolean ; readRejects ?: boolean } = { } ) {
19+ interface StreamOptions {
20+ cancelRejects ?: boolean ;
21+ leaveOpen ?: boolean ;
22+ readRejects ?: boolean ;
23+ pending ?: boolean ;
24+ }
25+
26+ function responseWithChunks ( chunks : Uint8Array [ ] , options : StreamOptions = { } ) {
2027 let cancelCalls = 0 ;
21- let releaseCalls = 0 ;
22- let index = 0 ;
23- const reader = {
24- async read ( ) : Promise < ReadableStreamReadResult < Uint8Array > > {
28+ let cancelReason : unknown ;
29+ const stream = new ReadableStream < Uint8Array > ( {
30+ start ( controller ) {
2531 if ( options . readRejects ) {
26- throw new Error ( "read failed" ) ;
32+ controller . error ( new Error ( "read failed" ) ) ;
33+ return ;
2734 }
28- const value = chunks [ index ++ ] ;
29- return value === undefined ? { done : true , value : undefined } : { done : false , value } ;
30- } ,
31- async cancel ( ) : Promise < void > {
32- cancelCalls ++ ;
33- if ( options . cancelRejects ) {
34- throw new Error ( "cancel failed" ) ;
35+ if ( ! options . pending ) {
36+ for ( const chunk of chunks ) controller . enqueue ( chunk ) ;
37+ if ( ! options . leaveOpen ) controller . close ( ) ;
3538 }
3639 } ,
37- releaseLock ( ) : void {
38- releaseCalls ++ ;
40+ cancel ( reason ) {
41+ cancelCalls ++ ;
42+ cancelReason = reason ;
43+ if ( options . cancelRejects ) return Promise . reject ( new Error ( "cancel failed" ) ) ;
44+ return undefined ;
3945 } ,
40- } ;
46+ } ) ;
47+ const response = new Response ( stream ) ;
4148 return {
42- response : {
43- body : {
44- getReader : ( ) => reader ,
45- cancel : ( ) => reader . cancel ( ) ,
46- } ,
47- } as Response ,
49+ response,
4850 getCancelCalls : ( ) => cancelCalls ,
49- getReleaseCalls : ( ) => releaseCalls ,
51+ getCancelReason : ( ) => cancelReason ,
5052 } ;
5153}
5254
@@ -62,6 +64,16 @@ function createLogger() {
6264 } ;
6365}
6466
67+ async function expectPromptAbort ( promise : Promise < unknown > , reason : Error ) : Promise < void > {
68+ await assert . rejects (
69+ Promise . race ( [
70+ promise ,
71+ new Promise ( ( _ , reject ) => setTimeout ( ( ) => reject ( new Error ( "abort was not prompt" ) ) , 50 ) ) ,
72+ ] ) ,
73+ ( error : unknown ) => error === reason ,
74+ ) ;
75+ }
76+
6577async function runTests ( ) {
6678 console . log ( "🧪 Testing: searxng-response.ts\n" ) ;
6779
@@ -71,7 +83,7 @@ async function runTests() {
7183 assert . equal ( PREVIEW_MAX_SEARXNG_RESPONSE_BYTES , 64 * 1024 ) ;
7284 } , results ) ;
7385
74- await testFunction ( "resolves only accepted response-size configuration and warns once without raw values " , async ( ) => {
86+ await testFunction ( "pins accepted and rejected response-size configuration forms with value-free once-only warnings " , async ( ) => {
7587 const previous = process . env . SEARXNG_MAX_RESPONSE_BYTES ;
7688 const logger = createLogger ( ) ;
7789 resetSearxngResponseConfigWarningsForTesting ( ) ;
@@ -82,18 +94,21 @@ async function runTests() {
8294 assert . equal ( resolveSearxngResponseMaxBytes ( logger . server as any ) , 1 ) ;
8395 process . env . SEARXNG_MAX_RESPONSE_BYTES = " 16777216 " ;
8496 assert . equal ( resolveSearxngResponseMaxBytes ( logger . server as any ) , HARD_MAX_SEARXNG_RESPONSE_BYTES ) ;
97+ process . env . SEARXNG_MAX_RESPONSE_BYTES = "+5242880" ;
98+ assert . equal ( resolveSearxngResponseMaxBytes ( logger . server as any ) , 5242880 ) ;
99+ process . env . SEARXNG_MAX_RESPONSE_BYTES = "0005242880" ;
100+ assert . equal ( resolveSearxngResponseMaxBytes ( logger . server as any ) , 5242880 ) ;
85101 process . env . SEARXNG_MAX_RESPONSE_BYTES = " " ;
86102 assert . equal ( resolveSearxngResponseMaxBytes ( logger . server as any ) , DEFAULT_SEARXNG_RESPONSE_MAX_BYTES ) ;
87- process . env . SEARXNG_MAX_RESPONSE_BYTES = "0" ;
88- assert . equal ( resolveSearxngResponseMaxBytes ( logger . server as any ) , DEFAULT_SEARXNG_RESPONSE_MAX_BYTES ) ;
89- process . env . SEARXNG_MAX_RESPONSE_BYTES = "16777217" ;
90- assert . equal ( resolveSearxngResponseMaxBytes ( logger . server as any ) , DEFAULT_SEARXNG_RESPONSE_MAX_BYTES ) ;
91- process . env . SEARXNG_MAX_RESPONSE_BYTES = "5.5" ;
92- assert . equal ( resolveSearxngResponseMaxBytes ( logger . server as any ) , DEFAULT_SEARXNG_RESPONSE_MAX_BYTES ) ;
103+ for ( const invalid of [ "0" , "16777217" , "5.5" , "-0" , "1e3" , "5MB" , "9007199254740992" ] ) {
104+ process . env . SEARXNG_MAX_RESPONSE_BYTES = invalid ;
105+ assert . equal ( resolveSearxngResponseMaxBytes ( logger . server as any ) , DEFAULT_SEARXNG_RESPONSE_MAX_BYTES ) ;
106+ }
93107 await Promise . resolve ( ) ;
94108 assert . equal ( logger . messages . length , 1 ) ;
95- assert . ok ( ! logger . messages [ 0 ] . includes ( "16777217 " ) ) ;
109+ assert . ok ( ! logger . messages [ 0 ] . includes ( "9007199254740992 " ) ) ;
96110 resetSearxngResponseConfigWarningsForTesting ( ) ;
111+ process . env . SEARXNG_MAX_RESPONSE_BYTES = "5MB" ;
97112 resolveSearxngResponseMaxBytes ( logger . server as any ) ;
98113 await Promise . resolve ( ) ;
99114 assert . equal ( logger . messages . length , 2 ) ;
@@ -104,44 +119,48 @@ async function runTests() {
104119 }
105120 } , results ) ;
106121
107- await testFunction ( "complete mode accepts exact bytes, rejects the next byte, and releases its lock " , async ( ) => {
122+ await testFunction ( "complete mode accepts exact bytes, rejects the next byte, cancels, and unlocks actual bodies " , async ( ) => {
108123 const exact = responseWithChunks ( [ encoder . encode ( "abc" ) ] ) ;
109124 assert . deepEqual ( await readSearxngResponseBody ( exact . response , 3 ) , {
110125 text : "abc" , bytesRead : 3 , truncated : false ,
111126 } ) ;
112- assert . equal ( exact . getReleaseCalls ( ) , 1 ) ;
113- const overflow = responseWithChunks ( [ encoder . encode ( "abcd" ) ] ) ;
127+ assert . equal ( exact . response . body ! . locked , false ) ;
128+ const overflow = responseWithChunks ( [ encoder . encode ( "abcd" ) ] , { leaveOpen : true } ) ;
114129 await assert . rejects ( ( ) => readSearxngResponseBody ( overflow . response , 3 ) , / S e a r X N G r e s p o n s e e x c e e d s c o n f i g u r e d b y t e l i m i t / ) ;
115130 assert . equal ( overflow . getCancelCalls ( ) , 1 ) ;
116- assert . equal ( overflow . getReleaseCalls ( ) , 1 ) ;
117- const overflowWithCancelFailure = responseWithChunks ( [ encoder . encode ( "abcd" ) ] , { cancelRejects : true } ) ;
131+ assert . equal ( overflow . response . body ! . locked , false ) ;
132+ const overflowWithCancelFailure = responseWithChunks ( [ encoder . encode ( "abcd" ) ] , { cancelRejects : true , leaveOpen : true } ) ;
118133 await assert . rejects (
119134 ( ) => readSearxngResponseBody ( overflowWithCancelFailure . response , 3 ) ,
120135 / S e a r X N G r e s p o n s e e x c e e d s c o n f i g u r e d b y t e l i m i t / ,
121136 ) ;
122- assert . equal ( overflowWithCancelFailure . getReleaseCalls ( ) , 1 ) ;
137+ assert . equal ( overflowWithCancelFailure . getCancelCalls ( ) , 1 ) ;
138+ assert . equal ( overflowWithCancelFailure . response . body ! . locked , false ) ;
123139 } , results ) ;
124140
125- await testFunction ( "complete mode measures raw UTF-8 bytes before decoding" , async ( ) => {
141+ await testFunction ( "complete mode measures raw UTF-8 bytes before decoding with actual bodies " , async ( ) => {
126142 const response = responseWithChunks ( [ new Uint8Array ( [ 0xe2 ] ) , new Uint8Array ( [ 0x82 , 0xac ] ) ] ) ;
127143 assert . deepEqual ( await readSearxngResponseBody ( response . response , 3 ) , {
128144 text : "€" , bytesRead : 3 , truncated : false ,
129145 } ) ;
146+ assert . equal ( response . response . body ! . locked , false ) ;
130147 const partial = responseWithChunks ( [ new Uint8Array ( [ 0xe2 ] ) , new Uint8Array ( [ 0x82 ] ) ] ) ;
131148 assert . equal ( ( await readSearxngResponseBody ( partial . response , 2 ) ) . text , "�" ) ;
149+ assert . equal ( partial . response . body ! . locked , false ) ;
132150 } , results ) ;
133151
134- await testFunction ( "preview mode truncates at its effective limit and preserves cancellation result " , async ( ) => {
135- const truncated = responseWithChunks ( [ encoder . encode ( "abcd" ) ] ) ;
152+ await testFunction ( "preview mode truncates at its effective limit, cancels, and unlocks actual bodies " , async ( ) => {
153+ const truncated = responseWithChunks ( [ encoder . encode ( "abcd" ) ] , { leaveOpen : true } ) ;
136154 assert . deepEqual ( await readSearxngResponseBody ( truncated . response , 10 , { preview : true , previewMaxBytes : 3 } ) , {
137155 text : "abc" , bytesRead : 4 , truncated : true ,
138156 } ) ;
139157 assert . equal ( truncated . getCancelCalls ( ) , 1 ) ;
140- assert . equal ( truncated . getReleaseCalls ( ) , 1 ) ;
141- const cancelRejects = responseWithChunks ( [ encoder . encode ( "abcd" ) ] , { cancelRejects : true } ) ;
158+ assert . equal ( truncated . response . body ! . locked , false ) ;
159+ const cancelRejects = responseWithChunks ( [ encoder . encode ( "abcd" ) ] , { cancelRejects : true , leaveOpen : true } ) ;
142160 const result = await readSearxngResponseBody ( cancelRejects . response , 3 , { preview : true } ) ;
143161 assert . equal ( result . truncated , true ) ;
144- assert . equal ( cancelRejects . getReleaseCalls ( ) , 1 ) ;
162+ assert . equal ( cancelRejects . getCancelCalls ( ) , 1 ) ;
163+ assert . equal ( cancelRejects . response . body ! . locked , false ) ;
145164 } , results ) ;
146165
147166 await testFunction ( "null body is empty while an absent body is rejected without text fallback" , async ( ) => {
@@ -152,13 +171,37 @@ async function runTests() {
152171 await assert . rejects ( ( ) => readSearxngResponseBody ( invalidResponse , 3 ) , / I n v a l i d S e a r X N G r e s p o n s e b o d y / ) ;
153172 } , results ) ;
154173
155- await testFunction ( "reader failures release the lock and auxiliary cancellation never surfaces cancellation failures " , async ( ) => {
174+ await testFunction ( "reader failures unlock actual bodies and auxiliary cancellation hides cancellation rejection " , async ( ) => {
156175 const rejectedRead = responseWithChunks ( [ ] , { readRejects : true } ) ;
157176 await assert . rejects ( ( ) => readSearxngResponseBody ( rejectedRead . response , 3 ) , / r e a d f a i l e d / ) ;
158- assert . equal ( rejectedRead . getReleaseCalls ( ) , 1 ) ;
159- const auxiliary = responseWithChunks ( [ ] , { cancelRejects : true } ) ;
177+ assert . equal ( rejectedRead . response . body ! . locked , false ) ;
178+ const auxiliary = responseWithChunks ( [ ] , { pending : true , cancelRejects : true } ) ;
160179 await cancelAuxiliaryResponseBody ( auxiliary . response ) ;
161180 assert . equal ( auxiliary . getCancelCalls ( ) , 1 ) ;
181+ assert . equal ( auxiliary . response . body ! . locked , false ) ;
182+ } , results ) ;
183+
184+ await testFunction ( "signal abort cancels through the reader, unlocks the actual body, and keeps abort stable when cancel rejects" , async ( ) => {
185+ const controller = new AbortController ( ) ;
186+ const reason = new Error ( "caller aborted" ) ;
187+ const pending = responseWithChunks ( [ ] , { pending : true } ) ;
188+ const read = readSearxngResponseBody ( pending . response , 3 , { signal : controller . signal } ) ;
189+ await Promise . resolve ( ) ;
190+ controller . abort ( reason ) ;
191+ await expectPromptAbort ( read , reason ) ;
192+ assert . equal ( pending . getCancelCalls ( ) , 1 ) ;
193+ assert . equal ( pending . getCancelReason ( ) , reason ) ;
194+ assert . equal ( pending . response . body ! . locked , false ) ;
195+
196+ const rejectingController = new AbortController ( ) ;
197+ const rejectingReason = new Error ( "caller aborted with rejecting cancel" ) ;
198+ const rejecting = responseWithChunks ( [ ] , { pending : true , cancelRejects : true } ) ;
199+ const rejectingRead = readSearxngResponseBody ( rejecting . response , 3 , { signal : rejectingController . signal } ) ;
200+ await Promise . resolve ( ) ;
201+ rejectingController . abort ( rejectingReason ) ;
202+ await expectPromptAbort ( rejectingRead , rejectingReason ) ;
203+ assert . equal ( rejecting . getCancelCalls ( ) , 1 ) ;
204+ assert . equal ( rejecting . response . body ! . locked , false ) ;
162205 } , results ) ;
163206
164207 printTestSummary ( results , "SearXNG Response Module" ) ;
0 commit comments