diff --git a/ctx.go b/ctx.go index 9d73c08bd4..3af6b600e7 100644 --- a/ctx.go +++ b/ctx.go @@ -1986,3 +1986,20 @@ func (c *DefaultCtx) Drop() error { //nolint:wrapcheck // error wrapping is avoided to keep the operation lightweight and focused on connection closure. return c.RequestCtx().Conn().Close() } + +// End immediately flushes the current response and closes the underlying connection. +func (c *DefaultCtx) End() error { + ctx := c.RequestCtx() + conn := ctx.Conn() + + bw := bufio.NewWriter(conn) + if err := ctx.Response.Write(bw); err != nil { + return err + } + + if err := bw.Flush(); err != nil { + return err //nolint:wrapcheck // unnecessary to wrap it + } + + return conn.Close() //nolint:wrapcheck // unnecessary to wrap it +} diff --git a/ctx_interface_gen.go b/ctx_interface_gen.go index cd93c48905..101068a269 100644 --- a/ctx_interface_gen.go +++ b/ctx_interface_gen.go @@ -12,7 +12,8 @@ import ( "github.com/valyala/fasthttp" ) -// Ctx represents the Context which hold the HTTP request and response.\nIt has methods for the request query string, parameters, body, HTTP headers and so on. +// Ctx represents the Context which hold the HTTP request and response. +// It has methods for the request query string, parameters, body, HTTP headers and so on. type Ctx interface { // Accepts checks if the specified extensions or content types are acceptable. Accepts(offers ...string) string @@ -353,4 +354,6 @@ type Ctx interface { // This can be useful for silently terminating client connections, such as in DDoS mitigation // or when blocking access to sensitive endpoints. Drop() error + // End immediately flushes the current response and closes the underlying connection. + End() error } diff --git a/ctx_test.go b/ctx_test.go index f094a2c494..af3088662c 100644 --- a/ctx_test.go +++ b/ctx_test.go @@ -5931,6 +5931,83 @@ func Test_Ctx_DropWithMiddleware(t *testing.T) { require.Nil(t, resp) } +// go test -run Test_Ctx_End +func Test_Ctx_End(t *testing.T) { + app := New() + + app.Get("/", func(c Ctx) error { + c.SendString("Hello, World!") //nolint:errcheck // unnecessary to check error + return c.End() + }) + + resp, err := app.Test(httptest.NewRequest(MethodGet, "/", nil)) + require.NoError(t, err) + require.NotNil(t, resp) + require.Equal(t, StatusOK, resp.StatusCode) + body, err := io.ReadAll(resp.Body) + require.NoError(t, err, "io.ReadAll(resp.Body)") + require.Equal(t, "Hello, World!", string(body)) +} + +// go test -run Test_Ctx_End_after_timeout +func Test_Ctx_End_after_timeout(t *testing.T) { + app := New() + + // Early flushing handler + app.Get("/", func(c Ctx) error { + time.Sleep(2 * time.Second) + return c.End() + }) + + resp, err := app.Test(httptest.NewRequest(MethodGet, "/", nil)) + require.ErrorIs(t, err, os.ErrDeadlineExceeded) + require.Nil(t, resp) +} + +// go test -run Test_Ctx_End_with_drop_middleware +func Test_Ctx_End_with_drop_middleware(t *testing.T) { + app := New() + + // Middleware that will drop connections + // that persist after c.Next() + app.Use(func(c Ctx) error { + c.Next() //nolint:errcheck // unnecessary to check error + return c.Drop() + }) + + // Early flushing handler + app.Get("/", func(c Ctx) error { + c.SendStatus(StatusOK) //nolint:errcheck // unnecessary to check error + return c.End() + }) + + resp, err := app.Test(httptest.NewRequest(MethodGet, "/", nil)) + require.NoError(t, err) + require.NotNil(t, resp) + require.Equal(t, StatusOK, resp.StatusCode) +} + +// go test -run Test_Ctx_End_after_drop +func Test_Ctx_End_after_drop(t *testing.T) { + app := New() + + // Middleware that ends the request + // after c.Next() + app.Use(func(c Ctx) error { + c.Next() //nolint:errcheck // unnecessary to check error + return c.End() + }) + + // Early flushing handler + app.Get("/", func(c Ctx) error { + return c.Drop() + }) + + resp, err := app.Test(httptest.NewRequest(MethodGet, "/", nil)) + require.ErrorIs(t, err, ErrTestGotEmptyResponse) + require.Nil(t, resp) +} + // go test -run Test_GenericParseTypeString func Test_GenericParseTypeString(t *testing.T) { t.Parallel() diff --git a/docs/api/ctx.md b/docs/api/ctx.md index b65532532a..fda9f37328 100644 --- a/docs/api/ctx.md +++ b/docs/api/ctx.md @@ -484,6 +484,54 @@ app.Get("/", func(c fiber.Ctx) error { }) ``` +## End + +End immediately flushes the current response and closes the underlying connection. + +```go title="Signature" +func (c fiber.Ctx) End() error +``` + +```go title="Example" +app.Get("/", func(c fiber.Ctx) error { + c.SendString("Hello World!") + return c.End() +}) +``` + +:::caution +Calling `c.End()` will disallow further writes to the underlying connection. +::: + +End can be used to stop a middleware from modifying a response of a handler/other middleware down the method chain +when they regain control after calling `c.Next()`. + +```go title="Example" +// Error Logging/Responding middleware +app.Use(func(c fiber.Ctx) error { + err := c.Next() + + // Log errors & write the error to the response + if err != nil { + log.Printf("Got error in middleware: %v", err) + return c.Writef("(got error %v)", err) + } + + // No errors occured + return nil +}) + +// Handler with simulated error +app.Get("/", func(c fiber.Ctx) error { + // Closes the connection instantly after writing from this handler + // and disallow further modification of its response + defer c.End() + + c.SendString("Hello, ... I forgot what comes next!") + return errors.New("some error") +}) +``` + ## Format Performs content-negotiation on the [Accept](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept) HTTP header. It uses [Accepts](ctx.md#accepts) to select a proper format from the supplied offers. A default handler can be provided by setting the `MediaType` to `"default"`. If no offers match and no default is provided, a 406 (Not Acceptable) response is sent. The Content-Type is automatically set when a handler is selected. diff --git a/docs/whats_new.md b/docs/whats_new.md index 7501c2b57e..0c4d749cd7 100644 --- a/docs/whats_new.md +++ b/docs/whats_new.md @@ -341,6 +341,7 @@ testConfig := fiber.TestConfig{ - **String**: Similar to Express.js, converts a value to a string. - **ViewBind**: Binds data to a view, replacing the old `Bind` method. - **CBOR**: Introducing [CBOR](https://cbor.io/) binary encoding format for both request & response body. CBOR is a binary data serialization format which is both compact and efficient, making it ideal for use in web applications. +- **End**: Similar to Express.js, immediately flushes the current response and closes the underlying connection. ### Removed Methods @@ -403,6 +404,41 @@ app.Get("/sse", func(c fiber.Ctx) { You can find more details about this feature in [/docs/api/ctx.md](./api/ctx.md). +### End + +In v3, we introduced a new method to match the Express.js API's `res.end()` method. + +```go +func (c Ctx) End() +``` + +With this method, you can: + +- Stop middleware from controlling the connection after a handler further up the method chain + by immediately flushing the current response and closing the connection. +- Use `return c.End()` as an alternative to `return nil` + +```go +app.Use(func (c fiber.Ctx) error { + err := c.Next() + if err != nil { + log.Println("Got error: %v", err) + return c.SendString(err.Error()) // Will be unsuccessful since the response ended below + } + return nil +}) + +app.Get("/hello", func (c fiber.Ctx) error { + query := c.Query("name", "") + if query == "" { + c.SendString("You don't have a name?") + c.End() // Closes the underlying connection + return errors.New("No name provided") + } + return c.SendString("Hello, " + query + "!") +}) +``` + --- ## 🌎 Client package