From 5c83d40ce063516af473ae672373633a35b4c2b3 Mon Sep 17 00:00:00 2001 From: tigerwill90 Date: Sun, 9 Apr 2023 14:16:32 +0200 Subject: [PATCH] Fix, optimization and doc improvement --- README.md | 295 +++++++++++++++++++----------------- assets/tree-apply-patch.png | Bin 40227 -> 0 bytes context.go | 16 +- error.go | 31 ++++ fox.go | 46 +++++- fox_test.go | 93 ++++++++---- options.go | 14 +- recovery.go | 4 +- response_writer.go | 28 ++-- 9 files changed, 337 insertions(+), 190 deletions(-) delete mode 100644 assets/tree-apply-patch.png diff --git a/README.md b/README.md index 0e792a9..c5b4904 100644 --- a/README.md +++ b/README.md @@ -58,15 +58,15 @@ type Greeting struct { Say string } -func (h *Greeting) Greet(c fox.Context) { - _ = c.String(http.StatusOK, "%s %s\n", h.Say, c.Param("name")) +func (h *Greeting) Greet(c fox.Context) error { + return c.String(http.StatusOK, "%s %s\n", h.Say, c.Param("name")) } func main() { r := fox.New(fox.DefaultOptions()) - err := r.Handle(http.MethodGet, "/", func(c fox.Context) { - _ = c.String(http.StatusOK, "Welcome\n") + err := r.Handle(http.MethodGet, "/", func(c fox.Context) error { + return c.String(http.StatusOK, "Welcome\n") }) if err != nil { panic(err) @@ -97,11 +97,34 @@ if errors.Is(err, fox.ErrRouteConflict) { } ``` +In addition, Fox also provides a centralized way to handle errors that may occur during the execution of a HandlerFunc. + +````go +var MyCustomError = errors.New("my custom error") + +r := fox.New( + fox.WithRouteError(func(c fox.Context, err error) { + if !c.Writer().Written() { + if errors.Is(err, MyCustomError) { + http.Error(c.Writer(), err.Error(), http.StatusInternalServerError) + return + } + http.Error(c.Writer(), http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } + }), +) + +r.MustHandle(http.MethodGet, "/hello/{name}", func(c fox.Context) error { + return MyCustomError +}) +```` + #### Named parameters A route can be defined using placeholder (e.g `{name}`). The matching segment are recorder into the `fox.Params` slice accessible via `fox.Context`. The `Param` and `Get` methods are helpers to retrieve the value using the placeholder name. -``` +```` Pattern /avengers/{name} /avengers/ironman match @@ -113,12 +136,12 @@ Pattern /users/uuid:{id} /users/uuid:123 match /users/uuid no match -``` +```` #### Catch all parameter Catch-all parameters can be used to match everything at the end of a route. The placeholder start with `*` followed by a regular named parameter (e.g. `*{name}`). -``` +```` Pattern /src/*{filepath} /src/ match @@ -130,7 +153,7 @@ Patter /src/file=*{path} /src/file= match /src/file=config.txt match /src/file=/dir/config.txt match -``` +```` #### Priority rules Routes are prioritized based on specificity, with static segments taking precedence over wildcard segments. @@ -148,18 +171,18 @@ GET /users/{id}/{actions} POST /users/{name}/emails ```` -#### Warning about params slice -`fox.Context` is freed once ServeHTTP returns and may be reused later to save resource. Therefore, if you need to hold `fox.Params` -longer, use the `Clone` methods. +#### Warning about context +The `fox.Context` instance is freed once the request handler function returns to optimize resource allocation. +If you need to retain `fox.Context` or `fox.Params` beyond the scope of the handler, use the `Clone` methods. ````go -func Hello(c fox.Context) { +func Hello(c fox.Context) error { cc := c.Clone() // cp := c.Params().Clone() go func() { time.Sleep(2 * time.Second) log.Println(cc.Param("name")) // Safe }() - _ = c.String(http.StatusOK, "Hello %s\n", c.Param("name")) + return c.String(http.StatusOK, "Hello %s\n", c.Param("name")) } ```` @@ -171,7 +194,7 @@ into a **patch**, which is then applied to the tree in a **single atomic operati For example, here we are inserting the new key `toast` into to the tree which require an existing node to be split:

- +

When traversing the tree during a patch, reading threads will either see the **old version** or the **new version** of the (sub-)tree, but both version are @@ -196,6 +219,7 @@ package main import ( "encoding/json" + "errors" "fmt" "github.com/tigerwill90/fox" "log" @@ -203,11 +227,10 @@ import ( "strings" ) -func Action(c fox.Context) { +func Action(c fox.Context) error { var data map[string]string if err := json.NewDecoder(c.Request().Body).Decode(&data); err != nil { - http.Error(c.Writer(), err.Error(), http.StatusBadRequest) - return + return fox.NewHTTPError(http.StatusBadRequest, err) } method := strings.ToUpper(data["method"]) @@ -215,33 +238,30 @@ func Action(c fox.Context) { text := data["text"] if path == "" || method == "" { - http.Error(c.Writer(), "missing method or path", http.StatusBadRequest) - return + return fox.NewHTTPError(http.StatusBadRequest, errors.New("missing method or path")) } var err error action := c.Param("action") switch action { case "add": - err = c.Fox().Handle(method, path, func(c fox.Context) { - _, _ = fmt.Fprintln(c.Writer(), text) + err = c.Fox().Handle(method, path, func(c fox.Context) error { + return c.String(http.StatusOK, text) }) case "update": - err = c.Fox().Update(method, path, func(c fox.Context) { - _, _ = fmt.Fprintln(c.Writer(), text) + err = c.Fox().Update(method, path, func(c fox.Context) error { + return c.String(http.StatusOK, text) }) case "delete": err = c.Fox().Remove(method, path) default: - http.Error(c.Writer(), fmt.Sprintf("action %q is not allowed", action), http.StatusBadRequest) - return + return fox.NewHTTPError(http.StatusBadRequest, fmt.Errorf("action %q is not allowed", action)) } if err != nil { - http.Error(c.Writer(), err.Error(), http.StatusConflict) - return + return fox.NewHTTPError(http.StatusConflict, err) } - _, _ = fmt.Fprintf(c.Writer(), "%s route [%s] %s: success\n", action, method, path) + return c.String(http.StatusOK, "%s route [%s] %s: success\n", action, method, path) } func main() { @@ -262,7 +282,6 @@ import ( "fox-by-example/db" "github.com/tigerwill90/fox" "html/template" - "io" "log" "net/http" "strings" @@ -273,10 +292,13 @@ type HtmlRenderer struct { Template template.HTML } -func (h *HtmlRenderer) Render(c fox.Context) { +func (h *HtmlRenderer) Render(c fox.Context) error { log.Printf("matched handler path: %s", c.Path()) - c.Writer().Header().Set(fox.HeaderContentType, fox.MIMETextHTMLCharsetUTF8) - _, _ = io.Copy(c.Writer(), strings.NewReader(string(h.Template))) + return c.Stream( + http.StatusInternalServerError, + fox.MIMETextHTMLCharsetUTF8, + strings.NewReader(string(h.Template)), + ) } func main() { @@ -333,9 +355,13 @@ func Upsert(t *fox.Tree, method, path string, handler fox.HandlerFunc) error { ```` #### Concurrent safety and proper usage of Tree APIs -Some important consideration to keep in mind when using `Tree` API. Each instance as its own `sync.Mutex` that may be -used to serialize write . Since the router tree may be swapped at any given time, you **MUST always copy the pointer -locally** to avoid inadvertently causing a deadlock by locking/unlocking the wrong `Tree`. +When working with the `Tree` API, it's important to keep some considerations in mind. Each instance has its +own `sync.Mutex` that can be used to serialize writes. However, unlike the router API, the lower-level `Tree` API +does not automatically lock the tree when writing to it. Therefore, it is the user's responsibility to ensure +all writes are executed serially. + +Moreover, since the router tree may be swapped at any given time, you MUST always copy the pointer locally to +avoid inadvertently causing a deadlock by locking/unlocking the wrong `Tree`. ````go // Good @@ -348,9 +374,10 @@ r.Tree().Lock() defer r.Tree().Unlock() // Dramatically bad, may cause deadlock -func handle(c fox.Context) { +func handle(c fox.Context) error { c.Fox().Tree().Lock() defer c.Fox().Tree().Unlock() + return nil } ```` @@ -358,9 +385,10 @@ Note that `fox.Context` carries a local copy of the `Tree` that is being used to the risk of deadlock when using the `Tree` within the context. ````go // Ok -func handle(c fox.Context) { +func handle(c fox.Context) error { c.Tree().Lock() defer c.Tree().Unlock() + return nil } ```` @@ -368,7 +396,7 @@ func handle(c fox.Context) { Fox itself implements the `http.Handler` interface which make easy to chain any compatible middleware before the router. Moreover, the router provides convenient `fox.WrapF`, `fox.WrapH` and `fox.WrapM` adapter to be use with `http.Handler`. -Wrapping an http.Handler +Wrapping an `http.Handler` ```go articles := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { _, _ = fmt.Fprintln(w, "get articles") @@ -378,11 +406,11 @@ r := fox.New(fox.DefaultOptions()) r.MustHandle(http.MethodGet, "/articles", fox.WrapH(httpRateLimiter.RateLimit(articles))) ``` -Wrapping an http.Handler compatible middleware +Wrapping an `http.Handler` compatible middleware ````go r := fox.New(fox.DefaultOptions(), fox.WithMiddleware(fox.WrapM(httpRateLimiter.RateLimit))) -r.MustHandle(http.MethodGet, "/articles/{id}", func(c fox.Context) { - _ = c.String(http.StatusOK, "Article id: %s\n", c.Param("id")) +r.MustHandle(http.MethodGet, "/articles/{id}", func(c fox.Context) error { + return c.String(http.StatusOK, "Article id: %s\n", c.Param("id")) }) ```` @@ -394,6 +422,7 @@ to create and apply automatically a simple logging middleware to all route. package main import ( + "fmt" "github.com/tigerwill90/fox" "log" "net/http" @@ -401,30 +430,33 @@ import ( ) var logger = fox.MiddlewareFunc(func(next fox.HandlerFunc) fox.HandlerFunc { - return func(c fox.Context) { + return func(c fox.Context) error { start := time.Now() - next(c) - log.Printf( - "route: %s, latency: %s, status: %d, size: %d", + err := next(c) + msg := fmt.Sprintf("route: %s, latency: %s, status: %d, size: %d", c.Path(), time.Since(start), c.Writer().Status(), c.Writer().Size(), ) + if err != nil { + msg += fmt.Sprintf(", error: %s", err) + } + log.Println(msg) + return err } }) func main() { r := fox.New(fox.WithMiddleware(logger)) - r.MustHandle(http.MethodGet, "/", func(c fox.Context) { + r.MustHandle(http.MethodGet, "/", func(c fox.Context) error { resp, err := http.Get("https://api.coindesk.com/v1/bpi/currentprice.json") if err != nil { - http.Error(c.Writer(), http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - return + return fox.NewHTTPError(http.StatusInternalServerError) } defer resp.Body.Close() - _ = c.Stream(http.StatusOK, fox.MIMEApplicationJSON, resp.Body) + return c.Stream(http.StatusOK, fox.MIMEApplicationJSON, resp.Body) }) log.Fatalln(http.ListenAndServe(":8080", r)) @@ -432,55 +464,53 @@ func main() { ```` ## Benchmark -The primary goal of Fox is to be a lightweight, high performance router which allow routes modification while in operation. -The following benchmarks attempt to compare Fox to various popular alternatives. Some are fully featured web framework, and other -are lightweight request router. This is based on [julienschmidt/go-http-routing-benchmark](https://github.com/julienschmidt/go-http-routing-benchmark) +The primary goal of Fox is to be a lightweight, high performance router which allow routes modification at runtime. +The following benchmarks attempt to compare Fox to various popular alternatives, including both fully-featured web frameworks +and lightweight request routers. These benchmarks are based on the [julienschmidt/go-http-routing-benchmark](https://github.com/julienschmidt/go-http-routing-benchmark) repository. +Please note that these benchmarks should not be taken too seriously, as the comparison may not be entirely fair due to +the differences in feature sets offered by each framework. Performance should be evaluated in the context of your specific +use case and requirements. While Fox aims to excel in performance, it's important to consider the trade-offs and +functionality provided by different web frameworks and routers when making your selection. + ### Config ``` GOOS: Linux GOARCH: amd64 -GO: 1.19 +GO: 1.20 CPU: Intel(R) Core(TM) i9-9900K CPU @ 3.60GHz ``` ### Static Routes It is just a collection of random static paths inspired by the structure of the Go directory. It might not be a realistic URL-structure. -**GOMAXPROCS: 0** +**GOMAXPROCS: 1** ``` -BenchmarkDenco_StaticAll 352584 3350 ns/op 0 B/op 0 allocs/op -BenchmarkHttpRouter_StaticAll 159259 7400 ns/op 0 B/op 0 allocs/op -BenchmarkKocha_StaticAll 154405 7793 ns/op 0 B/op 0 allocs/op -BenchmarkFox_StaticAll 130474 8899 ns/op 0 B/op 0 allocs/op -BenchmarkHttpTreeMux_StaticAll 127754 9065 ns/op 0 B/op 0 allocs/op -BenchmarkGin_StaticAll 96139 12393 ns/op 0 B/op 0 allocs/op -BenchmarkBeego_StaticAll 10000 103464 ns/op 55264 B/op 471 allocs/op -BenchmarkGorillaMux_StaticAll 2307 501554 ns/op 113041 B/op 1099 allocs/op -BenchmarkMartini_StaticAll 1357 886524 ns/op 129210 B/op 2031 allocs/op -BenchmarkTraffic_StaticAll 990 1183413 ns/op 753608 B/op 14601 allocs/op -BenchmarkPat_StaticAll 972 1193521 ns/op 602832 B/op 12559 allocs/op +BenchmarkHttpRouter_StaticAll 161659 7570 ns/op 0 B/op 0 allocs/op +BenchmarkHttpTreeMux_StaticAll 132446 8836 ns/op 0 B/op 0 allocs/op +BenchmarkFox_StaticAll 102577 11348 ns/op 0 B/op 0 allocs/op +BenchmarkStdMux_StaticAll 91304 13382 ns/op 0 B/op 0 allocs/op +BenchmarkGin_StaticAll 78224 15433 ns/op 0 B/op 0 allocs/op +BenchmarkEcho_StaticAll 77923 15739 ns/op 0 B/op 0 allocs/op +BenchmarkBeego_StaticAll 10000 101094 ns/op 55264 B/op 471 allocs/op +BenchmarkGorillaMux_StaticAll 2283 525683 ns/op 113041 B/op 1099 allocs/op +BenchmarkMartini_StaticAll 1330 936928 ns/op 129210 B/op 2031 allocs/op +BenchmarkTraffic_StaticAll 1064 1140959 ns/op 753611 B/op 14601 allocs/op +BenchmarkPat_StaticAll 967 1230424 ns/op 602832 B/op 12559 allocs/op ``` -In this benchmark, Fox performs as well as `Gin`, `HttpTreeMux` and `HttpRouter` which are all Radix Tree based routers. An interesting fact is +In this benchmark, Fox performs as well as `Gin`, `Echo` which are both Radix Tree based routers. An interesting fact is that [HttpTreeMux](https://github.com/dimfeld/httptreemux) also support [adding route while serving request concurrently](https://github.com/dimfeld/httptreemux#concurrency). However, it takes a slightly different approach, by using an optional `RWMutex` that may not scale as well as Fox under heavy load. The next test compare `HttpTreeMux`, `HttpTreeMux_SafeAddRouteFlag` (concurrent reads and writes), `HttpRouter` and `Fox` in parallel benchmark. **GOMAXPROCS: 16** ``` -Route: /progs/image_package4.out - -BenchmarkHttpRouter_StaticSingleParallel-16 211819790 5.640 ns/op 0 B/op 0 allocs/op -BenchmarkFox_StaticSingleParallel-16 157547185 7.418 ns/op 0 B/op 0 allocs/op -BenchmarkHttpTreeMux_StaticSingleParallel-16 154222639 7.774 ns/op 0 B/op 0 allocs/op -BenchmarkHttpTreeMux_SafeAddRouteFlag_StaticSingleParallel-16 29904204 38.52 ns/op 0 B/op 0 allocs/op - Route: all -BenchmarkHttpRouter_StaticAllParallel-16 1446759 832.1 ns/op 0 B/op 0 allocs/op -BenchmarkHttpTreeMux_StaticAllParallel-16 997074 1100 ns/op 0 B/op 0 allocs/op -BenchmarkFox_StaticAllParallel-16 1000000 1105 ns/op 0 B/op 0 allocs/op -BenchmarkHttpTreeMux_SafeAddRouteFlag_StaticAllParallel-16 197578 6017 ns/op 0 B/op 0 allocs/op +BenchmarkFox_StaticAll-16 99322 11369 ns/op 0 B/op 0 allocs/op +BenchmarkFox_StaticAllParallel-16 831354 1422 ns/op 0 B/op 0 allocs/op +BenchmarkHttpTreeMux_StaticAll-16 135560 8861 ns/op 0 B/op 0 allocs/op +BenchmarkHttpTreeMux_StaticAllParallel-16 172714 6916 ns/op 0 B/op 0 allocs/op ``` As you can see, this benchmark highlight the cost of using higher synchronisation primitive like `RWMutex` to be able to register new route while handling requests. @@ -490,47 +520,45 @@ The following benchmarks measure the cost of some very basic operations. In the first benchmark, only a single route, containing a parameter, is loaded into the routers. Then a request for a URL matching this pattern is made and the router has to call the respective registered handler function. End. -**GOMAXPROCS: 0** +**GOMAXPROCS: 1** ``` -BenchmarkFox_Param 29995566 39.04 ns/op 0 B/op 0 allocs/op -BenchmarkGin_Param 30710918 39.08 ns/op 0 B/op 0 allocs/op -BenchmarkHttpRouter_Param 20026911 55.88 ns/op 32 B/op 1 allocs/op -BenchmarkDenco_Param 15964747 70.04 ns/op 32 B/op 1 allocs/op -BenchmarkKocha_Param 8392696 138.5 ns/op 56 B/op 3 allocs/op -BenchmarkHttpTreeMux_Param 4469318 265.6 ns/op 352 B/op 3 allocs/op -BenchmarkBeego_Param 2241368 530.9 ns/op 352 B/op 3 allocs/op -BenchmarkPat_Param 1788819 666.8 ns/op 512 B/op 10 allocs/op -BenchmarkGorillaMux_Param 1208638 995.1 ns/op 1024 B/op 8 allocs/op -BenchmarkTraffic_Param 606530 1700 ns/op 1848 B/op 21 allocs/op -BenchmarkMartini_Param 455662 2419 ns/op 1096 B/op 12 allocs/op +BenchmarkFox_Param 33024534 36.61 ns/op 0 B/op 0 allocs/op +BenchmarkEcho_Param 31472508 38.71 ns/op 0 B/op 0 allocs/op +BenchmarkGin_Param 25826832 52.88 ns/op 0 B/op 0 allocs/op +BenchmarkHttpRouter_Param 21230490 60.83 ns/op 32 B/op 1 allocs/op +BenchmarkHttpTreeMux_Param 3960292 280.4 ns/op 352 B/op 3 allocs/op +BenchmarkBeego_Param 2247776 518.9 ns/op 352 B/op 3 allocs/op +BenchmarkPat_Param 1603902 676.6 ns/op 512 B/op 10 allocs/op +BenchmarkGorillaMux_Param 1000000 1011 ns/op 1024 B/op 8 allocs/op +BenchmarkTraffic_Param 648986 1686 ns/op 1848 B/op 21 allocs/op +BenchmarkMartini_Param 485839 2446 ns/op 1096 B/op 12 allocs/op ``` Same as before, but now with multiple parameters, all in the same single route. The intention is to see how the routers scale with the number of parameters. **GOMAXPROCS: 0** ``` -BenchmarkGin_Param5 16470636 73.09 ns/op 0 B/op 0 allocs/op -BenchmarkFox_Param5 14716213 82.05 ns/op 0 B/op 0 allocs/op -BenchmarkHttpRouter_Param5 7614333 154.7 ns/op 160 B/op 1 allocs/op -BenchmarkDenco_Param5 6513253 179.5 ns/op 160 B/op 1 allocs/op -BenchmarkKocha_Param5 2073741 604.3 ns/op 440 B/op 10 allocs/op -BenchmarkHttpTreeMux_Param5 1801978 659.2 ns/op 576 B/op 6 allocs/op -BenchmarkBeego_Param5 1764513 669.1 ns/op 352 B/op 3 allocs/op -BenchmarkGorillaMux_Param5 657648 1578 ns/op 1088 B/op 8 allocs/op -BenchmarkPat_Param5 633555 1700 ns/op 800 B/op 24 allocs/op -BenchmarkTraffic_Param5 374895 2744 ns/op 2200 B/op 27 allocs/op -BenchmarkMartini_Param5 403650 2835 ns/op 1256 B/op 13 allocs/op - -BenchmarkGin_Param20 6136497 189.9 ns/op 0 B/op 0 allocs/op -BenchmarkFox_Param20 4187372 283.2 ns/op 0 B/op 0 allocs/op -BenchmarkHttpRouter_Param20 2536359 483.4 ns/op 640 B/op 1 allocs/op -BenchmarkDenco_Param20 2110105 567.7 ns/op 640 B/op 1 allocs/op -BenchmarkKocha_Param20 593958 1744 ns/op 1808 B/op 27 allocs/op -BenchmarkBeego_Param20 741110 1747 ns/op 352 B/op 3 allocs/op -BenchmarkHttpTreeMux_Param20 341913 3079 ns/op 3195 B/op 10 allocs/op -BenchmarkGorillaMux_Param20 282345 3671 ns/op 3196 B/op 10 allocs/op -BenchmarkMartini_Param20 210543 5222 ns/op 3619 B/op 15 allocs/op -BenchmarkPat_Param20 151778 7343 ns/op 4096 B/op 73 allocs/op -BenchmarkTraffic_Param20 113230 9989 ns/op 7847 B/op 47 allocs/op +BenchmarkFox_Param5 16608495 72.84 ns/op 0 B/op 0 allocs/op +BenchmarkGin_Param5 13098740 92.22 ns/op 0 B/op 0 allocs/op +BenchmarkEcho_Param5 12025460 96.33 ns/op 0 B/op 0 allocs/op +BenchmarkHttpRouter_Param5 8233530 148.1 ns/op 160 B/op 1 allocs/op +BenchmarkHttpTreeMux_Param5 1986019 616.9 ns/op 576 B/op 6 allocs/op +BenchmarkBeego_Param5 1836229 655.3 ns/op 352 B/op 3 allocs/op +BenchmarkGorillaMux_Param5 757936 1572 ns/op 1088 B/op 8 allocs/op +BenchmarkPat_Param5 645847 1724 ns/op 800 B/op 24 allocs/op +BenchmarkTraffic_Param5 424431 2729 ns/op 2200 B/op 27 allocs/op +BenchmarkMartini_Param5 424806 2772 ns/op 1256 B/op 13 allocs/op + + +BenchmarkGin_Param20 4636416 244.6 ns/op 0 B/op 0 allocs/op +BenchmarkFox_Param20 4667533 250.7 ns/op 0 B/op 0 allocs/op +BenchmarkEcho_Param20 4352486 277.1 ns/op 0 B/op 0 allocs/op +BenchmarkHttpRouter_Param20 2618958 455.2 ns/op 640 B/op 1 allocs/op +BenchmarkBeego_Param20 847029 1688 ns/op 352 B/op 3 allocs/op +BenchmarkHttpTreeMux_Param20 369500 2972 ns/op 3195 B/op 10 allocs/op +BenchmarkGorillaMux_Param20 318134 3561 ns/op 3195 B/op 10 allocs/op +BenchmarkMartini_Param20 223070 5117 ns/op 3619 B/op 15 allocs/op +BenchmarkPat_Param20 157380 7442 ns/op 4094 B/op 73 allocs/op +BenchmarkTraffic_Param20 119677 9864 ns/op 7847 B/op 47 allocs/op ``` Now let's see how expensive it is to access a parameter. The handler function reads the value (by the name of the parameter, e.g. with a map @@ -538,17 +566,16 @@ lookup; depends on the router) and writes it to `/dev/null` **GOMAXPROCS: 0** ``` -BenchmarkFox_ParamWrite 21061758 56.96 ns/op 0 B/op 0 allocs/op -BenchmarkGin_ParamWrite 17973256 66.54 ns/op 0 B/op 0 allocs/op -BenchmarkHttpRouter_ParamWrite 15953065 74.64 ns/op 32 B/op 1 allocs/op -BenchmarkDenco_ParamWrite 12553562 89.93 ns/op 32 B/op 1 allocs/op -BenchmarkKocha_ParamWrite 7356948 156.7 ns/op 56 B/op 3 allocs/op -BenchmarkHttpTreeMux_ParamWrite 4075486 286.4 ns/op 352 B/op 3 allocs/op -BenchmarkBeego_ParamWrite 2126341 567.4 ns/op 360 B/op 4 allocs/op -BenchmarkPat_ParamWrite 1197910 996.5 ns/op 936 B/op 14 allocs/op -BenchmarkGorillaMux_ParamWrite 1139376 1048 ns/op 1024 B/op 8 allocs/op -BenchmarkTraffic_ParamWrite 496440 2057 ns/op 2272 B/op 25 allocs/op -BenchmarkMartini_ParamWrite 398594 2799 ns/op 1168 B/op 16 allocs/op +BenchmarkFox_ParamWrite 16707409 72.53 ns/op 0 B/op 0 allocs/op +BenchmarkHttpRouter_ParamWrite 16478174 73.30 ns/op 32 B/op 1 allocs/op +BenchmarkGin_ParamWrite 15828385 75.73 ns/op 0 B/op 0 allocs/op +BenchmarkEcho_ParamWrite 13187766 95.18 ns/op 8 B/op 1 allocs/op +BenchmarkHttpTreeMux_ParamWrite 4132832 279.9 ns/op 352 B/op 3 allocs/op +BenchmarkBeego_ParamWrite 2172572 554.3 ns/op 360 B/op 4 allocs/op +BenchmarkPat_ParamWrite 1200334 996.8 ns/op 936 B/op 14 allocs/op +BenchmarkGorillaMux_ParamWrite 1000000 1005 ns/op 1024 B/op 8 allocs/op +BenchmarkMartini_ParamWrite 454255 2667 ns/op 1168 B/op 16 allocs/op +BenchmarkTraffic_ParamWrite 511766 2021 ns/op 2272 B/op 25 allocs/op ``` In those micro benchmarks, we can see that `Fox` scale really well, even with long wildcard routes. Like `Gin`, this router reuse the @@ -562,18 +589,16 @@ Finally, this benchmark execute a request for each GitHub API route (203 routes) **GOMAXPROCS: 0** ``` -BenchmarkGin_GithubAll 68384 17425 ns/op 0 B/op 0 allocs/op -BenchmarkFox_GithubAll 67162 17631 ns/op 0 B/op 0 allocs/op -BenchmarkHttpRouter_GithubAll 44085 27449 ns/op 13792 B/op 167 allocs/op -BenchmarkDenco_GithubAll 35019 33651 ns/op 20224 B/op 167 allocs/op -BenchmarkKocha_GithubAll 19186 62243 ns/op 23304 B/op 843 allocs/op -BenchmarkHttpTreeMuxSafeAddRoute_GithubAll 14907 79919 ns/op 65856 B/op 671 allocs/op -BenchmarkHttpTreeMux_GithubAll 14952 80280 ns/op 65856 B/op 671 allocs/op -BenchmarkBeego_GithubAll 9712 136414 ns/op 71456 B/op 609 allocs/op -BenchmarkTraffic_GithubAll 637 1824477 ns/op 819052 B/op 14114 allocs/op -BenchmarkMartini_GithubAll 572 2042852 ns/op 231419 B/op 2731 allocs/op -BenchmarkGorillaMux_GithubAll 562 2110880 ns/op 199683 B/op 1588 allocs/op -BenchmarkPat_GithubAll 550 2117715 ns/op 1410624 B/op 22515 allocs/op +BenchmarkFox_GithubAll 63984 18555 ns/op 0 B/op 0 allocs/op +BenchmarkEcho_GithubAll 49312 23353 ns/op 0 B/op 0 allocs/op +BenchmarkGin_GithubAll 48422 24926 ns/op 0 B/op 0 allocs/op +BenchmarkHttpRouter_GithubAll 45706 26818 ns/op 14240 B/op 171 allocs/op +BenchmarkHttpTreeMux_GithubAll 14731 80133 ns/op 67648 B/op 691 allocs/op +BenchmarkBeego_GithubAll 7692 137926 ns/op 72929 B/op 625 allocs/op +BenchmarkTraffic_GithubAll 636 1916586 ns/op 845114 B/op 14634 allocs/op +BenchmarkMartini_GithubAll 530 2205947 ns/op 238546 B/op 2813 allocs/op +BenchmarkGorillaMux_GithubAll 529 2246380 ns/op 203844 B/op 1620 allocs/op +BenchmarkPat_GithubAll 424 2899405 ns/op 1843501 B/op 29064 allocs/op ``` ## Road to v1 diff --git a/assets/tree-apply-patch.png b/assets/tree-apply-patch.png deleted file mode 100644 index aaf894be05ca3f508ba62f381ba881ac6629b929..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 40227 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 diff --git a/context.go b/context.go index 78a90cb..758c1fc 100644 --- a/context.go +++ b/context.go @@ -162,7 +162,9 @@ func (c *context) Path() string { // String sends a formatted string with the specified status code. func (c *context) String(code int, format string, values ...any) (err error) { - c.w.Header().Set(HeaderContentType, MIMETextPlainCharsetUTF8) + if c.w.Header().Get(HeaderContentType) == "" { + c.w.Header().Set(HeaderContentType, MIMETextPlainCharsetUTF8) + } c.w.WriteHeader(code) _, err = fmt.Fprintf(c.w, format, values...) return @@ -243,15 +245,17 @@ func (c *context) getQueries() url.Values { // WrapF is an adapter for wrapping http.HandlerFunc and returns a HandlerFunc function. func WrapF(f http.HandlerFunc) HandlerFunc { - return func(c Context) { + return func(c Context) error { f.ServeHTTP(c.Writer(), c.Request()) + return nil } } // WrapH is an adapter for wrapping http.Handler and returns a HandlerFunc function. func WrapH(h http.Handler) HandlerFunc { - return func(c Context) { + return func(c Context) error { h.ServeHTTP(c.Writer(), c.Request()) + return nil } } @@ -259,11 +263,13 @@ func WrapH(h http.Handler) HandlerFunc { // MiddlewareFunc function. func WrapM(m func(handler http.Handler) http.Handler) MiddlewareFunc { return func(next HandlerFunc) HandlerFunc { - return func(c Context) { + return func(c Context) error { + var err error adapter := m(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - next(c) + err = next(c) })) adapter.ServeHTTP(c.Writer(), c.Request()) + return err } } } diff --git a/error.go b/error.go index efcaa28..e9d43a2 100644 --- a/error.go +++ b/error.go @@ -7,6 +7,7 @@ package fox import ( "errors" "fmt" + "net/http" "strings" ) @@ -58,3 +59,33 @@ func (e *RouteConflictError) updateError() string { func (e *RouteConflictError) Unwrap() error { return e.err } + +// HTTPError represents an HTTP error with a status code (HTTPErrorCode) +// and an optional error message. If no error message is provided, +// the default error message for the status code will be used. +type HTTPError struct { + Code int + Err error +} + +// Error returns the error message associated with the HTTPError, +// or the default error message for the status code if none is provided. +func (e HTTPError) Error() string { + if e.Err == nil { + return http.StatusText(e.Code) + } + return e.Err.Error() +} + +// NewHTTPError creates a new HTTPError with the given status code +// and an optional error message. +func NewHTTPError(code int, err ...error) HTTPError { + var e error + if len(err) > 0 { + e = err[0] + } + return HTTPError{ + Code: code, + Err: e, + } +} diff --git a/fox.go b/fox.go index 4bfc847..25d9770 100644 --- a/fox.go +++ b/fox.go @@ -27,7 +27,7 @@ var commonVerbs = [verb]string{http.MethodGet, http.MethodPost, http.MethodPut, // response, panic with the value http.ErrAbortHandler. // // HandlerFunc functions should be thread-safe, as they will be called concurrently. -type HandlerFunc func(c Context) +type HandlerFunc func(c Context) error // MiddlewareFunc is a function type for implementing HandlerFunc middleware. // The returned HandlerFunc usually wraps the input HandlerFunc, allowing you to perform operations @@ -35,11 +35,14 @@ type HandlerFunc func(c Context) // be thread-safe, as they will be called concurrently. type MiddlewareFunc func(next HandlerFunc) HandlerFunc +type ErrorHandlerFunc func(c Context, err error) + // Router is a lightweight high performance HTTP request router that support mutation on its routing tree // while handling request concurrently. type Router struct { noRoute HandlerFunc noMethod HandlerFunc + errRoute ErrorHandlerFunc tree atomic.Pointer[Tree] mws []MiddlewareFunc handleMethodNotAllowed bool @@ -55,6 +58,8 @@ func New(opts ...Option) *Router { r.noRoute = NotFoundHandler() r.noMethod = MethodNotAllowedHandler() + r.errRoute = RouteErrorHandler() + for _, opt := range opts { opt.apply(r) } @@ -217,14 +222,31 @@ Next: // with a “404 page not found” reply. func NotFoundHandler() HandlerFunc { http.NotFoundHandler() - return func(c Context) { http.Error(c.Writer(), "404 page not found", http.StatusNotFound) } + return func(c Context) error { + http.Error(c.Writer(), "404 page not found", http.StatusNotFound) + return nil + } } // MethodNotAllowedHandler returns a simple HandlerFunc that replies to each request // with a “405 Method Not Allowed” reply. func MethodNotAllowedHandler() HandlerFunc { - return func(c Context) { + return func(c Context) error { http.Error(c.Writer(), http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) + return nil + } +} + +// RouteErrorHandler returns an ErrorHandlerFunc that handle HandlerFunc error. +func RouteErrorHandler() ErrorHandlerFunc { + return func(c Context, err error) { + if !c.Writer().Written() { + if e, ok := err.(HTTPError); ok { + http.Error(c.Writer(), e.Error(), e.Code) + return + } + http.Error(c.Writer(), http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + } } } @@ -235,8 +257,7 @@ func (fox *Router) ServeHTTP(w http.ResponseWriter, r *http.Request) { tsr bool ) - tree := fox.Tree() - + tree := fox.tree.Load() c := tree.ctx.Get().(*context) c.reset(fox, w, r) @@ -249,7 +270,10 @@ func (fox *Router) ServeHTTP(w http.ResponseWriter, r *http.Request) { n, tsr = tree.lookup(nds[index], r.URL.Path, c.params, c.skipNds, false) if n != nil { c.path = n.path - n.handler(c) + err := n.handler(c) + if err != nil { + fox.errRoute(c, err) + } // Put back the context, if not extended more than max params or max depth, allowing // the slice to naturally grow within the constraint. if cap(*c.params) <= int(tree.maxParams.Load()) && cap(*c.skipNds) <= int(tree.maxDepth.Load()) { @@ -311,13 +335,19 @@ NoMethodFallback: allowed := sb.String() if allowed != "" { w.Header().Set("Allow", allowed) - fox.noMethod(c) + err := fox.noMethod(c) + if err != nil { + fox.errRoute(c, err) + } c.Close() return } } - fox.noRoute(c) + err := fox.noRoute(c) + if err != nil { + fox.errRoute(c, err) + } c.Close() } diff --git a/fox_test.go b/fox_test.go index 5a6e50a..ab7013d 100644 --- a/fox_test.go +++ b/fox_test.go @@ -5,6 +5,7 @@ package fox import ( + "errors" "fmt" fuzz "github.com/google/gofuzz" "github.com/stretchr/testify/assert" @@ -21,9 +22,9 @@ import ( "time" ) -var emptyHandler = HandlerFunc(func(c Context) {}) -var pathHanlder = HandlerFunc(func(c Context) { _, _ = c.Writer().Write([]byte(c.Request().URL.Path)) }) -var routeHandler = HandlerFunc(func(c Context) { _, _ = c.Writer().Write([]byte(c.Path())) }) +var emptyHandler = HandlerFunc(func(c Context) error { return nil }) +var pathHandler = HandlerFunc(func(c Context) error { return c.String(200, c.Request().URL.Path) }) +var routeHandler = HandlerFunc(func(c Context) error { return c.String(200, c.Path()) }) type mockResponseWriter struct{} @@ -564,7 +565,7 @@ func TestStaticRoute(t *testing.T) { r := New() for _, route := range staticRoutes { - require.NoError(t, r.Tree().Handle(route.method, route.path, pathHanlder)) + require.NoError(t, r.Tree().Handle(route.method, route.path, pathHandler)) } for _, route := range staticRoutes { @@ -594,7 +595,7 @@ func TestStaticRouteMalloc(t *testing.T) { func TestParamsRoute(t *testing.T) { rx := regexp.MustCompile("({|\\*{)[A-z]+[}]") r := New() - h := func(c Context) { + h := func(c Context) error { matches := rx.FindAllString(c.Request().URL.Path, -1) for _, match := range matches { var key string @@ -607,7 +608,7 @@ func TestParamsRoute(t *testing.T) { assert.Equal(t, value, c.Param(key)) } assert.Equal(t, c.Request().URL.Path, c.Path()) - _, _ = c.Writer().Write([]byte(c.Request().URL.Path)) + return c.String(200, c.Request().URL.Path) } for _, route := range githubAPI { require.NoError(t, r.Tree().Handle(route.method, route.path, h)) @@ -661,7 +662,7 @@ func TestRouterWildcard(t *testing.T) { } for _, route := range routes { - require.NoError(t, r.Tree().Handle(http.MethodGet, route.path, pathHanlder)) + require.NoError(t, r.Tree().Handle(http.MethodGet, route.path, pathHandler)) } for _, route := range routes { @@ -1654,9 +1655,9 @@ func TestRecoveryMiddleware(t *testing.T) { r := New(WithMiddleware(m)) const errMsg = "unexpected error" - h := func(c Context) { + h := func(c Context) error { func() { panic(errMsg) }() - _, _ = c.Writer().Write([]byte("foo")) + return c.String(200, "foo") } require.NoError(t, r.Tree().Handle(http.MethodPost, "/", h)) @@ -1720,6 +1721,38 @@ func TestHas(t *testing.T) { } } +func TestErrorHandling(t *testing.T) { + r := New() + + req := httptest.NewRequest(http.MethodGet, "/foo/bar", nil) + w := httptest.NewRecorder() + + r.MustHandle(http.MethodGet, "/foo/bar", func(c Context) error { + return NewHTTPError(http.StatusBadRequest, errors.New("oups")) + }) + + r.ServeHTTP(w, req) + assert.Equal(t, http.StatusBadRequest, w.Code) + assert.Equal(t, "oups\n", w.Body.String()) +} + +func TestCustomErrorHandling(t *testing.T) { + r := New(WithRouteError(func(c Context, err error) { + http.Error(c.Writer(), err.Error(), http.StatusInternalServerError) + })) + + req := httptest.NewRequest(http.MethodGet, "/foo/bar", nil) + w := httptest.NewRecorder() + + r.MustHandle(http.MethodGet, "/foo/bar", func(c Context) error { + return errors.New("something went wrong") + }) + + r.ServeHTTP(w, req) + assert.Equal(t, http.StatusInternalServerError, w.Code) + assert.Equal(t, "something went wrong\n", w.Body.String()) +} + func TestReverse(t *testing.T) { routes := []string{ "/foo/bar", @@ -1773,9 +1806,9 @@ func TestAbortHandler(t *testing.T) { r := New(WithMiddleware(m)) - h := func(c Context) { + h := func(c Context) error { func() { panic(http.ErrAbortHandler) }() - _, _ = c.Writer().Write([]byte("foo")) + return c.String(200, "foo") } require.NoError(t, r.Tree().Handle(http.MethodPost, "/", h)) @@ -1903,8 +1936,8 @@ func TestDataRace(t *testing.T) { var wg sync.WaitGroup start, wait := atomicSync() - h := HandlerFunc(func(c Context) {}) - newH := HandlerFunc(func(c Context) {}) + h := HandlerFunc(func(c Context) error { return nil }) + newH := HandlerFunc(func(c Context) error { return nil }) r := New() @@ -1956,24 +1989,24 @@ func TestConcurrentRequestHandling(t *testing.T) { r := New() // /repos/{owner}/{repo}/keys - h1 := HandlerFunc(func(c Context) { + h1 := HandlerFunc(func(c Context) error { assert.Equal(t, "john", c.Param("owner")) assert.Equal(t, "fox", c.Param("repo")) - _, _ = fmt.Fprint(c.Writer(), c.Path()) + return c.String(200, c.Path()) }) // /repos/{owner}/{repo}/contents/*{path} - h2 := HandlerFunc(func(c Context) { + h2 := HandlerFunc(func(c Context) error { assert.Equal(t, "alex", c.Param("owner")) assert.Equal(t, "vault", c.Param("repo")) assert.Equal(t, "file.txt", c.Param("path")) - _, _ = fmt.Fprint(c.Writer(), c.Path()) + return c.String(200, c.Path()) }) // /users/{user}/received_events/public - h3 := HandlerFunc(func(c Context) { + h3 := HandlerFunc(func(c Context) error { assert.Equal(t, "go", c.Param("user")) - _, _ = fmt.Fprint(c.Writer(), c.Path()) + return c.String(200, c.Path()) }) require.NoError(t, r.Handle(http.MethodGet, "/repos/{owner}/{repo}/keys", h1)) @@ -2044,17 +2077,18 @@ func ExampleNew() { // Define a custom middleware to measure the time taken for request processing and // log the URL, route, time elapsed, and status code metrics := func(next HandlerFunc) HandlerFunc { - return func(c Context) { + return func(c Context) error { start := time.Now() - next(c) + err := next(c) log.Printf("url=%s; route=%s; time=%d; status=%d", c.Request().URL, c.Path(), time.Since(start), c.Writer().Status()) + return err } } // Define a route with the path "/hello/{name}", apply the custom "metrics" middleware, // and set a simple handler that greets the user by their name - r.MustHandle(http.MethodGet, "/hello/{name}", metrics(func(c Context) { - _ = c.String(200, "Hello %s\n", c.Param("name")) + r.MustHandle(http.MethodGet, "/hello/{name}", metrics(func(c Context) error { + return c.String(200, "Hello %s\n", c.Param("name")) })) // Start the HTTP server using the router as the handler and listen on port 8080 @@ -2069,17 +2103,18 @@ func ExampleWithMiddleware() { // Define a custom middleware to measure the time taken for request processing and // log the URL, route, time elapsed, and status code metrics := func(next HandlerFunc) HandlerFunc { - return func(c Context) { + return func(c Context) error { start := time.Now() - next(c) + err := next(c) log.Printf("url=%s; route=%s; time=%d; status=%d", c.Request().URL, c.Path(), time.Since(start), c.Writer().Status()) + return err } } r := New(WithMiddleware(metrics)) - r.MustHandle(http.MethodGet, "/hello/{name}", func(c Context) { - _ = c.String(200, "Hello %s\n", c.Param("name")) + r.MustHandle(http.MethodGet, "/hello/{name}", func(c Context) error { + return c.String(200, "Hello %s\n", c.Param("name")) }) } @@ -2100,11 +2135,11 @@ func ExampleRouter_Tree() { return tree.Handle(method, path, handler) } - _ = upsert(http.MethodGet, "/foo/bar", func(c Context) { + _ = upsert(http.MethodGet, "/foo/bar", func(c Context) error { // Note the tree accessible from fox.Context is already a local copy so the golden rule above does not apply. c.Tree().Lock() defer c.Tree().Unlock() - _, _ = fmt.Fprintln(c.Writer(), "foo bar") + return c.String(200, "foo bar") }) // Bad, instead make a local copy of the tree! diff --git a/options.go b/options.go index fb94276..438ea08 100644 --- a/options.go +++ b/options.go @@ -14,7 +14,7 @@ func (o optionFunc) apply(r *Router) { o(r) } -// WithRouteNotFound register a http.Handler which is called when no matching route is found. +// WithRouteNotFound register an HandlerFunc which is called when no matching route is found. // By default, the NotFoundHandler is used. func WithRouteNotFound(handler HandlerFunc, m ...MiddlewareFunc) Option { return optionFunc(func(r *Router) { @@ -24,7 +24,7 @@ func WithRouteNotFound(handler HandlerFunc, m ...MiddlewareFunc) Option { }) } -// WithMethodNotAllowed register a http.Handler which is called when the request cannot be routed, +// WithMethodNotAllowed register an HandlerFunc which is called when the request cannot be routed, // but the same route exist for other methods. The "Allow" header it automatically set // before calling the handler. Set WithHandleMethodNotAllowed to enable this option. By default, // the MethodNotAllowedHandler is used. @@ -36,6 +36,16 @@ func WithMethodNotAllowed(handler HandlerFunc, m ...MiddlewareFunc) Option { }) } +// WithRouteError register an ErrorHandlerFunc which is called when an HandlerFunc returns an error. +// By default, the RouteErrorHandler is used. +func WithRouteError(handler ErrorHandlerFunc) Option { + return optionFunc(func(r *Router) { + if handler != nil { + r.errRoute = handler + } + }) +} + // WithMiddleware attaches a global middleware to the router. Middlewares provided will be chained // in the order they were added. Note that it does NOT apply the middlewares to the NotFound and MethodNotAllowed handlers. func WithMiddleware(m ...MiddlewareFunc) Option { diff --git a/recovery.go b/recovery.go index 5380ca0..d535100 100644 --- a/recovery.go +++ b/recovery.go @@ -26,9 +26,9 @@ type RecoveryFunc func(c Context, err any) // allowing the http server to handle it as an abort. func Recovery(handle RecoveryFunc) MiddlewareFunc { return func(next HandlerFunc) HandlerFunc { - return func(c Context) { + return func(c Context) error { defer recovery(c, handle) - next(c) + return next(c) } } } diff --git a/response_writer.go b/response_writer.go index e0c565d..98cce9d 100644 --- a/response_writer.go +++ b/response_writer.go @@ -82,13 +82,23 @@ func (r *recorder) Write(buf []byte) (n int, err error) { return } +func (r *recorder) WriteString(s string) (n int, err error) { + if !r.Written() { + r.size = 0 + r.ResponseWriter.WriteHeader(r.status) + } + n, err = io.WriteString(r.ResponseWriter, s) + r.size += n + return +} + //nolint:unused type hijackWriter struct { *recorder } //nolint:unused -func (w *hijackWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { +func (w hijackWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { if !w.recorder.Written() { w.recorder.size = 0 } @@ -101,7 +111,7 @@ type flushHijackWriter struct { } //nolint:unused -func (w *flushHijackWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { +func (w flushHijackWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { if !w.recorder.Written() { w.recorder.size = 0 } @@ -109,7 +119,7 @@ func (w *flushHijackWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { } //nolint:unused -func (w *flushHijackWriter) Flush() { +func (w flushHijackWriter) Flush() { if !w.recorder.Written() { w.recorder.size = 0 } @@ -120,7 +130,7 @@ type flushWriter struct { *recorder } -func (w *flushWriter) Flush() { +func (w flushWriter) Flush() { if !w.recorder.Written() { w.recorder.size = 0 } @@ -131,7 +141,7 @@ type h1Writer struct { *recorder } -func (w *h1Writer) ReadFrom(r io.Reader) (n int64, err error) { +func (w h1Writer) ReadFrom(r io.Reader) (n int64, err error) { rf := w.recorder.ResponseWriter.(io.ReaderFrom) // If not written, status is OK w.recorder.WriteHeader(w.recorder.status) @@ -140,14 +150,14 @@ func (w *h1Writer) ReadFrom(r io.Reader) (n int64, err error) { return } -func (w *h1Writer) Hijack() (net.Conn, *bufio.ReadWriter, error) { +func (w h1Writer) Hijack() (net.Conn, *bufio.ReadWriter, error) { if !w.recorder.Written() { w.recorder.size = 0 } return w.recorder.ResponseWriter.(http.Hijacker).Hijack() } -func (w *h1Writer) Flush() { +func (w h1Writer) Flush() { if !w.recorder.Written() { w.recorder.size = 0 } @@ -158,11 +168,11 @@ type h2Writer struct { *recorder } -func (w *h2Writer) Push(target string, opts *http.PushOptions) error { +func (w h2Writer) Push(target string, opts *http.PushOptions) error { return w.recorder.ResponseWriter.(http.Pusher).Push(target, opts) } -func (w *h2Writer) Flush() { +func (w h2Writer) Flush() { if !w.recorder.Written() { w.recorder.size = 0 }