Skip to content

Commit

Permalink
πŸ”₯ feat: Add End() method to Ctx (#3280)
Browse files Browse the repository at this point in the history
* πŸ”₯ Feature(v3): Add End() method to Ctx

* 🎨 Style(Ctx): Respect linter in tests

* 🚨 Test(End): Add timeout test for c.End()

* πŸ“š Doc: Update End() documentation examples to use 4 spaces

* 🚨 Test: Update `c.End()` tests to use StatusOK

---------

Co-authored-by: Giovanni Rivera <grivera64@users.noreply.github.com>
Co-authored-by: Juan Calderon-Perez <835733+gaby@users.noreply.github.com>
  • Loading branch information
3 people authored Jan 16, 2025
1 parent 44b971a commit a42ddc1
Show file tree
Hide file tree
Showing 5 changed files with 182 additions and 1 deletion.
17 changes: 17 additions & 0 deletions ctx.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
5 changes: 4 additions & 1 deletion ctx_interface_gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

77 changes: 77 additions & 0 deletions ctx_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
48 changes: 48 additions & 0 deletions docs/api/ctx.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
36 changes: 36 additions & 0 deletions docs/whats_new.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down

1 comment on commit a42ddc1

@github-actions
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Performance Alert ⚠️

Possible performance regression was detected for benchmark.
Benchmark result of this commit is worse than the previous benchmark result exceeding threshold 1.50.

Benchmark suite Current: a42ddc1 Previous: e04f815 Ratio
Benchmark_Ctx_Send 6.578 ns/op 0 B/op 0 allocs/op 4.343 ns/op 0 B/op 0 allocs/op 1.51
Benchmark_Ctx_Send - ns/op 6.578 ns/op 4.343 ns/op 1.51
Benchmark_Utils_GetOffer/1_parameter 225.5 ns/op 0 B/op 0 allocs/op 136.5 ns/op 0 B/op 0 allocs/op 1.65
Benchmark_Utils_GetOffer/1_parameter - ns/op 225.5 ns/op 136.5 ns/op 1.65
`Benchmark_RoutePatternMatch//api/:param/fixedEnd_ not_match _/api/abc/def/fixedEnd - allocs/op` 14 allocs/op
Benchmark_Middleware_BasicAuth - B/op 80 B/op 48 B/op 1.67
Benchmark_Middleware_BasicAuth - allocs/op 5 allocs/op 3 allocs/op 1.67
Benchmark_Middleware_BasicAuth_Upper - B/op 80 B/op 48 B/op 1.67
Benchmark_Middleware_BasicAuth_Upper - allocs/op 5 allocs/op 3 allocs/op 1.67
Benchmark_CORS_NewHandler - B/op 16 B/op 0 B/op +∞
Benchmark_CORS_NewHandler - allocs/op 1 allocs/op 0 allocs/op +∞
Benchmark_CORS_NewHandlerSingleOrigin - B/op 16 B/op 0 B/op +∞
Benchmark_CORS_NewHandlerSingleOrigin - allocs/op 1 allocs/op 0 allocs/op +∞
Benchmark_CORS_NewHandlerPreflight - B/op 104 B/op 0 B/op +∞
Benchmark_CORS_NewHandlerPreflight - allocs/op 5 allocs/op 0 allocs/op +∞
Benchmark_CORS_NewHandlerPreflightSingleOrigin - B/op 104 B/op 0 B/op +∞
Benchmark_CORS_NewHandlerPreflightSingleOrigin - allocs/op 5 allocs/op 0 allocs/op +∞
Benchmark_CORS_NewHandlerPreflightWildcard - B/op 104 B/op 0 B/op +∞
Benchmark_CORS_NewHandlerPreflightWildcard - allocs/op 5 allocs/op 0 allocs/op +∞
Benchmark_Middleware_CSRF_GenerateToken - B/op 514 B/op 341 B/op 1.51
Benchmark_Middleware_CSRF_GenerateToken - allocs/op 10 allocs/op 6 allocs/op 1.67

This comment was automatically generated by workflow using github-action-benchmark.

Please sign in to comment.