Skip to content

Commit 141b203

Browse files
authored
Merge pull request #147 from Acconut/interim-responses
Add tests for interim response handling
2 parents ac21ca9 + 1b31a0d commit 141b203

19 files changed

+436
-10
lines changed

CONTRIBUTING.md

+4
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,9 @@ Possible members of a request object:
7272
`false`.
7373
- `response_body` - String to send as the response body from the origin. Defaults to
7474
the test identifier.
75+
- `interim_responses` - An array of interim responses to send before the final response. Each item can be either:
76+
- `[status_code]` - Just a status code (e.g., `[102]`)
77+
- `[status_code, headers_array]` - Status code and headers, where headers_array is an array of `[header_name, header_value]` pairs
7578
- `response_pause` - Integer number of seconds for the server to pause before generating a response.
7679
- `check_body` - Whether to check the response body. Default `true`.
7780
- `expected_type` - One of:
@@ -98,6 +101,7 @@ Possible members of a request object:
98101
- `header_name_string` representing headers to check that the response on the client does not include.
99102
- `[header_name_string, header_value_string]`: headers to check that the response is either missing, or if they're present, that they do _not_ contain the given value string (evaluated against the whole header value).
100103
- `expected_response_text` - A string to check the response body against on the client.
104+
- `expected_interim_responses` - An array of interim responses expected to be received by the client. Format is the same as `interim_responses`.
101105
- `setup` - Boolean to indicate whether this is a setup request; failures don't mean the actual test failed.
102106
- `setup_tests` - Array of values that indicate whether the specified check is part of setup; failures don't mean the actual test failed. One of: `["expected_type", "expected_method", "expected_status", "expected_response_headers", "expected_response_text", "expected_request_headers"]`
103107

docker/nginx/nginx.conf

+1
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,6 @@ server {
99
proxy_pass http://localhost:8000;
1010
proxy_cache my-cache;
1111
proxy_cache_revalidate on;
12+
proxy_http_version 1.1;
1213
}
1314
}

package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@
1111
"dependencies": {
1212
"liquidjs": "^10.9.2",
1313
"marked": "^15.0.0",
14-
"npm": "^11.0.0"
14+
"npm": "^11.0.0",
15+
"undici": "^7.4.0"
1516
},
1617
"scripts": {
1718
"server": "node test-engine/server/server.mjs",

results/apache.json

+4
Original file line numberDiff line numberDiff line change
@@ -423,6 +423,10 @@
423423
"heuristic-delta-60": true,
424424
"heuristic-delta-600": true,
425425
"heuristic-delta-86400": true,
426+
"interim-102": true,
427+
"interim-103": true,
428+
"interim-no-header-reuse": true,
429+
"interim-not-cached": true,
426430
"invalidate-DELETE": true,
427431
"invalidate-DELETE-cl": [
428432
"Assertion",

results/caddy.json

+16
Original file line numberDiff line numberDiff line change
@@ -537,6 +537,22 @@
537537
"Assertion",
538538
"Response 2 does not come from cache"
539539
],
540+
"interim-102": [
541+
"Assertion",
542+
"Interim response 1 not received"
543+
],
544+
"interim-103": [
545+
"Assertion",
546+
"Interim response 1 not received"
547+
],
548+
"interim-no-header-reuse": [
549+
"Assertion",
550+
"Interim response 1 not received"
551+
],
552+
"interim-not-cached": [
553+
"Assertion",
554+
"Interim response 1 not received"
555+
],
540556
"invalidate-DELETE": true,
541557
"invalidate-DELETE-cl": [
542558
"Assertion",

results/haproxy.json

+4
Original file line numberDiff line numberDiff line change
@@ -522,6 +522,10 @@
522522
"heuristic-delta-60": true,
523523
"heuristic-delta-600": true,
524524
"heuristic-delta-86400": true,
525+
"interim-102": true,
526+
"interim-103": true,
527+
"interim-no-header-reuse": true,
528+
"interim-not-cached": true,
525529
"invalidate-DELETE": true,
526530
"invalidate-DELETE-cl": [
527531
"Assertion",

results/nginx.json

+16
Original file line numberDiff line numberDiff line change
@@ -564,6 +564,22 @@
564564
"Assertion",
565565
"Response 2 does not come from cache"
566566
],
567+
"interim-102": [
568+
"Assertion",
569+
"Response 2 does not come from cache"
570+
],
571+
"interim-103": [
572+
"AbortError",
573+
"This operation was aborted"
574+
],
575+
"interim-no-header-reuse": [
576+
"Assertion",
577+
"Response 2 does not come from cache"
578+
],
579+
"interim-not-cached": [
580+
"Assertion",
581+
"Response 2 does not come from cache"
582+
],
567583
"invalidate-DELETE": [
568584
"Assertion",
569585
"Response 3 comes from cache"

results/squid.json

+4
Original file line numberDiff line numberDiff line change
@@ -438,6 +438,10 @@
438438
"heuristic-delta-60": true,
439439
"heuristic-delta-600": true,
440440
"heuristic-delta-86400": true,
441+
"interim-102": true,
442+
"interim-103": true,
443+
"interim-no-header-reuse": true,
444+
"interim-not-cached": true,
441445
"invalidate-DELETE": true,
442446
"invalidate-DELETE-cl": true,
443447
"invalidate-DELETE-failed": true,

results/trafficserver.json

+16
Original file line numberDiff line numberDiff line change
@@ -456,6 +456,22 @@
456456
"Assertion",
457457
"Response 2 does not come from cache"
458458
],
459+
"interim-102": [
460+
"AbortError",
461+
"This operation was aborted"
462+
],
463+
"interim-103": [
464+
"Assertion",
465+
"Interim response 1 not received"
466+
],
467+
"interim-no-header-reuse": [
468+
"Assertion",
469+
"Interim response 1 not received"
470+
],
471+
"interim-not-cached": [
472+
"Assertion",
473+
"Interim response 1 not received"
474+
],
459475
"invalidate-DELETE": [
460476
"Setup",
461477
"Response 2 status is 403, not 200"

results/varnish.json

+16
Original file line numberDiff line numberDiff line change
@@ -498,6 +498,22 @@
498498
"Assertion",
499499
"Response 2 does not come from cache"
500500
],
501+
"interim-102": [
502+
"Setup",
503+
"Response 1 status is 503, not 200"
504+
],
505+
"interim-103": [
506+
"Setup",
507+
"Response 1 status is 503, not 200"
508+
],
509+
"interim-no-header-reuse": [
510+
"Setup",
511+
"Response 1 status is 503, not 200"
512+
],
513+
"interim-not-cached": [
514+
"Setup",
515+
"Response 1 status is 503, not 200"
516+
],
501517
"invalidate-DELETE": [
502518
"Assertion",
503519
"Response 3 comes from cache"

test-engine/client/test.mjs

+44-4
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export async function makeTest (test) {
1818
const fetchFunctions = []
1919
for (let i = 0; i < requests.length; ++i) {
2020
fetchFunctions.push({
21-
code: idx => {
21+
code: async idx => {
2222
const reqConfig = requests[idx]
2323
const reqNum = idx + 1
2424
const url = clientUtils.makeTestUrl(uuid, reqConfig)
@@ -32,11 +32,20 @@ export async function makeTest (test) {
3232
controller.abort()
3333
}, config.requestTimeout * 1000)
3434
init.signal = controller.signal
35+
36+
const interimResponses = []
37+
if ('expected_interim_responses' in reqConfig) {
38+
// Dynamic import since undici is only available in Node.js
39+
const undici = await import('undici')
40+
const dispatcher = new undici.Agent().compose(clientUtils.interimResponsesCollectingInterceptor(interimResponses))
41+
init.dispatcher = dispatcher
42+
}
43+
3544
if (test.dump === true) clientUtils.logRequest(url, init, reqNum)
3645
return fetch(url, init)
3746
.then(response => {
3847
responses.push(response)
39-
return checkResponse(test, requests, idx, response)
48+
return checkResponse(test, requests, idx, response, interimResponses)
4049
})
4150
.finally(() => {
4251
clearTimeout(timeout)
@@ -84,11 +93,11 @@ export async function makeTest (test) {
8493
})
8594
}
8695

87-
function checkResponse (test, requests, idx, response) {
96+
function checkResponse (test, requests, idx, response, interimResponses) {
8897
const reqNum = idx + 1
8998
const reqConfig = requests[idx]
9099
const resNum = parseInt(response.headers.get('Server-Request-Count'))
91-
if (test.dump === true) clientUtils.logResponse(response, reqNum)
100+
if (test.dump === true) clientUtils.logResponse(response, interimResponses, reqNum)
92101

93102
// catch retries
94103
if (response.headers.has('Request-Numbers')) {
@@ -185,6 +194,37 @@ function checkResponse (test, requests, idx, response) {
185194
}
186195
})
187196
}
197+
198+
// check interim responses
199+
if ('expected_interim_responses' in reqConfig) {
200+
const isSetup = setupCheck(reqConfig, 'expected_interim_responses')
201+
202+
reqConfig.expected_interim_responses.forEach(([statusCode, headers = []], idx) => {
203+
if (interimResponses[idx] == null) {
204+
assert(isSetup, false, `Interim response ${idx + 1} not received`)
205+
} else {
206+
assert(isSetup, interimResponses[idx][0] === statusCode, `Interim response ${idx + 1} status is ${interimResponses[idx][0]}, not ${statusCode}`)
207+
208+
const receivedHeaders = interimResponses[idx][1]
209+
headers.forEach(([header, value]) => {
210+
if (typeof header === 'string') {
211+
assert(isSetup, header in receivedHeaders,
212+
`Interim response ${idx + 1} ${header} header not present.`)
213+
} else if (header.length > 2) {
214+
assert(isSetup, header in receivedHeaders,
215+
`Interim response ${idx + 1} ${header} header not present.`)
216+
217+
const receivedValue = receivedHeaders[header]
218+
assert(isSetup, value === receivedValue,
219+
`Interim response ${idx + 1} header ${header} is ${receivedValue}, should ${value}`)
220+
} else {
221+
console.log('ERROR: Unknown header item in expected_interim_responses', header)
222+
}
223+
})
224+
}
225+
})
226+
}
227+
188228
return response.text().then(makeCheckResponseBody(test, reqConfig, response.status))
189229
}
190230

test-engine/client/utils.mjs

+51-1
Original file line numberDiff line numberDiff line change
@@ -72,11 +72,61 @@ export function logRequest (url, init, reqNum) {
7272
console.log('')
7373
}
7474

75-
export function logResponse (response, reqNum) {
75+
export function logResponse (response, interimResponses, reqNum) {
7676
console.log(`${defines.GREEN}=== Client response ${reqNum}${defines.NC}`)
77+
for (const [statusCode, headers] of interimResponses) {
78+
console.log(` HTTP ${statusCode}`)
79+
for (const [key, value] of Object.entries(headers)) {
80+
console.log(` ${key}: ${value}`)
81+
}
82+
console.log('')
83+
}
7784
console.log(` HTTP ${response.status} ${response.statusText}`)
7885
response.headers.forEach((hvalue, hname) => { // for some reason, node-fetch reverses these
7986
console.log(` ${hname}: ${hvalue}`)
8087
})
8188
console.log('')
8289
}
90+
91+
class InterimResponsesCollectingHandler {
92+
#handler
93+
#interimResponses
94+
95+
constructor (handler, interimResponses) {
96+
this.#handler = handler
97+
this.#interimResponses = interimResponses
98+
}
99+
100+
onRequestStart (controller, context) {
101+
this.#handler.onRequestStart?.(controller, context)
102+
}
103+
104+
onRequestUpgrade (controller, statusCode, headers, socket) {
105+
this.#handler.onRequestUpgrade?.(controller, statusCode, headers, socket)
106+
}
107+
108+
onResponseStart (controller, statusCode, headers, statusMessage) {
109+
if (statusCode < 200) this.#interimResponses.push([statusCode, headers])
110+
this.#handler.onResponseStart?.(controller, statusCode, headers, statusMessage)
111+
}
112+
113+
onResponseData (controller, data) {
114+
this.#handler.onResponseData?.(controller, data)
115+
}
116+
117+
onResponseEnd (controller, trailers) {
118+
this.#handler.onResponseEnd?.(controller, trailers)
119+
}
120+
121+
onResponseError (controller, err) {
122+
this.#handler.onResponseError?.(controller, err)
123+
}
124+
}
125+
126+
export function interimResponsesCollectingInterceptor (collectInto) {
127+
return (dispatch) => {
128+
return (opts, handler) => {
129+
return dispatch(opts, new InterimResponsesCollectingHandler(handler, collectInto))
130+
}
131+
}
132+
}

0 commit comments

Comments
 (0)