diff --git a/HISTORY.md b/HISTORY.md
index 0d78cb1a..6b81232e 100644
--- a/HISTORY.md
+++ b/HISTORY.md
@@ -394,31 +394,33 @@ Other Improvements:
New Context Methods:
-- `context.IsHTTP2() bool` reports whether the protocol version for incoming request was HTTP/2
-- `context.IsGRPC() bool` reports whether the request came from a gRPC client
-- `context.UpsertCookie(*http.Cookie, cookieOptions ...context.CookieOption)` upserts a cookie, fixes [#1485](https://github.com/kataras/iris/issues/1485) too
-- `context.StopWithStatus(int)` stops the handlers chain and writes the status code
-- `context.StopWithText(int, string)` stops the handlers chain, writes thre status code and a plain text message
-- `context.StopWithError(int, error)` stops the handlers chain, writes thre status code and the error's message
-- `context.StopWithJSON(int, interface{})` stops the handlers chain, writes the status code and sends a JSON response
-- `context.StopWithProblem(int, iris.Problem)` stops the handlers, writes the status code and sends an `application/problem+json` response
-- `context.Protobuf(proto.Message)` sends protobuf to the client
-- `context.MsgPack(interface{})` sends msgpack format data to the client
-- `context.ReadProtobuf(ptr)` binds request body to a proto message
-- `context.ReadMsgPack(ptr)` binds request body of a msgpack format to a struct
-- `context.ReadBody(ptr)` binds the request body to the "ptr" depending on the request's Method and Content-Type
-- `context.SetSameSite(http.SameSite)` to set cookie "SameSite" option (respectful by sessions too)
-- `context.Defer(Handler)` works like `Party.Done` but for the request life-cycle instead
-- `context.ReflectValue() []reflect.Value` stores and returns the `[]reflect.ValueOf(context)`
-- `context.Controller() reflect.Value` returns the current MVC Controller value.
+- `Context.ServeContentWithRate`, `ServeFileWithRate` and `SendFileWithRate` methods to throttle the "download" speed of the client.
+- `Context.IsHTTP2() bool` reports whether the protocol version for incoming request was HTTP/2
+- `Context.IsGRPC() bool` reports whether the request came from a gRPC client
+- `Context.UpsertCookie(*http.Cookie, cookieOptions ...context.CookieOption)` upserts a cookie, fixes [#1485](https://github.com/kataras/iris/issues/1485) too
+- `Context.StopWithStatus(int)` stops the handlers chain and writes the status code
+- `Context.StopWithText(int, string)` stops the handlers chain, writes thre status code and a plain text message
+- `Context.StopWithError(int, error)` stops the handlers chain, writes thre status code and the error's message
+- `Context.StopWithJSON(int, interface{})` stops the handlers chain, writes the status code and sends a JSON response
+- `Context.StopWithProblem(int, iris.Problem)` stops the handlers, writes the status code and sends an `application/problem+json` response
+- `Context.Protobuf(proto.Message)` sends protobuf to the client
+- `Context.MsgPack(interface{})` sends msgpack format data to the client
+- `Context.ReadProtobuf(ptr)` binds request body to a proto message
+- `Context.ReadMsgPack(ptr)` binds request body of a msgpack format to a struct
+- `Context.ReadBody(ptr)` binds the request body to the "ptr" depending on the request's Method and Content-Type
+- `Context.SetSameSite(http.SameSite)` to set cookie "SameSite" option (respectful by sessions too)
+- `Context.Defer(Handler)` works like `Party.Done` but for the request life-cycle instead
+- `Context.ReflectValue() []reflect.Value` stores and returns the `[]reflect.ValueOf(ctx)`
+- `Context.Controller() reflect.Value` returns the current MVC Controller value.
Breaking Changes:
-Change the MIME type of `Javascript .js` and `JSONP` as the HTML specification now recommends to `"text/javascript"` instead of the obselete `"application/javascript"`. This change was pushed to the `Go` language itself as well. See .
-
+- Change the MIME type of `Javascript .js` and `JSONP` as the HTML specification now recommends to `"text/javascript"` instead of the obselete `"application/javascript"`. This change was pushed to the `Go` language itself as well. See .
+- Remove the last input argument of `enableGzipCompression` in `Context.ServeContent`, `ServeFile` methods. This was deprecated a few versions ago. A middleware (`app.Use(iris.Gzip)`) or a prior call to `Context.Gzip(true)` will enable gzip compression. Also these two methods and `Context.SendFile` one now support `Content-Range` and `Accept-Ranges` correctly out of the box (`net/http` had a bug, which is now fixed).
+- `Context.ServeContent` no longer returns an error, see `ServeContentWithRate`, `ServeFileWithRate` and `SendFileWithRate` new methods too.
- `route.Trace() string` changed to `route.Trace(w io.Writer)`, to achieve the same result just pass a `bytes.Buffer`
-- `var mvc.AutoBinding` removed as the default behavior now resolves such dependencies automatically (see [[FEATURE REQUEST] MVC serving gRPC-compatible controller](https://github.com/kataras/iris/issues/1449))
-- `mvc#Application.SortByNumMethods()` removed as the default behavior now binds the "thinnest" empty `interface{}` automatically (see [MVC: service injecting fails](https://github.com/kataras/iris/issues/1343))
+- `var mvc.AutoBinding` removed as the default behavior now resolves such dependencies automatically (see [[FEATURE REQUEST] MVC serving gRPC-compatible controller](https://github.com/kataras/iris/issues/1449)).
+- `mvc#Application.SortByNumMethods()` removed as the default behavior now binds the "thinnest" empty `interface{}` automatically (see [MVC: service injecting fails](https://github.com/kataras/iris/issues/1343)).
- `mvc#BeforeActivation.Dependencies().Add` should be replaced with `mvc#BeforeActivation.Dependencies().Register` instead
- **REMOVE** the `kataras/iris/v12/typescript` package in favor of the new [iris-cli](https://github.com/kataras/iris-cli). Also, the alm typescript online editor was removed as it is deprecated by its author, please consider using the [designtsx](https://designtsx.com/) instead.
diff --git a/_examples/README.md b/_examples/README.md
index 723f27dd..cae94200 100644
--- a/_examples/README.md
+++ b/_examples/README.md
@@ -237,7 +237,7 @@ You can serve [quicktemplate](https://github.com/valyala/quicktemplate) and [her
- [Basic](file-server/basic/main.go)
- [Embedding Files Into App Executable File](file-server/embedding-files-into-app/main.go)
- [Embedding Gziped Files Into App Executable File](file-server/embedding-gziped-files-into-app/main.go)
-- [Send/Force-Download Files](file-server/send-files/main.go)
+- [Send/Force-Download Files](file-server/send-files/main.go) **UPDATED**
- Single Page Applications
* [single Page Application](file-server/single-page-application/basic/main.go)
* [embedded Single Page Application](file-server/single-page-application/embedded-single-page-application/main.go)
@@ -246,7 +246,7 @@ You can serve [quicktemplate](https://github.com/valyala/quicktemplate) and [her
### How to Read from `context.Request() *http.Request`
- [Read JSON](http_request/read-json/main.go)
- * [Struct Validation](http_request/read-json-struct-validation/main.go) **UPDaTE**
+ * [Struct Validation](http_request/read-json-struct-validation/main.go) **UPDATED**
- [Read XML](http_request/read-xml/main.go)
- [Read MsgPack](http_request/read-msgpack/main.go) **NEW**
- [Read YAML](http_request/read-yaml/main.go)
diff --git a/_examples/file-server/send-files/files/first.zip b/_examples/file-server/send-files/files/first.zip
index 431fa5fd..c98f5ac8 100644
Binary files a/_examples/file-server/send-files/files/first.zip and b/_examples/file-server/send-files/files/first.zip differ
diff --git a/_examples/file-server/send-files/main.go b/_examples/file-server/send-files/main.go
index bc03b0a1..3e5c46a1 100644
--- a/_examples/file-server/send-files/main.go
+++ b/_examples/file-server/send-files/main.go
@@ -6,11 +6,26 @@ import (
func main() {
app := iris.New()
+ app.Logger().SetLevel("debug")
- app.Get("/", func(ctx iris.Context) {
- file := "./files/first.zip"
- ctx.SendFile(file, "c.zip")
- })
+ app.Get("/", download)
+ app.Get("/download", downloadWithRateLimit)
app.Listen(":8080")
}
+
+func download(ctx iris.Context) {
+ src := "./files/first.zip"
+ ctx.SendFile(src, "client.zip")
+}
+
+func downloadWithRateLimit(ctx iris.Context) {
+ // REPLACE THAT WITH A BIG LOCAL FILE OF YOUR OWN.
+ src := "./files/first.zip"
+ dest := "" /* optionally, keep it empty to resolve the filename based on the "src" */
+
+ // Limit download speed to ~50Kb/s with a burst of 100KB.
+ limit := 50.0 * iris.KB
+ burst := 100 * iris.KB
+ ctx.SendFileWithRate(src, dest, limit, burst)
+}
diff --git a/_examples/http_responsewriter/write-gzip/main.go b/_examples/http_responsewriter/write-gzip/main.go
index f1d6369d..080e4164 100644
--- a/_examples/http_responsewriter/write-gzip/main.go
+++ b/_examples/http_responsewriter/write-gzip/main.go
@@ -4,6 +4,9 @@ import "github.com/kataras/iris/v12"
func main() {
app := iris.New()
+ // app.Use(iris.Gzip)
+ // func(ctx iris.Context) { ctx.Gzip(true/false)}
+ // OR:
app.Get("/", func(ctx iris.Context) {
ctx.WriteGzip([]byte("Hello World!"))
ctx.Header("X-Custom",
diff --git a/context/context.go b/context/context.go
index 18e57cf5..2eff50dc 100644
--- a/context/context.go
+++ b/context/context.go
@@ -2,6 +2,7 @@ package context
import (
"bytes"
+ stdContext "context"
"encoding/json"
"encoding/xml"
"errors"
@@ -35,6 +36,7 @@ import (
jsoniter "github.com/json-iterator/go"
"github.com/microcosm-cc/bluemonday"
"github.com/vmihailenco/msgpack/v5"
+ "golang.org/x/time/rate"
"gopkg.in/yaml.v3"
)
@@ -916,31 +918,57 @@ type Context interface {
// | Serve files |
// +------------------------------------------------------------+
- // ServeContent serves content, headers are autoset
- // receives three parameters, it's low-level function, instead you can use .ServeFile(string,bool)/SendFile(string,string)
+ // ServeContent replies to the request using the content in the
+ // provided ReadSeeker. The main benefit of ServeContent over io.Copy
+ // is that it handles Range requests properly, sets the MIME type, and
+ // handles If-Match, If-Unmodified-Since, If-None-Match, If-Modified-Since,
+ // and If-Range requests.
//
+ // If the response's Content-Type header is not set, ServeContent
+ // first tries to deduce the type from name's file extension.
//
- // You can define your own "Content-Type" with `context#ContentType`, before this function call.
+ // The name is otherwise unused; in particular it can be empty and is
+ // never sent in the response.
//
- // This function doesn't support resuming (by range),
- // use ctx.SendFile or router's `HandleDir` instead.
- ServeContent(content io.ReadSeeker, filename string, modtime time.Time, gzipCompression bool) error
- // ServeFile serves a file (to send a file, a zip for example to the client you should use the `SendFile` instead)
- // receives two parameters
- // filename/path (string)
- // gzipCompression (bool)
+ // If modtime is not the zero time or Unix epoch, ServeContent
+ // includes it in a Last-Modified header in the response. If the
+ // request includes an If-Modified-Since header, ServeContent uses
+ // modtime to decide whether the content needs to be sent at all.
//
- // You can define your own "Content-Type" with `context#ContentType`, before this function call.
+ // The content's Seek method must work: ServeContent uses
+ // a seek to the end of the content to determine its size.
//
- // This function doesn't support resuming (by range),
- // use ctx.SendFile or router's `HandleDir` instead.
+ // If the caller has set w's ETag header formatted per RFC 7232, section 2.3,
+ // ServeContent uses it to handle requests using If-Match, If-None-Match, or If-Range.
//
- // Use it when you want to serve dynamic files to the client.
- ServeFile(filename string, gzipCompression bool) error
- // SendFile sends file for force-download to the client
+ // Note that *os.File implements the io.ReadSeeker interface.
+ // Note that gzip compression can be registered through `ctx.Gzip(true)` or `app.Use(iris.Gzip)`.
+ ServeContent(content io.ReadSeeker, filename string, modtime time.Time)
+ // ServeContentWithRate same as `ServeContent` but it can throttle the speed of reading
+ // and though writing the "content" to the client.
+ ServeContentWithRate(content io.ReadSeeker, filename string, modtime time.Time, limit float64, burst int)
+ // ServeFile replies to the request with the contents of the named
+ // file or directory.
//
- // Use this instead of ServeFile to 'force-download' bigger files to the client.
+ // If the provided file or directory name is a relative path, it is
+ // interpreted relative to the current directory and may ascend to
+ // parent directories. If the provided name is constructed from user
+ // input, it should be sanitized before calling `ServeFile`.
+ //
+ // Use it when you want to serve assets like css and javascript files.
+ // If client should confirm and save the file use the `SendFile` instead.
+ // Note that gzip compression can be registered through `ctx.Gzip(true)` or `app.Use(iris.Gzip)`.
+ ServeFile(filename string) error
+ // ServeFileWithRate same as `ServeFile` but it can throttle the speed of reading
+ // and though writing the file to the client.
+ ServeFileWithRate(filename string, limit float64, burst int) error
+ // SendFile sends a file as an attachment, that is downloaded and saved locally from client.
+ // Note that gzip compression can be registered through `ctx.Gzip(true)` or `app.Use(iris.Gzip)`.
+ // Use `ServeFile` if a file should be served as a page asset instead.
SendFile(filename string, destinationName string) error
+ // SendFileWithRate same as `SendFile` but it can throttle the speed of reading
+ // and though writing the file to the client.
+ SendFileWithRate(src, destName string, limit float64, burst int) error
// +------------------------------------------------------------+
// | Cookies |
@@ -4544,65 +4572,135 @@ func (n *NegotiationAcceptBuilder) EncodingGzip() *NegotiationAcceptBuilder {
// | Serve files |
// +------------------------------------------------------------+
-// ServeContent serves content, headers are autoset
-// receives three parameters, it's low-level function, instead you can use .ServeFile(string,bool)/SendFile(string,string)
+// ServeContent replies to the request using the content in the
+// provided ReadSeeker. The main benefit of ServeContent over io.Copy
+// is that it handles Range requests properly, sets the MIME type, and
+// handles If-Match, If-Unmodified-Since, If-None-Match, If-Modified-Since,
+// and If-Range requests.
//
-// You can define your own "Content-Type" header also, after this function call
-// Doesn't implements resuming (by range), use ctx.SendFile instead
-func (ctx *context) ServeContent(content io.ReadSeeker, filename string, modtime time.Time, gzipCompression bool) error {
- if modified, err := ctx.CheckIfModifiedSince(modtime); !modified && err == nil {
- ctx.WriteNotModified()
- return nil
+// If the response's Content-Type header is not set, ServeContent
+// first tries to deduce the type from name's file extension.
+//
+// The name is otherwise unused; in particular it can be empty and is
+// never sent in the response.
+//
+// If modtime is not the zero time or Unix epoch, ServeContent
+// includes it in a Last-Modified header in the response. If the
+// request includes an If-Modified-Since header, ServeContent uses
+// modtime to decide whether the content needs to be sent at all.
+//
+// The content's Seek method must work: ServeContent uses
+// a seek to the end of the content to determine its size.
+//
+// If the caller has set w's ETag header formatted per RFC 7232, section 2.3,
+// ServeContent uses it to handle requests using If-Match, If-None-Match, or If-Range.
+//
+// Note that *os.File implements the io.ReadSeeker interface.
+// Note that gzip compression can be registered through `ctx.Gzip(true)` or `app.Use(iris.Gzip)`.
+func (ctx *context) ServeContent(content io.ReadSeeker, filename string, modtime time.Time) {
+ ctx.ServeContentWithRate(content, filename, modtime, 0, 0)
+}
+
+// rateReadSeeker is a io.ReadSeeker that is rate limited by
+// the given token bucket. Each token in the bucket
+// represents one byte. See "golang.org/x/time/rate" package.
+type rateReadSeeker struct {
+ io.ReadSeeker
+ ctx stdContext.Context
+ limiter *rate.Limiter
+}
+
+func (rs *rateReadSeeker) Read(buf []byte) (int, error) {
+ n, err := rs.ReadSeeker.Read(buf)
+ if n <= 0 {
+ return n, err
+ }
+ rs.limiter.WaitN(rs.ctx, n)
+ return n, err
+}
+
+// ServeContentWithRate same as `ServeContent` but it can throttle the speed of reading
+// and though writing the "content" to the client.
+func (ctx *context) ServeContentWithRate(content io.ReadSeeker, filename string, modtime time.Time, limit float64, burst int) {
+ if limit > 0 {
+ content = &rateReadSeeker{
+ ReadSeeker: content,
+ ctx: ctx.request.Context(),
+ limiter: rate.NewLimiter(rate.Limit(limit), burst),
+ }
}
if ctx.GetContentType() == "" {
ctx.ContentType(filename)
}
- ctx.SetLastModified(modtime)
- var out io.Writer
- if gzipCompression && ctx.ClientSupportsGzip() {
- AddGzipHeaders(ctx.writer)
-
- gzipWriter := acquireGzipWriter(ctx.writer)
- defer releaseGzipWriter(gzipWriter)
- out = gzipWriter
- } else {
- out = ctx.writer
- }
- _, err := io.Copy(out, content)
- return err ///TODO: add an int64 as return value for the content length written like other writers or let it as it's in order to keep the stable api?
+ http.ServeContent(ctx.writer, ctx.request, filename, modtime, content)
}
-// ServeFile serves a view file, to send a file ( zip for example) to the client you should use the SendFile(serverfilename,clientfilename)
-// receives two parameters
-// filename/path (string)
-// gzipCompression (bool)
+// ServeFile replies to the request with the contents of the named
+// file or directory.
//
-// You can define your own "Content-Type" header also, after this function call
-// This function doesn't implement resuming (by range), use ctx.SendFile instead
+// If the provided file or directory name is a relative path, it is
+// interpreted relative to the current directory and may ascend to
+// parent directories. If the provided name is constructed from user
+// input, it should be sanitized before calling `ServeFile`.
//
-// Use it when you want to serve css/js/... files to the client, for bigger files and 'force-download' use the SendFile.
-func (ctx *context) ServeFile(filename string, gzipCompression bool) error {
+// Use it when you want to serve assets like css and javascript files.
+// If client should confirm and save the file use the `SendFile` instead.
+// Note that gzip compression can be registered through `ctx.Gzip(true)` or `app.Use(iris.Gzip)`.
+func (ctx *context) ServeFile(filename string) error {
+ return ctx.ServeFileWithRate(filename, 0, 0)
+}
+
+// ServeFileWithRate same as `ServeFile` but it can throttle the speed of reading
+// and though writing the file to the client.
+func (ctx *context) ServeFileWithRate(filename string, limit float64, burst int) error {
f, err := os.Open(filename)
if err != nil {
- return fmt.Errorf("%d", http.StatusNotFound)
+ ctx.StatusCode(http.StatusNotFound)
+ return err
}
defer f.Close()
- fi, _ := f.Stat()
- if fi.IsDir() {
- return ctx.ServeFile(path.Join(filename, "index.html"), gzipCompression)
+
+ st, err := f.Stat()
+ if err != nil {
+ code := http.StatusInternalServerError
+ if os.IsNotExist(err) {
+ code = http.StatusNotFound
+ }
+
+ if os.IsPermission(err) {
+ code = http.StatusForbidden
+ }
+
+ ctx.StatusCode(code)
+ return err
}
- return ctx.ServeContent(f, fi.Name(), fi.ModTime(), gzipCompression)
+ if st.IsDir() {
+ return ctx.ServeFile(path.Join(filename, "index.html"))
+ }
+
+ ctx.ServeContentWithRate(f, st.Name(), st.ModTime(), limit, burst)
+ return nil
}
-// SendFile sends file for force-download to the client
-//
-// Use this instead of ServeFile to 'force-download' bigger files to the client.
-func (ctx *context) SendFile(filename string, destinationName string) error {
- ctx.writer.Header().Set(ContentDispositionHeaderKey, "attachment;filename="+destinationName)
- return ctx.ServeFile(filename, false)
+// SendFile sends a file as an attachment, that is downloaded and saved locally from client.
+// Note that gzip compression can be registered through `ctx.Gzip(true)` or `app.Use(iris.Gzip)`.
+// Use `ServeFile` if a file should be served as a page asset instead.
+func (ctx *context) SendFile(src string, destName string) error {
+ return ctx.SendFileWithRate(src, destName, 0, 0)
+}
+
+// SendFileWithRate same as `SendFile` but it can throttle the speed of reading
+// and though writing the file to the client.
+func (ctx *context) SendFileWithRate(src, destName string, limit float64, burst int) error {
+ if destName == "" {
+ destName = filepath.Base(src)
+ }
+
+ ctx.writer.Header().Set(ContentDispositionHeaderKey, "attachment;filename="+destName)
+ return ctx.ServeFileWithRate(src, limit, burst)
}
// +------------------------------------------------------------+
diff --git a/iris.go b/iris.go
index a4fa5fda..3c1823f6 100644
--- a/iris.go
+++ b/iris.go
@@ -591,6 +591,17 @@ const (
ReferrerGoogleAdwords = context.ReferrerGoogleAdwords
)
+// Byte unit helpers.
+const (
+ B = 1 << (10 * iota)
+ KB
+ MB
+ GB
+ TB
+ PB
+ EB
+)
+
// ConfigureHost accepts one or more `host#Configuration`, these configurators functions
// can access the host created by `app.Run`,
// they're being executed when application is ready to being served to the public.
diff --git a/middleware/rate/rate.go b/middleware/rate/rate.go
index 40fffb86..ce29ba9f 100644
--- a/middleware/rate/rate.go
+++ b/middleware/rate/rate.go
@@ -50,10 +50,11 @@ type (
Limiter struct {
clientDataFunc func(ctx context.Context) interface{} // fill the Client's Data field.
exceedHandler context.Handler // when too many requests.
+ limit rate.Limit
+ burstSize int
clients map[string]*Client
mu sync.RWMutex // mutex for clients.
- pool *sync.Pool // object pool for clients.
}
Client struct {
@@ -68,14 +69,10 @@ type (
const Inf = math.MaxFloat64
func Limit(limit float64, burst int, options ...Option) context.Handler {
- rateLimit := rate.Limit(limit)
-
l := &Limiter{
- clients: make(map[string]*Client),
- pool: &sync.Pool{New: func() interface{} {
- return &Client{limiter: rate.NewLimiter(rateLimit, burst)}
- }},
-
+ clients: make(map[string]*Client),
+ limit: rate.Limit(limit),
+ burstSize: burst,
exceedHandler: func(ctx context.Context) {
ctx.StopWithStatus(429) // Too Many Requests.
},
@@ -88,21 +85,10 @@ func Limit(limit float64, burst int, options ...Option) context.Handler {
return l.serveHTTP
}
-func (l *Limiter) acquire() *Client {
- return l.pool.Get().(*Client)
-}
-
-func (l *Limiter) release(client *Client) {
- client.IP = ""
- client.Data = nil
- l.pool.Put(client)
-}
-
func (l *Limiter) Purge(condition func(*Client) bool) {
l.mu.Lock()
for ip, client := range l.clients {
if condition(client) {
- l.release(client)
delete(l.clients, ip)
}
}
@@ -116,12 +102,15 @@ func (l *Limiter) serveHTTP(ctx context.Context) {
l.mu.RUnlock()
if !ok {
- client = l.acquire()
- client.IP = ip
+ client = &Client{
+ limiter: rate.NewLimiter(l.limit, l.burstSize),
+ IP: ip,
+ }
if l.clientDataFunc != nil {
client.Data = l.clientDataFunc(ctx)
}
+
// if l.store(ctx, client) {
// ^ no, let's keep it simple.
l.mu.Lock()