{"id":13764094,"url":"https://github.com/nbari/violetear","last_synced_at":"2025-08-22T00:31:26.903Z","repository":{"id":57480297,"uuid":"37734350","full_name":"nbari/violetear","owner":"nbari","description":"Go HTTP router","archived":false,"fork":false,"pushed_at":"2022-09-27T12:18:03.000Z","size":1384,"stargazers_count":107,"open_issues_count":1,"forks_count":11,"subscribers_count":5,"default_branch":"master","last_synced_at":"2024-12-07T04:15:31.104Z","etag":null,"topics":["dynamic","http-server","http2","http2-push","httprouter","mux","rest-api","versioning","www"],"latest_commit_sha":null,"homepage":"http://violetear.org","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"bsd-3-clause","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/nbari.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null},"funding":{"github":"nbari"}},"created_at":"2015-06-19T16:49:41.000Z","updated_at":"2023-12-19T17:37:51.000Z","dependencies_parsed_at":"2022-09-26T17:41:31.072Z","dependency_job_id":null,"html_url":"https://github.com/nbari/violetear","commit_stats":null,"previous_names":[],"tags_count":57,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nbari%2Fvioletear","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nbari%2Fvioletear/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nbari%2Fvioletear/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nbari%2Fvioletear/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/nbari","download_url":"https://codeload.github.com/nbari/violetear/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":230542288,"owners_count":18242332,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["dynamic","http-server","http2","http2-push","httprouter","mux","rest-api","versioning","www"],"created_at":"2024-08-03T15:01:13.620Z","updated_at":"2024-12-20T06:06:44.673Z","avatar_url":"https://github.com/nbari.png","language":"Go","readme":"[![GoDoc](https://godoc.org/github.com/nbari/violetear?status.svg)](https://godoc.org/github.com/nbari/violetear)\n[![test](https://github.com/nbari/violetear/actions/workflows/test.yml/badge.svg)](https://github.com/nbari/violetear/actions/workflows/test.yml)\n[![Coverage Status](https://coveralls.io/repos/nbari/violetear/badge.svg?branch=develop\u0026service=github)](https://coveralls.io/github/nbari/violetear?branch=develop)\n[![Go Report Card](https://goreportcard.com/badge/github.com/nbari/violetear)](https://goreportcard.com/report/github.com/nbari/violetear)\n\n# violetear\nGo HTTP router\n\nhttp://violetear.org\n\n### Design Goals\n* Keep it simple and small, avoiding extra complexity at all cost. [KISS](https://en.wikipedia.org/wiki/KISS_principle)\n* Support for static and dynamic routing.\n* Easy middleware compatibility so that it satisfies the http.Handler interface.\n* Common context between middleware.\n* Trace Request-ID per request.\n* HTTP/2 native support [Push Example](https://gist.github.com/nbari/e19f195c233c92061e27f5beaaae45a3)\n* Versioning based on Accept header `application/vnd.*`\n\nPackage [GoDoc](https://godoc.org/github.com/nbari/violetear)\n\n\nHow it works\n------------\n\nThe router is capable off handle any kind or URI, static,\ndynamic or catchall and based on the\n[HTTP request Method](http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html)\naccept or discard the request.\n\nFor example, suppose we have an API that exposes a service that allow to ping\nany IP address.\n\nTo handle only \"GET\" request for any IPv4 addresss:\n\n    http://api.violetear.org/command/ping/127.0.0.1\n                            \\______/\\___/\\________/\n                                |     |      |\n                                 static      |\n                                          dynamic\n\nThe router ``HandlerFunc``  would be:\n\n    router.HandleFunc(\"/command/ping/:ip\", ip_handler, \"GET\")\n\nFor this to work, first the regex matching ``:ip`` should be added:\n\n    router.AddRegex(\":ip\", `^(?:[0-9]{1,3}\\.){3}[0-9]{1,3}$`)\n\nNow let's say you also want to be available to ping ipv6 or any host:\n\n    http://api.violetear.org/command/ping/*\n                            \\______/\\___/\\_/\n                                |     |   |\n                                 static   |\n                                       catch-all\n\nA catch-all could be used and also a different handler, for example:\n\n    router.HandleFunc(\"/command/ping/*\", any_handler, \"GET, HEAD\")\n\nThe ``*`` indicates the router to behave like a catch-all therefore it\nwill match anything after the ``/command/ping/`` if no other condition matches\nbefore.\n\nNotice also the \"GET, HEAD\", that indicates that only does HTTP methods will be\naccepted, and any other will not be allowed, router will return a 405 the one\ncan also be customised.\n\n\nUsage\n-----\n\nRequirementes go \u003e= 1.7 (https://golang.org/pkg/context/ required)\n\n    import \"github.com/nbari/violetear\"\n\n\n**HandleFunc**:\n\n     func HandleFunc(path string, handler http.HandlerFunc, http_methods ...string)\n\n**Handle** (useful for middleware):\n\n     func Handle(path string, handler http.Handler, http_methods ...string)\n\n**http_methods** is a comma separted list of allowed HTTP methods, example:\n\n    router.HandleFunc(\"/view\", handleView, \"GET, HEAD\")\n\n**AddRegex** adds a \":named\" regular expression to the dynamicRoutes, example:\n\n    router.AddRegex(\":ip\", `^(?:[0-9]{1,3}\\.){3}[0-9]{1,3}$`)\n\n\nBasic example:\n\n```go\npackage main\n\nimport (\n    \"github.com/nbari/violetear\"\n    \"log\"\n    \"net/http\"\n)\n\nfunc catchAll(w http.ResponseWriter, r *http.Request) {\n    w.Write([]byte(\"I'm catching all\\n\"))\n}\n\nfunc handleGET(w http.ResponseWriter, r *http.Request) {\n    w.Write([]byte(\"I handle GET requests\\n\"))\n}\n\nfunc handlePOST(w http.ResponseWriter, r *http.Request) {\n    w.Write([]byte(\"I handle POST requests\\n\"))\n}\n\nfunc handleUUID(w http.ResponseWriter, r *http.Request) {\n    w.Write([]byte(\"I handle dynamic requests\\n\"))\n}\n\nfunc main() {\n    router := violetear.New()\n    router.LogRequests = true\n    router.RequestID = \"Request-ID\"\n\n    router.AddRegex(\":uuid\", `[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}`)\n\n    router.HandleFunc(\"*\", catchAll)\n    router.HandleFunc(\"/method\", handleGET, \"GET\")\n    router.HandleFunc(\"/method\", handlePOST, \"POST\")\n    router.HandleFunc(\"/:uuid\", handleUUID, \"GET,HEAD\")\n\n    srv := \u0026http.Server{\n        Addr:           \":8080\",\n        Handler:        router,\n        ReadTimeout:    5 * time.Second,\n        WriteTimeout:   7 * time.Second,\n        MaxHeaderBytes: 1 \u003c\u003c 20,\n    }\n    log.Fatal(srv.ListenAndServe())\n\n}\n```\n\nRunning this code will show something like this:\n\n```sh\n$ go run test.go\n2015/10/22 17:14:18 Adding path: * [ALL]\n2015/10/22 17:14:18 Adding path: /method [GET]\n2015/10/22 17:14:18 Adding path: /method [POST]\n2015/10/22 17:14:18 Adding path: /:uuid [GET,HEAD]\n```\n\nUsing ``router.Verbose = false`` will omit printing the paths.\n\n\u003e test.go contains the code show above\n\nTesting using curl or [http](https://github.com/jkbrzt/httpie)\n\nAny request 'catch-all':\n\n```sh\n$ http POST http://localhost:8080/\nHTTP/1.1 200 OK\nContent-Length: 17\nContent-Type: text/plain; charset=utf-8\nDate: Thu, 22 Oct 2015 15:18:49 GMT\nRequest-Id: POST-1445527129854964669-1\n\nI'm catching all\n```\n\nA GET request:\n\n```sh\n$ http http://localhost:8080/method\nHTTP/1.1 200 OK\nContent-Length: 22\nContent-Type: text/plain; charset=utf-8\nDate: Thu, 22 Oct 2015 15:43:25 GMT\nRequest-Id: GET-1445528605902591921-1\n\nI handle GET requests\n```\n\nA POST request:\n\n```sh\n$ http POST http://localhost:8080/method\nHTTP/1.1 200 OK\nContent-Length: 23\nContent-Type: text/plain; charset=utf-8\nDate: Thu, 22 Oct 2015 15:44:28 GMT\nRequest-Id: POST-1445528668557478433-2\n\nI handle POST requests\n```\n\nA dynamic request using an [UUID](https://en.wikipedia.org/wiki/Universally_unique_identifier) as the URL resource:\n\n```sh\n$ http http://localhost:8080/50244127-45F6-4210-A89D-FFB0DA039425\nHTTP/1.1 200 OK\nContent-Length: 26\nContent-Type: text/plain; charset=utf-8\nDate: Thu, 22 Oct 2015 15:45:33 GMT\nRequest-Id: GET-1445528733916239110-5\n\nI handle dynamic requests\n```\n\nTrying to use POST on the ``/:uuid`` resource will cause a\n*Method not Allowed 405* this because only ``GET`` and ``HEAD``\nmethods are allowed:\n\n```sh\n$ http POST http://localhost:8080/50244127-45F6-4210-A89D-FFB0DA039425\nHTTP/1.1 405 Method Not Allowed\nContent-Length: 19\nContent-Type: text/plain; charset=utf-8\nDate: Thu, 22 Oct 2015 15:47:19 GMT\nRequest-Id: POST-1445528839403536403-6\nX-Content-Type-Options: nosniff\n\nMethod Not Allowed\n```\n\nRequestID\n---------\n\nTo keep track of the \"requests\" an existing \"request ID\" header can be used, if\nthe header name for example is **Request-ID** therefore to continue using it,\nthe router needs to know the name, example:\n\n    router := violetear.New()\n    router.RequestID = \"X-Appengine-Request-Log-Id\"\n\nIf the proxy is using another name, for example \"RID\" then use something like:\n\n    router := violetear.New()\n    router.RequestID = \"RID\"\n\nIf ``router.RequestID`` is not set, no \"request ID\" is going to be added to the\nheaders. This can be extended using a middleware same has the logger check the\nAppEngine example.\n\n\nNotFoundHandler\n---------------\n\nFor defining a custom ``http.Handler`` to handle **404 Not Found** example:\n\n    ...\n\n    func my404() http.Handler {\n        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n            http.Error(w, \"ne ne ne\", 404)\n        })\n    }\n\n    func main() {\n        router := violetear.New()\n        router.NotFoundHandler = my404()\n        ...\n\nNotAllowedHandler\n-----------------\n\nFor defining a custom ``http.Handler`` to handle **405 Method Not Allowed**.\n\nPanicHandler\n------------\n\nFor using a custom http.HandlerFunc to handle panics\n\nMiddleware\n----------\n\nVioletear uses [Alice](http://justinas.org/alice-painless-middleware-chaining-for-go/) to handle [middleware](middleware).\n\nExample:\n\n```go\npackage main\n\nimport (\n\t\"context\"\n\t\"log\"\n\t\"net/http\"\n\n\t\"github.com/nbari/violetear\"\n\t\"github.com/nbari/violetear/middleware\"\n)\n\nfunc commonHeaders(next http.Handler) http.Handler {\n\treturn http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tw.Header().Set(\"X-app-Version\", \"1.0\")\n\t\tnext.ServeHTTP(w, r)\n\t})\n}\n\nfunc middlewareOne(next http.Handler) http.Handler {\n\treturn http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tlog.Println(\"Executing middlewareOne\")\n\t\tctx := context.WithValue(r.Context(), \"m1\", \"m1\")\n\t\tctx = context.WithValue(ctx, \"key\", 1)\n\t\tnext.ServeHTTP(w, r.WithContext(ctx))\n\t\tlog.Println(\"Executing middlewareOne again\")\n\t})\n}\n\nfunc middlewareTwo(next http.Handler) http.Handler {\n\treturn http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tlog.Println(\"Executing middlewareTwo\")\n\t\tif r.URL.Path != \"/\" {\n\t\t\treturn\n\t\t}\n\t\tctx := context.WithValue(r.Context(), \"m2\", \"m2\")\n\t\tnext.ServeHTTP(w, r.WithContext(ctx))\n\t\tlog.Println(\"Executing middlewareTwo again\")\n\t})\n}\n\nfunc catchAll(w http.ResponseWriter, r *http.Request) {\n\tlog.Printf(\"Executing finalHandler\\nm1:%s\\nkey:%d\\nm2:%s\\n\",\n\t\tr.Context().Value(\"m1\"),\n\t\tr.Context().Value(\"key\"),\n\t\tr.Context().Value(\"m2\"),\n\t)\n\tw.Write([]byte(\"I catch all\"))\n}\n\nfunc foo(w http.ResponseWriter, r *http.Request) {\n\tpanic(\"this will never happen, because of the return\")\n}\n\nfunc main() {\n\trouter := violetear.New()\n\n\tstdChain := middleware.New(commonHeaders, middlewareOne, middlewareTwo)\n\n\trouter.Handle(\"/\", stdChain.ThenFunc(catchAll), \"GET,HEAD\")\n\trouter.Handle(\"/foo\", stdChain.ThenFunc(foo), \"GET,HEAD\")\n\trouter.HandleFunc(\"/bar\", foo)\n\n\tlog.Fatal(http.ListenAndServe(\":8080\", router))\n}\n```\n\n\u003e Notice the use or router.Handle and router.HandleFunc when using middleware\nyou normally would use route.Handle\n\nRequest output example:\n\n```sh\n$ http http://localhost:8080/\nHTTP/1.1 200 OK\nContent-Length: 11\nContent-Type: text/plain; charset=utf-8\nDate: Thu, 22 Oct 2015 16:08:18 GMT\nRequest-Id: GET-1445530098002701428-3\nX-App-Version: 1.0\n\nI catch all\n```\n\nOn the server you will see something like this:\n\n```sh\n$ go run test.go\n2016/08/17 18:08:42 Adding path: / [GET,HEAD]\n2016/08/17 18:08:42 Adding path: /foo [GET,HEAD]\n2016/08/17 18:08:42 Adding path: /bar [ALL]\n2016/08/17 18:08:47 Executing middlewareOne\n2016/08/17 18:08:47 Executing middlewareTwo\n2016/08/17 18:08:47 Executing finalHandler\nm1:m1\nkey:1\nm2:m2\n2016/08/17 18:08:47 Executing middlewareTwo again\n2016/08/17 18:08:47 Executing middlewareOne again\n```\n\nAppEngine\n---------\n\nThe app.yaml file:\n\n```yaml\napplication: 'app-name'\nversion: 1\nruntime: go\napi_version: go1\n\nhandlers:\n\n- url: /.*\n  script: _go_app\n```\n\nThe app.go file:\n\n```go\npackage app\n\nimport (\n    \"appengine\"\n    \"github.com/nbari/violetear\"\n    \"github.com/nbari/violetear/middleware\"\n    \"net/http\"\n)\n\nfunc init() {\n    router := violetear.New()\n    stdChain := middleware.New(requestID)\n    router.Handle(\"*\", stdChain.ThenFunc(index), \"GET, HEAD\")\n    http.Handle(\"/\", router)\n}\n\nfunc requestID(next http.Handler) http.Handler {\n    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n        c := appengine.NewContext(r)\n        w.Header().Set(\"Request-ID\", appengine.RequestID(c))\n        next.ServeHTTP(w, r)\n    })\n}\n\nfunc index(w http.ResponseWriter, r *http.Request) {\n    w.Write([]byte(\"Hello world!\"))\n}\n```\n\nDemo: http://api.violetear.org\n\nUsing ``curl`` or ``http``:\n\n```sh\n$ http http://api.violetear.org\nHTTP/1.1 200 OK\nCache-Control: private\nContent-Encoding: gzip\nContent-Length: 32\nContent-Type: text/html; charset=utf-8\nDate: Sun, 25 Oct 2015 06:14:55 GMT\nRequest-Id: 562c735f00ff0902f823e514a90001657e76696f6c65746561722d31313037000131000100\nServer: Google Frontend\n\nHello world!\n```\n\nContext \u0026 Named parameters\n==========================\n\nIn some cases there is a need to pass data across\nhandlers/middlewares, for doing this **Violetear** uses\n[net/context](https://godoc.org/golang.org/x/net/context).\n\nWhen using dynamic routes `:regex`, you can use `GetParam` or `GetParams`, see below.\n\nExample:\n\n```go\npackage main\n\nimport (\n    \"context\"\n    \"fmt\"\n    \"log\"\n    \"net/http\"\n\n    \"github.com/nbari/violetear\"\n)\n\nfunc catchAll(w http.ResponseWriter, r *http.Request) {\n    // Get \u0026 print the content of named-param *\n    params := r.Context().Value(violetear.ParamsKey).(violetear.Params)\n    fmt.Fprintf(w, \"CatchAll value:, %q\", params[\"*\"])\n}\n\nfunc handleUUID(w http.ResponseWriter, r *http.Request) {\n    // get router params\n    params := r.Context().Value(violetear.ParamsKey).(violetear.Params)\n    // using GetParam\n    uuid := violetear.GetParam(\"uuid\", r)\n    // add a key-value pair to the context\n    ctx := context.WithValue(r.Context(), \"key\", \"my-value\")\n    // print current value for :uuid\n    fmt.Fprintf(w, \"Named parameter: %q, uuid; %q,  key: %s\",\n        params[\":uuid\"],\n        uuid,\n        ctx.Value(\"key\"),\n    )\n}\n\nfunc main() {\n    router := violetear.New()\n\n    router.AddRegex(\":uuid\", `[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}`)\n\n    router.HandleFunc(\"*\", catchAll)\n    router.HandleFunc(\"/:uuid\", handleUUID, \"GET,HEAD\")\n\n    srv := \u0026http.Server{\n        Addr:           \":8080\",\n        Handler:        router,\n        ReadTimeout:    5 * time.Second,\n        WriteTimeout:   7 * time.Second,\n        MaxHeaderBytes: 1 \u003c\u003c 20,\n    }\n    log.Fatal(srv.ListenAndServe())\n}\n```\n\n## Duplicated named parameters\n\nIn cases where the same named parameter is used multiple times, example:\n\n    /test/:uuid/:uuid/\n\nAn slice is created, for getting the values you need to do something like:\n\n    params := r.Context().Value(violetear.ParamsKey).(violetear.Params)\n    uuid := params[\":uuid\"].([]string)\n\n\u003e Notice the ``:`` prefix when getting the named_parameters\n\nOr by using `GetParams`:\n\n    uuid := violetear.GetParams(\"uuid\")\n\nAfter this you can access the slice like normal:\n\n    fmt.Println(uuid[0], uuid[1])\n","funding_links":["https://github.com/sponsors/nbari"],"categories":["Web Frameworks","Web 框架","路由","Web框架","Routers"],"sub_categories":["Routers","高級控制台界面","创建http中间件的代码库","\u003cspan id=\"高级控制台用户界面-advanced-console-uis\"\u003e高级控制台用户界面 Advanced Console UIs\u003c/span\u003e","路由器","Advanced Console UIs","交流","高级控制台界面"],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnbari%2Fvioletear","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fnbari%2Fvioletear","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnbari%2Fvioletear/lists"}