diff --git a/.gitattributes b/.gitattributes
index 79acd4a2..a9609ad0 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -17,4 +17,5 @@ LICENSE text eol=lf
# Exclude `website` and `examples` from Github's language statistics
# https://github.com/github/linguist#using-gitattributes
examples/* linguist-documentation
+recipes/* linguist-documentation
website/* linguist-documentation
diff --git a/.gitignore b/.gitignore
index 01f796c2..45cf8e1f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,10 +1,10 @@
# Website
-site/
-.publish/
+site
+.publish
# Node.js
-node_modules/
+node_modules
# IntelliJ
-.idea/
+.idea
*.iml
diff --git a/README.md b/README.md
index 2e2840ed..2f4e4fdb 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,6 @@
-# [Echo](http://echo.labstack.com) [![GoDoc](http://img.shields.io/badge/go-documentation-blue.svg?style=flat-square)](http://godoc.org/github.com/labstack/echo) [![Build Status](http://img.shields.io/travis/labstack/echo.svg?style=flat-square)](https://travis-ci.org/labstack/echo) [![Coverage Status](http://img.shields.io/coveralls/labstack/echo.svg?style=flat-square)](https://coveralls.io/r/labstack/echo) [![Join the chat at https://gitter.im/labstack/echo](https://img.shields.io/badge/gitter-join%20chat-brightgreen.svg?style=flat-square)](https://gitter.im/labstack/echo)
-Echo is a fast HTTP router (zero dynamic memory allocation) and micro web framework in Go.
+# [Echo](http://echo.labstack.com) [![GoDoc](http://img.shields.io/badge/go-documentation-blue.svg?style=flat-square)](http://godoc.org/github.com/labstack/echo) [![License](http://img.shields.io/badge/license-mit-blue.svg?style=flat-square)](https://raw.githubusercontent.com/labstack/echo/master/LICENSE) [![Build Status](http://img.shields.io/travis/labstack/echo.svg?style=flat-square)](https://travis-ci.org/labstack/echo) [![Coverage Status](http://img.shields.io/coveralls/labstack/echo.svg?style=flat-square)](https://coveralls.io/r/labstack/echo) [![Join the chat at https://gitter.im/labstack/echo](https://img.shields.io/badge/gitter-join%20chat-brightgreen.svg?style=flat-square)](https://gitter.im/labstack/echo)
+
+A fast and unfancy micro web framework for Go.
## Features
@@ -20,16 +21,27 @@ Echo is a fast HTTP router (zero dynamic memory allocation) and micro web framew
- `http.HandlerFunc`
- `func(http.ResponseWriter, *http.Request)`
- Sub-router/Groups
-- Handy encoding/decoding functions.
+- Handy functions to send variety of HTTP response:
+ - HTML
+ - HTML via templates
+ - String
+ - JSON
+ - JSONP
+ - XML
+ - File
+ - NoContent
+ - Redirect
+ - Error
- Build-in support for:
+ - Favicon
+ - Index file
- Static files
- WebSocket
-- API to serve index and favicon.
- Centralized HTTP error handling.
-- Customizable request binding function.
-- Customizable response rendering function, allowing you to use any HTML template engine.
+- Customizable HTTP request binding function.
+- Customizable HTTP response rendering function, allowing you to use any HTML template engine.
-## Benchmark
+## Performance
Based on [vishr/go-http-routing-benchmark] (https://github.com/vishr/go-http-routing-benchmark), June 5, 2015.
@@ -75,16 +87,28 @@ BenchmarkZeus_GithubAll 2000 748827 ns/op 30068
$ go get github.com/labstack/echo
```
-##[Examples](https://github.com/labstack/echo/tree/master/examples)
+## [Recipes](https://github.com/labstack/echo/tree/master/recipes)
-- [Hello, World!](https://github.com/labstack/echo/tree/master/examples/hello)
-- [CRUD](https://github.com/labstack/echo/tree/master/examples/crud)
-- [Website](https://github.com/labstack/echo/tree/master/examples/website)
-- [Middleware](https://github.com/labstack/echo/tree/master/examples/middleware)
-- [Stream](https://github.com/labstack/echo/tree/master/examples/stream)
+- [File Upload](http://echo.labstack.com/recipes/file-upload)
+- [Streaming File Upload](http://echo.labstack.com/recipes/streaming-file-upload)
+- [Streaming Response](http://echo.labstack.com/recipes/streaming-response)
+- [WebSocket](http://echo.labstack.com/recipes/websocket)
+- [Subdomains](http://echo.labstack.com/recipes/subdomains)
+- [JWT Authentication](http://echo.labstack.com/recipes/jwt-authentication)
+- [Graceful Shutdown](http://echo.labstack.com/recipes/graceful-shutdown)
##[Guide](http://echo.labstack.com/guide)
+## Echo System
+
+Community created packages for Echo
+
+- [echo-logrus](https://github.com/deoxxa/echo-logrus)
+- [go_middleware](https://github.com/rightscale/go_middleware)
+- [permissions2](https://github.com/xyproto/permissions2)
+- [permissionbolt](https://github.com/xyproto/permissionbolt)
+- [echo-middleware](https://github.com/syntaqx/echo-middleware)
+
## Contribute
**Use issues for everything**
@@ -98,7 +122,3 @@ $ go get github.com/labstack/echo
- [Vishal Rana](https://github.com/vishr) - Author
- [Nitin Rana](https://github.com/nr17) - Consultant
- [Contributors](https://github.com/labstack/echo/graphs/contributors)
-
-## License
-
-[MIT](https://github.com/labstack/echo/blob/master/LICENSE)
diff --git a/context.go b/context.go
index bc9a64f0..c7beed9e 100644
--- a/context.go
+++ b/context.go
@@ -2,7 +2,13 @@ package echo
import (
"encoding/json"
+ "encoding/xml"
"net/http"
+ "path"
+
+ "fmt"
+
+ "net/url"
"golang.org/x/net/websocket"
)
@@ -16,6 +22,7 @@ type (
socket *websocket.Conn
pnames []string
pvalues []string
+ query url.Values
store store
echo *Echo
}
@@ -69,6 +76,19 @@ func (c *Context) Param(name string) (value string) {
return
}
+// Query returns query parameter by name.
+func (c *Context) Query(name string) string {
+ if c.query == nil {
+ c.query = c.request.URL.Query()
+ }
+ return c.query.Get(name)
+}
+
+// Form returns form parameter by name.
+func (c *Context) Form(name string) string {
+ return c.request.FormValue(name)
+}
+
// Get retrieves data from the context.
func (c *Context) Get(key string) interface{} {
return c.store[key]
@@ -76,47 +96,82 @@ func (c *Context) Get(key string) interface{} {
// Set saves data in the context.
func (c *Context) Set(key string, val interface{}) {
+ if c.store == nil {
+ c.store = make(store)
+ }
c.store[key] = val
}
-// Bind binds the request body into specified type v. Default binder does it
-// based on Content-Type header.
+// Bind binds the request body into specified type `i`. The default binder does
+// it based on Content-Type header.
func (c *Context) Bind(i interface{}) error {
- return c.echo.binder(c.request, i)
+ return c.echo.binder.Bind(c.request, i)
}
-// Render invokes the registered HTML template renderer and sends a text/html
-// response with status code.
+// Render renders a template with data and sends a text/html response with status
+// code. Templates can be registered using `Echo.SetRenderer()`.
func (c *Context) Render(code int, name string, data interface{}) error {
if c.echo.renderer == nil {
return RendererNotRegistered
}
- c.response.Header().Set(ContentType, TextHTML)
+ c.response.Header().Set(ContentType, TextHTMLCharsetUTF8)
c.response.WriteHeader(code)
return c.echo.renderer.Render(c.response, name, data)
}
-// JSON sends an application/json response with status code.
+// HTML formats according to a format specifier and sends HTML response with
+// status code.
+func (c *Context) HTML(code int, format string, a ...interface{}) (err error) {
+ c.response.Header().Set(ContentType, TextHTMLCharsetUTF8)
+ c.response.WriteHeader(code)
+ _, err = fmt.Fprintf(c.response, format, a...)
+ return
+}
+
+// String formats according to a format specifier and sends text response with status
+// code.
+func (c *Context) String(code int, format string, a ...interface{}) (err error) {
+ c.response.Header().Set(ContentType, TextPlain)
+ c.response.WriteHeader(code)
+ _, err = fmt.Fprintf(c.response, format, a...)
+ return
+}
+
+// JSON sends a JSON response with status code.
func (c *Context) JSON(code int, i interface{}) error {
- c.response.Header().Set(ContentType, ApplicationJSON)
+ c.response.Header().Set(ContentType, ApplicationJSONCharsetUTF8)
c.response.WriteHeader(code)
return json.NewEncoder(c.response).Encode(i)
}
-// String sends a text/plain response with status code.
-func (c *Context) String(code int, s string) error {
- c.response.Header().Set(ContentType, TextPlain)
+// JSONP sends a JSONP response with status code. It uses `callback` to construct
+// the JSONP payload.
+func (c *Context) JSONP(code int, callback string, i interface{}) (err error) {
+ c.response.Header().Set(ContentType, ApplicationJavaScriptCharsetUTF8)
c.response.WriteHeader(code)
- _, err := c.response.Write([]byte(s))
- return err
+ c.response.Write([]byte(callback + "("))
+ if err = json.NewEncoder(c.response).Encode(i); err == nil {
+ c.response.Write([]byte(");"))
+ }
+ return
}
-// HTML sends a text/html response with status code.
-func (c *Context) HTML(code int, html string) error {
- c.response.Header().Set(ContentType, TextHTML)
+// XML sends an XML response with status code.
+func (c *Context) XML(code int, i interface{}) error {
+ c.response.Header().Set(ContentType, ApplicationXMLCharsetUTF8)
c.response.WriteHeader(code)
- _, err := c.response.Write([]byte(html))
- return err
+ c.response.Write([]byte(xml.Header))
+ return xml.NewEncoder(c.response).Encode(i)
+}
+
+// File sends a response with the content of the file. If attachment is true, the
+// client is prompted to save the file.
+func (c *Context) File(name string, attachment bool) error {
+ dir, file := path.Split(name)
+ if attachment {
+ c.response.Header().Set(ContentDisposition, "attachment; filename="+file)
+ }
+ return serveFile(dir, file, c)
}
// NoContent sends a response with no body and a status code.
@@ -126,11 +181,15 @@ func (c *Context) NoContent(code int) error {
}
// Redirect redirects the request using http.Redirect with status code.
-func (c *Context) Redirect(code int, url string) {
+func (c *Context) Redirect(code int, url string) error {
+ if code < http.StatusMultipleChoices || code > http.StatusTemporaryRedirect {
+ return InvalidRedirectCode
+ }
http.Redirect(c.response, c.request, url, code)
+ return nil
}
-// Error invokes the registered HTTP error handler. Usually used by middleware.
+// Error invokes the registered HTTP error handler. Generally used by middleware.
func (c *Context) Error(err error) {
c.echo.httpErrorHandler(err, c)
}
@@ -138,5 +197,7 @@ func (c *Context) Error(err error) {
func (c *Context) reset(r *http.Request, w http.ResponseWriter, e *Echo) {
c.request = r
c.response.reset(w)
+ c.query = nil
+ c.store = nil
c.echo = e
}
diff --git a/context_test.go b/context_test.go
index 1a40351f..77e4b7d8 100644
--- a/context_test.go
+++ b/context_test.go
@@ -10,6 +10,9 @@ import (
"strings"
+ "encoding/xml"
+ "net/url"
+
"github.com/stretchr/testify/assert"
)
@@ -24,8 +27,10 @@ func (t *Template) Render(w io.Writer, name string, data interface{}) error {
}
func TestContext(t *testing.T) {
- usr := `{"id":"1","name":"Joe"}`
- req, _ := http.NewRequest(POST, "/", strings.NewReader(usr))
+ userJSON := `{"id":"1","name":"Joe"}`
+ userXML := `1Joe`
+
+ req, _ := http.NewRequest(POST, "/", strings.NewReader(userJSON))
rec := httptest.NewRecorder()
c := NewContext(req, NewResponse(rec), New())
@@ -38,16 +43,12 @@ func TestContext(t *testing.T) {
// Socket
assert.Nil(t, c.Socket())
- //-------
- // Param
- //-------
-
- // By id
+ // Param by id
c.pnames = []string{"id"}
c.pvalues = []string{"1"}
assert.Equal(t, "1", c.P(0))
- // By name
+ // Param by name
assert.Equal(t, "1", c.Param("id"))
// Store
@@ -59,19 +60,14 @@ func TestContext(t *testing.T) {
//------
// JSON
- testBind(t, c, ApplicationJSON)
+ testBind(t, c, "application/json")
- // TODO: Form
- c.request.Header.Set(ContentType, ApplicationForm)
- u := new(user)
- err := c.Bind(u)
- assert.NoError(t, err)
+ // XML
+ c.request, _ = http.NewRequest(POST, "/", strings.NewReader(userXML))
+ testBind(t, c, ApplicationXML)
// Unsupported
- c.request.Header.Set(ContentType, "")
- u = new(user)
- err = c.Bind(u)
- assert.Error(t, err)
+ testBind(t, c, "")
//--------
// Render
@@ -81,7 +77,7 @@ func TestContext(t *testing.T) {
templates: template.Must(template.New("hello").Parse("Hello, {{.}}!")),
}
c.echo.SetRenderer(tpl)
- err = c.Render(http.StatusOK, "hello", "Joe")
+ err := c.Render(http.StatusOK, "hello", "Joe")
if assert.NoError(t, err) {
assert.Equal(t, http.StatusOK, rec.Code)
assert.Equal(t, "Hello, Joe!", rec.Body.String())
@@ -92,18 +88,37 @@ func TestContext(t *testing.T) {
assert.Error(t, err)
// JSON
- req.Header.Set(Accept, ApplicationJSON)
rec = httptest.NewRecorder()
c = NewContext(req, NewResponse(rec), New())
err = c.JSON(http.StatusOK, user{"1", "Joe"})
if assert.NoError(t, err) {
assert.Equal(t, http.StatusOK, rec.Code)
- assert.Equal(t, ApplicationJSON, rec.Header().Get(ContentType))
- assert.Equal(t, usr, strings.TrimSpace(rec.Body.String()))
+ assert.Equal(t, ApplicationJSONCharsetUTF8, rec.Header().Get(ContentType))
+ assert.Equal(t, userJSON+"\n", rec.Body.String())
+ }
+
+ // JSONP
+ rec = httptest.NewRecorder()
+ c = NewContext(req, NewResponse(rec), New())
+ callback := "callback"
+ err = c.JSONP(http.StatusOK, callback, user{"1", "Joe"})
+ if assert.NoError(t, err) {
+ assert.Equal(t, http.StatusOK, rec.Code)
+ assert.Equal(t, ApplicationJavaScriptCharsetUTF8, rec.Header().Get(ContentType))
+ assert.Equal(t, callback+"("+userJSON+"\n);", rec.Body.String())
+ }
+
+ // XML
+ rec = httptest.NewRecorder()
+ c = NewContext(req, NewResponse(rec), New())
+ err = c.XML(http.StatusOK, user{"1", "Joe"})
+ if assert.NoError(t, err) {
+ assert.Equal(t, http.StatusOK, rec.Code)
+ assert.Equal(t, ApplicationXMLCharsetUTF8, rec.Header().Get(ContentType))
+ assert.Equal(t, xml.Header, xml.Header, rec.Body.String())
}
// String
- req.Header.Set(Accept, TextPlain)
rec = httptest.NewRecorder()
c = NewContext(req, NewResponse(rec), New())
err = c.String(http.StatusOK, "Hello, World!")
@@ -114,16 +129,34 @@ func TestContext(t *testing.T) {
}
// HTML
- req.Header.Set(Accept, TextHTML)
rec = httptest.NewRecorder()
c = NewContext(req, NewResponse(rec), New())
err = c.HTML(http.StatusOK, "Hello, World!")
if assert.NoError(t, err) {
assert.Equal(t, http.StatusOK, rec.Code)
- assert.Equal(t, TextHTML, rec.Header().Get(ContentType))
+ assert.Equal(t, TextHTMLCharsetUTF8, rec.Header().Get(ContentType))
assert.Equal(t, "Hello, World!", rec.Body.String())
}
+ // File
+ rec = httptest.NewRecorder()
+ c = NewContext(req, NewResponse(rec), New())
+ err = c.File("test/fixture/walle.png", false)
+ if assert.NoError(t, err) {
+ assert.Equal(t, http.StatusOK, rec.Code)
+ assert.Equal(t, 219885, rec.Body.Len())
+ }
+
+ // File as attachment
+ rec = httptest.NewRecorder()
+ c = NewContext(req, NewResponse(rec), New())
+ err = c.File("test/fixture/walle.png", true)
+ if assert.NoError(t, err) {
+ assert.Equal(t, http.StatusOK, rec.Code)
+ assert.Equal(t, rec.Header().Get(ContentDisposition), "attachment; filename=walle.png")
+ assert.Equal(t, 219885, rec.Body.Len())
+ }
+
// NoContent
rec = httptest.NewRecorder()
c = NewContext(req, NewResponse(rec), New())
@@ -133,7 +166,7 @@ func TestContext(t *testing.T) {
// Redirect
rec = httptest.NewRecorder()
c = NewContext(req, NewResponse(rec), New())
- c.Redirect(http.StatusMovedPermanently, "http://labstack.github.io/echo")
+ assert.Equal(t, nil, c.Redirect(http.StatusMovedPermanently, "http://labstack.github.io/echo"))
// Error
rec = httptest.NewRecorder()
@@ -145,11 +178,42 @@ func TestContext(t *testing.T) {
c.reset(req, NewResponse(httptest.NewRecorder()), New())
}
+func TestContextQuery(t *testing.T) {
+ q := make(url.Values)
+ q.Set("name", "joe")
+ q.Set("email", "joe@labstack.com")
+
+ req, err := http.NewRequest(GET, "/", nil)
+ assert.NoError(t, err)
+ req.URL.RawQuery = q.Encode()
+
+ c := NewContext(req, nil, New())
+ assert.Equal(t, "joe", c.Query("name"))
+ assert.Equal(t, "joe@labstack.com", c.Query("email"))
+
+}
+
+func TestContextForm(t *testing.T) {
+ f := make(url.Values)
+ f.Set("name", "joe")
+ f.Set("email", "joe@labstack.com")
+
+ req, err := http.NewRequest(POST, "/", strings.NewReader(f.Encode()))
+ assert.NoError(t, err)
+ req.Header.Add(ContentType, ApplicationForm)
+
+ c := NewContext(req, nil, New())
+ assert.Equal(t, "joe", c.Form("name"))
+ assert.Equal(t, "joe@labstack.com", c.Form("email"))
+}
+
func testBind(t *testing.T, c *Context, ct string) {
c.request.Header.Set(ContentType, ct)
u := new(user)
err := c.Bind(u)
- if assert.NoError(t, err) {
+ if ct == "" {
+ assert.Error(t, UnsupportedMediaType)
+ } else if assert.NoError(t, err) {
assert.Equal(t, "1", u.ID)
assert.Equal(t, "Joe", u.Name)
}
diff --git a/echo.go b/echo.go
index d226e62f..0dc66abf 100644
--- a/echo.go
+++ b/echo.go
@@ -14,8 +14,10 @@ import (
"strings"
"sync"
+ "encoding/xml"
+
"github.com/bradfitz/http2"
- "github.com/mattn/go-colorable"
+ "github.com/labstack/gommon/color"
"golang.org/x/net/websocket"
)
@@ -28,10 +30,11 @@ type (
notFoundHandler HandlerFunc
defaultHTTPErrorHandler HTTPErrorHandler
httpErrorHandler HTTPErrorHandler
- binder BindFunc
+ binder Binder
renderer Renderer
pool sync.Pool
debug bool
+ stripTrailingSlash bool
router *Router
}
@@ -54,12 +57,20 @@ type (
// HTTPErrorHandler is a centralized HTTP error handler.
HTTPErrorHandler func(error, *Context)
- BindFunc func(*http.Request, interface{}) error
+ // Binder is the interface that wraps the Bind method.
+ Binder interface {
+ Bind(*http.Request, interface{}) error
+ }
+
+ binder struct {
+ }
+
+ // Validator is the interface that wraps the Validate method.
+ Validator interface {
+ Validate() error
+ }
// Renderer is the interface that wraps the Render method.
- //
- // Render renders the HTML template with given name and specified data.
- // It writes the output to w.
Renderer interface {
Render(w io.Writer, name string, data interface{}) error
}
@@ -89,27 +100,41 @@ const (
// Media types
//-------------
- ApplicationJSON = "application/json"
- ApplicationProtobuf = "application/protobuf"
- ApplicationMsgpack = "application/msgpack"
- TextPlain = "text/plain"
- TextHTML = "text/html"
- ApplicationForm = "application/x-www-form-urlencoded"
- MultipartForm = "multipart/form-data"
+ ApplicationJSON = "application/json"
+ ApplicationJSONCharsetUTF8 = ApplicationJSON + "; " + CharsetUTF8
+ ApplicationJavaScript = "application/javascript"
+ ApplicationJavaScriptCharsetUTF8 = ApplicationJavaScript + "; " + CharsetUTF8
+ ApplicationXML = "application/xml"
+ ApplicationXMLCharsetUTF8 = ApplicationXML + "; " + CharsetUTF8
+ ApplicationForm = "application/x-www-form-urlencoded"
+ ApplicationProtobuf = "application/protobuf"
+ ApplicationMsgpack = "application/msgpack"
+ TextHTML = "text/html"
+ TextHTMLCharsetUTF8 = TextHTML + "; " + CharsetUTF8
+ TextPlain = "text/plain"
+ TextPlainCharsetUTF8 = TextPlain + "; " + CharsetUTF8
+ MultipartForm = "multipart/form-data"
+
+ //---------
+ // Charset
+ //---------
+
+ CharsetUTF8 = "charset=utf-8"
//---------
// Headers
//---------
- Accept = "Accept"
AcceptEncoding = "Accept-Encoding"
+ Authorization = "Authorization"
ContentDisposition = "Content-Disposition"
ContentEncoding = "Content-Encoding"
ContentLength = "Content-Length"
ContentType = "Content-Type"
- Authorization = "Authorization"
+ Location = "Location"
Upgrade = "Upgrade"
Vary = "Vary"
+ WWWAuthenticate = "WWW-Authenticate"
//-----------
// Protocols
@@ -139,9 +164,22 @@ var (
UnsupportedMediaType = errors.New("echo ⇒ unsupported media type")
RendererNotRegistered = errors.New("echo ⇒ renderer not registered")
+ InvalidRedirectCode = errors.New("echo ⇒ invalid redirect status code")
+
+ //----------------
+ // Error handlers
+ //----------------
+
+ notFoundHandler = func(c *Context) error {
+ return NewHTTPError(http.StatusNotFound)
+ }
+
+ badRequestHandler = func(c *Context) error {
+ return NewHTTPError(http.StatusBadRequest)
+ }
)
-// New creates an Echo instance.
+// New creates an instance of Echo.
func New() (e *Echo) {
e = &Echo{maxParam: new(int)}
e.pool.New = func() interface{} {
@@ -153,10 +191,10 @@ func New() (e *Echo) {
// Defaults
//----------
- e.HTTP2(false)
- e.notFoundHandler = func(c *Context) error {
- return NewHTTPError(http.StatusNotFound)
+ if runtime.GOOS == "windows" {
+ e.DisableColoredLog()
}
+ e.HTTP2()
e.defaultHTTPErrorHandler = func(err error, c *Context) {
code := http.StatusInternalServerError
msg := http.StatusText(code)
@@ -167,19 +205,13 @@ func New() (e *Echo) {
if e.debug {
msg = err.Error()
}
- http.Error(c.response, msg, code)
+ if !c.response.committed {
+ http.Error(c.response, msg, code)
+ }
+ log.Println(err)
}
e.SetHTTPErrorHandler(e.defaultHTTPErrorHandler)
- e.SetBinder(func(r *http.Request, v interface{}) error {
- ct := r.Header.Get(ContentType)
- err := UnsupportedMediaType
- if strings.HasPrefix(ct, ApplicationJSON) {
- err = json.NewDecoder(r.Body).Decode(v)
- } else if strings.HasPrefix(ct, ApplicationForm) {
- err = nil
- }
- return err
- })
+ e.SetBinder(&binder{})
return
}
@@ -188,9 +220,14 @@ func (e *Echo) Router() *Router {
return e.router
}
+// DisableColoredLog disables colored log.
+func (e *Echo) DisableColoredLog() {
+ color.Disable()
+}
+
// HTTP2 enables HTTP2 support.
-func (e *Echo) HTTP2(on bool) {
- e.http2 = on
+func (e *Echo) HTTP2() {
+ e.http2 = true
}
// DefaultHTTPErrorHandler invokes the default HTTP error handler.
@@ -204,7 +241,7 @@ func (e *Echo) SetHTTPErrorHandler(h HTTPErrorHandler) {
}
// SetBinder registers a custom binder. It's invoked by Context.Bind().
-func (e *Echo) SetBinder(b BindFunc) {
+func (e *Echo) SetBinder(b Binder) {
e.binder = b
}
@@ -213,14 +250,14 @@ func (e *Echo) SetRenderer(r Renderer) {
e.renderer = r
}
-// SetDebug sets debug mode.
-func (e *Echo) SetDebug(on bool) {
- e.debug = on
+// Debug enables debug mode.
+func (e *Echo) Debug() {
+ e.debug = true
}
-// Debug returns debug mode.
-func (e *Echo) Debug() bool {
- return e.debug
+// StripTrailingSlash enables removing trailing slash from the request path.
+func (e *Echo) StripTrailingSlash() {
+ e.stripTrailingSlash = true
}
// Use adds handler to the middleware chain.
@@ -275,6 +312,20 @@ func (e *Echo) Trace(path string, h Handler) {
e.add(TRACE, path, h)
}
+// Any adds a route > handler to the router for all HTTP methods.
+func (e *Echo) Any(path string, h Handler) {
+ for _, m := range methods {
+ e.add(m, path, h)
+ }
+}
+
+// Match adds a route > handler to the router for multiple HTTP methods provided.
+func (e *Echo) Match(methods []string, path string, h Handler) {
+ for _, m := range methods {
+ e.add(m, path, h)
+ }
+}
+
// WebSocket adds a WebSocket route > handler to the router.
func (e *Echo) WebSocket(path string, h HandlerFunc) {
e.Get(path, func(c *Context) (err error) {
@@ -389,7 +440,7 @@ func (e *Echo) URI(h Handler, params ...interface{}) string {
return uri.String()
}
-// URL is an alias for URI.
+// URL is an alias for `URI` function.
func (e *Echo) URL(h Handler, params ...interface{}) string {
return e.URI(h, params...)
}
@@ -399,6 +450,7 @@ func (e *Echo) Routes() []Route {
return e.router.routes
}
+// ServeHTTP implements `http.Handler` interface, which serves HTTP requests.
func (e *Echo) ServeHTTP(w http.ResponseWriter, r *http.Request) {
c := e.pool.Get().(*Context)
h, echo := e.router.Find(r.Method, r.URL.Path, c)
@@ -406,9 +458,6 @@ func (e *Echo) ServeHTTP(w http.ResponseWriter, r *http.Request) {
e = echo
}
c.reset(r, w, e)
- if h == nil {
- h = e.notFoundHandler
- }
// Chain middleware with handler in the end
for i := len(e.middleware) - 1; i >= 0; i-- {
@@ -423,7 +472,7 @@ func (e *Echo) ServeHTTP(w http.ResponseWriter, r *http.Request) {
e.pool.Put(c)
}
-// Server returns the internal *http.Server
+// Server returns the internal *http.Server.
func (e *Echo) Server(addr string) *http.Server {
s := &http.Server{Addr: addr}
s.Handler = e
@@ -446,13 +495,13 @@ func (e *Echo) RunTLS(addr, certFile, keyFile string) {
}
// RunServer runs a custom server.
-func (e *Echo) RunServer(srv *http.Server) {
- e.run(srv)
+func (e *Echo) RunServer(s *http.Server) {
+ e.run(s)
}
// RunTLSServer runs a custom server with TLS configuration.
-func (e *Echo) RunTLSServer(srv *http.Server, certFile, keyFile string) {
- e.run(srv, certFile, keyFile)
+func (e *Echo) RunTLSServer(s *http.Server, certFile, keyFile string) {
+ e.run(s, certFile, keyFile)
}
func (e *Echo) run(s *http.Server, files ...string) {
@@ -489,7 +538,7 @@ func (e *HTTPError) Error() string {
return e.message
}
-// wraps middleware
+// wrapMiddleware wraps middleware.
func wrapMiddleware(m Middleware) MiddlewareFunc {
switch m := m.(type) {
case MiddlewareFunc:
@@ -520,7 +569,7 @@ func wrapMiddleware(m Middleware) MiddlewareFunc {
}
}
-// Wraps HandlerFunc middleware
+// wrapHandlerFuncMW wraps HandlerFunc middleware.
func wrapHandlerFuncMW(m HandlerFunc) MiddlewareFunc {
return func(h HandlerFunc) HandlerFunc {
return func(c *Context) error {
@@ -532,7 +581,7 @@ func wrapHandlerFuncMW(m HandlerFunc) MiddlewareFunc {
}
}
-// Wraps http.HandlerFunc middleware
+// wrapHTTPHandlerFuncMW wraps http.HandlerFunc middleware.
func wrapHTTPHandlerFuncMW(m http.HandlerFunc) MiddlewareFunc {
return func(h HandlerFunc) HandlerFunc {
return func(c *Context) error {
@@ -544,7 +593,7 @@ func wrapHTTPHandlerFuncMW(m http.HandlerFunc) MiddlewareFunc {
}
}
-// wraps handler
+// wrapHandler wraps handler.
func wrapHandler(h Handler) HandlerFunc {
switch h := h.(type) {
case HandlerFunc:
@@ -566,6 +615,13 @@ func wrapHandler(h Handler) HandlerFunc {
}
}
-func init() {
- log.SetOutput(colorable.NewColorableStdout())
+func (binder) Bind(r *http.Request, i interface{}) (err error) {
+ ct := r.Header.Get(ContentType)
+ err = UnsupportedMediaType
+ if strings.HasPrefix(ct, ApplicationJSON) {
+ err = json.NewDecoder(r.Body).Decode(i)
+ } else if strings.HasPrefix(ct, ApplicationXML) {
+ err = xml.NewDecoder(r.Body).Decode(i)
+ }
+ return
}
diff --git a/echo_test.go b/echo_test.go
index 1b4279c1..aec0b056 100644
--- a/echo_test.go
+++ b/echo_test.go
@@ -18,8 +18,8 @@ import (
type (
user struct {
- ID string `json:"id"`
- Name string `json:"name"`
+ ID string `json:"id" xml:"id"`
+ Name string `json:"name" xml:"name"`
}
)
@@ -33,8 +33,8 @@ func TestEcho(t *testing.T) {
assert.NotNil(t, e.Router())
// Debug
- e.SetDebug(true)
- assert.True(t, e.Debug())
+ e.Debug()
+ assert.True(t, e.debug)
// DefaultHTTPErrorHandler
e.DefaultHTTPErrorHandler(errors.New("error"), c)
@@ -246,6 +246,20 @@ func TestEchoTrace(t *testing.T) {
testMethod(t, TRACE, "/", e)
}
+func TestEchoAny(t *testing.T) { // JFC
+ e := New()
+ e.Any("/", func(c *Context) error {
+ return c.String(http.StatusOK, "Any")
+ })
+}
+
+func TestEchoMatch(t *testing.T) { // JFC
+ e := New()
+ e.Match([]string{GET, POST}, "/", func(c *Context) error {
+ return c.String(http.StatusOK, "Match")
+ })
+}
+
func TestEchoWebSocket(t *testing.T) {
e := New()
e.WebSocket("/ws", func(c *Context) error {
@@ -368,6 +382,14 @@ func TestEchoNotFound(t *testing.T) {
assert.Equal(t, http.StatusNotFound, w.Code)
}
+func TestEchoBadRequest(t *testing.T) {
+ e := New()
+ r, _ := http.NewRequest("INVALID", "/files", nil)
+ w := httptest.NewRecorder()
+ e.ServeHTTP(w, r)
+ assert.Equal(t, http.StatusBadRequest, w.Code)
+}
+
func TestEchoHTTPError(t *testing.T) {
m := http.StatusText(http.StatusBadRequest)
he := NewHTTPError(http.StatusBadRequest, m)
@@ -381,12 +403,20 @@ func TestEchoServer(t *testing.T) {
assert.IsType(t, &http.Server{}, s)
}
+func TestStripTrailingSlash(t *testing.T) {
+ e := New()
+ e.StripTrailingSlash()
+ r, _ := http.NewRequest(GET, "/users/", nil)
+ w := httptest.NewRecorder()
+ e.ServeHTTP(w, r)
+ assert.Equal(t, http.StatusNotFound, w.Code)
+}
+
func testMethod(t *testing.T, method, path string, e *Echo) {
m := fmt.Sprintf("%c%s", method[0], strings.ToLower(method[1:]))
p := reflect.ValueOf(path)
h := reflect.ValueOf(func(c *Context) error {
- c.String(http.StatusOK, method)
- return nil
+ return c.String(http.StatusOK, method)
})
i := interface{}(e)
reflect.ValueOf(i).MethodByName(m).Call([]reflect.Value{p, h})
diff --git a/examples/middleware/server.go b/examples/middleware/server.go
index 503210c3..98fa0d75 100644
--- a/examples/middleware/server.go
+++ b/examples/middleware/server.go
@@ -17,7 +17,7 @@ func main() {
e := echo.New()
// Debug mode
- e.SetDebug(true)
+ e.Debug()
//------------
// Middleware
@@ -37,16 +37,6 @@ func main() {
return false
}))
- //-------
- // Slash
- //-------
-
- e.Use(mw.StripTrailingSlash())
-
- // or
-
- // e.Use(mw.RedirectToSlash())
-
// Gzip
e.Use(mw.Gzip())
diff --git a/examples/websocket/server.go b/examples/websocket/server.go
deleted file mode 100644
index 1b8894b2..00000000
--- a/examples/websocket/server.go
+++ /dev/null
@@ -1,18 +0,0 @@
-package main
-
-import (
- "io"
-
- "github.com/labstack/echo"
- mw "github.com/labstack/echo/middleware"
-)
-
-func main() {
- e := echo.New()
- e.Use(mw.Logger())
- e.WebSocket("/ws", func(c *echo.Context) error {
- io.Copy(c.Socket(), c.Socket())
- return nil
- })
- e.Run(":1323")
-}
diff --git a/middleware/auth.go b/middleware/auth.go
index 9f471084..c47890e8 100644
--- a/middleware/auth.go
+++ b/middleware/auth.go
@@ -2,25 +2,22 @@ package middleware
import (
"encoding/base64"
- "github.com/dgrijalva/jwt-go"
- "github.com/labstack/echo"
"net/http"
+
+ "github.com/labstack/echo"
)
type (
BasicValidateFunc func(string, string) bool
- JWTValidateFunc func(string, jwt.SigningMethod) ([]byte, error)
)
const (
- Basic = "Basic"
- Bearer = "Bearer"
+ Basic = "Basic"
)
// BasicAuth returns an HTTP basic authentication middleware.
//
// For valid credentials it calls the next handler.
-// For invalid Authorization header it sends "404 - Bad Request" response.
// For invalid credentials, it sends "401 - Unauthorized" response.
func BasicAuth(fn BasicValidateFunc) echo.HandlerFunc {
return func(c *echo.Context) error {
@@ -31,7 +28,6 @@ func BasicAuth(fn BasicValidateFunc) echo.HandlerFunc {
auth := c.Request().Header.Get(echo.Authorization)
l := len(Basic)
- he := echo.NewHTTPError(http.StatusBadRequest)
if len(auth) > l+1 && auth[:l] == Basic {
b, err := base64.StdEncoding.DecodeString(auth[l+1:])
@@ -43,47 +39,11 @@ func BasicAuth(fn BasicValidateFunc) echo.HandlerFunc {
if fn(cred[:i], cred[i+1:]) {
return nil
}
- he.SetCode(http.StatusUnauthorized)
}
}
}
}
- return he
- }
-}
-
-// JWTAuth returns a JWT authentication middleware.
-//
-// For valid token it sets JWT claims in the context with key `_claims` and calls
-// the next handler.
-// For invalid Authorization header it sends "404 - Bad Request" response.
-// For invalid credentials, it sends "401 - Unauthorized" response.
-func JWTAuth(fn JWTValidateFunc) echo.HandlerFunc {
- return func(c *echo.Context) error {
- // Skip WebSocket
- if (c.Request().Header.Get(echo.Upgrade)) == echo.WebSocket {
- return nil
- }
-
- auth := c.Request().Header.Get("Authorization")
- l := len(Bearer)
- he := echo.NewHTTPError(http.StatusBadRequest)
-
- if len(auth) > l+1 && auth[:l] == Bearer {
- t, err := jwt.Parse(auth[l+1:], func(token *jwt.Token) (interface{}, error) {
- // Lookup key and verify method
- if kid := token.Header["kid"]; kid != nil {
- return fn(kid.(string), token.Method)
- }
- return fn("", token.Method)
- })
- if err == nil && t.Valid {
- c.Set("_claims", t.Claims)
- return nil
- } else {
- he.SetCode(http.StatusUnauthorized)
- }
- }
- return he
+ c.Response().Header().Set(echo.WWWAuthenticate, Basic + " realm=Restricted")
+ return echo.NewHTTPError(http.StatusUnauthorized)
}
}
diff --git a/middleware/auth_test.go b/middleware/auth_test.go
index d251be19..278a75ef 100644
--- a/middleware/auth_test.go
+++ b/middleware/auth_test.go
@@ -3,13 +3,11 @@ package middleware
import (
"encoding/base64"
"net/http"
+ "net/http/httptest"
"testing"
- "github.com/dgrijalva/jwt-go"
"github.com/labstack/echo"
"github.com/stretchr/testify/assert"
- "net/http/httptest"
- "time"
)
func TestBasicAuth(t *testing.T) {
@@ -38,80 +36,22 @@ func TestBasicAuth(t *testing.T) {
req.Header.Set(echo.Authorization, auth)
he := ba(c).(*echo.HTTPError)
assert.Equal(t, http.StatusUnauthorized, he.Code())
+ assert.Equal(t, Basic + " realm=Restricted", rec.Header().Get(echo.WWWAuthenticate))
// Empty Authorization header
req.Header.Set(echo.Authorization, "")
he = ba(c).(*echo.HTTPError)
- assert.Equal(t, http.StatusBadRequest, he.Code())
+ assert.Equal(t, http.StatusUnauthorized, he.Code())
+ assert.Equal(t, Basic + " realm=Restricted", rec.Header().Get(echo.WWWAuthenticate))
// Invalid Authorization header
- auth = base64.StdEncoding.EncodeToString([]byte(" :secret"))
+ auth = base64.StdEncoding.EncodeToString([]byte("invalid"))
req.Header.Set(echo.Authorization, auth)
he = ba(c).(*echo.HTTPError)
- assert.Equal(t, http.StatusBadRequest, he.Code())
-
- // Invalid scheme
- auth = "Base " + base64.StdEncoding.EncodeToString([]byte(" :secret"))
- req.Header.Set(echo.Authorization, auth)
- he = ba(c).(*echo.HTTPError)
- assert.Equal(t, http.StatusBadRequest, he.Code())
+ assert.Equal(t, http.StatusUnauthorized, he.Code())
+ assert.Equal(t, Basic + " realm=Restricted", rec.Header().Get(echo.WWWAuthenticate))
// WebSocket
c.Request().Header.Set(echo.Upgrade, echo.WebSocket)
assert.NoError(t, ba(c))
}
-
-func TestJWTAuth(t *testing.T) {
- req, _ := http.NewRequest(echo.GET, "/", nil)
- rec := httptest.NewRecorder()
- c := echo.NewContext(req, echo.NewResponse(rec), echo.New())
- key := []byte("key")
- fn := func(kid string, method jwt.SigningMethod) ([]byte, error) {
- return key, nil
- }
- ja := JWTAuth(fn)
- token := jwt.New(jwt.SigningMethodHS256)
- token.Claims["foo"] = "bar"
- token.Claims["exp"] = time.Now().Add(time.Hour * 72).Unix()
- ts, err := token.SignedString(key)
- assert.NoError(t, err)
-
- // Valid credentials
- auth := Bearer + " " + ts
- req.Header.Set(echo.Authorization, auth)
- assert.NoError(t, ja(c))
-
- //---------------------
- // Invalid credentials
- //---------------------
-
- // Expired token
- token.Claims["exp"] = time.Now().Add(-time.Second).Unix()
- ts, err = token.SignedString(key)
- assert.NoError(t, err)
- auth = Bearer + " " + ts
- req.Header.Set(echo.Authorization, auth)
- he := ja(c).(*echo.HTTPError)
- assert.Equal(t, http.StatusUnauthorized, he.Code())
-
- // Empty Authorization header
- req.Header.Set(echo.Authorization, "")
- he = ja(c).(*echo.HTTPError)
- assert.Equal(t, http.StatusBadRequest, he.Code())
-
- // Invalid Authorization header
- auth = "token"
- req.Header.Set(echo.Authorization, auth)
- he = ja(c).(*echo.HTTPError)
- assert.Equal(t, http.StatusBadRequest, he.Code())
-
- // Invalid scheme
- auth = "Bear token"
- req.Header.Set(echo.Authorization, auth)
- he = ja(c).(*echo.HTTPError)
- assert.Equal(t, http.StatusBadRequest, he.Code())
-
- // WebSocket
- c.Request().Header.Set(echo.Upgrade, echo.WebSocket)
- assert.NoError(t, ja(c))
-}
diff --git a/middleware/recover_test.go b/middleware/recover_test.go
index 003c7519..0dd2f5f9 100644
--- a/middleware/recover_test.go
+++ b/middleware/recover_test.go
@@ -11,7 +11,7 @@ import (
func TestRecover(t *testing.T) {
e := echo.New()
- e.SetDebug(true)
+ e.Debug()
req, _ := http.NewRequest(echo.GET, "/", nil)
rec := httptest.NewRecorder()
c := echo.NewContext(req, echo.NewResponse(rec), e)
diff --git a/middleware/slash.go b/middleware/slash.go
deleted file mode 100644
index a768a8ec..00000000
--- a/middleware/slash.go
+++ /dev/null
@@ -1,46 +0,0 @@
-package middleware
-
-import (
- "net/http"
-
- "github.com/labstack/echo"
-)
-
-type (
- RedirectToSlashOptions struct {
- Code int
- }
-)
-
-// StripTrailingSlash returns a middleware which removes trailing slash from request
-// path.
-func StripTrailingSlash() echo.HandlerFunc {
- return func(c *echo.Context) error {
- p := c.Request().URL.Path
- l := len(p)
- if p[l-1] == '/' {
- c.Request().URL.Path = p[:l-1]
- }
- return nil
- }
-}
-
-// RedirectToSlash returns a middleware which redirects requests without trailing
-// slash path to trailing slash path.
-func RedirectToSlash(opts ...RedirectToSlashOptions) echo.HandlerFunc {
- code := http.StatusMovedPermanently
-
- if len(opts) > 0 {
- o := opts[0]
- code = o.Code
- }
-
- return func(c *echo.Context) error {
- p := c.Request().URL.Path
- l := len(p)
- if p[l-1] != '/' {
- c.Redirect(code, p+"/")
- }
- return nil
- }
-}
diff --git a/middleware/slash_test.go b/middleware/slash_test.go
deleted file mode 100644
index 59eaf238..00000000
--- a/middleware/slash_test.go
+++ /dev/null
@@ -1,27 +0,0 @@
-package middleware
-
-import (
- "net/http"
- "net/http/httptest"
- "testing"
-
- "github.com/labstack/echo"
- "github.com/stretchr/testify/assert"
-)
-
-func TestStripTrailingSlash(t *testing.T) {
- req, _ := http.NewRequest(echo.GET, "/users/", nil)
- rec := httptest.NewRecorder()
- c := echo.NewContext(req, echo.NewResponse(rec), echo.New())
- StripTrailingSlash()(c)
- assert.Equal(t, "/users", c.Request().URL.Path)
-}
-
-func TestRedirectToSlash(t *testing.T) {
- req, _ := http.NewRequest(echo.GET, "/users", nil)
- rec := httptest.NewRecorder()
- c := echo.NewContext(req, echo.NewResponse(rec), echo.New())
- RedirectToSlash(RedirectToSlashOptions{Code: http.StatusTemporaryRedirect})(c)
- assert.Equal(t, http.StatusTemporaryRedirect, rec.Code)
- assert.Equal(t, "/users/", c.Response().Header().Get("Location"))
-}
diff --git a/recipes/file-upload/public/index.html b/recipes/file-upload/public/index.html
new file mode 100644
index 00000000..82f38b49
--- /dev/null
+++ b/recipes/file-upload/public/index.html
@@ -0,0 +1,16 @@
+
+
+
+
+ File Upload
+
+
+ Upload Files
+
+
+
diff --git a/recipes/file-upload/server.go b/recipes/file-upload/server.go
new file mode 100644
index 00000000..fef5c853
--- /dev/null
+++ b/recipes/file-upload/server.go
@@ -0,0 +1,56 @@
+package main
+
+import (
+ "io"
+ "os"
+
+ "net/http"
+
+ "github.com/labstack/echo"
+ mw "github.com/labstack/echo/middleware"
+)
+
+func upload(c *echo.Context) error {
+ req := c.Request()
+
+ // req.ParseMultipartForm(16 << 20) // Max memory 16 MiB
+
+ // Read form fields
+ name := c.Form("name")
+ email := c.Form("email")
+
+ // Read files
+ files := req.MultipartForm.File["files"]
+ for _, f := range files {
+ // Source file
+ src, err := f.Open()
+ if err != nil {
+ return err
+ }
+ defer src.Close()
+
+ // Destination file
+ dst, err := os.Create(f.Filename)
+ if err != nil {
+ return err
+ }
+ defer dst.Close()
+
+ if _, err = io.Copy(dst, src); err != nil {
+ return err
+ }
+ }
+ return c.String(http.StatusOK, "Thank You! %s <%s>, %d files uploaded successfully.",
+ name, email, len(files))
+}
+
+func main() {
+ e := echo.New()
+ e.Use(mw.Logger())
+ e.Use(mw.Recover())
+
+ e.Static("/", "public")
+ e.Post("/upload", upload)
+
+ e.Run(":1323")
+}
diff --git a/examples/grace/server.go b/recipes/graceful-shutdown/grace/server.go
similarity index 68%
rename from examples/grace/server.go
rename to recipes/graceful-shutdown/grace/server.go
index fee4ffae..9dbeb7d4 100644
--- a/examples/grace/server.go
+++ b/recipes/graceful-shutdown/grace/server.go
@@ -11,10 +11,8 @@ func main() {
// Setup
e := echo.New()
e.Get("/", func(c *echo.Context) error {
- c.String(http.StatusOK, "Six sick bricks tick")
- return nil
+ return c.String(http.StatusOK, "Six sick bricks tick")
})
- // Use github.com/facebookgo/grace/gracehttp
gracehttp.Serve(e.Server(":1323"))
}
diff --git a/examples/graceful/server.go b/recipes/graceful-shutdown/graceful/server.go
similarity index 69%
rename from examples/graceful/server.go
rename to recipes/graceful-shutdown/graceful/server.go
index 2c862c92..3a872691 100644
--- a/examples/graceful/server.go
+++ b/recipes/graceful-shutdown/graceful/server.go
@@ -12,10 +12,8 @@ func main() {
// Setup
e := echo.New()
e.Get("/", func(c *echo.Context) error {
- c.String(http.StatusOK, "Sue sews rose on slow jor crows nose")
- return nil
+ return c.String(http.StatusOK, "Sue sews rose on slow jor crows nose")
})
- // Use github.com/tylerb/graceful
graceful.ListenAndServe(e.Server(":1323"), 5*time.Second)
}
diff --git a/recipes/jwt-authentication/server.go b/recipes/jwt-authentication/server.go
new file mode 100644
index 00000000..96db0bf8
--- /dev/null
+++ b/recipes/jwt-authentication/server.go
@@ -0,0 +1,76 @@
+package main
+
+import (
+ "fmt"
+ "net/http"
+
+ "github.com/dgrijalva/jwt-go"
+ "github.com/labstack/echo"
+ mw "github.com/labstack/echo/middleware"
+)
+
+const (
+ Bearer = "Bearer"
+ SigningKey = "somethingsupersecret"
+)
+
+// A JSON Web Token middleware
+func JWTAuth(key string) echo.HandlerFunc {
+ return func(c *echo.Context) error {
+
+ // Skip WebSocket
+ if (c.Request().Header.Get(echo.Upgrade)) == echo.WebSocket {
+ return nil
+ }
+
+ auth := c.Request().Header.Get("Authorization")
+ l := len(Bearer)
+ he := echo.NewHTTPError(http.StatusUnauthorized)
+
+ if len(auth) > l+1 && auth[:l] == Bearer {
+ t, err := jwt.Parse(auth[l+1:], func(token *jwt.Token) (interface{}, error) {
+
+ // Always check the signing method
+ if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
+ return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
+ }
+
+ // Return the key for validation
+ return []byte(key), nil
+ })
+ if err == nil && t.Valid {
+ // Store token claims in echo.Context
+ c.Set("claims", t.Claims)
+ return nil
+ }
+ }
+ return he
+ }
+}
+
+func accessible(c *echo.Context) error {
+ return c.String(http.StatusOK, "No auth required for this route.\n")
+}
+
+func restricted(c *echo.Context) error {
+ return c.String(http.StatusOK, "Access granted with JWT.\n")
+}
+
+func main() {
+ // Echo instance
+ e := echo.New()
+
+ // Logger
+ e.Use(mw.Logger())
+
+ // Unauthenticated route
+ e.Get("/", accessible)
+
+ // Restricted group
+ r := e.Group("/restricted")
+ r.Use(JWTAuth(SigningKey))
+ r.Get("", restricted)
+
+ // Start server
+ e.Run(":1323")
+}
diff --git a/recipes/jwt-authentication/token/token.go b/recipes/jwt-authentication/token/token.go
new file mode 100644
index 00000000..0e309ab1
--- /dev/null
+++ b/recipes/jwt-authentication/token/token.go
@@ -0,0 +1,24 @@
+package main
+
+import (
+ "fmt"
+ "time"
+
+ "github.com/dgrijalva/jwt-go"
+)
+
+const SigningKey = "somethingsupersecret"
+
+func main() {
+
+ // New web token.
+ token := jwt.New(jwt.SigningMethodHS256)
+
+ // Set a header and a claim
+ token.Header["typ"] = "JWT"
+ token.Claims["exp"] = time.Now().Add(time.Hour * 96).Unix()
+
+ // Generate encoded token
+ t, _ := token.SignedString([]byte(SigningKey))
+ fmt.Println(t)
+}
diff --git a/recipes/streaming-file-upload/public/index.html b/recipes/streaming-file-upload/public/index.html
new file mode 100644
index 00000000..b5cfe81e
--- /dev/null
+++ b/recipes/streaming-file-upload/public/index.html
@@ -0,0 +1,16 @@
+
+
+
+
+ File Upload
+
+
+Upload Files
+
+
+
diff --git a/recipes/streaming-file-upload/server.go b/recipes/streaming-file-upload/server.go
new file mode 100644
index 00000000..6a826439
--- /dev/null
+++ b/recipes/streaming-file-upload/server.go
@@ -0,0 +1,79 @@
+package main
+
+import (
+ "io/ioutil"
+
+ "github.com/labstack/echo"
+ mw "github.com/labstack/echo/middleware"
+ "io"
+ "net/http"
+ "os"
+)
+
+func upload(c *echo.Context) error {
+ mr, err := c.Request().MultipartReader()
+ if err != nil {
+ return err
+ }
+
+ // Read form field `name`
+ part, err := mr.NextPart()
+ if err != nil {
+ return err
+ }
+ defer part.Close()
+ b, err := ioutil.ReadAll(part)
+ if err != nil {
+ return err
+ }
+ name := string(b)
+
+ // Read form field `email`
+ part, err = mr.NextPart()
+ if err != nil {
+ return err
+ }
+ defer part.Close()
+ b, err = ioutil.ReadAll(part)
+ if err != nil {
+ return err
+ }
+ email := string(b)
+
+ // Read files
+ i := 0
+ for {
+ part, err := mr.NextPart()
+ if err != nil {
+ if err == io.EOF {
+ break
+ }
+ return err
+ }
+ defer part.Close()
+
+ file, err := os.Create(part.FileName())
+ if err != nil {
+ return err
+ }
+ defer file.Close()
+
+ if _, err := io.Copy(file, part); err != nil {
+ return err
+ }
+ i++
+ }
+ return c.String(http.StatusOK, "Thank You! %s <%s>, %d files uploaded successfully.",
+ name, email, i)
+}
+
+func main() {
+ e := echo.New()
+ e.Use(mw.Logger())
+ e.Use(mw.Recover())
+
+ e.Static("/", "public")
+ e.Post("/upload", upload)
+
+ e.Run(":1323")
+}
diff --git a/examples/stream/server.go b/recipes/streaming-response/server.go
similarity index 93%
rename from examples/stream/server.go
rename to recipes/streaming-response/server.go
index 3d88e37f..0dbb1808 100644
--- a/examples/stream/server.go
+++ b/recipes/streaming-response/server.go
@@ -29,7 +29,7 @@ var (
func main() {
e := echo.New()
- e.Get("/stream", func(c *echo.Context) error {
+ e.Get("/", func(c *echo.Context) error {
c.Response().Header().Set(echo.ContentType, echo.ApplicationJSON)
c.Response().WriteHeader(http.StatusOK)
for _, l := range locations {
diff --git a/recipes/subdomains/server.go b/recipes/subdomains/server.go
new file mode 100644
index 00000000..2da3cf26
--- /dev/null
+++ b/recipes/subdomains/server.go
@@ -0,0 +1,67 @@
+package main
+
+import (
+ "net/http"
+
+ "github.com/labstack/echo"
+ mw "github.com/labstack/echo/middleware"
+)
+
+type Hosts map[string]http.Handler
+
+func (h Hosts) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+ if handler := h[r.Host]; handler != nil {
+ handler.ServeHTTP(w, r)
+ } else {
+ http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
+ }
+}
+
+func main() {
+ // Host map
+ hosts := make(Hosts)
+
+ //-----
+ // API
+ //-----
+
+ api := echo.New()
+ api.Use(mw.Logger())
+ api.Use(mw.Recover())
+
+ hosts["api.localhost:1323"] = api
+
+ api.Get("/", func(c *echo.Context) error {
+ return c.String(http.StatusOK, "API")
+ })
+
+ //------
+ // Blog
+ //------
+
+ blog := echo.New()
+ blog.Use(mw.Logger())
+ blog.Use(mw.Recover())
+
+ hosts["blog.localhost:1323"] = blog
+
+ blog.Get("/", func(c *echo.Context) error {
+ return c.String(http.StatusOK, "Blog")
+ })
+
+ //---------
+ // Website
+ //---------
+
+ site := echo.New()
+ site.Use(mw.Logger())
+ site.Use(mw.Recover())
+
+ hosts["localhost:1323"] = site
+
+ site.Get("/", func(c *echo.Context) error {
+ return c.String(http.StatusOK, "Welcome!")
+ })
+
+ http.ListenAndServe(":1323", hosts)
+}
diff --git a/recipes/websocket/public/index.html b/recipes/websocket/public/index.html
new file mode 100644
index 00000000..3a307896
--- /dev/null
+++ b/recipes/websocket/public/index.html
@@ -0,0 +1,36 @@
+
+
+
+
+ WebSocket
+
+
+
+
+
+
+
diff --git a/recipes/websocket/server.go b/recipes/websocket/server.go
new file mode 100644
index 00000000..b98acb85
--- /dev/null
+++ b/recipes/websocket/server.go
@@ -0,0 +1,35 @@
+package main
+
+import (
+ "fmt"
+
+ "github.com/labstack/echo"
+ mw "github.com/labstack/echo/middleware"
+ "golang.org/x/net/websocket"
+)
+
+func main() {
+ e := echo.New()
+
+ e.Use(mw.Logger())
+ e.Use(mw.Recover())
+
+ e.Static("/", "public")
+ e.WebSocket("/ws", func(c *echo.Context) (err error) {
+ ws := c.Socket()
+ msg := ""
+
+ for {
+ if err = websocket.Message.Send(ws, "Hello, Client!"); err != nil {
+ return
+ }
+ if err = websocket.Message.Receive(ws, &msg); err != nil {
+ return
+ }
+ fmt.Println(msg)
+ }
+ return
+ })
+
+ e.Run(":1323")
+}
diff --git a/response.go b/response.go
index 84de7c9c..715df158 100644
--- a/response.go
+++ b/response.go
@@ -80,3 +80,8 @@ func (r *Response) reset(w http.ResponseWriter) {
r.status = http.StatusOK
r.committed = false
}
+
+//func (r *Response) clear() {
+// r.Header().Del(ContentType)
+// r.committed = false
+//}
diff --git a/router.go b/router.go
index 70ca7595..82a57282 100644
--- a/router.go
+++ b/router.go
@@ -4,9 +4,17 @@ import "net/http"
type (
Router struct {
- trees [21]*node
- routes []Route
- echo *Echo
+ connectTree *node
+ deleteTree *node
+ getTree *node
+ headTree *node
+ optionsTree *node
+ patchTree *node
+ postTree *node
+ putTree *node
+ traceTree *node
+ routes []Route
+ echo *Echo
}
node struct {
typ ntype
@@ -28,19 +36,20 @@ const (
mtype
)
-func NewRouter(e *Echo) (r *Router) {
- r = &Router{
- // trees: make(map[string]*node),
- routes: []Route{},
- echo: e,
+func NewRouter(e *Echo) *Router {
+ return &Router{
+ connectTree: new(node),
+ deleteTree: new(node),
+ getTree: new(node),
+ headTree: new(node),
+ optionsTree: new(node),
+ patchTree: new(node),
+ postTree: new(node),
+ putTree: new(node),
+ traceTree: new(node),
+ routes: []Route{},
+ echo: e,
}
- for _, m := range methods {
- r.trees[r.treeIndex(m)] = &node{
- prefix: "",
- children: children{},
- }
- }
- return
}
func (r *Router) Add(method, path string, h HandlerFunc, e *Echo) {
@@ -66,7 +75,7 @@ func (r *Router) Add(method, path string, h HandlerFunc, e *Echo) {
} else if path[i] == '*' {
r.insert(method, path[:i], nil, stype, nil, e)
pnames = append(pnames, "_name")
- r.insert(method, path[:i+1], h, mtype, pnames, e)
+ r.insert(method, path[:i + 1], h, mtype, pnames, e)
return
}
}
@@ -81,7 +90,10 @@ func (r *Router) insert(method, path string, h HandlerFunc, t ntype, pnames []st
*e.maxParam = l
}
- cn := r.trees[r.treeIndex(method)] // Current node as root
+ cn := r.findTree(method) // Current node as root
+ if cn == nil {
+ panic("echo => invalid method")
+ }
search := path
for {
@@ -200,21 +212,89 @@ func (n *node) findChildWithType(t ntype) *node {
return nil
}
-func (r *Router) treeIndex(method string) uint8 {
- if method[0] == 'P' {
- return method[0]%10 + method[1] - 65
- } else {
- return method[0] % 10
+func (r *Router) findTree(method string) (n *node) {
+ switch method[0] {
+ case 'G': // GET
+ m := uint32(method[2]) << 8 | uint32(method[1]) << 16 | uint32(method[0]) << 24
+ if m == 0x47455400 {
+ n = r.getTree
+ }
+ case 'P': // POST, PUT or PATCH
+ switch method[1] {
+ case 'O': // POST
+ m := uint32(method[3]) | uint32(method[2]) << 8 | uint32(method[1]) << 16 |
+ uint32(method[0]) << 24
+ if m == 0x504f5354 {
+ n = r.postTree
+ }
+ case 'U': // PUT
+ m := uint32(method[2]) << 8 | uint32(method[1]) << 16 | uint32(method[0]) << 24
+ if m == 0x50555400 {
+ n = r.putTree
+ }
+ case 'A': // PATCH
+ m := uint64(method[4]) << 24 | uint64(method[3]) << 32 | uint64(method[2]) << 40 |
+ uint64(method[1]) << 48 | uint64(method[0]) << 56
+ if m == 0x5041544348000000 {
+ n = r.patchTree
+ }
+ }
+ case 'D': // DELETE
+ m := uint64(method[5]) << 16 | uint64(method[4]) << 24 | uint64(method[3]) << 32 |
+ uint64(method[2]) << 40 | uint64(method[1]) << 48 | uint64(method[0]) << 56
+ if m == 0x44454c4554450000 {
+ n = r.deleteTree
+ }
+ case 'C': // CONNECT
+ m := uint64(method[6]) << 8 | uint64(method[5]) << 16 | uint64(method[4]) << 24 |
+ uint64(method[3]) << 32 | uint64(method[2]) << 40 | uint64(method[1]) << 48 |
+ uint64(method[0]) << 56
+ if m == 0x434f4e4e45435400 {
+ n = r.connectTree
+ }
+ case 'H': // HEAD
+ m := uint32(method[3]) | uint32(method[2]) << 8 | uint32(method[1]) << 16 |
+ uint32(method[0]) << 24
+ if m == 0x48454144 {
+ n = r.headTree
+ }
+ case 'O': // OPTIONS
+ m := uint64(method[6]) << 8 | uint64(method[5]) << 16 | uint64(method[4]) << 24 |
+ uint64(method[3]) << 32 | uint64(method[2]) << 40 | uint64(method[1]) << 48 |
+ uint64(method[0]) << 56
+ if m == 0x4f5054494f4e5300 {
+ n = r.optionsTree
+ }
+ case 'T': // TRACE
+ m := uint64(method[4]) << 24 | uint64(method[3]) << 32 | uint64(method[2]) << 40 |
+ uint64(method[1]) << 48 | uint64(method[0]) << 56
+ if m == 0x5452414345000000 {
+ n = r.traceTree
+ }
}
+ return
}
func (r *Router) Find(method, path string, ctx *Context) (h HandlerFunc, e *Echo) {
- cn := r.trees[r.treeIndex(method)] // Current node as root
- search := path
+ h = notFoundHandler
+ cn := r.findTree(method) // Current node as root
+ if cn == nil {
+ h = badRequestHandler
+ return
+ }
+
+ // Strip trailing slash
+ if r.echo.stripTrailingSlash {
+ l := len(path)
+ if path[l - 1] == '/' {
+ path = path[:l - 1]
+ }
+ }
var (
+ search = path
c *node // Child node
- n int // Param counter
+ n int // Param counter
nt ntype // Next type
nn *node // Next node
ns string // Next search
@@ -225,10 +305,12 @@ func (r *Router) Find(method, path string, ctx *Context) (h HandlerFunc, e *Echo
// Search order static > param > match-any
for {
if search == "" {
- // Found
- ctx.pnames = cn.pnames
- h = cn.handler
- e = cn.echo
+ if cn.handler != nil {
+ // Found
+ ctx.pnames = cn.pnames
+ h = cn.handler
+ e = cn.echo
+ }
return
}
@@ -287,7 +369,7 @@ func (r *Router) Find(method, path string, ctx *Context) (h HandlerFunc, e *Echo
}
// Param node
- Param:
+ Param:
c = cn.findChildWithType(ptype)
if c != nil {
// Save next
@@ -307,7 +389,7 @@ func (r *Router) Find(method, path string, ctx *Context) (h HandlerFunc, e *Echo
}
// Match-any node
- MatchAny:
+ MatchAny:
// c = cn.getChild()
c = cn.findChildWithType(mtype)
if c != nil {
@@ -326,10 +408,8 @@ func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
c := r.echo.pool.Get().(*Context)
h, _ := r.Find(req.Method, req.URL.Path, c)
c.reset(req, w, r.echo)
- if h == nil {
- c.Error(NewHTTPError(http.StatusNotFound))
- } else {
- h(c)
+ if err := h(c); err != nil {
+ r.echo.httpErrorHandler(err, c)
}
r.echo.pool.Put(c)
}
diff --git a/router_test.go b/router_test.go
index caaadd74..a9a622c8 100644
--- a/router_test.go
+++ b/router_test.go
@@ -316,9 +316,6 @@ func TestRouterTwoParam(t *testing.T) {
assert.Equal(t, "1", c.P(0))
assert.Equal(t, "1", c.P(1))
}
-
- h, _ = r.Find(GET, "/users/1", c)
- assert.Nil(t, h)
}
func TestRouterMatchAny(t *testing.T) {
@@ -384,7 +381,10 @@ func TestRouterMultiRoute(t *testing.T) {
// Route > /user
h, _ = r.Find(GET, "/user", c)
- assert.Nil(t, h)
+ if assert.IsType(t, new(HTTPError), h(c)) {
+ he := h(c).(*HTTPError)
+ assert.Equal(t, http.StatusNotFound, he.code)
+ }
}
func TestRouterPriority(t *testing.T) {
@@ -537,6 +537,16 @@ func TestRouterAPI(t *testing.T) {
}
}
+func TestRouterAddInvalidMethod(t *testing.T) {
+ e := New()
+ r := e.router
+ assert.Panics(t, func() {
+ r.Add("INVALID", "/", func(*Context) error {
+ return nil
+ }, e)
+ })
+}
+
func TestRouterServeHTTP(t *testing.T) {
e := New()
r := e.router
diff --git a/test/fixture/walle.png b/test/fixture/walle.png
new file mode 100644
index 00000000..493985d4
Binary files /dev/null and b/test/fixture/walle.png differ
diff --git a/website/docs/guide.md b/website/docs/guide.md
index a20bd7a9..790b5e8b 100644
--- a/website/docs/guide.md
+++ b/website/docs/guide.md
@@ -23,7 +23,7 @@ $ go get -u github.com/labstack/echo
```
Echo follows [semantic versioning](http://semver.org) managed through GitHub releases.
-Specific version of Echo can be installed using any [package manager](https://github.com/avelino/awesome-go#package-management).
+Specific version of Echo can be installed using a [package manager](https://github.com/avelino/awesome-go#package-management).
## Customization
@@ -42,54 +42,45 @@ and message `HTTPError.Message`.
### Debug
-`Echo.SetDebug(on bool)`
+`Echo.Debug()`
Enables debug mode.
+### Disable colored log
+
+`Echo.DisableColoredLog()`
+
+### StripTrailingSlash
+
+StripTrailingSlash enables removing trailing slash from the request path.
+
+`e.StripTrailingSlash()`
+
## Routing
Echo's router is [fast, optimized](https://github.com/labstack/echo#benchmark) and
-flexible. It's based on [redix tree](http://en.wikipedia.org/wiki/Radix_tree)
-data structure which makes routing lookup really fast. It leverages
-[sync pool](https://golang.org/pkg/sync/#Pool) to reuse memory and achieve
-zero dynamic memory allocation with no GC overhead.
+flexible. It's based on [radix tree](http://en.wikipedia.org/wiki/Radix_tree) data
+structure which makes route lookup really fast. Router leverages [sync pool](https://golang.org/pkg/sync/#Pool)
+to reuse memory and achieve zero dynamic memory allocation with no GC overhead.
Routes can be registered by specifying HTTP method, path and a handler. For example,
code below registers a route for method `GET`, path `/hello` and a handler which sends
`Hello!` HTTP response.
```go
-echo.Get("/hello", func(c *echo.Context) error {
+e.Get("/hello", func(c *echo.Context) error {
return c.String(http.StatusOK, "Hello!")
})
```
-Echo's default handler is `func(*echo.Context) error` where `echo.Context`
-primarily holds HTTP request and response objects. Echo also has a support for other
-types of handlers.
-
-### Path parameter
-
-Request path parameters can be extracted either by name `Echo.Context.Param(name string) string`
-or by index `Echo.Context.P(i int) string`. Getting parameter by index gives a
-slightly better performance.
-
-```go
-echo.Get("/users/:id", func(c *echo.Context) error {
- // By name
- id := c.Param("id")
-
- // By index
- id := c.P(0)
-
- return c.String(http.StatusOK, id)
-})
-```
+Echo's default handler is `func(*echo.Context) error` where `echo.Context` primarily
+holds HTTP request and response objects. Echo also has a support for other types
+of handlers.
### Match-any
Matches zero or more characters in the path. For example, pattern `/users/*` will
-match
+match:
- `/users/`
- `/users/1`
@@ -118,13 +109,13 @@ e.Get("/users/1/files/*", func(c *echo.Context) error {
})
```
-Above routes would resolve in order
+Above routes would resolve in the following order:
- `/users/new`
- `/users/:id`
- `/users/1/files/*`
-Routes can be written in any order.
+> Routes can be written in any order.
### Group
@@ -150,15 +141,15 @@ e.Use(mw.BasicAuth(func(usr, pwd string) bool {
### URI building
-`Echo.URI` can be used generate URI for any handler with specified path parameters.
+`Echo.URI` can be used to generate URI for any handler with specified path parameters.
It's helpful to centralize all your URI patterns which ease in refactoring your
application.
-`echo.URI(h, 1)` will generate `/users/1` for the route registered below
+`e.URI(h, 1)` will generate `/users/1` for the route registered below
```go
// Handler
-h := func(*echo.Context) error {
+h := func(c *echo.Context) error {
return c.String(http.StatusOK, "OK")
}
@@ -168,9 +159,9 @@ e.Get("/users/:id", h)
## Middleware
-Middleware is function which is chained in the HTTP request-response cycle. Middleware
+Middleware is a function which is chained in the HTTP request-response cycle. Middleware
has access to the request and response objects which it utilizes to perform a specific
-action for example, logging every request. Echo supports variety of [middleware](/#features).
+action, for example, logging every request.
### Logger
@@ -195,7 +186,7 @@ BasicAuth middleware provides an HTTP basic authentication.
*Example*
```go
-echo.Group("/admin")
+e.Group("/admin")
e.Use(mw.BasicAuth(func(usr, pwd string) bool {
if usr == "joe" && pwd == "secret" {
return true
@@ -225,63 +216,163 @@ to the centralized [HTTPErrorHandler](#error-handling).
e.Use(mw.Recover())
```
-### StripTrailingSlash
+[Examples](https://github.com/labstack/echo/tree/master/examples/middleware)
-StripTrailingSlash middleware removes the trailing slash from request path.
+## Request
+
+### Path parameter
+
+Path parameter can be retrieved either by name `Context.Param(name string) string`
+or by index `Context.P(i int) string`. Getting parameter by index gives a slightly
+better performance.
*Example*
```go
-e.Use(mw.StripTrailingSlash())
+e.Get("/users/:name", func(c *echo.Context) error {
+ // By name
+ name := c.Param("name")
+
+ // By index
+ name := c.P(0)
+
+ return c.String(http.StatusOK, name)
+})
```
-### RedirectToSlash
-RedirectToSlash middleware redirects requests without trailing slash path to trailing
-slash path.
+```sh
+$ curl http://localhost:1323/users/joe
+```
+
+### Query parameter
+
+Query parameter can be retrieved by name using `Context.Query(name string)`.
+
+*Example*
-*Options*
```go
-RedirectToSlashOptions struct {
- Code int
+e.Get("/users", func(c *echo.Context) error {
+ name := c.Query("name")
+ return c.String(http.StatusOK, name)
+})
+```
+
+```sh
+$ curl -G -d "name=joe" http://localhost:1323/users
+```
+
+### Form parameter
+
+Form parameter can be retrieved by name using `Context.Form(name string)`.
+
+*Example*
+
+```go
+e.Post("/users", func(c *echo.Context) error {
+ name := c.Form("name")
+ return c.String(http.StatusOK, name)
+})
+```
+
+```sh
+$ curl -d "name=joe" http://localhost:1323/users
+```
+
+## Response
+
+### Template
+
+```go
+Context.Render(code int, name string, data interface{}) error
+```
+Renders a template with data and sends a text/html response with status code. Templates
+can be registered using `Echo.SetRenderer()`, allowing us to use any template engine.
+
+Below is an example using Go `html/template`
+
+- Implement `echo.Render` interface
+
+```go
+Template struct {
+ templates *template.Template
+}
+
+func (t *Template) Render(w io.Writer, name string, data interface{}) error {
+ return t.templates.ExecuteTemplate(w, name, data)
}
```
-*Example*
+- Pre-compile templates
-```go
-e.Use(mw.RedirectToSlash())
+`public/views/hello.html`
+
+```html
+{{define "hello"}}Hello, {{.}}!{{end}}
```
-> StripTrailingSlash and RedirectToSlash middleware should not be used together.
+```go
+t := &Template{
+ templates: template.Must(template.ParseGlob("public/views/*.html")),
+}
+```
-[Examples](https://github.com/labstack/echo/tree/master/examples/middleware)
+- Register templates
-## Response
+```go
+e := echo.New()
+e.SetRenderer(t)
+e.Get("/hello", Hello)
+```
+
+- Render template
+
+```go
+func Hello(c *echo.Context) error {
+ return c.Render(http.StatusOK, "hello", "World")
+}
+```
### JSON
```go
-context.JSON(code int, v interface{}) error
+Context.JSON(code int, v interface{}) error
```
Sends a JSON HTTP response with status code.
-### String
+### XML
```go
-context.String(code int, s string) error
+Context.XML(code int, v interface{}) error
```
-Sends a text/plain HTTP response with status code.
+Sends an XML HTTP response with status code.
### HTML
```go
-func (c *Context) HTML(code int, html string) error
+Context.HTML(code int, html string) error
```
Sends an HTML HTTP response with status code.
+### String
+
+```go
+Context.String(code int, s string) error
+```
+
+Sends a text/plain HTTP response with status code.
+
+### File
+
+```go
+Context.File(name string, attachment bool) error
+```
+
+File sends a response with the content of the file. If attachment is `true`, the client
+is prompted to save the file.
+
### Static files
`Echo.Static(path, root string)` serves static files. For example, code below serves
@@ -360,7 +451,3 @@ func welcome(c *echo.Context) error {
```
See how [HTTPErrorHandler](#customization) handles it.
-
-## Deployment
-
-*WIP*
diff --git a/website/docs/index.md b/website/docs/index.md
index 1f2f633a..0bd0bbac 100644
--- a/website/docs/index.md
+++ b/website/docs/index.md
@@ -1,13 +1,9 @@
# Echo
-Build simple and performant systems!
+A fast and unfancy micro web framework for Go.
---
-## Overview
-
-Echo is a fast HTTP router (zero dynamic memory allocation) and micro web framework in Go.
-
## Features
- Fast HTTP router which smartly prioritize routes.
@@ -27,14 +23,25 @@ Echo is a fast HTTP router (zero dynamic memory allocation) and micro web framew
- `http.HandlerFunc`
- `func(http.ResponseWriter, *http.Request)`
- Sub-router/Groups
-- Handy encoding/decoding functions.
+- Handy functions to send variety of HTTP response:
+ - HTML
+ - HTML via templates
+ - String
+ - JSON
+ - JSONP
+ - XML
+ - File
+ - NoContent
+ - Redirect
+ - Error
- Build-in support for:
+ - Favicon
+ - Index file
- Static files
- WebSocket
-- API to serve index and favicon.
- Centralized HTTP error handling.
-- Customizable request binding function.
-- Customizable response rendering function, allowing you to use any HTML template engine.
+- Customizable HTTP request binding function.
+- Customizable HTTP response rendering function, allowing you to use any HTML template engine.
## Performance
@@ -48,9 +55,9 @@ Echo is a fast HTTP router (zero dynamic memory allocation) and micro web framew
$ go get github.com/labstack/echo
```
-###[Hello, World!](https://github.com/labstack/echo/tree/master/examples/hello)
+### Hello, World!
-Create `server.go` with the following content
+Create `server.go`
```go
package main
@@ -83,24 +90,7 @@ func main() {
}
```
-`echo.New()` returns a new instance of Echo.
-
-`e.Use(mw.Logger())` adds logging middleware to the chain. It logs every HTTP request
-made to the server, producing output
-
-```sh
-2015/06/07 18:16:16 GET / 200 13.238µs 14
-```
-
-`e.Get("/", hello)` Registers hello handler for HTTP method `GET` and path `/`, so
-whenever server receives an HTTP request at `/`, hello function is called.
-
-In hello handler `c.String(http.StatusOK, "Hello, World!\n")` sends a text/plain
-HTTP response to the client with 200 status code.
-
-`e.Run(":1323")` Starts HTTP server at network address `:1323`.
-
-Now start the server using command
+Start server
```sh
$ go run server.go
@@ -110,7 +100,7 @@ Browse to [http://localhost:1323](http://localhost:1323) and you should see
Hello, World! on the page.
### Next?
-- Browse [examples](https://github.com/labstack/echo/tree/master/examples)
+- Browse [recipes](https://github.com/labstack/echo/tree/master/recipes)
- Head over to [Guide](guide.md)
## Contribute
diff --git a/website/docs/recipes/file-upload.md b/website/docs/recipes/file-upload.md
new file mode 100644
index 00000000..a8628ac5
--- /dev/null
+++ b/website/docs/recipes/file-upload.md
@@ -0,0 +1,96 @@
+## File Upload
+
+- Multipart/form-data file upload
+- Multiple form fields and files
+
+Use `req.ParseMultipartForm(16 << 20)` for manually parsing multipart form. It gives
+us an option to specify the maximum memory used while parsing the request body.
+
+## Server
+
+`server.go`
+
+```go
+package main
+
+import (
+ "io"
+ "os"
+
+ "net/http"
+
+ "github.com/labstack/echo"
+ mw "github.com/labstack/echo/middleware"
+)
+
+func upload(c *echo.Context) error {
+ req := c.Request()
+
+ // req.ParseMultipartForm(16 << 20) // Max memory 16 MiB
+
+ // Read form fields
+ name := c.Form("name")
+ email := c.Form("email")
+
+ // Read files
+ files := req.MultipartForm.File["files"]
+ for _, f := range files {
+ // Source file
+ src, err := f.Open()
+ if err != nil {
+ return err
+ }
+ defer src.Close()
+
+ // Destination file
+ dst, err := os.Create(f.Filename)
+ if err != nil {
+ return err
+ }
+ defer dst.Close()
+
+ if _, err = io.Copy(dst, src); err != nil {
+ return err
+ }
+ }
+ return c.String(http.StatusOK, "Thank You! %s <%s>, %d files uploaded successfully.",
+ name, email, len(files))
+}
+
+func main() {
+ e := echo.New()
+ e.Use(mw.Logger())
+ e.Use(mw.Recover())
+
+ e.Static("/", "public")
+ e.Post("/upload", upload)
+
+ e.Run(":1323")
+}
+```
+
+## Client
+
+`index.html`
+
+```html
+
+
+
+
+ File Upload
+
+
+ Upload Files
+
+
+
+
+```
+
+## [Source Code](https://github.com/labstack/echo/blob/master/recipes/file-upload)
diff --git a/website/docs/recipes/graceful-shutdown.md b/website/docs/recipes/graceful-shutdown.md
new file mode 100644
index 00000000..ed0134b1
--- /dev/null
+++ b/website/docs/recipes/graceful-shutdown.md
@@ -0,0 +1,58 @@
+## Graceful Shutdown
+
+### With [graceful](https://github.com/tylerb/graceful)
+
+`server.go`
+
+```go
+package main
+
+import (
+ "net/http"
+ "time"
+
+ "github.com/labstack/echo"
+ "github.com/tylerb/graceful"
+)
+
+func main() {
+ // Setup
+ e := echo.New()
+ e.Get("/", func(c *echo.Context) error {
+ return c.String(http.StatusOK, "Sue sews rose on slow jor crows nose")
+ })
+
+ graceful.ListenAndServe(e.Server(":1323"), 5*time.Second)
+}
+```
+
+### With [grace](https://github.com/facebookgo/grace)
+
+`server.go`
+
+```go
+package main
+
+import (
+ "net/http"
+
+ "github.com/facebookgo/grace/gracehttp"
+ "github.com/labstack/echo"
+)
+
+func main() {
+ // Setup
+ e := echo.New()
+ e.Get("/", func(c *echo.Context) error {
+ return c.String(http.StatusOK, "Six sick bricks tick")
+ })
+
+ gracehttp.Serve(e.Server(":1323"))
+}
+```
+
+## Source Code
+
+[`graceful`](https://github.com/labstack/echo/blob/master/recipes/graceful-shutdown/graceful)
+
+[`grace`](https://github.com/labstack/echo/blob/master/recipes/graceful-shutdown/grace)
diff --git a/website/docs/recipes/jwt-authentication.md b/website/docs/recipes/jwt-authentication.md
new file mode 100644
index 00000000..59efe776
--- /dev/null
+++ b/website/docs/recipes/jwt-authentication.md
@@ -0,0 +1,156 @@
+## JWT Authentication
+
+Most applications dealing with client authentication will require a more secure
+mechanism than that provided by [basic authentication](https://github.com/labstack/echo/blob/master/middleware/auth.go). [JSON Web Tokens](http://jwt.io/)
+are one such mechanism - JWTs are a compact means of transferring cryptographically
+signed claims between the client and server.
+
+This recipe demonstrates the use of a simple JWT authentication Echo middleware
+using Dave Grijalva's [jwt-go](https://github.com/dgrijalva/jwt-go). This middleware
+expects the token to be present in an Authorization HTTP header using the method
+"Bearer", although JWTs are also frequently sent using cookies, the request URL,
+or even the request body. We will use the HS236 signing method, note that several
+other algorithms are available.
+
+`server.go`
+
+```go
+package main
+
+import (
+ "fmt"
+ "net/http"
+
+ "github.com/dgrijalva/jwt-go"
+ "github.com/labstack/echo"
+ mw "github.com/labstack/echo/middleware"
+)
+
+const (
+ Bearer = "Bearer"
+ SigningKey = "somethingsupersecret"
+)
+
+// A JSON Web Token middleware
+func JWTAuth(key string) echo.HandlerFunc {
+ return func(c *echo.Context) error {
+
+ // Skip WebSocket
+ if (c.Request().Header.Get(echo.Upgrade)) == echo.WebSocket {
+ return nil
+ }
+
+ auth := c.Request().Header.Get("Authorization")
+ l := len(Bearer)
+ he := echo.NewHTTPError(http.StatusUnauthorized)
+
+ if len(auth) > l+1 && auth[:l] == Bearer {
+ t, err := jwt.Parse(auth[l+1:], func(token *jwt.Token) (interface{}, error) {
+
+ // Always check the signing method
+ if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
+ return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
+ }
+
+ // Return the key for validation
+ return []byte(key), nil
+ })
+ if err == nil && t.Valid {
+ // Store token claims in echo.Context
+ c.Set("claims", t.Claims)
+ return nil
+ }
+ }
+ return he
+ }
+}
+
+func accessible(c *echo.Context) error {
+ return c.String(http.StatusOK, "No auth required for this route.\n")
+}
+
+func restricted(c *echo.Context) error {
+ return c.String(http.StatusOK, "Access granted with JWT.\n")
+}
+
+func main() {
+
+ // Echo instance
+ e := echo.New()
+
+ // Logger
+ e.Use(mw.Logger())
+
+ // Unauthenticated route
+ e.Get("/", accessible)
+
+ // Restricted group
+ r := e.Group("/restricted")
+ r.Use(JWTAuth(SigningKey))
+ r.Get("", restricted)
+
+ // Start server
+ e.Run(":1323")
+}
+```
+
+Run `server.go` and making a request to the root path `/` returns a 200 OK response,
+as this route does not use our JWT authentication middleware. Sending requests to
+`/restricted` (our authenticated route) with either no Authorization header or invalid
+Authorization headers / tokens will return 401 Unauthorized.
+
+```sh
+# Unauthenticated route
+$ curl localhost:1323/ => No auth required for this route.
+
+# No Authentication header
+$ curl localhost:1323/restricted => Unauthorized
+
+# Invalid Authentication method
+$ curl localhost:1323/restricted -H "Authorization: Invalid " => Unauthorized
+
+# Invalid token
+$ curl localhost:1323/restricted -H "Authorization: Bearer InvalidToken" => Unauthorized
+```
+
+Running `token.go` (source) will print JWT that is valid against this middleware
+to stdout. You can use this token to test succesful authentication on the `/restricted` path.
+
+```go
+package main
+
+import (
+ "fmt"
+ "time"
+
+ "github.com/dgrijalva/jwt-go"
+)
+
+const SigningKey = "somethingsupersecret"
+
+func main() {
+
+ // New web token.
+ token := jwt.New(jwt.SigningMethodHS256)
+
+ // Set a header and a claim
+ token.Header["typ"] = "JWT"
+ token.Claims["exp"] = time.Now().Add(time.Hour * 96).Unix()
+
+ // Generate encoded token
+ t, _ := token.SignedString([]byte(SigningKey))
+ fmt.Println(t)
+}
+```
+
+```sh
+# Valid token
+$ curl localhost:1323/restricted -H "Authorization: Bearer " => Access granted with JWT.
+```
+
+## [Source Code](https://github.com/labstack/echo/blob/master/recipes/jwt-authentication)
+
+
+
+
+
diff --git a/website/docs/recipes/streaming-file-upload.md b/website/docs/recipes/streaming-file-upload.md
new file mode 100644
index 00000000..8d9699fe
--- /dev/null
+++ b/website/docs/recipes/streaming-file-upload.md
@@ -0,0 +1,117 @@
+## Streaming File Upload
+
+- Streaming multipart/form-data file upload
+- Multiple form fields and files
+
+## Server
+
+`server.go`
+
+```go
+package main
+
+import (
+ "io/ioutil"
+
+ "github.com/labstack/echo"
+ mw "github.com/labstack/echo/middleware"
+ "io"
+ "net/http"
+ "os"
+)
+
+func upload(c *echo.Context) error {
+ mr, err := c.Request().MultipartReader()
+ if err != nil {
+ return err
+ }
+
+ // Read form field `name`
+ part, err := mr.NextPart()
+ if err != nil {
+ return err
+ }
+ defer part.Close()
+ b, err := ioutil.ReadAll(part)
+ if err != nil {
+ return err
+ }
+ name := string(b)
+
+ // Read form field `email`
+ part, err = mr.NextPart()
+ if err != nil {
+ return err
+ }
+ defer part.Close()
+ b, err = ioutil.ReadAll(part)
+ if err != nil {
+ return err
+ }
+ email := string(b)
+
+ // Read files
+ i := 0
+ for {
+ part, err := mr.NextPart()
+ if err != nil {
+ if err == io.EOF {
+ break
+ }
+ return err
+ }
+ defer part.Close()
+
+ file, err := os.Create(part.FileName())
+ if err != nil {
+ return err
+ }
+ defer file.Close()
+
+ if _, err := io.Copy(file, part); err != nil {
+ return err
+ }
+ i++
+ }
+ return c.String(http.StatusOK, "Thank You! %s <%s>, %d files uploaded successfully.",
+ name, email, i)
+}
+
+func main() {
+ e := echo.New()
+ e.Use(mw.Logger())
+ e.Use(mw.Recover())
+
+ e.Static("/", "public")
+ e.Post("/upload", upload)
+
+ e.Run(":1323")
+}
+```
+
+## Client
+
+`index.html`
+
+```html
+
+
+
+
+ File Upload
+
+
+ Upload Files
+
+
+
+
+```
+
+## [Source Code](https://github.com/labstack/echo/blob/master/recipes/streaming-file-upload)
+
diff --git a/website/docs/recipes/streaming-response.md b/website/docs/recipes/streaming-response.md
new file mode 100644
index 00000000..7aa77cf8
--- /dev/null
+++ b/website/docs/recipes/streaming-response.md
@@ -0,0 +1,75 @@
+## Streaming Response
+
+- Send data as it is produced
+- Streaming JSON response with chunked transfer encoding
+
+## Server
+
+`server.go`
+
+```go
+package main
+
+import (
+ "net/http"
+ "time"
+
+ "encoding/json"
+
+ "github.com/labstack/echo"
+)
+
+type (
+ Geolocation struct {
+ Altitude float64
+ Latitude float64
+ Longitude float64
+ }
+)
+
+var (
+ locations = []Geolocation{
+ {-97, 37.819929, -122.478255},
+ {1899, 39.096849, -120.032351},
+ {2619, 37.865101, -119.538329},
+ {42, 33.812092, -117.918974},
+ {15, 37.77493, -122.419416},
+ }
+)
+
+func main() {
+ e := echo.New()
+ e.Get("/", func(c *echo.Context) error {
+ c.Response().Header().Set(echo.ContentType, echo.ApplicationJSON)
+ c.Response().WriteHeader(http.StatusOK)
+ for _, l := range locations {
+ if err := json.NewEncoder(c.Response()).Encode(l); err != nil {
+ return err
+ }
+ c.Response().Flush()
+ time.Sleep(1 * time.Second)
+ }
+ return nil
+ })
+ e.Run(":1323")
+}
+```
+
+## Client
+
+```sh
+$ curl localhost:1323
+```
+
+## Output
+
+```sh
+{"Altitude":-97,"Latitude":37.819929,"Longitude":-122.478255}
+{"Altitude":1899,"Latitude":39.096849,"Longitude":-120.032351}
+{"Altitude":2619,"Latitude":37.865101,"Longitude":-119.538329}
+{"Altitude":42,"Latitude":33.812092,"Longitude":-117.918974}
+{"Altitude":15,"Latitude":37.77493,"Longitude":-122.419416}
+```
+
+## [Source Code](https://github.com/labstack/echo/blob/master/recipes/streaming-response)
+
diff --git a/website/docs/recipes/subdomains.md b/website/docs/recipes/subdomains.md
new file mode 100644
index 00000000..ea4e7246
--- /dev/null
+++ b/website/docs/recipes/subdomains.md
@@ -0,0 +1,75 @@
+## Subdomains
+
+`server.go`
+
+```go
+package main
+
+import (
+ "net/http"
+
+ "github.com/labstack/echo"
+ mw "github.com/labstack/echo/middleware"
+)
+
+type Hosts map[string]http.Handler
+
+func (h Hosts) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+ if handler := h[r.Host]; handler != nil {
+ handler.ServeHTTP(w, r)
+ } else {
+ http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
+ }
+}
+
+func main() {
+ // Host map
+ hosts := make(Hosts)
+
+ //-----
+ // API
+ //-----
+
+ api := echo.New()
+ api.Use(mw.Logger())
+ api.Use(mw.Recover())
+
+ hosts["api.localhost:1323"] = api
+
+ api.Get("/", func(c *echo.Context) error {
+ return c.String(http.StatusOK, "API")
+ })
+
+ //------
+ // Blog
+ //------
+
+ blog := echo.New()
+ blog.Use(mw.Logger())
+ blog.Use(mw.Recover())
+
+ hosts["blog.localhost:1323"] = blog
+
+ blog.Get("/", func(c *echo.Context) error {
+ return c.String(http.StatusOK, "Blog")
+ })
+
+ //---------
+ // Website
+ //---------
+
+ site := echo.New()
+ site.Use(mw.Logger())
+ site.Use(mw.Recover())
+
+ hosts["localhost:1323"] = site
+
+ site.Get("/", func(c *echo.Context) error {
+ return c.String(http.StatusOK, "Welcome!")
+ })
+
+ http.ListenAndServe(":1323", hosts)
+}
+```
+
+## [Source Code](https://github.com/labstack/echo/blob/master/recipes/subdomains)
diff --git a/website/docs/recipes/websocket.md b/website/docs/recipes/websocket.md
new file mode 100644
index 00000000..78f19478
--- /dev/null
+++ b/website/docs/recipes/websocket.md
@@ -0,0 +1,111 @@
+## WebSocket
+
+## Server
+
+`server.go`
+
+```go
+package main
+
+import (
+ "fmt"
+
+ "github.com/labstack/echo"
+ mw "github.com/labstack/echo/middleware"
+ "golang.org/x/net/websocket"
+)
+
+func main() {
+ e := echo.New()
+
+ e.Use(mw.Logger())
+ e.Use(mw.Recover())
+
+ e.Static("/", "public")
+ e.WebSocket("/ws", func(c *echo.Context) (err error) {
+ ws := c.Socket()
+ msg := ""
+
+ for {
+ if err = websocket.Message.Send(ws, "Hello, Client!"); err != nil {
+ return
+ }
+ if err = websocket.Message.Receive(ws, &msg); err != nil {
+ return
+ }
+ fmt.Println(msg)
+ }
+ return
+ })
+
+ e.Run(":1323")
+}
+```
+
+## Client
+
+`index.html`
+
+```html
+
+
+
+
+ WebSocket
+
+
+
+
+
+
+
+```
+
+## Output
+
+`Client`
+
+```sh
+Hello, Client!
+Hello, Client!
+Hello, Client!
+Hello, Client!
+Hello, Client!
+```
+
+`Server`
+
+```sh
+Hello, Server!
+Hello, Server!
+Hello, Server!
+Hello, Server!
+Hello, Server!
+```
+
+## [Source Code](https://github.com/labstack/echo/blob/master/recipes/websocket)
+
diff --git a/website/echo/base.html b/website/echo/base.html
index 4f0260c1..75df7a29 100644
--- a/website/echo/base.html
+++ b/website/echo/base.html
@@ -10,11 +10,11 @@
{% if favicon %}
{% else %}{% endif %}
- {% if page_title %}{{ page_title }} - {% endif %}{{ site_name }}
+ {% if page_title %}{{ page_title }} - {% endif %}{{ config.extra.site_title }}
-
+
{%- for path in extra_css %}
@@ -75,4 +75,4 @@
{%- endfor %}