|
|
@@ -0,0 +1,472 @@ |
|
|
# Custom HTTP Routing in Go |
|
|
|
|
|
## Basic Routing |
|
|
|
|
|
Responding to requests via simple route matching is built in to Go's [`net/http`](https://golang.org/pkg/net/http/) standard library package. Just register the path prefixes and callbacks you want invoked and then call the [`ListenAndServe`](https://golang.org/pkg/net/http/#ListenAndServe) to have the default request handler invoked on each request. For example: |
|
|
|
|
|
```go |
|
|
package main |
|
|
|
|
|
import ( |
|
|
"io" |
|
|
"log" |
|
|
"net/http" |
|
|
) |
|
|
|
|
|
func main() { |
|
|
|
|
|
http.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) { |
|
|
w.Header().Set("Content-Type", "text/plain") |
|
|
w.WriteHeader(http.StatusOK) |
|
|
|
|
|
io.WriteString(w, "Hello world\n") |
|
|
}) |
|
|
|
|
|
err := http.ListenAndServe(":9000", nil) |
|
|
|
|
|
if err != nil { |
|
|
log.Fatalf("Could not start server: %s\n", err.Error()) |
|
|
} |
|
|
} |
|
|
``` |
|
|
|
|
|
While it may look strange to pass `nil` as the second parameter to `ListenAndServe`, this causes Go to use the `DefaultServeMux` request multiplexer. Requests to the configured endpoint look like this: |
|
|
|
|
|
``` |
|
|
$ curl -i http://localhost:9000/hello |
|
|
HTTP/1.1 200 OK |
|
|
Content-Type: text/plain |
|
|
Date: Mon, 26 Jun 2017 23:58:40 GMT |
|
|
Content-Length: 12 |
|
|
|
|
|
Hello world |
|
|
``` |
|
|
|
|
|
By default, this handler will respond with a 404 when it can't find a match: |
|
|
|
|
|
|
|
|
``` |
|
|
$ curl -i http://localhost:9000/ |
|
|
HTTP/1.1 404 Not Found |
|
|
Content-Type: text/plain; charset=utf-8 |
|
|
X-Content-Type-Options: nosniff |
|
|
Date: Mon, 26 Jun 2017 23:59:11 GMT |
|
|
Content-Length: 19 |
|
|
|
|
|
404 page not found |
|
|
``` |
|
|
|
|
|
As you can see above, we're not controlling the content of the response, so the `Content-Type` header reverts to Go's default. If you would prefer not to rely on the "magic" of the default multiplexer, you can configure your own by creating a [`ServeMux` instance](https://golang.org/pkg/net/http/#ServeMux). |
|
|
|
|
|
## Routing with ServeMux |
|
|
|
|
|
The process of registering callbacks with this method is similar to the previous example, but in this case we call [`HandleFunc`](https://golang.org/pkg/net/http/#ServeMux.HandleFunc) on the multiplexer that we create: |
|
|
|
|
|
```go |
|
|
package main |
|
|
|
|
|
import ( |
|
|
"fmt" |
|
|
"io" |
|
|
"log" |
|
|
"net/http" |
|
|
"strings" |
|
|
) |
|
|
|
|
|
func main() { |
|
|
handler := http.NewServeMux() |
|
|
|
|
|
handler.HandleFunc("/hello/", func(w http.ResponseWriter, r *http.Request) { |
|
|
name := strings.Replace(r.URL.Path, "/hello/", "", 1) |
|
|
|
|
|
w.Header().Set("Content-Type", "text/plain") |
|
|
w.WriteHeader(http.StatusOK) |
|
|
|
|
|
io.WriteString(w, fmt.Sprintf("Hello %s\n", name)) |
|
|
}) |
|
|
|
|
|
handler.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) { |
|
|
w.Header().Set("Content-Type", "text/plain") |
|
|
w.WriteHeader(http.StatusOK) |
|
|
|
|
|
io.WriteString(w, "Hello world\n") |
|
|
}) |
|
|
|
|
|
handler.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { |
|
|
w.Header().Set("Content-Type", "text/plain") |
|
|
w.WriteHeader(http.StatusNotFound) |
|
|
|
|
|
io.WriteString(w, "Not found\n") |
|
|
}) |
|
|
|
|
|
err := http.ListenAndServe(":9000", handler) |
|
|
|
|
|
if err != nil { |
|
|
log.Fatalf("Could not start server: %s\n", err.Error()) |
|
|
} |
|
|
} |
|
|
``` |
|
|
|
|
|
You'll notice two changes here: |
|
|
|
|
|
1. The route prefix for `/hello/` that will match against any subtree that matches this prefix |
|
|
1. The custom handler for `/` that responds with a 404 status when the request was not matched by any previous pattern |
|
|
|
|
|
Here are the responses for each: |
|
|
|
|
|
``` |
|
|
$ curl -i http://localhost:9000/hello/Patrick |
|
|
HTTP/1.1 200 OK |
|
|
Content-Type: text/plain |
|
|
Date: Tue, 27 Jun 2017 04:44:30 GMT |
|
|
Content-Length: 14 |
|
|
|
|
|
Hello Patrick |
|
|
``` |
|
|
|
|
|
``` |
|
|
$ curl -i http://localhost:9000/asdf |
|
|
HTTP/1.1 404 Not Found |
|
|
Content-Type: text/plain |
|
|
Date: Tue, 27 Jun 2017 04:44:36 GMT |
|
|
Content-Length: 10 |
|
|
|
|
|
Not found |
|
|
``` |
|
|
|
|
|
The duplication present in each handler is something that can be easily refactored by moving away from interacting with `http.ResponseWriter` directly, so we'll do that next. |
|
|
|
|
|
## Customizing the HTTP Response |
|
|
|
|
|
The common tasks of writing the `Content-Type` header, setting the HTTP status, and returning the body can be moved to a single function by embedding `http.ResponseWriter` in a new struct and moving the duplicate code to our new struct. For example: |
|
|
|
|
|
```go |
|
|
package main |
|
|
|
|
|
import ( |
|
|
"fmt" |
|
|
"io" |
|
|
"log" |
|
|
"net/http" |
|
|
"strings" |
|
|
) |
|
|
|
|
|
type Response struct { |
|
|
http.ResponseWriter |
|
|
} |
|
|
|
|
|
func (r *Response) Text(code int, body string) { |
|
|
r.Header().Set("Content-Type", "text/plain") |
|
|
r.WriteHeader(code) |
|
|
|
|
|
io.WriteString(r, fmt.Sprintf("%s\n", body)) |
|
|
} |
|
|
|
|
|
func main() { |
|
|
handler := http.NewServeMux() |
|
|
|
|
|
handler.HandleFunc("/hello/", func(w http.ResponseWriter, r *http.Request) { |
|
|
name := strings.Replace(r.URL.Path, "/hello/", "", 1) |
|
|
|
|
|
resp := Response{w} |
|
|
resp.Text(http.StatusOK, fmt.Sprintf("Hello %s", name)) |
|
|
}) |
|
|
|
|
|
handler.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) { |
|
|
resp := Response{w} |
|
|
resp.Text(http.StatusOK, "Hello world") |
|
|
}) |
|
|
|
|
|
handler.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { |
|
|
resp := Response{w} |
|
|
resp.Text(http.StatusNotFound, "Not found") |
|
|
}) |
|
|
|
|
|
err := http.ListenAndServe(":9000", handler) |
|
|
|
|
|
if err != nil { |
|
|
log.Fatalf("Could not start server: %s\n", err.Error()) |
|
|
} |
|
|
|
|
|
} |
|
|
``` |
|
|
|
|
|
The only difference between this and the previous example is that we've condensed the 3 lines needed to write a response down to a single call to `Response.Text()` to set the status and send the body to the client. While assigning a local `Response` instance is tedious, it's necessary in this case due to the specific function signature required by `HandleFunc`. To see how we might simplify this, we'll have to write our own HTTP handler. |
|
|
|
|
|
## Writing a Custom Handler |
|
|
|
|
|
Per the [documentation](https://golang.org/pkg/net/http/#Handler), a struct can be treated as a handler if it implements the `ServeHTTP()` method. So, if we wanted to ditch the request multiplexer altogether, we could respond to requests directly from a custom handler defined in a new struct: |
|
|
|
|
|
```go |
|
|
package main |
|
|
|
|
|
import ( |
|
|
"io" |
|
|
"log" |
|
|
"net/http" |
|
|
) |
|
|
|
|
|
type App struct{} |
|
|
|
|
|
func (a *App) ServeHTTP(w http.ResponseWriter, r *http.Request) { |
|
|
w.Header().Set("Content-Type", "text/plain") |
|
|
w.WriteHeader(http.StatusOK) |
|
|
|
|
|
io.WriteString(w, "Hello world\n") |
|
|
} |
|
|
|
|
|
func main() { |
|
|
err := http.ListenAndServe(":9000", &App{}) |
|
|
|
|
|
if err != nil { |
|
|
log.Fatalf("Could not start server: %s\n", err.Error()) |
|
|
} |
|
|
} |
|
|
``` |
|
|
|
|
|
Since our `App` struct responds to `ServeHTTP`, we can pass it directly to `ListenAndServe` and it will receive all HTTP requests. In this simple example, all requests receive the same response regardless of the request path: |
|
|
|
|
|
``` |
|
|
$ curl -i http://localhost:9000/ok/pal |
|
|
HTTP/1.1 200 OK |
|
|
Content-Type: text/plain |
|
|
Date: Tue, 27 Jun 2017 04:58:39 GMT |
|
|
Content-Length: 12 |
|
|
|
|
|
Hello world |
|
|
``` |
|
|
|
|
|
This isn't very interesting in itself, but it *does* now give us the ability to wrap each incoming request before passing it off to a custom handler function. |
|
|
|
|
|
## Custom Regular Expression-Based Router |
|
|
|
|
|
By using a custom handler, we now have control over when the requests are intercepted, we can pass our own request and response structs to the matched route. Since we are no longer using a `http.ServeMux` instance, I've introduced a custom regular expression-based router: |
|
|
|
|
|
|
|
|
```go |
|
|
package main |
|
|
|
|
|
import ( |
|
|
"fmt" |
|
|
"io" |
|
|
"log" |
|
|
"net/http" |
|
|
"regexp" |
|
|
) |
|
|
|
|
|
type Handler func(*Response, *Request) |
|
|
|
|
|
type Route struct { |
|
|
Pattern *regexp.Regexp |
|
|
Handler Handler |
|
|
} |
|
|
|
|
|
type App struct { |
|
|
Routes []Route |
|
|
DefaultRoute Handler |
|
|
} |
|
|
|
|
|
func NewApp() *App { |
|
|
app := &App{ |
|
|
DefaultRoute: func(resp *Response, req *Request) { |
|
|
resp.Text(http.StatusNotFound, "Not found") |
|
|
}, |
|
|
} |
|
|
|
|
|
return app |
|
|
} |
|
|
|
|
|
func (a *App) Handle(pattern string, handler Handler) { |
|
|
re := regexp.MustCompile(pattern) |
|
|
route := Route{Pattern: re, Handler: handler} |
|
|
|
|
|
a.Routes = append(a.Routes, route) |
|
|
} |
|
|
|
|
|
func (a *App) ServeHTTP(w http.ResponseWriter, r *http.Request) { |
|
|
req := &Request{Request: r} |
|
|
resp := &Response{w} |
|
|
|
|
|
for _, rt := range a.Routes { |
|
|
if matches := rt.Pattern.FindStringSubmatch(r.URL.Path); len(matches) > 0 { |
|
|
if len(matches) > 1 { |
|
|
req.Params = matches[1:] |
|
|
} |
|
|
|
|
|
rt.Handler(resp, req) |
|
|
return |
|
|
} |
|
|
} |
|
|
|
|
|
a.DefaultRoute(resp, req) |
|
|
} |
|
|
|
|
|
type Request struct { |
|
|
*http.Request |
|
|
Params []string |
|
|
} |
|
|
|
|
|
type Response struct { |
|
|
http.ResponseWriter |
|
|
} |
|
|
|
|
|
func (r *Response) Text(code int, body string) { |
|
|
r.Header().Set("Content-Type", "text/plain") |
|
|
r.WriteHeader(code) |
|
|
|
|
|
io.WriteString(r, fmt.Sprintf("%s\n", body)) |
|
|
} |
|
|
|
|
|
func main() { |
|
|
app := NewApp() |
|
|
|
|
|
app.Handle(`^/hello$`, func(resp *Response, req *Request) { |
|
|
resp.Text(http.StatusOK, "Hello world") |
|
|
}) |
|
|
|
|
|
app.Handle(`/hello/([\w\._-]+)$`, func(resp *Response, req *Request) { |
|
|
resp.Text(http.StatusOK, fmt.Sprintf("Hello %s", req.Params[0])) |
|
|
}) |
|
|
|
|
|
err := http.ListenAndServe(":9000", app) |
|
|
|
|
|
if err != nil { |
|
|
log.Fatalf("Could not start server: %s\n", err.Error()) |
|
|
} |
|
|
|
|
|
} |
|
|
``` |
|
|
|
|
|
The `App` struct now has a collection of routes, each with a corresponding callback. If the request path matches the configured pattern, that callback will be triggered. Otherwise, the default route will be invoked and the server will respond with a 404 status: |
|
|
|
|
|
``` |
|
|
$ curl -i http://localhost:9000/hello |
|
|
HTTP/1.1 200 OK |
|
|
Content-Type: text/plain |
|
|
Date: Tue, 27 Jun 2017 14:39:24 GMT |
|
|
Content-Length: 12 |
|
|
|
|
|
Hello world |
|
|
``` |
|
|
|
|
|
``` |
|
|
$ curl -i http://localhost:9000/hello/Patrick |
|
|
HTTP/1.1 200 OK |
|
|
Content-Type: text/plain |
|
|
Date: Tue, 27 Jun 2017 14:39:28 GMT |
|
|
Content-Length: 14 |
|
|
|
|
|
Hello Patrick |
|
|
``` |
|
|
``` |
|
|
$ curl -i http://localhost:9000/missing |
|
|
HTTP/1.1 404 Not Found |
|
|
Content-Type: text/plain |
|
|
Date: Tue, 27 Jun 2017 14:39:32 GMT |
|
|
Content-Length: 10 |
|
|
|
|
|
Not found |
|
|
``` |
|
|
|
|
|
The custom `Request` struct is worth examining -- rather than performing substring replacement to determine the dynamic message, it keeps track of any capture groups in the route pattern and exposes them through the `Params` field. While this gets the job done, it's not ideal from a design perspective. |
|
|
|
|
|
## Wrapping it All in a Context |
|
|
|
|
|
Rather than storing `Params` on the `Request` struct, we can instead introduce another struct to capture these values and use type embedding to have `http.Request` and`http.ResponseWriter` handle the traditional HTTP interactions. This will also simplify the `Handler` signature as it now only needs to take a single `Context` struct and can perform all the response handling needed: |
|
|
|
|
|
```go |
|
|
package main |
|
|
|
|
|
import ( |
|
|
"fmt" |
|
|
"io" |
|
|
"log" |
|
|
"net/http" |
|
|
"regexp" |
|
|
) |
|
|
|
|
|
type Handler func(*Context) |
|
|
|
|
|
type Route struct { |
|
|
Pattern *regexp.Regexp |
|
|
Handler Handler |
|
|
} |
|
|
|
|
|
type App struct { |
|
|
Routes []Route |
|
|
DefaultRoute Handler |
|
|
} |
|
|
|
|
|
func NewApp() *App { |
|
|
app := &App{ |
|
|
DefaultRoute: func(ctx *Context) { |
|
|
ctx.Text(http.StatusNotFound, "Not found") |
|
|
}, |
|
|
} |
|
|
|
|
|
return app |
|
|
} |
|
|
|
|
|
func (a *App) Handle(pattern string, handler Handler) { |
|
|
re := regexp.MustCompile(pattern) |
|
|
route := Route{Pattern: re, Handler: handler} |
|
|
|
|
|
a.Routes = append(a.Routes, route) |
|
|
} |
|
|
|
|
|
func (a *App) ServeHTTP(w http.ResponseWriter, r *http.Request) { |
|
|
ctx := &Context{Request: r, ResponseWriter: w} |
|
|
|
|
|
for _, rt := range a.Routes { |
|
|
if matches := rt.Pattern.FindStringSubmatch(ctx.URL.Path); len(matches) > 0 { |
|
|
if len(matches) > 1 { |
|
|
ctx.Params = matches[1:] |
|
|
} |
|
|
|
|
|
rt.Handler(ctx) |
|
|
return |
|
|
} |
|
|
} |
|
|
|
|
|
a.DefaultRoute(ctx) |
|
|
} |
|
|
|
|
|
type Context struct { |
|
|
http.ResponseWriter |
|
|
*http.Request |
|
|
Params []string |
|
|
} |
|
|
|
|
|
func (c *Context) Text(code int, body string) { |
|
|
c.ResponseWriter.Header().Set("Content-Type", "text/plain") |
|
|
c.WriteHeader(code) |
|
|
|
|
|
io.WriteString(c.ResponseWriter, fmt.Sprintf("%s\n", body)) |
|
|
} |
|
|
|
|
|
func main() { |
|
|
app := NewApp() |
|
|
|
|
|
app.Handle(`^/hello$`, func(ctx *Context) { |
|
|
ctx.Text(http.StatusOK, "Hello world") |
|
|
}) |
|
|
|
|
|
app.Handle(`/hello/([\w\._-]+)$`, func(ctx *Context) { |
|
|
ctx.Text(http.StatusOK, fmt.Sprintf("Hello %s", ctx.Params[0])) |
|
|
}) |
|
|
|
|
|
err := http.ListenAndServe(":9000", app) |
|
|
|
|
|
if err != nil { |
|
|
log.Fatalf("Could not start server: %s\n", err.Error()) |
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
``` |
|
|
|
|
|
This is just the start of what's possible when customizing an HTTP response handler. You can take this further by: |
|
|
|
|
|
* Matching requests against a specific HTTP method (e.g. GET / POST) and having different handler for the different request types |
|
|
* Matching against `Content-Type` to invoke a separate handler for different content requests (e.g. `text/html`, `application/json`, etc...) |
|
|
* Adding additional response methods (e.g. `ctx.JSON`) to send a more appropriate response for the requested `Content-Type` |