mirror of
https://github.com/labstack/echo.git
synced 2024-12-24 20:14:31 +02:00
Merge branch 'master' into routing_misses_performance_improvements
This commit is contained in:
commit
045bec51d4
@ -42,11 +42,14 @@ For older versions, please use the latest v3 tag.
|
|||||||
|
|
||||||
## Benchmarks
|
## Benchmarks
|
||||||
|
|
||||||
Date: 2018/03/15<br>
|
Date: 2020/11/11<br>
|
||||||
Source: https://github.com/vishr/web-framework-benchmark<br>
|
Source: https://github.com/vishr/web-framework-benchmark<br>
|
||||||
Lower is better!
|
Lower is better!
|
||||||
|
|
||||||
<img src="https://i.imgur.com/I32VdMJ.png">
|
<img src="https://i.imgur.com/qwPNQbl.png">
|
||||||
|
<img src="https://i.imgur.com/s8yKQjx.png">
|
||||||
|
|
||||||
|
The benchmarks above were run on an Intel(R) Core(TM) i7-6820HQ CPU @ 2.70GHz
|
||||||
|
|
||||||
## [Guide](https://echo.labstack.com/guide)
|
## [Guide](https://echo.labstack.com/guide)
|
||||||
|
|
||||||
|
25
echo.go
25
echo.go
@ -49,7 +49,6 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"reflect"
|
"reflect"
|
||||||
"runtime"
|
"runtime"
|
||||||
@ -92,6 +91,7 @@ type (
|
|||||||
Renderer Renderer
|
Renderer Renderer
|
||||||
Logger Logger
|
Logger Logger
|
||||||
IPExtractor IPExtractor
|
IPExtractor IPExtractor
|
||||||
|
ListenerNetwork string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Route contains a handler and information for matching against requests.
|
// Route contains a handler and information for matching against requests.
|
||||||
@ -281,6 +281,7 @@ var (
|
|||||||
ErrInvalidRedirectCode = errors.New("invalid redirect status code")
|
ErrInvalidRedirectCode = errors.New("invalid redirect status code")
|
||||||
ErrCookieNotFound = errors.New("cookie not found")
|
ErrCookieNotFound = errors.New("cookie not found")
|
||||||
ErrInvalidCertOrKeyType = errors.New("invalid cert or key type, must be string or []byte")
|
ErrInvalidCertOrKeyType = errors.New("invalid cert or key type, must be string or []byte")
|
||||||
|
ErrInvalidListenerNetwork = errors.New("invalid listener network")
|
||||||
)
|
)
|
||||||
|
|
||||||
// Error handlers
|
// Error handlers
|
||||||
@ -302,9 +303,10 @@ func New() (e *Echo) {
|
|||||||
AutoTLSManager: autocert.Manager{
|
AutoTLSManager: autocert.Manager{
|
||||||
Prompt: autocert.AcceptTOS,
|
Prompt: autocert.AcceptTOS,
|
||||||
},
|
},
|
||||||
Logger: log.New("echo"),
|
Logger: log.New("echo"),
|
||||||
colorer: color.New(),
|
colorer: color.New(),
|
||||||
maxParam: new(int),
|
maxParam: new(int),
|
||||||
|
ListenerNetwork: "tcp",
|
||||||
}
|
}
|
||||||
e.Server.Handler = e
|
e.Server.Handler = e
|
||||||
e.TLSServer.Handler = e
|
e.TLSServer.Handler = e
|
||||||
@ -483,7 +485,7 @@ func (common) static(prefix, root string, get func(string, HandlerFunc, ...Middl
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
name := filepath.Join(root, path.Clean("/"+p)) // "/"+ for security
|
name := filepath.Join(root, filepath.Clean("/"+p)) // "/"+ for security
|
||||||
fi, err := os.Stat(name)
|
fi, err := os.Stat(name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// The access path does not exist
|
// The access path does not exist
|
||||||
@ -714,7 +716,7 @@ func (e *Echo) StartServer(s *http.Server) (err error) {
|
|||||||
|
|
||||||
if s.TLSConfig == nil {
|
if s.TLSConfig == nil {
|
||||||
if e.Listener == nil {
|
if e.Listener == nil {
|
||||||
e.Listener, err = newListener(s.Addr)
|
e.Listener, err = newListener(s.Addr, e.ListenerNetwork)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -725,7 +727,7 @@ func (e *Echo) StartServer(s *http.Server) (err error) {
|
|||||||
return s.Serve(e.Listener)
|
return s.Serve(e.Listener)
|
||||||
}
|
}
|
||||||
if e.TLSListener == nil {
|
if e.TLSListener == nil {
|
||||||
l, err := newListener(s.Addr)
|
l, err := newListener(s.Addr, e.ListenerNetwork)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -754,7 +756,7 @@ func (e *Echo) StartH2CServer(address string, h2s *http2.Server) (err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if e.Listener == nil {
|
if e.Listener == nil {
|
||||||
e.Listener, err = newListener(s.Addr)
|
e.Listener, err = newListener(s.Addr, e.ListenerNetwork)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -875,8 +877,11 @@ func (ln tcpKeepAliveListener) Accept() (c net.Conn, err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func newListener(address string) (*tcpKeepAliveListener, error) {
|
func newListener(address, network string) (*tcpKeepAliveListener, error) {
|
||||||
l, err := net.Listen("tcp", address)
|
if network != "tcp" && network != "tcp4" && network != "tcp6" {
|
||||||
|
return nil, ErrInvalidListenerNetwork
|
||||||
|
}
|
||||||
|
l, err := net.Listen(network, address)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
198
echo_test.go
198
echo_test.go
@ -4,6 +4,7 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
stdContext "context"
|
stdContext "context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
@ -59,45 +60,105 @@ func TestEcho(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestEchoStatic(t *testing.T) {
|
func TestEchoStatic(t *testing.T) {
|
||||||
e := New()
|
var testCases = []struct {
|
||||||
|
name string
|
||||||
|
givenPrefix string
|
||||||
|
givenRoot string
|
||||||
|
whenURL string
|
||||||
|
expectStatus int
|
||||||
|
expectHeaderLocation string
|
||||||
|
expectBodyStartsWith string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "ok",
|
||||||
|
givenPrefix: "/images",
|
||||||
|
givenRoot: "_fixture/images",
|
||||||
|
whenURL: "/images/walle.png",
|
||||||
|
expectStatus: http.StatusOK,
|
||||||
|
expectBodyStartsWith: string([]byte{0x89, 0x50, 0x4e, 0x47}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "No file",
|
||||||
|
givenPrefix: "/images",
|
||||||
|
givenRoot: "_fixture/scripts",
|
||||||
|
whenURL: "/images/bolt.png",
|
||||||
|
expectStatus: http.StatusNotFound,
|
||||||
|
expectBodyStartsWith: "{\"message\":\"Not Found\"}\n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Directory",
|
||||||
|
givenPrefix: "/images",
|
||||||
|
givenRoot: "_fixture/images",
|
||||||
|
whenURL: "/images/",
|
||||||
|
expectStatus: http.StatusNotFound,
|
||||||
|
expectBodyStartsWith: "{\"message\":\"Not Found\"}\n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Directory Redirect",
|
||||||
|
givenPrefix: "/",
|
||||||
|
givenRoot: "_fixture",
|
||||||
|
whenURL: "/folder",
|
||||||
|
expectStatus: http.StatusMovedPermanently,
|
||||||
|
expectHeaderLocation: "/folder/",
|
||||||
|
expectBodyStartsWith: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Directory with index.html",
|
||||||
|
givenPrefix: "/",
|
||||||
|
givenRoot: "_fixture",
|
||||||
|
whenURL: "/",
|
||||||
|
expectStatus: http.StatusOK,
|
||||||
|
expectBodyStartsWith: "<!doctype html>",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Sub-directory with index.html",
|
||||||
|
givenPrefix: "/",
|
||||||
|
givenRoot: "_fixture",
|
||||||
|
whenURL: "/folder/",
|
||||||
|
expectStatus: http.StatusOK,
|
||||||
|
expectBodyStartsWith: "<!doctype html>",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "do not allow directory traversal (backslash - windows separator)",
|
||||||
|
givenPrefix: "/",
|
||||||
|
givenRoot: "_fixture/",
|
||||||
|
whenURL: `/..\\middleware/basic_auth.go`,
|
||||||
|
expectStatus: http.StatusNotFound,
|
||||||
|
expectBodyStartsWith: "{\"message\":\"Not Found\"}\n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "do not allow directory traversal (slash - unix separator)",
|
||||||
|
givenPrefix: "/",
|
||||||
|
givenRoot: "_fixture/",
|
||||||
|
whenURL: `/../middleware/basic_auth.go`,
|
||||||
|
expectStatus: http.StatusNotFound,
|
||||||
|
expectBodyStartsWith: "{\"message\":\"Not Found\"}\n",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
assert := assert.New(t)
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
// OK
|
e := New()
|
||||||
e.Static("/images", "_fixture/images")
|
e.Static(tc.givenPrefix, tc.givenRoot)
|
||||||
c, b := request(http.MethodGet, "/images/walle.png", e)
|
req := httptest.NewRequest(http.MethodGet, tc.whenURL, nil)
|
||||||
assert.Equal(http.StatusOK, c)
|
rec := httptest.NewRecorder()
|
||||||
assert.NotEmpty(b)
|
e.ServeHTTP(rec, req)
|
||||||
|
assert.Equal(t, tc.expectStatus, rec.Code)
|
||||||
// No file
|
body := rec.Body.String()
|
||||||
e.Static("/images", "_fixture/scripts")
|
if tc.expectBodyStartsWith != "" {
|
||||||
c, _ = request(http.MethodGet, "/images/bolt.png", e)
|
assert.True(t, strings.HasPrefix(body, tc.expectBodyStartsWith))
|
||||||
assert.Equal(http.StatusNotFound, c)
|
} else {
|
||||||
|
assert.Equal(t, "", body)
|
||||||
// Directory
|
}
|
||||||
e.Static("/images", "_fixture/images")
|
|
||||||
c, _ = request(http.MethodGet, "/images/", e)
|
|
||||||
assert.Equal(http.StatusNotFound, c)
|
|
||||||
|
|
||||||
// Directory Redirect
|
|
||||||
e.Static("/", "_fixture")
|
|
||||||
req := httptest.NewRequest(http.MethodGet, "/folder", nil)
|
|
||||||
rec := httptest.NewRecorder()
|
|
||||||
e.ServeHTTP(rec, req)
|
|
||||||
assert.Equal(http.StatusMovedPermanently, rec.Code)
|
|
||||||
assert.Equal("/folder/", rec.HeaderMap["Location"][0])
|
|
||||||
|
|
||||||
// Directory with index.html
|
|
||||||
e.Static("/", "_fixture")
|
|
||||||
c, r := request(http.MethodGet, "/", e)
|
|
||||||
assert.Equal(http.StatusOK, c)
|
|
||||||
assert.Equal(true, strings.HasPrefix(r, "<!doctype html>"))
|
|
||||||
|
|
||||||
// Sub-directory with index.html
|
|
||||||
c, r = request(http.MethodGet, "/folder/", e)
|
|
||||||
assert.Equal(http.StatusOK, c)
|
|
||||||
assert.Equal(true, strings.HasPrefix(r, "<!doctype html>"))
|
|
||||||
|
|
||||||
|
if tc.expectHeaderLocation != "" {
|
||||||
|
assert.Equal(t, tc.expectHeaderLocation, rec.Result().Header["Location"][0])
|
||||||
|
} else {
|
||||||
|
_, ok := rec.Result().Header["Location"]
|
||||||
|
assert.False(t, ok)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestEchoFile(t *testing.T) {
|
func TestEchoFile(t *testing.T) {
|
||||||
@ -658,6 +719,69 @@ func TestEchoShutdown(t *testing.T) {
|
|||||||
assert.Equal(t, err.Error(), "http: Server closed")
|
assert.Equal(t, err.Error(), "http: Server closed")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var listenerNetworkTests = []struct {
|
||||||
|
test string
|
||||||
|
network string
|
||||||
|
address string
|
||||||
|
}{
|
||||||
|
{"tcp ipv4 address", "tcp", "127.0.0.1:1323"},
|
||||||
|
{"tcp ipv6 address", "tcp", "[::1]:1323"},
|
||||||
|
{"tcp4 ipv4 address", "tcp4", "127.0.0.1:1323"},
|
||||||
|
{"tcp6 ipv6 address", "tcp6", "[::1]:1323"},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEchoListenerNetwork(t *testing.T) {
|
||||||
|
for _, tt := range listenerNetworkTests {
|
||||||
|
t.Run(tt.test, func(t *testing.T) {
|
||||||
|
e := New()
|
||||||
|
e.ListenerNetwork = tt.network
|
||||||
|
|
||||||
|
// HandlerFunc
|
||||||
|
e.GET("/ok", func(c Context) error {
|
||||||
|
return c.String(http.StatusOK, "OK")
|
||||||
|
})
|
||||||
|
|
||||||
|
errCh := make(chan error)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
errCh <- e.Start(tt.address)
|
||||||
|
}()
|
||||||
|
|
||||||
|
time.Sleep(200 * time.Millisecond)
|
||||||
|
|
||||||
|
if resp, err := http.Get(fmt.Sprintf("http://%s/ok", tt.address)); err == nil {
|
||||||
|
defer resp.Body.Close()
|
||||||
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||||
|
|
||||||
|
if body, err := ioutil.ReadAll(resp.Body); err == nil {
|
||||||
|
assert.Equal(t, "OK", string(body))
|
||||||
|
} else {
|
||||||
|
assert.Fail(t, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
assert.Fail(t, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := e.Close(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEchoListenerNetworkInvalid(t *testing.T) {
|
||||||
|
e := New()
|
||||||
|
e.ListenerNetwork = "unix"
|
||||||
|
|
||||||
|
// HandlerFunc
|
||||||
|
e.GET("/ok", func(c Context) error {
|
||||||
|
return c.String(http.StatusOK, "OK")
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.Equal(t, ErrInvalidListenerNetwork, e.Start(":1323"))
|
||||||
|
}
|
||||||
|
|
||||||
func TestEchoReverse(t *testing.T) {
|
func TestEchoReverse(t *testing.T) {
|
||||||
assert := assert.New(t)
|
assert := assert.New(t)
|
||||||
|
|
||||||
|
@ -31,3 +31,20 @@ func TestRequestID(t *testing.T) {
|
|||||||
h(c)
|
h(c)
|
||||||
assert.Equal(t, rec.Header().Get(echo.HeaderXRequestID), "customGenerator")
|
assert.Equal(t, rec.Header().Get(echo.HeaderXRequestID), "customGenerator")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRequestID_IDNotAltered(t *testing.T) {
|
||||||
|
e := echo.New()
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||||
|
req.Header.Add(echo.HeaderXRequestID, "<sample-request-id>")
|
||||||
|
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
c := e.NewContext(req, rec)
|
||||||
|
handler := func(c echo.Context) error {
|
||||||
|
return c.String(http.StatusOK, "test")
|
||||||
|
}
|
||||||
|
|
||||||
|
rid := RequestIDWithConfig(RequestIDConfig{})
|
||||||
|
h := rid(handler)
|
||||||
|
_ = h(c)
|
||||||
|
assert.Equal(t, rec.Header().Get(echo.HeaderXRequestID), "<sample-request-id>")
|
||||||
|
}
|
||||||
|
@ -167,7 +167,7 @@ func StaticWithConfig(config StaticConfig) echo.MiddlewareFunc {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
name := filepath.Join(config.Root, path.Clean("/"+p)) // "/"+ for security
|
name := filepath.Join(config.Root, filepath.Clean("/"+p)) // "/"+ for security
|
||||||
|
|
||||||
if config.IgnoreBase {
|
if config.IgnoreBase {
|
||||||
routePath := path.Base(strings.TrimRight(c.Path(), "/*"))
|
routePath := path.Base(strings.TrimRight(c.Path(), "/*"))
|
||||||
|
Loading…
Reference in New Issue
Block a user