v3.4.0 593c1c3

transfer_idle_test.go RawHistoryFolder

1package middleware
2
3import (
4	"bufio"
5	"context"
6	"crypto/tls"
7	"io"
8	"net"
9	"net/http"
10	"net/http/httptest"
11	"net/http/httputil"
12	"net/url"
13	"strings"
14	"sync"
15	"sync/atomic"
16	"testing"
17	"time"
18
19	"github.com/stretchr/testify/assert"
20	"github.com/stretchr/testify/require"
21)
22
23const testIdle = 300 * time.Millisecond
24
25func newIdleServer(t *testing.T, handler http.Handler) *httptest.Server {
26	t.Helper()
27	srv := httptest.NewUnstartedServer(TransferIdleTimeout(testIdle)(handler))
28	srv.Config.ReadHeaderTimeout = 5 * time.Second
29	srv.Start()
30	t.Cleanup(srv.Close)
31	return srv
32}
33
34func TestTransferIdleTimeout_SlowActiveDownloadCompletes(t *testing.T) {
35	const chunks = 12 // 12 x 50ms = 600ms, twice the idle timeout
36	srv := newIdleServer(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
37		for range chunks {
38			_, _ = w.Write([]byte("x"))
39			w.(http.Flusher).Flush()
40			time.Sleep(50 * time.Millisecond)
41		}
42	}))
43
44	start := time.Now()
45	resp, err := http.Get(srv.URL)
46	require.NoError(t, err)
47	defer resp.Body.Close()
48	body, err := io.ReadAll(resp.Body)
49	require.NoError(t, err)
50	assert.Len(t, body, chunks)
51	assert.Greater(t, time.Since(start), testIdle, "transfer must outlast the idle timeout")
52}
53
54func TestTransferIdleTimeout_SilentResponseIsCut(t *testing.T) {
55	srv := newIdleServer(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
56		_, _ = w.Write([]byte("start"))
57		w.(http.Flusher).Flush()
58		time.Sleep(3 * testIdle)
59		_, _ = w.Write([]byte("late"))
60		w.(http.Flusher).Flush()
61	}))
62
63	resp, err := http.Get(srv.URL)
64	require.NoError(t, err)
65	defer resp.Body.Close()
66	body, err := io.ReadAll(resp.Body)
67	require.Error(t, err, "connection must be cut after the idle timeout")
68	assert.Equal(t, "start", string(body))
69}
70
71func TestTransferIdleTimeout_SilentHandlerBeforeFirstByteIsCut(t *testing.T) {
72	srv := newIdleServer(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
73		time.Sleep(3 * testIdle)
74		_, _ = w.Write([]byte("late"))
75	}))
76
77	resp, err := http.Get(srv.URL)
78	if err == nil {
79		defer resp.Body.Close()
80		_, err = io.ReadAll(resp.Body)
81	}
82	require.Error(t, err)
83}
84
85func TestTransferIdleTimeout_SlowActiveUploadCompletes(t *testing.T) {
86	const chunks = 10 // 10 x 60ms = 600ms
87	srv := newIdleServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
88		n, err := io.Copy(io.Discard, r.Body)
89		if err != nil {
90			http.Error(w, err.Error(), http.StatusBadRequest)
91			return
92		}
93		_, _ = w.Write([]byte(strings.Repeat("o", int(n))))
94	}))
95
96	pr, pw := io.Pipe()
97	go func() {
98		for range chunks {
99			_, _ = pw.Write([]byte("u"))
100			time.Sleep(60 * time.Millisecond)
101		}
102		_ = pw.Close()
103	}()
104	resp, err := http.Post(srv.URL, "application/octet-stream", pr)
105	require.NoError(t, err)
106	defer resp.Body.Close()
107	body, err := io.ReadAll(resp.Body)
108	require.NoError(t, err)
109	assert.Equal(t, http.StatusOK, resp.StatusCode)
110	assert.Len(t, body, chunks)
111}
112
113func TestTransferIdleTimeout_StalledUploadIsCut(t *testing.T) {
114	var readErr atomic.Value
115	done := make(chan struct{})
116	srv := newIdleServer(t, http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) {
117		defer close(done)
118		_, err := io.Copy(io.Discard, r.Body)
119		if err != nil {
120			readErr.Store(err)
121		}
122	}))
123
124	conn, err := net.Dial("tcp", srv.Listener.Addr().String())
125	require.NoError(t, err)
126	defer conn.Close()
127	_, err = conn.Write([]byte("POST / HTTP/1.1\r\nHost: x\r\nContent-Length: 100\r\n\r\nabc"))
128	require.NoError(t, err)
129
130	select {
131	case <-done:
132	case <-time.After(5 * testIdle):
133		t.Fatal("stalled upload was not cut")
134	}
135	assert.NotNil(t, readErr.Load(), "body read must fail after the idle timeout")
136}
137
138func TestTransferIdleTimeout_PreservesFlusherAndUnwrap(t *testing.T) {
139	var isFlusher, unwrapped bool
140	h := TransferIdleTimeout(time.Second)(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
141		_, isFlusher = w.(http.Flusher)
142		_, unwrapped = w.(interface{ Unwrap() http.ResponseWriter })
143	}))
144	h.ServeHTTP(httptest.NewRecorder(), httptest.NewRequest(http.MethodGet, "/", nil))
145	assert.True(t, isFlusher)
146	assert.True(t, unwrapped)
147}
148
149func TestTransferIdleTimeout_HijackStillWorks(t *testing.T) {
150	srv := newIdleServer(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
151		hj, ok := w.(http.Hijacker)
152		if !ok {
153			http.Error(w, "no hijacker", http.StatusInternalServerError)
154			return
155		}
156		conn, rw, err := hj.Hijack()
157		if err != nil {
158			return
159		}
160		defer conn.Close()
161		_, _ = rw.WriteString("HTTP/1.1 101 Switching Protocols\r\nConnection: Upgrade\r\nUpgrade: test\r\n\r\n")
162		_ = rw.Flush()
163		// Stay open longer than the idle timeout: hijacked streams are exempt.
164		time.Sleep(2 * testIdle)
165		_, _ = rw.WriteString("hello")
166		_ = rw.Flush()
167	}))
168
169	conn, err := net.Dial("tcp", srv.Listener.Addr().String())
170	require.NoError(t, err)
171	defer conn.Close()
172	_, err = conn.Write([]byte("GET / HTTP/1.1\r\nHost: x\r\nConnection: Upgrade\r\nUpgrade: test\r\n\r\n"))
173	require.NoError(t, err)
174
175	br := bufio.NewReader(conn)
176	resp, err := http.ReadResponse(br, nil)
177	require.NoError(t, err)
178	assert.Equal(t, http.StatusSwitchingProtocols, resp.StatusCode)
179	buf := make([]byte, 5)
180	_, err = io.ReadFull(br, buf)
181	require.NoError(t, err)
182	assert.Equal(t, "hello", string(buf))
183}
184
185func TestTransferIdleTimeout_ReverseProxyFlushesStream(t *testing.T) {
186	// Flush propagation through a wrapped writer, as used by httputil.ReverseProxy.
187	srv := newIdleServer(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
188		rc := http.NewResponseController(w)
189		for range 8 {
190			_, _ = w.Write([]byte("."))
191			if err := rc.Flush(); err != nil {
192				return
193			}
194			time.Sleep(50 * time.Millisecond)
195		}
196	}))
197	resp, err := http.Get(srv.URL)
198	require.NoError(t, err)
199	defer resp.Body.Close()
200	body, err := io.ReadAll(resp.Body)
201	require.NoError(t, err)
202	assert.Len(t, body, 8)
203}
204
205func TestTransferIdleTimeout_DisabledWhenNonPositive(t *testing.T) {
206	called := false
207	inner := http.HandlerFunc(func(http.ResponseWriter, *http.Request) { called = true })
208	h := TransferIdleTimeout(0)(inner)
209	h.ServeHTTP(httptest.NewRecorder(), httptest.NewRequest(http.MethodGet, "/", nil))
210	assert.True(t, called)
211}
212
213func TestTransferIdleTimeout_HTTP2(t *testing.T) {
214	const chunks = 12
215	srv := httptest.NewUnstartedServer(TransferIdleTimeout(testIdle)(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
216		for range chunks {
217			_, _ = w.Write([]byte("x"))
218			w.(http.Flusher).Flush()
219			time.Sleep(50 * time.Millisecond)
220		}
221	})))
222	srv.EnableHTTP2 = true
223	srv.StartTLS()
224	defer srv.Close()
225
226	client := srv.Client()
227	resp, err := client.Get(srv.URL)
228	require.NoError(t, err)
229	defer resp.Body.Close()
230	assert.Equal(t, 2, resp.ProtoMajor)
231	body, err := io.ReadAll(resp.Body)
232	require.NoError(t, err)
233	assert.Len(t, body, chunks)
234}
235
236// pipeListener serves connections created with dial over net.Pipe, which has no
237// kernel buffering: a write completes only when the peer reads it.
238type pipeListener struct {
239	conns chan net.Conn
240	done  chan struct{}
241}
242
243func newPipeListener() *pipeListener {
244	return &pipeListener{conns: make(chan net.Conn), done: make(chan struct{})}
245}
246
247func (l *pipeListener) Accept() (net.Conn, error) {
248	select {
249	case c := <-l.conns:
250		return c, nil
251	case <-l.done:
252		return nil, net.ErrClosed
253	}
254}
255
256func (l *pipeListener) Close() error {
257	select {
258	case <-l.done:
259	default:
260		close(l.done)
261	}
262	return nil
263}
264
265func (l *pipeListener) Addr() net.Addr { return pipeAddr{} }
266
267func (l *pipeListener) dial() net.Conn {
268	client, server := net.Pipe()
269	l.conns <- server
270	return client
271}
272
273type pipeAddr struct{}
274
275func (pipeAddr) Network() string { return "pipe" }
276func (pipeAddr) String() string  { return "pipe" }
277
278// slowReadConn reads at most 4 KiB every pause, like a congested client.
279type slowReadConn struct {
280	net.Conn
281	pause time.Duration
282}
283
284func (c *slowReadConn) Read(p []byte) (int, error) {
285	time.Sleep(c.pause)
286	return c.Conn.Read(p[:min(len(p), 4096)])
287}
288
289func servePipe(t *testing.T, handler http.Handler) *pipeListener {
290	t.Helper()
291	ln := newPipeListener()
292	srv := &http.Server{Handler: TransferIdleTimeout(testIdle)(handler), ReadHeaderTimeout: 5 * time.Second}
293	go func() { _ = srv.Serve(ln) }()
294	t.Cleanup(func() { _ = srv.Close() })
295	return ln
296}
297
298func TestTransferIdleTimeout_BackpressuredButReadingClientIsNotCut(t *testing.T) {
299	const size = 64 * 1024
300	ln := servePipe(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
301		_, _ = w.Write([]byte(strings.Repeat("d", size)))
302	}))
303	client := &http.Client{Transport: &http.Transport{
304		DisableKeepAlives: true,
305		DialContext: func(context.Context, string, string) (net.Conn, error) {
306			return &slowReadConn{Conn: ln.dial(), pause: 25 * time.Millisecond}, nil
307		},
308	}}
309
310	start := time.Now()
311	resp, err := client.Get("http://pipe/")
312	require.NoError(t, err)
313	defer resp.Body.Close()
314	body, err := io.ReadAll(resp.Body)
315	require.NoError(t, err, "a client that keeps reading must not be cut")
316	assert.Len(t, body, size)
317	assert.Greater(t, time.Since(start), testIdle, "the transfer must outlast the idle timeout")
318}
319
320func TestTransferIdleTimeout_NonReadingClientUnblocksWrite(t *testing.T) {
321	writeErr := make(chan error, 1)
322	ln := servePipe(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
323		_, err := w.Write([]byte(strings.Repeat("d", 1<<20)))
324		writeErr <- err
325	}))
326	conn := ln.dial()
327	defer conn.Close()
328	_, err := conn.Write([]byte("GET / HTTP/1.1\r\nHost: x\r\n\r\n"))
329	require.NoError(t, err)
330
331	select {
332	case err := <-writeErr:
333		require.Error(t, err, "write to a client that never reads must fail")
334	case <-time.After(10 * testIdle):
335		t.Fatal("blocked write was not unblocked by the inactivity timeout")
336	}
337}
338
339func TestTransferIdleTimeout_SilentHandlerContextCanceledAndConnectionClosed(t *testing.T) {
340	ctxDone := make(chan struct{})
341	srv := newIdleServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
342		_, _ = w.Write([]byte("start"))
343		w.(http.Flusher).Flush()
344		<-r.Context().Done() // a ReverseProxy would abort its upstream request here
345		close(ctxDone)
346	}))
347
348	client := &http.Client{Timeout: 10 * time.Second}
349	begin := time.Now()
350	resp, err := client.Get(srv.URL)
351	require.NoError(t, err)
352	defer resp.Body.Close()
353	body, err := io.ReadAll(resp.Body)
354	require.Error(t, err, "the client connection must be closed, not completed cleanly")
355	assert.Equal(t, "start", string(body))
356	assert.Less(t, time.Since(begin), 5*time.Second)
357
358	select {
359	case <-ctxDone:
360	case <-time.After(5 * time.Second):
361		t.Fatal("request context was not canceled")
362	}
363}
364
365func TestTransferIdleTimeout_ProgressKeepsContextAlive(t *testing.T) {
366	var canceled atomic.Bool
367	srv := newIdleServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
368		for range 12 {
369			_, _ = w.Write([]byte("x"))
370			w.(http.Flusher).Flush()
371			select {
372			case <-r.Context().Done():
373				canceled.Store(true)
374				return
375			case <-time.After(50 * time.Millisecond):
376			}
377		}
378	}))
379	resp, err := http.Get(srv.URL)
380	require.NoError(t, err)
381	defer resp.Body.Close()
382	body, err := io.ReadAll(resp.Body)
383	require.NoError(t, err)
384	assert.Len(t, body, 12)
385	assert.False(t, canceled.Load())
386}
387
388func TestTransferIdleTimeout_BodyUseAfterHandlerReturnedIsSafe(t *testing.T) {
389	var body io.ReadCloser
390	h := TransferIdleTimeout(time.Second)(http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) {
391		body = r.Body
392	}))
393	req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader("payload"))
394	h.ServeHTTP(httptest.NewRecorder(), req)
395
396	// Like http.Transport closing the outbound body after the handler returned.
397	require.NoError(t, body.Close())
398	require.NoError(t, body.Close(), "Close must be idempotent")
399	_, _ = body.Read(make([]byte, 4))
400}
401
402// TestTransferIdleTimeout_HTTP2ReverseProxyEarlyResponseDuringUpload covers the
403// transport closing the outbound request body after the handler returned, when
404// the upstream answers early (413) while the client is still uploading. Run
405// with -race.
406func TestTransferIdleTimeout_HTTP2ReverseProxyEarlyResponseDuringUpload(t *testing.T) {
407	upstream := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
408		http.Error(w, "too large", http.StatusRequestEntityTooLarge)
409	}))
410	upstream.EnableHTTP2 = true
411	upstream.StartTLS()
412	defer upstream.Close()
413
414	target, err := url.Parse(upstream.URL)
415	require.NoError(t, err)
416	proxy := httputil.NewSingleHostReverseProxy(target)
417	proxy.Transport = &http.Transport{
418		TLSClientConfig:   &tls.Config{InsecureSkipVerify: true}, //nolint:gosec // test upstream
419		ForceAttemptHTTP2: true,
420	}
421	front := httptest.NewUnstartedServer(TransferIdleTimeout(testIdle)(proxy))
422	front.EnableHTTP2 = true
423	front.StartTLS()
424	defer front.Close()
425
426	var wg sync.WaitGroup
427	for range 8 {
428		wg.Add(1)
429		go func() {
430			defer wg.Done()
431			for range 10 {
432				uploadUntilEarlyResponse(t, front)
433			}
434		}()
435	}
436	wg.Wait()
437}
438
439func uploadUntilEarlyResponse(t *testing.T, front *httptest.Server) {
440	t.Helper()
441	pr, pw := io.Pipe()
442	writerDone := make(chan struct{})
443	go func() {
444		defer close(writerDone)
445		chunk := []byte(strings.Repeat("u", 4096))
446		for range 200 {
447			if _, err := pw.Write(chunk); err != nil {
448				return
449			}
450			time.Sleep(time.Millisecond)
451		}
452		_ = pw.Close()
453	}()
454
455	defer func() {
456		_ = pr.CloseWithError(io.ErrClosedPipe)
457		<-writerDone
458	}()
459
460	req, err := http.NewRequest(http.MethodPost, front.URL, pr)
461	if !assert.NoError(t, err) {
462		return
463	}
464	resp, err := front.Client().Do(req)
465	if !assert.NoError(t, err, "the early 413 must reach the client") {
466		return
467	}
468	defer resp.Body.Close()
469	assert.Equal(t, 2, resp.ProtoMajor)
470	assert.Equal(t, http.StatusRequestEntityTooLarge, resp.StatusCode)
471	_, _ = io.Copy(io.Discard, resp.Body)
472}
473
474// TestTransferIdleTimeout_HTTP2BodyUsedAfterHandlerReturned uses the request
475// body after net/http's HTTP/2 server finished the handler (it drops its
476// response writer state then), as http.Transport does when it closes an
477// outbound body asynchronously. It must neither panic nor race.
478func TestTransferIdleTimeout_HTTP2BodyUsedAfterHandlerReturned(t *testing.T) {
479	bodyCh := make(chan io.ReadCloser, 1)
480	returned := make(chan struct{})
481	inner := TransferIdleTimeout(testIdle)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
482		bodyCh <- r.Body
483		_, _ = w.Write([]byte("early"))
484	}))
485	srv := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
486		inner.ServeHTTP(w, r)
487		close(returned)
488	}))
489	srv.EnableHTTP2 = true
490	srv.StartTLS()
491	defer srv.Close()
492
493	pr, pw := io.Pipe()
494	defer pw.Close()
495	req, err := http.NewRequest(http.MethodPost, srv.URL, pr)
496	require.NoError(t, err)
497	go func() { _, _ = pw.Write([]byte("partial")) }()
498	resp, err := srv.Client().Do(req)
499	require.NoError(t, err)
500	_, _ = io.Copy(io.Discard, resp.Body)
501	_ = resp.Body.Close()
502
503	body := <-bodyCh
504	<-returned
505	time.Sleep(100 * time.Millisecond) // let the server loop process handlerDone
506
507	_, _ = body.Read(make([]byte, 8))
508	require.NoError(t, body.Close())
509	require.NoError(t, body.Close())
510}
511
512// trackedPipeServer serves net.Pipe connections and reports when the server
513// side closes one, so tests can assert a stalled connection is released.
514func trackedPipeServer(t *testing.T, handler http.Handler) (*pipeListener, <-chan struct{}) {
515	t.Helper()
516	closed := make(chan struct{})
517	var once sync.Once
518	ln := newPipeListener()
519	srv := &http.Server{
520		Handler:           TransferIdleTimeout(testIdle)(handler),
521		ReadHeaderTimeout: 5 * time.Second,
522		ConnState: func(_ net.Conn, state http.ConnState) {
523			if state == http.StateClosed {
524				once.Do(func() { close(closed) })
525			}
526		},
527	}
528	go func() { _ = srv.Serve(ln) }()
529	t.Cleanup(func() { _ = srv.Close() })
530	return ln, closed
531}
532
533func requireClosed(t *testing.T, closed <-chan struct{}, what string) {
534	t.Helper()
535	select {
536	case <-closed:
537	case <-time.After(20 * testIdle):
538		t.Fatalf("server connection still held: %s", what)
539	}
540}
541
542// The handler returns with a small unflushed response; net/http's final flush
543// then blocks on a client that never reads (net.Pipe has no buffering).
544func TestTransferIdleTimeout_FinalFlushToNonReadingClientIsBounded(t *testing.T) {
545	ln, closed := trackedPipeServer(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
546		_, _ = w.Write([]byte("small"))
547	}))
548	conn := ln.dial()
549	defer conn.Close()
550	_, err := conn.Write([]byte("GET / HTTP/1.1\r\nHost: x\r\n\r\n"))
551	require.NoError(t, err)
552
553	requireClosed(t, closed, "final response flush to a non-reading client")
554}
555
556// The client declares a body it never sends and the handler answers without
557// reading it; net/http then tries to drain the unread body.
558func TestTransferIdleTimeout_UnreadDeclaredBodyDrainIsBounded(t *testing.T) {
559	ln, closed := trackedPipeServer(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
560		http.Error(w, "unauthorized", http.StatusUnauthorized)
561	}))
562	conn := ln.dial()
563	defer conn.Close()
564	go func() { _, _ = io.Copy(io.Discard, conn) }() // read the response, then stay silent
565	_, err := conn.Write([]byte("POST / HTTP/1.1\r\nHost: x\r\nContent-Length: 100\r\n\r\n"))
566	require.NoError(t, err)
567
568	requireClosed(t, closed, "drain of an unread declared request body")
569}
570
571func TestTransferIdleTimeout_UnreadDeclaredBodyDrainIsBoundedOverTCP(t *testing.T) {
572	closed := make(chan struct{})
573	var once sync.Once
574	srv := httptest.NewUnstartedServer(TransferIdleTimeout(testIdle)(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
575		http.Error(w, "unauthorized", http.StatusUnauthorized)
576	})))
577	srv.Config.ConnState = func(_ net.Conn, state http.ConnState) {
578		if state == http.StateClosed {
579			once.Do(func() { close(closed) })
580		}
581	}
582	srv.Start()
583	defer srv.Close()
584
585	conn, err := net.Dial("tcp", srv.Listener.Addr().String())
586	require.NoError(t, err)
587	defer conn.Close()
588	_, err = conn.Write([]byte("POST / HTTP/1.1\r\nHost: x\r\nContent-Length: 100\r\n\r\n"))
589	require.NoError(t, err)
590
591	requireClosed(t, closed, "drain of an unread declared request body over TCP")
592}
593
594// Leftover deadlines armed at handler return must not leak into the next
595// keep-alive request on the same connection.
596func TestTransferIdleTimeout_KeepAliveNextRequestNotAffectedByLeftoverDeadlines(t *testing.T) {
597	srv := httptest.NewUnstartedServer(TransferIdleTimeout(testIdle)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
598		_, _ = io.Copy(io.Discard, r.Body)
599		_, _ = w.Write([]byte("ok"))
600	})))
601	srv.Config.IdleTimeout = 10 * time.Second
602	srv.Start()
603	defer srv.Close()
604
605	client := &http.Client{Transport: &http.Transport{MaxConnsPerHost: 1}}
606	for i := range 3 {
607		resp, err := client.Post(srv.URL, "text/plain", strings.NewReader("body"))
608		require.NoError(t, err, "request %d", i)
609		_, err = io.ReadAll(resp.Body)
610		require.NoError(t, err)
611		_ = resp.Body.Close()
612		time.Sleep(2 * testIdle) // outlive the leftover deadline while the connection idles
613	}
614}
615
616// Hijack flushes pending headers; it must not hold the timer's mutex while it
617// blocks on a client that does not read.
618func TestTransferIdleTimeout_HijackBlockedOnNonReadingClientIsBounded(t *testing.T) {
619	hijackErr := make(chan error, 1)
620	ln := servePipe(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
621		w.WriteHeader(http.StatusOK)
622		conn, _, err := w.(http.Hijacker).Hijack()
623		if conn != nil {
624			_ = conn.Close()
625		}
626		hijackErr <- err
627	}))
628	conn := ln.dial()
629	defer conn.Close()
630	_, err := conn.Write([]byte("GET / HTTP/1.1\r\nHost: x\r\n\r\n"))
631	require.NoError(t, err)
632
633	select {
634	case <-hijackErr:
635	case <-time.After(20 * testIdle):
636		t.Fatal("Hijack blocked on a non-reading client was not bounded")
637	}
638}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close