@marcel91200 / caddy Mirror · read-only / commits / 053be179
Fork 0

Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS

Mirrored from https://github.com/Slazee/caddy.git

Synced Nov 11, 01:00 UTC

ci: test and release with Go 1.27 (#8210)

* caddyhttp: add HTTP/1 body reuse regression tests * ci: test and release with Go 1.27 --------- Co-authored-by: steadytao <mail@steadytao.com>

Krish Singh Hura committed Nov 3, 2026 at 10:11 UTC 053be179d8602b65d93db6980397c176ecba2f89
4 files changed +179 -9
.github/workflows/ci.yml
+4 -4
@@ -31,13 +31,13 @@ jobs:
31 - mac
32 - windows
33 go:
34 - - '1.26'
34 + - '1.27'
35
36 include:
37 # Set the minimum Go patch version for the given Go minor
38 # Usable via ${{ matrix.GO_SEMVER }}
39 - - go: '1.26'
40 - GO_SEMVER: '~1.26.0'
39 + - go: '1.27'
40 + GO_SEMVER: '~1.27.0'
41
42 # Set some variables per OS, usable via ${{ matrix.VAR }}
43 # OS_LABEL: the VM label from GitHub Actions (see https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners/about-github-hosted-runners#standard-github-hosted-runners-for-public-repositories)
@@ -237,7 +237,7 @@ jobs:
237 - name: Install Go
238 uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
239 with:
240 - go-version: "~1.26"
240 + go-version: "~1.27"
241 check-latest: true
242 - name: Install xcaddy
243 run: |
.github/workflows/lint.yml
+2 -2
@@ -52,7 +52,7 @@ jobs:
52 - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
53 - uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
54 with:
55 - go-version: '~1.26'
55 + go-version: '~1.27'
56 check-latest: true
57
58 - name: golangci-lint
@@ -80,7 +80,7 @@ jobs:
80 - name: govulncheck
81 uses: golang/govulncheck-action@b625fbe08f3bccbe446d94fbf87fcc875a4f50ee # v1.0.4
82 with:
83 - go-version-input: '~1.26.0'
83 + go-version-input: '~1.27.0'
84 check-latest: true
85
86 dependency-review:
.github/workflows/release.yml
+3 -3
@@ -334,13 +334,13 @@ jobs:
334 os:
335 - ubuntu-latest
336 go:
337 - - '1.26'
337 + - '1.27'
338
339 include:
340 # Set the minimum Go patch version for the given Go minor
341 # Usable via ${{ matrix.GO_SEMVER }}
342 - - go: '1.26'
343 - GO_SEMVER: '~1.26.0'
342 + - go: '1.27'
343 + GO_SEMVER: '~1.27.0'
344
345 runs-on: ${{ matrix.os }}
346 # https://github.com/sigstore/cosign/issues/1258#issuecomment-1002251233
modules/caddyhttp/server_test.go
+170
@@ -1773,3 +1773,173 @@ func TestServerLogLevel(t *testing.T) {
1773 })
1774 }
1775 }
1776 +
1777 +// TestServer_ClosedRequestBodyDoesNotDesyncHTTP1 verifies that an early
1778 +// response after a handler closes its request body cannot leave the unread
1779 +// body bytes to be parsed as the next request. This models a reverse proxy
1780 +// transport which receives an early upstream response and then closes the
1781 +// downstream request body.
1782 +func TestServer_ClosedRequestBodyDoesNotDesyncHTTP1(t *testing.T) {
1783 + const firstChunkSize = 1024
1784 +
1785 + for _, tc := range []struct {
1786 + name string
1787 + chunked bool
1788 + bodyLen int
1789 + }{
1790 + {name: "fixed length above drain limit", bodyLen: 1 << 20},
1791 + {name: "chunked above drain limit", chunked: true, bodyLen: 1 << 20},
1792 + } {
1793 + t.Run(tc.name, func(t *testing.T) {
1794 + secondRequest := make(chan struct{}, 1)
1795 + server := &Server{
1796 + ReadIdleTimeout: caddy.Duration(time.Minute),
1797 + logger: zap.NewNop(),
1798 + errorLogger: zap.NewNop(),
1799 + primaryHandlerChain: HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
1800 + switch r.URL.Path {
1801 + case "/early":
1802 + buf := make([]byte, firstChunkSize)
1803 + if _, err := io.ReadFull(r.Body, buf); err != nil {
1804 + return fmt.Errorf("read first request chunk: %w", err)
1805 + }
1806 + if err := r.Body.Close(); err != nil {
1807 + return fmt.Errorf("close first request body: %w", err)
1808 + }
1809 + w.Header().Set("Content-Length", "0")
1810 + w.WriteHeader(http.StatusLocked)
1811 + case "/second":
1812 + secondRequest <- struct{}{}
1813 + w.Header().Set("Content-Length", "0")
1814 + w.WriteHeader(http.StatusNoContent)
1815 + default:
1816 + return Error(http.StatusNotFound, nil)
1817 + }
1818 + return nil
1819 + }),
1820 + }
1821 + ts := httptest.NewServer(http.HandlerFunc(server.ServeHTTP))
1822 + t.Cleanup(ts.Close)
1823 +
1824 + conn, err := net.DialTimeout("tcp", ts.Listener.Addr().String(), time.Second)
1825 + require.NoError(t, err)
1826 + t.Cleanup(func() { _ = conn.Close() })
1827 + require.NoError(t, conn.SetDeadline(time.Now().Add(5*time.Second)))
1828 +
1829 + reader := bufio.NewReader(conn)
1830 + if tc.chunked {
1831 + _, err = fmt.Fprintf(conn, "PUT /early HTTP/1.1\r\nHost: %s\r\nTransfer-Encoding: chunked\r\n\r\n%x\r\n%s\r\n", ts.Listener.Addr(), firstChunkSize, bytes.Repeat([]byte{'a'}, firstChunkSize))
1832 + } else {
1833 + _, err = fmt.Fprintf(conn, "PUT /early HTTP/1.1\r\nHost: %s\r\nContent-Length: %d\r\n\r\n%s", ts.Listener.Addr(), tc.bodyLen, bytes.Repeat([]byte{'a'}, firstChunkSize))
1834 + }
1835 + require.NoError(t, err)
1836 +
1837 + writerDone := make(chan struct{})
1838 + if tc.chunked {
1839 + go func() {
1840 + defer close(writerDone)
1841 + for sent := firstChunkSize; sent < tc.bodyLen; {
1842 + n := min(8<<10, tc.bodyLen-sent)
1843 + if _, err := fmt.Fprintf(conn, "%x\r\n%s\r\n", n, bytes.Repeat([]byte{'b'}, n)); err != nil {
1844 + return
1845 + }
1846 + sent += n
1847 + time.Sleep(time.Millisecond)
1848 + }
1849 + _, _ = io.WriteString(conn, "0\r\n\r\n")
1850 + }()
1851 + }
1852 +
1853 + first, err := http.ReadResponse(reader, &http.Request{Method: http.MethodPut})
1854 + require.NoError(t, err)
1855 + require.Equal(t, http.StatusLocked, first.StatusCode)
1856 + require.NoError(t, first.Body.Close())
1857 +
1858 + if tc.chunked {
1859 + <-writerDone
1860 + } else {
1861 + _, _ = conn.Write(bytes.Repeat([]byte{'b'}, tc.bodyLen-firstChunkSize))
1862 + }
1863 + _, _ = fmt.Fprintf(conn, "GET /second HTTP/1.1\r\nHost: %s\r\nConnection: keep-alive\r\n\r\n", ts.Listener.Addr())
1864 +
1865 + second, err := http.ReadResponse(reader, &http.Request{Method: http.MethodGet})
1866 + if err != nil {
1867 + if errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF) || errors.Is(err, net.ErrClosed) {
1868 + return // The server safely closed the connection after the early response.
1869 + }
1870 + var netErr net.Error
1871 + if errors.As(err, &netErr) && !netErr.Timeout() {
1872 + return // The peer reset the connection after the early response.
1873 + }
1874 + t.Fatalf("read second response: %v", err)
1875 + }
1876 + defer second.Body.Close()
1877 + if second.StatusCode != http.StatusNoContent {
1878 + t.Fatalf("second response status = %d, want %d or a closed connection", second.StatusCode, http.StatusNoContent)
1879 + }
1880 + select {
1881 + case <-secondRequest:
1882 + case <-time.After(time.Second):
1883 + t.Fatal("second request response was not produced by the handler")
1884 + }
1885 + })
1886 + }
1887 +}
1888 +
1889 +func TestServer_FullyReadRequestBodyPreservesHTTP1Connection(t *testing.T) {
1890 + for _, tc := range []struct {
1891 + name string
1892 + chunked bool
1893 + }{
1894 + {name: "fixed length"},
1895 + {name: "chunked", chunked: true},
1896 + } {
1897 + t.Run(tc.name, func(t *testing.T) {
1898 + remoteAddrs := make(chan string, 2)
1899 + server := &Server{
1900 + ReadIdleTimeout: caddy.Duration(time.Minute),
1901 + logger: zap.NewNop(),
1902 + errorLogger: zap.NewNop(),
1903 + primaryHandlerChain: HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
1904 + remoteAddrs <- r.RemoteAddr
1905 + if r.URL.Path == "/first" {
1906 + if _, err := io.Copy(io.Discard, r.Body); err != nil {
1907 + return err
1908 + }
1909 + }
1910 + w.Header().Set("Content-Length", "0")
1911 + w.WriteHeader(http.StatusNoContent)
1912 + return nil
1913 + }),
1914 + }
1915 + ts := httptest.NewServer(http.HandlerFunc(server.ServeHTTP))
1916 + t.Cleanup(ts.Close)
1917 +
1918 + conn, err := net.DialTimeout("tcp", ts.Listener.Addr().String(), time.Second)
1919 + require.NoError(t, err)
1920 + t.Cleanup(func() { _ = conn.Close() })
1921 + require.NoError(t, conn.SetDeadline(time.Now().Add(5*time.Second)))
1922 +
1923 + if tc.chunked {
1924 + _, err = fmt.Fprintf(conn, "POST /first HTTP/1.1\r\nHost: %s\r\nTransfer-Encoding: chunked\r\n\r\n4\r\nbody\r\n0\r\n\r\n", ts.Listener.Addr())
1925 + } else {
1926 + _, err = fmt.Fprintf(conn, "POST /first HTTP/1.1\r\nHost: %s\r\nContent-Length: 4\r\n\r\nbody", ts.Listener.Addr())
1927 + }
1928 + require.NoError(t, err)
1929 + reader := bufio.NewReader(conn)
1930 + first, err := http.ReadResponse(reader, &http.Request{Method: http.MethodPost})
1931 + require.NoError(t, err)
1932 + require.Equal(t, http.StatusNoContent, first.StatusCode)
1933 + require.NoError(t, first.Body.Close())
1934 +
1935 + _, err = fmt.Fprintf(conn, "GET /second HTTP/1.1\r\nHost: %s\r\n\r\n", ts.Listener.Addr())
1936 + require.NoError(t, err)
1937 + second, err := http.ReadResponse(reader, &http.Request{Method: http.MethodGet})
1938 + require.NoError(t, err)
1939 + require.Equal(t, http.StatusNoContent, second.StatusCode)
1940 + require.NoError(t, second.Body.Close())
1941 +
1942 + assert.Equal(t, <-remoteAddrs, <-remoteAddrs)
1943 + })
1944 + }
1945 +}