github.com/grafana/synthetic-monitoring-agent@v0.64.2/internal/k6runner/http_test.go (about) 1 package k6runner 2 3 import ( 4 "bytes" 5 "context" 6 "encoding/json" 7 "fmt" 8 "io" 9 "net" 10 "net/http" 11 "net/http/httptest" 12 "os" 13 "strings" 14 "sync/atomic" 15 "testing" 16 "time" 17 18 "github.com/go-logfmt/logfmt" 19 "github.com/grafana/synthetic-monitoring-agent/internal/testhelper" 20 "github.com/prometheus/client_golang/prometheus" 21 "github.com/prometheus/common/expfmt" 22 "github.com/rs/zerolog" 23 "github.com/stretchr/testify/require" 24 ) 25 26 func TestHttpRunnerRun(t *testing.T) { 27 t.Parallel() 28 29 script := Script{ 30 Script: testhelper.MustReadFile(t, "testdata/test.js"), 31 Settings: Settings{ 32 Timeout: 1000, 33 }, 34 K6ChannelManifest: "test", 35 } 36 37 mux := http.NewServeMux() 38 mux.HandleFunc("/run", func(w http.ResponseWriter, r *http.Request) { 39 require.Equal(t, http.MethodPost, r.Method) 40 require.Equal(t, "application/json", r.Header.Get("Content-Type")) 41 42 var req struct { 43 Script []byte `json:"script"` 44 Settings Settings `json:"settings"` 45 NotAfter time.Time `json:"notAfter"` 46 K6ChannelManifest string `json:"k6ChannelManifest"` 47 ExecutionID string `json:"executionId"` 48 } 49 50 err := json.NewDecoder(r.Body).Decode(&req) 51 if err != nil { 52 t.Logf("decoding body: %v", err) 53 t.Fail() 54 w.WriteHeader(400) // Use 400 as the client won't retry this failure. 55 56 return 57 } 58 59 if time.Since(req.NotAfter) > time.Hour || time.Until(req.NotAfter) > time.Hour { 60 t.Log("unexpected value for NotAfter too far from the present") 61 t.Fail() 62 w.WriteHeader(400) // Use 400 as the client won't retry this failure. 63 64 return 65 } 66 67 if req.K6ChannelManifest == "" { 68 t.Log("Unexpected empty channel manifest") 69 t.Fail() 70 w.WriteHeader(400) // Use 400 as the client won't retry this failure. 71 72 return 73 } 74 75 if req.ExecutionID != "test-execution-id" { 76 t.Logf("unexpected executionID: got %q, want %q", req.ExecutionID, "test-execution-id") 77 t.Fail() 78 w.WriteHeader(400) 79 80 return 81 } 82 83 w.Header().Set("Content-Type", "application/json") 84 w.WriteHeader(http.StatusOK) 85 86 rr := &RunResponse{ 87 Metrics: testhelper.MustReadFile(t, "testdata/test.out"), 88 Logs: testhelper.MustReadFile(t, "testdata/test.log"), 89 } 90 _ = json.NewEncoder(w).Encode(rr) 91 }) 92 mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 93 w.WriteHeader(http.StatusBadRequest) 94 _, _ = w.Write([]byte(`{"msg": "bad request"}`)) 95 }) 96 97 srv := httptest.NewServer(mux) 98 t.Cleanup(srv.Close) 99 100 runner, err := New(RunnerOpts{Uri: srv.URL + "/run"}) 101 require.NoError(t, err) 102 require.IsType(t, &HttpRunner{}, runner) 103 104 ctx := context.Background() 105 ctx, cancel := testhelper.Context(ctx, t) 106 t.Cleanup(cancel) 107 108 _, err = runner.Run(ctx, script, SecretStore{}, "test-execution-id") 109 require.NoError(t, err) 110 } 111 112 func TestHttpRunnerRunError(t *testing.T) { 113 t.Parallel() 114 115 script := Script{ 116 Script: testhelper.MustReadFile(t, "testdata/test.js"), 117 Settings: Settings{ 118 Timeout: 1000, 119 }, 120 } 121 122 mux := http.NewServeMux() 123 mux.HandleFunc("/run", func(w http.ResponseWriter, r *http.Request) { 124 w.Header().Set("Content-Type", "application/json; charset=utf-8") 125 w.WriteHeader(http.StatusBadRequest) 126 resp := requestError{ 127 Err: http.StatusText(http.StatusBadRequest), 128 Message: "test error", 129 } 130 _ = json.NewEncoder(w).Encode(resp) 131 }) 132 mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 133 t.Log("http runner called the wrong endpoint") 134 t.Fail() 135 136 w.WriteHeader(http.StatusBadRequest) 137 resp := requestError{ 138 Err: http.StatusText(http.StatusBadRequest), 139 Message: "unexpected request to " + r.URL.Path, 140 } 141 _ = json.NewEncoder(w).Encode(resp) 142 }) 143 144 srv := httptest.NewServer(mux) 145 t.Cleanup(srv.Close) 146 147 runner, err := New(RunnerOpts{Uri: srv.URL + "/run"}) 148 require.NoError(t, err) 149 require.IsType(t, &HttpRunner{}, runner) 150 151 // HTTPRunner will retry until the context deadline is met, so we set a short one. 152 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 153 t.Cleanup(cancel) 154 155 ctx, cancel = testhelper.Context(ctx, t) 156 t.Cleanup(cancel) 157 158 _, err = runner.Run(ctx, script, SecretStore{}, "test-execution-id") 159 require.ErrorIs(t, err, ErrUnexpectedStatus) 160 } 161 162 // TestScriptHTTPRun tests that Script reports what it should depending on the status code and responses of the HTTP 163 // runner. 164 func TestScriptHTTPRun(t *testing.T) { 165 t.Parallel() 166 167 var ( 168 testMetrics = testhelper.MustReadFile(t, "testdata/test.out") 169 // testLogs is a file containing a bunch of log lines. All lines but one have level=debug, which are discarded 170 // by the loki submitter implementation. 171 testLogs = testhelper.MustReadFile(t, "testdata/test.log") 172 // nonkdebugLogLine is the only line on testLogs that does not have level=debug, therefore the only one that is 173 // actually submitted. We use it in the test table below as a sentinel to assert whether logs have been 174 // submitted or not. 175 nonDebugLogLine = `time="2023-06-01T13:40:26-06:00" level="test" msg="Non-debug message, for testing!"` + "\n" 176 ) 177 178 for _, tc := range []struct { 179 name string 180 response *RunResponse 181 delay time.Duration 182 statusCode int 183 expectSuccess bool 184 expectError error 185 expectErrorAs any // To accommodate return of unnamed errors. If set, expectError is ignored. 186 expectLogs string 187 }{ 188 { 189 name: "all good", 190 response: &RunResponse{ 191 Metrics: testMetrics, 192 Logs: testLogs, 193 }, 194 statusCode: http.StatusOK, 195 expectSuccess: true, 196 expectError: nil, 197 expectLogs: nonDebugLogLine, 198 }, 199 { 200 // HTTP runner should report failure and an error when the upstream status is not recognized. 201 name: "unexpected status", 202 response: &RunResponse{}, 203 statusCode: 999, 204 expectSuccess: false, 205 expectError: ErrUnexpectedStatus, 206 }, 207 { 208 // HTTP runner should report failure and an error when the error is unknown. 209 name: "non-user error", 210 response: &RunResponse{ 211 Metrics: testMetrics, 212 Logs: testLogs, 213 Error: "something went wrong", 214 ErrorCode: "something-wrong", 215 }, 216 statusCode: http.StatusOK, 217 expectSuccess: false, 218 expectError: ErrFromRunner, 219 expectLogs: nonDebugLogLine + fmt.Sprintf( 220 "level=\"error\" msg=\"script did not execute successfully\" error=%q errorCode=%q\n", 221 "something went wrong", 222 "something-wrong", 223 ), 224 }, 225 { 226 // HTTP runner should report failure but no error when the error is unknown. 227 name: "user error", 228 response: &RunResponse{ 229 Metrics: testMetrics, 230 Logs: testLogs, 231 Error: "syntax error somewhere or something", 232 ErrorCode: "aborted", 233 }, 234 statusCode: http.StatusOK, 235 expectSuccess: false, 236 expectError: nil, 237 expectLogs: nonDebugLogLine + fmt.Sprintf( 238 "level=\"error\" msg=\"script did not execute successfully\" error=%q errorCode=%q\n", 239 "syntax error somewhere or something", 240 "aborted", 241 ), 242 }, 243 { 244 name: "borked logs are sent best-effort", 245 response: &RunResponse{ 246 Metrics: testMetrics, 247 Logs: []byte(`level=error foo="b` + "\n"), 248 Error: "we killed k6", 249 ErrorCode: "aborted", 250 }, 251 statusCode: http.StatusUnprocessableEntity, 252 expectSuccess: false, 253 expectErrorAs: &logfmt.SyntaxError{}, 254 expectLogs: `level="error"` + "\n" + fmt.Sprintf( 255 "level=\"error\" msg=\"script did not execute successfully\" error=%q errorCode=%q\n", 256 "we killed k6", 257 "aborted", 258 ), 259 }, 260 { 261 name: "logs are sent on borked metrics", 262 response: &RunResponse{ 263 Metrics: []byte("probe_succ{"), 264 Logs: testLogs, 265 Error: "we killed k6", 266 ErrorCode: "aborted", 267 }, 268 statusCode: http.StatusUnprocessableEntity, 269 expectSuccess: false, 270 expectErrorAs: expfmt.ParseError{}, 271 expectLogs: nonDebugLogLine + fmt.Sprintf( 272 "level=\"error\" msg=\"script did not execute successfully\" error=%q errorCode=%q\n", 273 "we killed k6", 274 "aborted", 275 ), 276 }, 277 { 278 name: "inconsistent runner response A", 279 response: &RunResponse{ 280 Metrics: testMetrics, 281 Logs: testLogs, 282 Error: "set", 283 ErrorCode: "", 284 }, 285 statusCode: http.StatusInternalServerError, 286 expectSuccess: false, 287 expectError: ErrBuggyRunner, 288 expectLogs: "", 289 }, 290 { 291 name: "inconsistent runner response B", 292 response: &RunResponse{ 293 Metrics: testMetrics, 294 Logs: testLogs, 295 Error: "", 296 ErrorCode: "set", 297 }, 298 statusCode: http.StatusInternalServerError, 299 expectSuccess: false, 300 expectError: ErrBuggyRunner, 301 expectLogs: "", 302 }, 303 { 304 name: "request timeout", 305 response: &RunResponse{ 306 Metrics: testMetrics, 307 Logs: testLogs, 308 }, 309 delay: 3 * time.Second, // Beyond timeout + graceTime. 310 statusCode: http.StatusInternalServerError, 311 expectSuccess: false, 312 expectError: context.DeadlineExceeded, 313 }, 314 } { 315 t.Run(tc.name, func(t *testing.T) { 316 t.Parallel() 317 318 mux := http.NewServeMux() 319 mux.HandleFunc("/run", func(w http.ResponseWriter, r *http.Request) { 320 time.Sleep(tc.delay) 321 w.Header().Set("Content-Type", "application/json") 322 w.WriteHeader(tc.statusCode) 323 _ = json.NewEncoder(w).Encode(tc.response) 324 }) 325 srv := httptest.NewServer(mux) 326 t.Cleanup(srv.Close) 327 328 runner := HttpRunner{url: srv.URL, graceTime: time.Second, backoff: time.Second, metrics: NewHTTPMetrics(prometheus.NewRegistry())} 329 script, err := NewProcessor(Script{ 330 Script: []byte("tee-hee"), 331 Settings: Settings{ 332 Timeout: 1000, 333 }, 334 }, runner) 335 require.NoError(t, err) 336 337 baseCtx, baseCancel := context.WithTimeout(context.Background(), 4*time.Second) 338 t.Cleanup(baseCancel) 339 ctx, cancel := testhelper.Context(baseCtx, t) 340 t.Cleanup(cancel) 341 342 var ( 343 registry = prometheus.NewRegistry() 344 logBuf = bytes.Buffer{} 345 logger = recordingLogger{buf: &logBuf} 346 zlogger = testhelper.Logger(t) 347 ) 348 349 success, _, err := script.Run(ctx, registry, logger, zlogger, SecretStore{}, "test-execution-id") 350 require.Equal(t, tc.expectSuccess, success) 351 require.Equal(t, tc.expectLogs, logger.buf.String()) 352 353 if tc.expectErrorAs == nil { 354 require.ErrorIs(t, err, tc.expectError) 355 } else { 356 require.ErrorAs(t, err, &tc.expectErrorAs) 357 } 358 }) 359 } 360 } 361 362 func TestHTTPProcessorRetries(t *testing.T) { 363 t.Parallel() 364 365 t.Run("status codes", func(t *testing.T) { 366 t.Parallel() 367 368 for _, tc := range []struct { 369 name string 370 handler http.Handler 371 scriptTimeout time.Duration 372 graceTime time.Duration 373 globalTimeout time.Duration 374 expectRequests int64 375 expectError error 376 }{ 377 { 378 name: "no retries needed", 379 handler: emptyJSON(http.StatusOK), 380 scriptTimeout: time.Second, graceTime: time.Second, globalTimeout: 5 * time.Second, 381 expectRequests: 1, 382 expectError: nil, 383 }, 384 { 385 name: "does not retry 400", 386 handler: emptyJSON(http.StatusBadRequest), 387 scriptTimeout: time.Second, graceTime: time.Second, globalTimeout: 5 * time.Second, 388 expectRequests: 1, 389 expectError: ErrUnexpectedStatus, 390 }, 391 { 392 name: "does not retry 422", 393 handler: emptyJSON(http.StatusUnprocessableEntity), 394 scriptTimeout: time.Second, graceTime: time.Second, globalTimeout: 5 * time.Second, 395 expectRequests: 1, 396 expectError: nil, 397 }, 398 { 399 name: "does not retry 428", 400 handler: emptyJSON(http.StatusRequestTimeout), 401 scriptTimeout: time.Second, graceTime: time.Second, globalTimeout: 5 * time.Second, 402 expectRequests: 1, 403 expectError: nil, 404 }, 405 { 406 name: "does not retry 500", 407 handler: emptyJSON(http.StatusInternalServerError), 408 scriptTimeout: time.Second, graceTime: time.Second, globalTimeout: 5 * time.Second, 409 expectRequests: 1, 410 expectError: nil, 411 }, 412 { 413 name: "retries 503", 414 handler: afterAttempts(emptyJSON(http.StatusServiceUnavailable), 1, emptyJSON(http.StatusOK)), 415 scriptTimeout: time.Second, graceTime: time.Second, globalTimeout: 5 * time.Second, 416 expectRequests: 2, 417 expectError: nil, 418 }, 419 { 420 name: "retries 504", 421 handler: afterAttempts(emptyJSON(http.StatusGatewayTimeout), 1, emptyJSON(http.StatusOK)), 422 scriptTimeout: time.Second, graceTime: time.Second, globalTimeout: 5 * time.Second, 423 expectRequests: 2, 424 expectError: nil, 425 }, 426 { 427 name: "retries more than once", 428 handler: afterAttempts(emptyJSON(http.StatusGatewayTimeout), 2, emptyJSON(http.StatusOK)), 429 scriptTimeout: time.Second, graceTime: time.Second, globalTimeout: 5 * time.Second, 430 expectRequests: 3, 431 expectError: nil, 432 }, 433 { 434 name: "gives up eventually", 435 handler: emptyJSON(http.StatusServiceUnavailable), 436 scriptTimeout: time.Second, graceTime: time.Second, globalTimeout: 5 * time.Second, 437 // Context is forced to timeout after 5 seconds. This means 3 requests, with delays of 0, [1-2), [2-3), as 438 // the fourth request would need to wait [3-4) seconds after [3-5) have passed, thus guaranteed to 439 // go beyond the 5s deadline. 440 expectRequests: 3, 441 expectError: context.DeadlineExceeded, 442 }, 443 { 444 name: "gives up eventually when server hangs", 445 handler: delay(10*time.Second, emptyJSON(http.StatusServiceUnavailable)), 446 scriptTimeout: time.Second, graceTime: time.Second, globalTimeout: 5 * time.Second, 447 // Requests have 2s timeout and 0s backoff after that (backoff includes timeout in 448 // this implementation), so that means requests are made at the 0s, 2s and 4s marks. A fourth request 449 // would happen beyond the 5s timeline. 450 expectRequests: 3, 451 expectError: context.DeadlineExceeded, 452 }, 453 } { 454 t.Run(tc.name, func(t *testing.T) { 455 t.Parallel() 456 457 // Use atomic as we write to this in a handler and read from in on the test. 458 // go test -race trips if we use a regular int here. 459 var requests atomic.Int64 460 461 mux := http.NewServeMux() 462 mux.HandleFunc("/run", func(w http.ResponseWriter, r *http.Request) { 463 requests.Add(1) 464 tc.handler.ServeHTTP(w, r) 465 }) 466 srv := httptest.NewServer(mux) 467 t.Cleanup(srv.Close) 468 469 runner := HttpRunner{url: srv.URL, graceTime: tc.graceTime, backoff: time.Second, metrics: NewHTTPMetrics(prometheus.NewRegistry())} 470 processor, err := NewProcessor(Script{Script: nil, Settings: Settings{tc.scriptTimeout.Milliseconds()}}, runner) 471 require.NoError(t, err) 472 473 baseCtx, baseCancel := context.WithTimeout(context.Background(), tc.globalTimeout) 474 t.Cleanup(baseCancel) 475 ctx, cancel := testhelper.Context(baseCtx, t) 476 t.Cleanup(cancel) 477 478 var ( 479 registry = prometheus.NewRegistry() 480 logger testLogger 481 zlogger = zerolog.New(io.Discard) 482 ) 483 484 success, _, err := processor.Run(ctx, registry, &logger, zlogger, SecretStore{}, "test-execution-id") 485 require.ErrorIs(t, err, tc.expectError) 486 require.Equal(t, tc.expectError == nil, success) 487 require.Equal(t, tc.expectRequests, requests.Load()) 488 }) 489 } 490 }) 491 492 t.Run("retries network errors", func(t *testing.T) { 493 t.Parallel() 494 495 mux := http.NewServeMux() 496 mux.Handle("/run", emptyJSON(http.StatusOK)) 497 498 listenerCh := make(chan string) 499 500 go func() { 501 listener, err := net.Listen("tcp4", "localhost:") 502 require.NoError(t, err) 503 504 listenerCh <- listener.Addr().String() 505 506 err = http.Serve(listener, mux) 507 require.NoError(t, err) 508 509 t.Cleanup(func() { 510 listener.Close() 511 }) 512 }() 513 514 addr := <-listenerCh 515 516 runner := HttpRunner{url: "http://" + addr, graceTime: time.Second, backoff: time.Second, metrics: NewHTTPMetrics(prometheus.NewRegistry())} 517 processor, err := NewProcessor(Script{Script: nil, Settings: Settings{Timeout: 1000}}, runner) 518 require.NoError(t, err) 519 520 baseCtx, baseCancel := context.WithTimeout(context.Background(), 5*time.Second) 521 t.Cleanup(baseCancel) 522 ctx, cancel := testhelper.Context(baseCtx, t) 523 t.Cleanup(cancel) 524 525 var ( 526 registry = prometheus.NewRegistry() 527 logger testLogger 528 zlogger = zerolog.New(io.Discard) 529 ) 530 531 success, _, err := processor.Run(ctx, registry, &logger, zlogger, SecretStore{}, "test-execution-id") 532 require.NoError(t, err) 533 require.True(t, success) 534 }) 535 } 536 537 func TestHTTPVersions(t *testing.T) { 538 t.Parallel() 539 540 mux := http.NewServeMux() 541 mux.HandleFunc("/run", func(rw http.ResponseWriter, _ *http.Request) { 542 // Should never be called. 543 rw.WriteHeader(http.StatusInternalServerError) 544 t.Fail() 545 }) 546 mux.HandleFunc("/versions", func(rw http.ResponseWriter, r *http.Request) { 547 if !strings.Contains(r.Header.Get("accept"), "application/json") { 548 rw.WriteHeader(http.StatusNotAcceptable) 549 return 550 } 551 552 rw.Header().Add("content-type", "application/json") 553 rw.WriteHeader(http.StatusOK) 554 555 _, err := rw.Write([]byte(`{ 556 "versions": [ 557 {"path":"/usr/local/libexec/sm-k6/k6-v1", "version":"1.2.3"}, 558 {"path":"/usr/local/libexec/sm-k6/k6-v2", "version":"2.3.4"} 559 ] 560 }`)) 561 if err != nil { 562 t.Fatalf("writing response body: %v", err) 563 } 564 }) 565 566 srv := httptest.NewServer(mux) 567 568 logger := zerolog.New(os.Stderr) 569 570 runner := HttpRunner{ 571 logger: &logger, 572 url: srv.URL, 573 versionPollInterval: time.Second, 574 } 575 576 ctx, cancel := context.WithCancel(context.Background()) 577 t.Cleanup(cancel) 578 579 versionsCh := runner.Versions(ctx) 580 581 t.Run("Retrieves version at least once", func(t *testing.T) { 582 select { 583 case <-time.After(3 * time.Second): 584 t.Fatalf("No version was sent to the channel") 585 case vs := <-versionsCh: 586 require.ElementsMatch(t, vs, []string{"1.2.3", "2.3.4"}) 587 } 588 }) 589 590 t.Run("Closes channel and stops on context cancellation", func(t *testing.T) { 591 cancel() 592 593 select { 594 case <-time.After(3 * time.Second): 595 t.Fatalf("Channel was not closed within timeout after context cancellation") 596 case vs, ok := <-versionsCh: 597 if ok { 598 t.Fatalf("Expected nothing from a closed channel, got %v", vs) 599 } 600 } 601 }) 602 603 t.Cleanup(srv.Close) 604 } 605 606 func TestTrimSuffix(t *testing.T) { 607 t.Parallel() 608 609 for _, tc := range []struct { 610 name string 611 input string 612 expected string 613 }{ 614 { 615 name: "URL without run", 616 input: "http://test.local/", 617 expected: "http://test.local/", 618 }, 619 { 620 name: "URL without trailing slash", 621 input: "http://test.local", 622 expected: "http://test.local", 623 }, 624 { 625 name: "URL with /run not at the end", 626 input: "http://test.local/run/boo", 627 expected: "http://test.local/run/boo", 628 }, 629 { 630 name: "URL with /run", 631 input: "http://test.local/run/", 632 expected: "http://test.local", 633 }, 634 { 635 name: "URL with /run, no slash", 636 input: "http://test.local/run", 637 expected: "http://test.local", 638 }, 639 { 640 name: "URL with two runs", 641 input: "http://test.local/run/run", 642 expected: "http://test.local/run", 643 }, 644 { 645 name: "Invalid URL", 646 input: "!!!#nyanya~", 647 expected: "!!!#nyanya~", 648 }, 649 } { 650 t.Run(tc.name, func(t *testing.T) { 651 t.Parallel() 652 require.Equal(t, tc.expected, trimRunSuffix(tc.input)) 653 }) 654 } 655 } 656 657 func emptyJSON(status int) http.Handler { 658 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 659 w.Header().Set("Content-Type", "application/json") 660 w.WriteHeader(status) 661 _, _ = w.Write([]byte("{}")) 662 }) 663 } 664 665 // afterAttempts invokes the first handler if strictly less than [afterAttempts] requests have been made before to it. 666 // afterAttempts(a, 0, b) always invokes b. 667 // afterAttempts(a, 1, b) always invokes a for the first request, then b for the subsequent ones. 668 func afterAttempts(a http.Handler, afterAttempts int, b http.Handler) http.Handler { 669 pastAttempts := 0 670 671 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 672 if pastAttempts >= afterAttempts { 673 b.ServeHTTP(w, r) 674 return 675 } 676 677 a.ServeHTTP(w, r) 678 679 pastAttempts++ 680 }) 681 } 682 683 // delay calls next after some time has passed. It watches for incoming request context cancellation to avoid leaking 684 // connections. 685 func delay(d time.Duration, next http.Handler) http.Handler { 686 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 687 // Fully consume the request body before waiting. If we do not do this, cancelling the request context will not 688 // close the network connection, and httptest.Server will complain. 689 _, _ = io.Copy(io.Discard, r.Body) 690 691 select { 692 case <-r.Context().Done(): 693 // Abort waiting if the client closes the request. Again, if we don't, httptest.Server will complain. 694 case <-time.After(d): 695 next.ServeHTTP(w, r) 696 } 697 }) 698 }