Compare commits

..

1 Commits

Author SHA1 Message Date
Shirshendu Bhowmick
db9daf4551
Merge 16857146c8961f809f1a6735e84556d5835eaec9 into c79f5d466ee780c329663d5b599d40639b33cd93 2026-06-02 19:17:56 +05:30
8 changed files with 41 additions and 322 deletions

View File

@ -142,16 +142,6 @@ func (c *Context) Copy() *Context {
cp.Params = make([]Param, len(cParams))
copy(cp.Params, cParams)
if c.Errors != nil {
cp.Errors = make(errorMsgs, len(c.Errors))
copy(cp.Errors, c.Errors)
}
if c.Accepted != nil {
cp.Accepted = make([]string, len(c.Accepted))
copy(cp.Accepted, c.Accepted)
}
return &cp
}
@ -630,8 +620,8 @@ func (c *Context) DefaultPostForm(key, defaultValue string) string {
// For example, during a PATCH request to update the user's email:
//
// email=mail@example.com --> ("mail@example.com", true) := GetPostForm("email") // set email to "mail@example.com"
// email= --> ("", true) := GetPostForm("email") // set email to ""
// --> ("", false) := GetPostForm("email") // do nothing with email
// email= --> ("", true) := GetPostForm("email") // set email to ""
// --> ("", false) := GetPostForm("email") // do nothing with email
func (c *Context) GetPostForm(key string) (string, bool) {
if values, ok := c.GetPostFormArray(key); ok {
return values[0], ok
@ -727,11 +717,6 @@ func (c *Context) MultipartForm() (*multipart.Form, error) {
}
// SaveUploadedFile uploads the form file to specific dst.
// An optional perm argument specifies the permission bits used when creating
// the destination directory. If not provided, the default is 0750. The exact
// permission is enforced only on the destination directory and only when it is
// newly created by this call; pre-existing directories (e.g. /tmp) are not
// modified.
func (c *Context) SaveUploadedFile(file *multipart.FileHeader, dst string, perm ...fs.FileMode) error {
src, err := file.Open()
if err != nil {
@ -744,19 +729,11 @@ func (c *Context) SaveUploadedFile(file *multipart.FileHeader, dst string, perm
mode = perm[0]
}
dir := filepath.Dir(dst)
// Record whether the destination directory exists before MkdirAll, so we
// only chmod a directory we just created. Chmod'ing a pre-existing directory
// the process does not own (e.g. /tmp) fails with "operation not permitted"
// (#4622). A non-ErrNotExist stat error also skips chmod and lets MkdirAll
// surface the underlying failure.
_, statErr := os.Stat(dir)
if err = os.MkdirAll(dir, mode); err != nil {
return err
}
if errors.Is(statErr, os.ErrNotExist) {
if err = os.Chmod(dir, mode); err != nil {
return err
}
if err = os.Chmod(dir, mode); err != nil {
return err
}
out, err := os.Create(dst)
@ -1075,33 +1052,6 @@ func (c *Context) IsWebsocket() bool {
return false
}
// Scheme returns the HTTP scheme of the request ("http" or "https").
// When running behind reverse proxies or load balancers `Request.URL.Scheme` is usually empty.
// the original scheme is commonly forwarded via headers such as X-Forwarded-Proto.
// Reference:
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/X-Forwarded-Proto
func (c *Context) Scheme() string {
if c.Request.TLS != nil {
return "https"
}
if scheme := c.requestHeader("X-Forwarded-Proto"); scheme != "" {
return scheme
}
if scheme := c.requestHeader("X-Forwarded-Protocol"); scheme != "" {
return scheme
}
if ssl := c.requestHeader("X-Forwarded-Ssl"); ssl == "on" {
return "https"
}
if scheme := c.requestHeader("X-Url-Scheme"); scheme != "" {
return scheme
}
if scheme := c.Request.URL.Scheme; scheme != "" {
return scheme
}
return "http"
}
func (c *Context) requestHeader(key string) string {
return c.Request.Header.Get(key)
}

View File

@ -7,7 +7,6 @@ package gin
import (
"bytes"
"context"
"crypto/tls"
"errors"
"fmt"
"html/template"
@ -249,11 +248,13 @@ func TestSaveUploadedFileWithPermission(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "permission_test", f.Filename)
var mode fs.FileMode = 0o755
dst := filepath.Join(t.TempDir(), "subdir", "permission_test")
require.NoError(t, c.SaveUploadedFile(f, dst, mode))
info, err := os.Stat(filepath.Dir(dst))
require.NoError(t, c.SaveUploadedFile(f, "permission_test", mode))
t.Cleanup(func() {
assert.NoError(t, os.Remove("permission_test"))
})
info, err := os.Stat(filepath.Dir("permission_test"))
require.NoError(t, err)
assert.Equal(t, mode, info.Mode().Perm())
assert.Equal(t, info.Mode().Perm(), mode)
}
func TestSaveUploadedFileWithPermissionFailed(t *testing.T) {
@ -271,52 +272,7 @@ func TestSaveUploadedFileWithPermissionFailed(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "permission_test", f.Filename)
var mode fs.FileMode = 0o644
dst := filepath.Join(t.TempDir(), "test", "permission_test")
require.Error(t, c.SaveUploadedFile(f, dst, mode))
}
// TestSaveUploadedFileToExistingDir is a regression test for issue #4622.
// SaveUploadedFile must not call os.Chmod on a directory that already exists,
// because the process may not own it (e.g. /tmp on Linux/macOS), where chmod
// fails with "operation not permitted". This asserts the behavioral contract
// directly — a pre-existing directory's permissions are left unchanged — so it
// catches the regression on every platform, including environments (root/CI,
// user-owned $TMPDIR) where chmod on the temp dir would otherwise succeed.
func TestSaveUploadedFileToExistingDir(t *testing.T) {
buf := new(bytes.Buffer)
mw := multipart.NewWriter(buf)
w, err := mw.CreateFormFile("file", "existing_dir_test")
require.NoError(t, err)
_, err = w.Write([]byte("existing_dir_test"))
require.NoError(t, err)
mw.Close()
c, _ := CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest(http.MethodPost, "/", buf)
c.Request.Header.Set("Content-Type", mw.FormDataContentType())
f, err := c.FormFile("file")
require.NoError(t, err)
// A pre-existing directory owned by this process, set to a known mode.
dir := t.TempDir()
require.NoError(t, os.Chmod(dir, 0o700))
// Pass a perm that differs from the directory's current mode. The fix must
// not apply it to the pre-existing directory; the old code chmod'd it
// unconditionally, which also failed outright on unowned dirs like /tmp.
dst := filepath.Join(dir, "existing_dir_test.txt")
require.NoError(t, c.SaveUploadedFile(f, dst, 0o755))
// The pre-existing directory's permissions must be unchanged.
info, err := os.Stat(dir)
require.NoError(t, err)
assert.Equal(t, os.FileMode(0o700), info.Mode().Perm(),
"permissions of a pre-existing directory must not be modified")
// The file must still be written with the correct content.
content, err := os.ReadFile(dst)
require.NoError(t, err)
assert.Equal(t, "existing_dir_test", string(content))
require.Error(t, c.SaveUploadedFile(f, "test/permission_test", mode))
}
func TestContextReset(t *testing.T) {
@ -733,50 +689,6 @@ func TestContextCopy(t *testing.T) {
assert.Equal(t, cp.fullPath, c.fullPath)
}
func TestContextCopyCopiesErrors(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest(http.MethodGet, "/", nil)
_ = c.Error(errors.New("first error"))
_ = c.Error(errors.New("second error"))
cp := c.Copy()
// copied context has the same errors
assert.Len(t, cp.Errors, 2)
assert.Equal(t, c.Errors[0].Error(), cp.Errors[0].Error())
assert.Equal(t, c.Errors[1].Error(), cp.Errors[1].Error())
// mutations on the copy do not affect the original
_ = cp.Error(errors.New("third error"))
assert.Len(t, c.Errors, 2)
assert.Len(t, cp.Errors, 3)
}
func TestContextCopyCopiesAccepted(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest(http.MethodGet, "/", nil)
c.SetAccepted("application/json", "text/html")
cp := c.Copy()
assert.Equal(t, c.Accepted, cp.Accepted)
// mutations on the copy do not affect the original
cp.SetAccepted("text/plain")
assert.Equal(t, []string{"application/json", "text/html"}, c.Accepted)
assert.Equal(t, []string{"text/plain"}, cp.Accepted)
}
func TestContextCopyNilErrorsAndAccepted(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest(http.MethodGet, "/", nil)
cp := c.Copy()
assert.Nil(t, cp.Errors)
assert.Nil(t, cp.Accepted)
}
func TestContextHandlerName(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder())
c.handlers = HandlersChain{func(c *Context) {}, handlerNameTest}
@ -3050,65 +2962,6 @@ func TestWebsocketsRequired(t *testing.T) {
assert.False(t, c.IsWebsocket())
}
func TestContextScheme(t *testing.T) {
// TLS connection takes highest priority.
c, _ := CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest(http.MethodGet, "/", nil)
c.Request.TLS = &tls.ConnectionState{}
assert.Equal(t, "https", c.Scheme())
// X-Forwarded-Proto header.
c, _ = CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest(http.MethodGet, "/", nil)
c.Request.Header.Set("X-Forwarded-Proto", "https")
assert.Equal(t, "https", c.Scheme())
c, _ = CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest(http.MethodGet, "/", nil)
c.Request.Header.Set("X-Forwarded-Proto", "http")
assert.Equal(t, "http", c.Scheme())
// X-Forwarded-Protocol header.
c, _ = CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest(http.MethodGet, "/", nil)
c.Request.Header.Set("X-Forwarded-Protocol", "https")
assert.Equal(t, "https", c.Scheme())
// X-Forwarded-Ssl: on header.
c, _ = CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest(http.MethodGet, "/", nil)
c.Request.Header.Set("X-Forwarded-Ssl", "on")
assert.Equal(t, "https", c.Scheme())
c, _ = CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest(http.MethodGet, "/", nil)
c.Request.Header.Set("X-Forwarded-Ssl", "off")
assert.Equal(t, "http", c.Scheme())
// X-Url-Scheme header.
c, _ = CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest(http.MethodGet, "/", nil)
c.Request.Header.Set("X-Url-Scheme", "https")
assert.Equal(t, "https", c.Scheme())
// Request.URL.Scheme fallback.
c, _ = CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest(http.MethodGet, "https://example.com/", nil)
assert.Equal(t, "https", c.Scheme())
// Default fallback: plain http.
c, _ = CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest(http.MethodGet, "/", nil)
assert.Equal(t, "http", c.Scheme())
// TLS takes priority over X-Forwarded-Proto.
c, _ = CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest(http.MethodGet, "/", nil)
c.Request.TLS = &tls.ConnectionState{}
c.Request.Header.Set("X-Forwarded-Proto", "http")
assert.Equal(t, "https", c.Scheme())
}
func TestGetRequestHeaderValue(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest(http.MethodGet, "/chat", nil)

10
go.mod
View File

@ -12,11 +12,11 @@ require (
github.com/mattn/go-isatty v0.0.20
github.com/modern-go/reflect2 v1.0.2
github.com/pelletier/go-toml/v2 v2.2.4
github.com/quic-go/quic-go v0.60.0
github.com/quic-go/quic-go v0.59.0
github.com/stretchr/testify v1.11.1
github.com/ugorji/go/codec v1.3.1
go.mongodb.org/mongo-driver/v2 v2.5.0
golang.org/x/net v0.55.0
golang.org/x/net v0.52.0
google.golang.org/protobuf v1.36.11
)
@ -39,7 +39,7 @@ require (
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
go.uber.org/mock v0.6.0 // indirect
golang.org/x/arch v0.25.0 // indirect
golang.org/x/crypto v0.51.0 // indirect
golang.org/x/sys v0.45.0 // indirect
golang.org/x/text v0.37.0 // indirect
golang.org/x/crypto v0.49.0 // indirect
golang.org/x/sys v0.42.0 // indirect
golang.org/x/text v0.35.0 // indirect
)

22
go.sum
View File

@ -50,12 +50,10 @@ github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/quic-go/go-ossfuzz-seeds v0.1.0 h1:APacT+iIaNF6fd8AGEiN3bT/Jtkd2jz4v4TzM7MFjy0=
github.com/quic-go/go-ossfuzz-seeds v0.1.0/go.mod h1:3IOHRbJIc+L6YKMwfDtJAM9Vj9k0YY4muhuyUYk5tbk=
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
github.com/quic-go/quic-go v0.60.0 h1:xcQioE8OM66UQLeUMHltK1CCcOu3JbVB4JAQdDQSB+0=
github.com/quic-go/quic-go v0.60.0/go.mod h1:wpKpjmPpftl30sL6pFh7REVpjbcCVy4zt2vDyK1TuJk=
github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw=
github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
@ -79,15 +77,15 @@ go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
golang.org/x/arch v0.25.0 h1:qnk6Ksugpi5Bz32947rkUgDt9/s5qvqDPl/gBKdMJLE=
golang.org/x/arch v0.25.0/go.mod h1:0X+GdSIP+kL5wPmpK7sdkEVTt2XoYP0cSjQSbZBwOi8=
golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=
golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8=
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

View File

@ -106,12 +106,7 @@ func secureRequestDump(r *http.Request) string {
return strings.Join(lines, "\r\n")
}
func defaultHandleRecovery(c *Context, err any) {
e, ok := err.(error)
if !ok {
e = fmt.Errorf("%v", err)
}
c.Error(e) //nolint: errcheck
func defaultHandleRecovery(c *Context, _ any) {
c.AbortWithStatus(http.StatusInternalServerError)
}

View File

@ -5,7 +5,6 @@
package gin
import (
"errors"
"net"
"net/http"
"os"
@ -153,49 +152,6 @@ func TestPanicWithAbortHandler(t *testing.T) {
assert.NotContains(t, out, "panic recovered")
}
func TestPanicInHandlerRecordsError(t *testing.T) {
tests := []struct {
name string
recoveredErr any
expectedErr string
}{
{
name: "string panic",
recoveredErr: "Oops, Houston, we have a problem",
expectedErr: "Oops, Houston, we have a problem",
},
{
name: "error panic",
recoveredErr: errors.New("recovered error"),
expectedErr: "recovered error",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
router := New()
var recoveredErrors errorMsgs
router.Use(func(c *Context) {
c.Next()
recoveredErrors = c.Errors
})
router.Use(RecoveryWithWriter(nil))
router.GET("/recovery", func(_ *Context) {
panic(tt.recoveredErr)
})
w := PerformRequest(router, http.MethodGet, "/recovery")
assert.Equal(t, http.StatusInternalServerError, w.Code)
if assert.Len(t, recoveredErrors, 1) {
assert.EqualError(t, recoveredErrors[0], tt.expectedErr)
assert.Equal(t, ErrorTypePrivate, recoveredErrors[0].Type)
}
})
}
}
func TestCustomRecoveryWithWriter(t *testing.T) {
errBuffer := new(strings.Builder)
buffer := new(strings.Builder)

View File

@ -114,22 +114,15 @@ func (w *responseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
if w.size > 0 {
return nil, nil, errHijackAlreadyWritten
}
hijacker, ok := w.ResponseWriter.(http.Hijacker)
if !ok {
return nil, nil, http.ErrNotSupported
}
if w.size < 0 {
w.size = 0
}
return hijacker.Hijack()
return w.ResponseWriter.(http.Hijacker).Hijack()
}
// CloseNotify implements the http.CloseNotifier interface.
func (w *responseWriter) CloseNotify() <-chan bool {
if cn, ok := w.ResponseWriter.(http.CloseNotifier); ok {
return cn.CloseNotify()
}
return nil
return w.ResponseWriter.(http.CloseNotifier).CloseNotify()
}
// Flush implements the http.Flusher interface.

View File

@ -15,33 +15,10 @@ import (
"github.com/stretchr/testify/require"
)
// TestResponseWriterFlushWithFlusher verifies Flush() calls the underlying Flusher.
func TestResponseWriterFlushWithFlusher(t *testing.T) {
testWriter := httptest.NewRecorder()
writer := &responseWriter{ResponseWriter: testWriter}
writer.Flush()
assert.True(t, testWriter.Flushed)
}
// TestResponseWriterFlushWithNonFlusher verifies Flush() is a no-op
// when the underlying ResponseWriter does not implement http.Flusher.
// Guards against the panic reported in https://github.com/gin-gonic/gin/issues/4460
func TestResponseWriterFlushWithNonFlusher(t *testing.T) {
nonFlusher := &nonFlusherWriter{header: http.Header{}}
writer := &responseWriter{ResponseWriter: nonFlusher}
require.NotPanics(t, func() {
writer.Flush()
})
}
// nonFlusherWriter is a minimal http.ResponseWriter that does NOT implement http.Flusher.
type nonFlusherWriter struct {
header http.Header
}
func (w *nonFlusherWriter) Header() http.Header { return w.header }
func (w *nonFlusherWriter) Write(b []byte) (int, error) { return len(b), nil }
func (w *nonFlusherWriter) WriteHeader(code int) {}
// TODO
// func (w *responseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
// func (w *responseWriter) CloseNotify() <-chan bool {
// func (w *responseWriter) Flush() {
var (
_ ResponseWriter = &responseWriter{}
@ -136,18 +113,15 @@ func TestResponseWriterHijack(t *testing.T) {
writer.reset(testWriter)
w := ResponseWriter(writer)
// httptest.ResponseRecorder doesn't implement http.Hijacker; return
// http.ErrNotSupported instead of panicking (#4638). On unsupported the
// writer state stays untouched so the handler can still emit a normal
// HTTP response as a fallback.
conn, buf, err := w.Hijack()
assert.Nil(t, conn)
assert.Nil(t, buf)
require.ErrorIs(t, err, http.ErrNotSupported)
assert.False(t, w.Written())
assert.Panics(t, func() {
_, _, err := w.Hijack()
require.NoError(t, err)
})
assert.True(t, w.Written())
// CloseNotify on a non-CloseNotifier returns nil instead of panicking.
assert.Nil(t, w.CloseNotify())
assert.Panics(t, func() {
w.CloseNotify()
})
w.Flush()
}