Showing preview only (366K chars total). Download the full file or copy to clipboard to get everything.
Repository: go-chi/chi
Branch: master
Commit: a54874f0e2f1
Files: 94
Total size: 343.2 KB
Directory structure:
gitextract_uo24njyf/
├── .github/
│ ├── FUNDING.yml
│ └── workflows/
│ └── ci.yml
├── .gitignore
├── CHANGELOG.md
├── CONTRIBUTING.md
├── LICENSE
├── Makefile
├── README.md
├── SECURITY.md
├── _examples/
│ ├── README.md
│ ├── custom-handler/
│ │ └── main.go
│ ├── custom-method/
│ │ └── main.go
│ ├── fileserver/
│ │ ├── data/
│ │ │ └── notes.txt
│ │ └── main.go
│ ├── graceful/
│ │ └── main.go
│ ├── hello-world/
│ │ └── main.go
│ ├── limits/
│ │ └── main.go
│ ├── logging/
│ │ └── main.go
│ ├── pathvalue/
│ │ └── main.go
│ ├── rest/
│ │ ├── go.mod
│ │ ├── go.sum
│ │ ├── main.go
│ │ ├── routes.json
│ │ └── routes.md
│ ├── router-walk/
│ │ └── main.go
│ ├── todos-resource/
│ │ ├── main.go
│ │ ├── todos.go
│ │ └── users.go
│ └── versions/
│ ├── data/
│ │ ├── article.go
│ │ └── errors.go
│ ├── go.mod
│ ├── go.sum
│ ├── main.go
│ └── presenter/
│ ├── v1/
│ │ └── article.go
│ ├── v2/
│ │ └── article.go
│ └── v3/
│ └── article.go
├── chain.go
├── chi.go
├── context.go
├── context_test.go
├── go.mod
├── middleware/
│ ├── basic_auth.go
│ ├── clean_path.go
│ ├── compress.go
│ ├── compress_test.go
│ ├── content_charset.go
│ ├── content_charset_test.go
│ ├── content_encoding.go
│ ├── content_encoding_test.go
│ ├── content_type.go
│ ├── content_type_test.go
│ ├── get_head.go
│ ├── get_head_test.go
│ ├── heartbeat.go
│ ├── logger.go
│ ├── logger_test.go
│ ├── maybe.go
│ ├── middleware.go
│ ├── middleware_test.go
│ ├── nocache.go
│ ├── page_route.go
│ ├── path_rewrite.go
│ ├── profiler.go
│ ├── realip.go
│ ├── realip_test.go
│ ├── recoverer.go
│ ├── recoverer_test.go
│ ├── request_id.go
│ ├── request_id_test.go
│ ├── request_size.go
│ ├── route_headers.go
│ ├── route_headers_test.go
│ ├── strip.go
│ ├── strip_test.go
│ ├── sunset.go
│ ├── sunset_test.go
│ ├── supress_notfound.go
│ ├── terminal.go
│ ├── throttle.go
│ ├── throttle_test.go
│ ├── timeout.go
│ ├── url_format.go
│ ├── url_format_test.go
│ ├── value.go
│ ├── wrap_writer.go
│ └── wrap_writer_test.go
├── mux.go
├── mux_test.go
├── path_value_test.go
├── pattern_test.go
├── testdata/
│ ├── cert.pem
│ └── key.pem
├── tree.go
└── tree_test.go
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/FUNDING.yml
================================================
# These are supported funding model platforms
github: [pkieltyka] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
================================================
FILE: .github/workflows/ci.yml
================================================
on:
push:
branches: "**"
paths-ignore:
- "docs/**"
pull_request:
branches: "**"
paths-ignore:
- "docs/**"
name: Test
jobs:
test:
env:
GOPATH: ${{ github.workspace }}
defaults:
run:
working-directory: ${{ env.GOPATH }}/src/github.com/${{ github.repository }}
strategy:
matrix:
go-version: [1.23.x, 1.24.x, 1.25.x, 1.26.x]
os: [ubuntu-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- name: Install Go
uses: actions/setup-go@v5
with:
go-version: ${{ matrix.go-version }}
check-latest: true
cache: false
- name: Checkout code
uses: actions/checkout@v4
with:
path: ${{ env.GOPATH }}/src/github.com/${{ github.repository }}
- name: Test
run: |
go get -d -t ./...
make test
================================================
FILE: .gitignore
================================================
.idea
*.sw?
.vscode
================================================
FILE: CHANGELOG.md
================================================
# Changelog
## v5.0.12 (2024-02-16)
- History of changes: see https://github.com/go-chi/chi/compare/v5.0.11...v5.0.12
## v5.0.11 (2023-12-19)
- History of changes: see https://github.com/go-chi/chi/compare/v5.0.10...v5.0.11
## v5.0.10 (2023-07-13)
- Fixed small edge case in tests of v5.0.9 for older Go versions
- History of changes: see https://github.com/go-chi/chi/compare/v5.0.9...v5.0.10
## v5.0.9 (2023-07-13)
- History of changes: see https://github.com/go-chi/chi/compare/v5.0.8...v5.0.9
## v5.0.8 (2022-12-07)
- History of changes: see https://github.com/go-chi/chi/compare/v5.0.7...v5.0.8
## v5.0.7 (2021-11-18)
- History of changes: see https://github.com/go-chi/chi/compare/v5.0.6...v5.0.7
## v5.0.6 (2021-11-15)
- History of changes: see https://github.com/go-chi/chi/compare/v5.0.5...v5.0.6
## v5.0.5 (2021-10-27)
- History of changes: see https://github.com/go-chi/chi/compare/v5.0.4...v5.0.5
## v5.0.4 (2021-08-29)
- History of changes: see https://github.com/go-chi/chi/compare/v5.0.3...v5.0.4
## v5.0.3 (2021-04-29)
- History of changes: see https://github.com/go-chi/chi/compare/v5.0.2...v5.0.3
## v5.0.2 (2021-03-25)
- History of changes: see https://github.com/go-chi/chi/compare/v5.0.1...v5.0.2
## v5.0.1 (2021-03-10)
- Small improvements
- History of changes: see https://github.com/go-chi/chi/compare/v5.0.0...v5.0.1
## v5.0.0 (2021-02-27)
- chi v5, `github.com/go-chi/chi/v5` introduces the adoption of Go's SIV to adhere to the current state-of-the-tools in Go.
- chi v1.5.x did not work out as planned, as the Go tooling is too powerful and chi's adoption is too wide.
The most responsible thing to do for everyone's benefit is to just release v5 with SIV, so I present to you all,
chi v5 at `github.com/go-chi/chi/v5`. I hope someday the developer experience and ergonomics I've been seeking
will still come to fruition in some form, see https://github.com/golang/go/issues/44550
- History of changes: see https://github.com/go-chi/chi/compare/v1.5.4...v5.0.0
## v1.5.4 (2021-02-27)
- Undo prior retraction in v1.5.3 as we prepare for v5.0.0 release
- History of changes: see https://github.com/go-chi/chi/compare/v1.5.3...v1.5.4
## v1.5.3 (2021-02-21)
- Update go.mod to go 1.16 with new retract directive marking all versions without prior go.mod support
- History of changes: see https://github.com/go-chi/chi/compare/v1.5.2...v1.5.3
## v1.5.2 (2021-02-10)
- Reverting allocation optimization as a precaution as go test -race fails.
- Minor improvements, see history below
- History of changes: see https://github.com/go-chi/chi/compare/v1.5.1...v1.5.2
## v1.5.1 (2020-12-06)
- Performance improvement: removing 1 allocation by foregoing context.WithValue, thank you @bouk for
your contribution (https://github.com/go-chi/chi/pull/555). Note: new benchmarks posted in README.
- `middleware.CleanPath`: new middleware that clean's request path of double slashes
- deprecate & remove `chi.ServerBaseContext` in favour of stdlib `http.Server#BaseContext`
- plus other tiny improvements, see full commit history below
- History of changes: see https://github.com/go-chi/chi/compare/v4.1.2...v1.5.1
## v1.5.0 (2020-11-12) - now with go.mod support
`chi` dates back to 2016 with it's original implementation as one of the first routers to adopt the newly introduced
context.Context api to the stdlib -- set out to design a router that is faster, more modular and simpler than anything
else out there -- while not introducing any custom handler types or dependencies. Today, `chi` still has zero dependencies,
and in many ways is future proofed from changes, given it's minimal nature. Between versions, chi's iterations have been very
incremental, with the architecture and api being the same today as it was originally designed in 2016. For this reason it
makes chi a pretty easy project to maintain, as well thanks to the many amazing community contributions over the years
to who all help make chi better (total of 86 contributors to date -- thanks all!).
Chi has been a labour of love, art and engineering, with the goals to offer beautiful ergonomics, flexibility, performance
and simplicity when building HTTP services with Go. I've strived to keep the router very minimal in surface area / code size,
and always improving the code wherever possible -- and as of today the `chi` package is just 1082 lines of code (not counting
middlewares, which are all optional). As well, I don't have the exact metrics, but from my analysis and email exchanges from
companies and developers, chi is used by thousands of projects around the world -- thank you all as there is no better form of
joy for me than to have art I had started be helpful and enjoyed by others. And of course I use chi in all of my own projects too :)
For me, the aesthetics of chi's code and usage are very important. With the introduction of Go's module support
(which I'm a big fan of), chi's past versioning scheme choice to v2, v3 and v4 would mean I'd require the import path
of "github.com/go-chi/chi/v4", leading to the lengthy discussion at https://github.com/go-chi/chi/issues/462.
Haha, to some, you may be scratching your head why I've spent > 1 year stalling to adopt "/vXX" convention in the import
path -- which isn't horrible in general -- but for chi, I'm unable to accept it as I strive for perfection in it's API design,
aesthetics and simplicity. It just doesn't feel good to me given chi's simple nature -- I do not foresee a "v5" or "v6",
and upgrading between versions in the future will also be just incremental.
I do understand versioning is a part of the API design as well, which is why the solution for a while has been to "do nothing",
as Go supports both old and new import paths with/out go.mod. However, now that Go module support has had time to iron out kinks and
is adopted everywhere, it's time for chi to get with the times. Luckily, I've discovered a path forward that will make me happy,
while also not breaking anyone's app who adopted a prior versioning from tags in v2/v3/v4. I've made an experimental release of
v1.5.0 with go.mod silently, and tested it with new and old projects, to ensure the developer experience is preserved, and it's
largely unnoticed. Fortunately, Go's toolchain will check the tags of a repo and consider the "latest" tag the one with go.mod.
However, you can still request a specific older tag such as v4.1.2, and everything will "just work". But new users can just
`go get github.com/go-chi/chi` or `go get github.com/go-chi/chi@latest` and they will get the latest version which contains
go.mod support, which is v1.5.0+. `chi` will not change very much over the years, just like it hasn't changed much from 4 years ago.
Therefore, we will stay on v1.x from here on, starting from v1.5.0. Any breaking changes will bump a "minor" release and
backwards-compatible improvements/fixes will bump a "tiny" release.
For existing projects who want to upgrade to the latest go.mod version, run: `go get -u github.com/go-chi/chi@v1.5.0`,
which will get you on the go.mod version line (as Go's mod cache may still remember v4.x). Brand new systems can run
`go get -u github.com/go-chi/chi` or `go get -u github.com/go-chi/chi@latest` to install chi, which will install v1.5.0+
built with go.mod support.
My apologies to the developers who will disagree with the decisions above, but, hope you'll try it and see it's a very
minor request which is backwards compatible and won't break your existing installations.
Cheers all, happy coding!
---
## v4.1.2 (2020-06-02)
- fix that handles MethodNotAllowed with path variables, thank you @caseyhadden for your contribution
- fix to replace nested wildcards correctly in RoutePattern, thank you @@unmultimedio for your contribution
- History of changes: see https://github.com/go-chi/chi/compare/v4.1.1...v4.1.2
## v4.1.1 (2020-04-16)
- fix for issue https://github.com/go-chi/chi/issues/411 which allows for overlapping regexp
route to the correct handler through a recursive tree search, thanks to @Jahaja for the PR/fix!
- new middleware.RouteHeaders as a simple router for request headers with wildcard support
- History of changes: see https://github.com/go-chi/chi/compare/v4.1.0...v4.1.1
## v4.1.0 (2020-04-1)
- middleware.LogEntry: Write method on interface now passes the response header
and an extra interface type useful for custom logger implementations.
- middleware.WrapResponseWriter: minor fix
- middleware.Recoverer: a bit prettier
- History of changes: see https://github.com/go-chi/chi/compare/v4.0.4...v4.1.0
## v4.0.4 (2020-03-24)
- middleware.Recoverer: new pretty stack trace printing (https://github.com/go-chi/chi/pull/496)
- a few minor improvements and fixes
- History of changes: see https://github.com/go-chi/chi/compare/v4.0.3...v4.0.4
## v4.0.3 (2020-01-09)
- core: fix regexp routing to include default value when param is not matched
- middleware: rewrite of middleware.Compress
- middleware: suppress http.ErrAbortHandler in middleware.Recoverer
- History of changes: see https://github.com/go-chi/chi/compare/v4.0.2...v4.0.3
## v4.0.2 (2019-02-26)
- Minor fixes
- History of changes: see https://github.com/go-chi/chi/compare/v4.0.1...v4.0.2
## v4.0.1 (2019-01-21)
- Fixes issue with compress middleware: #382 #385
- History of changes: see https://github.com/go-chi/chi/compare/v4.0.0...v4.0.1
## v4.0.0 (2019-01-10)
- chi v4 requires Go 1.10.3+ (or Go 1.9.7+) - we have deprecated support for Go 1.7 and 1.8
- router: respond with 404 on router with no routes (#362)
- router: additional check to ensure wildcard is at the end of a url pattern (#333)
- middleware: deprecate use of http.CloseNotifier (#347)
- middleware: fix RedirectSlashes to include query params on redirect (#334)
- History of changes: see https://github.com/go-chi/chi/compare/v3.3.4...v4.0.0
## v3.3.4 (2019-01-07)
- Minor middleware improvements. No changes to core library/router. Moving v3 into its
- own branch as a version of chi for Go 1.7, 1.8, 1.9, 1.10, 1.11
- History of changes: see https://github.com/go-chi/chi/compare/v3.3.3...v3.3.4
## v3.3.3 (2018-08-27)
- Minor release
- See https://github.com/go-chi/chi/compare/v3.3.2...v3.3.3
## v3.3.2 (2017-12-22)
- Support to route trailing slashes on mounted sub-routers (#281)
- middleware: new `ContentCharset` to check matching charsets. Thank you
@csucu for your community contribution!
## v3.3.1 (2017-11-20)
- middleware: new `AllowContentType` handler for explicit whitelist of accepted request Content-Types
- middleware: new `SetHeader` handler for short-hand middleware to set a response header key/value
- Minor bug fixes
## v3.3.0 (2017-10-10)
- New chi.RegisterMethod(method) to add support for custom HTTP methods, see _examples/custom-method for usage
- Deprecated LINK and UNLINK methods from the default list, please use `chi.RegisterMethod("LINK")` and `chi.RegisterMethod("UNLINK")` in an `init()` function
## v3.2.1 (2017-08-31)
- Add new `Match(rctx *Context, method, path string) bool` method to `Routes` interface
and `Mux`. Match searches the mux's routing tree for a handler that matches the method/path
- Add new `RouteMethod` to `*Context`
- Add new `Routes` pointer to `*Context`
- Add new `middleware.GetHead` to route missing HEAD requests to GET handler
- Updated benchmarks (see README)
## v3.1.5 (2017-08-02)
- Setup golint and go vet for the project
- As per golint, we've redefined `func ServerBaseContext(h http.Handler, baseCtx context.Context) http.Handler`
to `func ServerBaseContext(baseCtx context.Context, h http.Handler) http.Handler`
## v3.1.0 (2017-07-10)
- Fix a few minor issues after v3 release
- Move `docgen` sub-pkg to https://github.com/go-chi/docgen
- Move `render` sub-pkg to https://github.com/go-chi/render
- Add new `URLFormat` handler to chi/middleware sub-pkg to make working with url mime
suffixes easier, ie. parsing `/articles/1.json` and `/articles/1.xml`. See comments in
https://github.com/go-chi/chi/blob/master/middleware/url_format.go for example usage.
## v3.0.0 (2017-06-21)
- Major update to chi library with many exciting updates, but also some *breaking changes*
- URL parameter syntax changed from `/:id` to `/{id}` for even more flexible routing, such as
`/articles/{month}-{day}-{year}-{slug}`, `/articles/{id}`, and `/articles/{id}.{ext}` on the
same router
- Support for regexp for routing patterns, in the form of `/{paramKey:regExp}` for example:
`r.Get("/articles/{name:[a-z]+}", h)` and `chi.URLParam(r, "name")`
- Add `Method` and `MethodFunc` to `chi.Router` to allow routing definitions such as
`r.Method("GET", "/", h)` which provides a cleaner interface for custom handlers like
in `_examples/custom-handler`
- Deprecating `mux#FileServer` helper function. Instead, we encourage users to create their
own using file handler with the stdlib, see `_examples/fileserver` for an example
- Add support for LINK/UNLINK http methods via `r.Method()` and `r.MethodFunc()`
- Moved the chi project to its own organization, to allow chi-related community packages to
be easily discovered and supported, at: https://github.com/go-chi
- *NOTE:* please update your import paths to `"github.com/go-chi/chi"`
- *NOTE:* chi v2 is still available at https://github.com/go-chi/chi/tree/v2
## v2.1.0 (2017-03-30)
- Minor improvements and update to the chi core library
- Introduced a brand new `chi/render` sub-package to complete the story of building
APIs to offer a pattern for managing well-defined request / response payloads. Please
check out the updated `_examples/rest` example for how it works.
- Added `MethodNotAllowed(h http.HandlerFunc)` to chi.Router interface
## v2.0.0 (2017-01-06)
- After many months of v2 being in an RC state with many companies and users running it in
production, the inclusion of some improvements to the middlewares, we are very pleased to
announce v2.0.0 of chi.
## v2.0.0-rc1 (2016-07-26)
- Huge update! chi v2 is a large refactor targeting Go 1.7+. As of Go 1.7, the popular
community `"net/context"` package has been included in the standard library as `"context"` and
utilized by `"net/http"` and `http.Request` to managing deadlines, cancelation signals and other
request-scoped values. We're very excited about the new context addition and are proud to
introduce chi v2, a minimal and powerful routing package for building large HTTP services,
with zero external dependencies. Chi focuses on idiomatic design and encourages the use of
stdlib HTTP handlers and middlewares.
- chi v2 deprecates its `chi.Handler` interface and requires `http.Handler` or `http.HandlerFunc`
- chi v2 stores URL routing parameters and patterns in the standard request context: `r.Context()`
- chi v2 lower-level routing context is accessible by `chi.RouteContext(r.Context()) *chi.Context`,
which provides direct access to URL routing parameters, the routing path and the matching
routing patterns.
- Users upgrading from chi v1 to v2, need to:
1. Update the old chi.Handler signature, `func(ctx context.Context, w http.ResponseWriter, r *http.Request)` to
the standard http.Handler: `func(w http.ResponseWriter, r *http.Request)`
2. Use `chi.URLParam(r *http.Request, paramKey string) string`
or `URLParamFromCtx(ctx context.Context, paramKey string) string` to access a url parameter value
## v1.0.0 (2016-07-01)
- Released chi v1 stable https://github.com/go-chi/chi/tree/v1.0.0 for Go 1.6 and older.
## v0.9.0 (2016-03-31)
- Reuse context objects via sync.Pool for zero-allocation routing [#33](https://github.com/go-chi/chi/pull/33)
- BREAKING NOTE: due to subtle API changes, previously `chi.URLParams(ctx)["id"]` used to access url parameters
has changed to: `chi.URLParam(ctx, "id")`
================================================
FILE: CONTRIBUTING.md
================================================
# Contributing
## Prerequisites
1. [Install Go][go-install].
2. Download the sources and switch the working directory:
```bash
go get -u -d github.com/go-chi/chi
cd $GOPATH/src/github.com/go-chi/chi
```
## Submitting a Pull Request
A typical workflow is:
1. [Fork the repository.][fork]
2. [Create a topic branch.][branch]
3. Add tests for your change.
4. Run `go test`. If your tests pass, return to the step 3.
5. Implement the change and ensure the steps from the previous step pass.
6. Run `goimports -w .`, to ensure the new code conforms to Go formatting guideline.
7. [Add, commit and push your changes.][git-help]
8. [Submit a pull request.][pull-req]
[go-install]: https://golang.org/doc/install
[fork]: https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/fork-a-repo
[branch]: https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/about-branches
[git-help]: https://docs.github.com/en
[pull-req]: https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/about-pull-requests
================================================
FILE: LICENSE
================================================
Copyright (c) 2015-present Peter Kieltyka (https://github.com/pkieltyka), Google Inc.
MIT License
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
================================================
FILE: Makefile
================================================
.PHONY: all
all:
@echo "**********************************************************"
@echo "** chi build tool **"
@echo "**********************************************************"
.PHONY: test
test:
go clean -testcache && $(MAKE) test-router && $(MAKE) test-middleware
.PHONY: test-router
test-router:
go test -race -v .
.PHONY: test-middleware
test-middleware:
go test -race -v ./middleware
.PHONY: docs
docs:
npx docsify-cli serve ./docs
================================================
FILE: README.md
================================================
# <img alt="chi" src="https://cdn.rawgit.com/go-chi/chi/master/_examples/chi.svg" width="220" />
[![GoDoc Widget]][GoDoc]
`chi` is a lightweight, idiomatic and composable router for building Go HTTP services. It's
especially good at helping you write large REST API services that are kept maintainable as your
project grows and changes. `chi` is built on the new `context` package introduced in Go 1.7 to
handle signaling, cancelation and request-scoped values across a handler chain.
The focus of the project has been to seek out an elegant and comfortable design for writing
REST API servers, written during the development of the Pressly API service that powers our
public API service, which in turn powers all of our client-side applications.
The key considerations of chi's design are: project structure, maintainability, standard http
handlers (stdlib-only), developer productivity, and deconstructing a large system into many small
parts. The core router `github.com/go-chi/chi` is quite small (less than 1000 LOC), but we've also
included some useful/optional subpackages: [middleware](/middleware), [render](https://github.com/go-chi/render)
and [docgen](https://github.com/go-chi/docgen). We hope you enjoy it too!
## Install
```sh
go get -u github.com/go-chi/chi/v5
```
## Features
* **Lightweight** - cloc'd in ~1000 LOC for the chi router
* **Fast** - yes, see [benchmarks](#benchmarks)
* **100% compatible with net/http** - use any http or middleware pkg in the ecosystem that is also compatible with `net/http`
* **Designed for modular/composable APIs** - middlewares, inline middlewares, route groups and sub-router mounting
* **Context control** - built on new `context` package, providing value chaining, cancellations and timeouts
* **Robust** - in production at Pressly, Cloudflare, Heroku, 99Designs, and many others (see [discussion](https://github.com/go-chi/chi/issues/91))
* **Doc generation** - `docgen` auto-generates routing documentation from your source to JSON or Markdown
* **Go.mod support** - as of v5, go.mod support (see [CHANGELOG](https://github.com/go-chi/chi/blob/master/CHANGELOG.md))
* **No external dependencies** - plain ol' Go stdlib + net/http
## Examples
See [_examples/](https://github.com/go-chi/chi/blob/master/_examples/) for a variety of examples.
**As easy as:**
```go
package main
import (
"net/http"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
)
func main() {
r := chi.NewRouter()
r.Use(middleware.Logger)
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("welcome"))
})
http.ListenAndServe(":3000", r)
}
```
**REST Preview:**
Here is a little preview of what routing looks like with chi. Also take a look at the generated routing docs
in JSON ([routes.json](https://github.com/go-chi/chi/blob/master/_examples/rest/routes.json)) and in
Markdown ([routes.md](https://github.com/go-chi/chi/blob/master/_examples/rest/routes.md)).
I highly recommend reading the source of the [examples](https://github.com/go-chi/chi/blob/master/_examples/) listed
above, they will show you all the features of chi and serve as a good form of documentation.
```go
import (
//...
"context"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
)
func main() {
r := chi.NewRouter()
// A good base middleware stack
r.Use(middleware.RequestID)
r.Use(middleware.RealIP)
r.Use(middleware.Logger)
r.Use(middleware.Recoverer)
// Set a timeout value on the request context (ctx), that will signal
// through ctx.Done() that the request has timed out and further
// processing should be stopped.
r.Use(middleware.Timeout(60 * time.Second))
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("hi"))
})
// RESTy routes for "articles" resource
r.Route("/articles", func(r chi.Router) {
r.With(paginate).Get("/", listArticles) // GET /articles
r.With(paginate).Get("/{month}-{day}-{year}", listArticlesByDate) // GET /articles/01-16-2017
r.Post("/", createArticle) // POST /articles
r.Get("/search", searchArticles) // GET /articles/search
// Regexp url parameters:
r.Get("/{articleSlug:[a-z-]+}", getArticleBySlug) // GET /articles/home-is-toronto
// Subrouters:
r.Route("/{articleID}", func(r chi.Router) {
r.Use(ArticleCtx)
r.Get("/", getArticle) // GET /articles/123
r.Put("/", updateArticle) // PUT /articles/123
r.Delete("/", deleteArticle) // DELETE /articles/123
})
})
// Mount the admin sub-router
r.Mount("/admin", adminRouter())
http.ListenAndServe(":3333", r)
}
func ArticleCtx(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
articleID := chi.URLParam(r, "articleID")
article, err := dbGetArticle(articleID)
if err != nil {
http.Error(w, http.StatusText(404), 404)
return
}
ctx := context.WithValue(r.Context(), "article", article)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
func getArticle(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
article, ok := ctx.Value("article").(*Article)
if !ok {
http.Error(w, http.StatusText(422), 422)
return
}
w.Write([]byte(fmt.Sprintf("title:%s", article.Title)))
}
// A completely separate router for administrator routes
func adminRouter() http.Handler {
r := chi.NewRouter()
r.Use(AdminOnly)
r.Get("/", adminIndex)
r.Get("/accounts", adminListAccounts)
return r
}
func AdminOnly(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
perm, ok := ctx.Value("acl.permission").(YourPermissionType)
if !ok || !perm.IsAdmin() {
http.Error(w, http.StatusText(403), 403)
return
}
next.ServeHTTP(w, r)
})
}
```
## Router interface
chi's router is based on a kind of [Patricia Radix trie](https://en.wikipedia.org/wiki/Radix_tree).
The router is fully compatible with `net/http`.
Built on top of the tree is the `Router` interface:
```go
// Router consisting of the core routing methods used by chi's Mux,
// using only the standard net/http.
type Router interface {
http.Handler
Routes
// Use appends one or more middlewares onto the Router stack.
Use(middlewares ...func(http.Handler) http.Handler)
// With adds inline middlewares for an endpoint handler.
With(middlewares ...func(http.Handler) http.Handler) Router
// Group adds a new inline-Router along the current routing
// path, with a fresh middleware stack for the inline-Router.
Group(fn func(r Router)) Router
// Route mounts a sub-Router along a `pattern` string.
Route(pattern string, fn func(r Router)) Router
// Mount attaches another http.Handler along ./pattern/*
Mount(pattern string, h http.Handler)
// Handle and HandleFunc adds routes for `pattern` that matches
// all HTTP methods.
Handle(pattern string, h http.Handler)
HandleFunc(pattern string, h http.HandlerFunc)
// Method and MethodFunc adds routes for `pattern` that matches
// the `method` HTTP method.
Method(method, pattern string, h http.Handler)
MethodFunc(method, pattern string, h http.HandlerFunc)
// HTTP-method routing along `pattern`
Connect(pattern string, h http.HandlerFunc)
Delete(pattern string, h http.HandlerFunc)
Get(pattern string, h http.HandlerFunc)
Head(pattern string, h http.HandlerFunc)
Options(pattern string, h http.HandlerFunc)
Patch(pattern string, h http.HandlerFunc)
Post(pattern string, h http.HandlerFunc)
Put(pattern string, h http.HandlerFunc)
Trace(pattern string, h http.HandlerFunc)
// NotFound defines a handler to respond whenever a route could
// not be found.
NotFound(h http.HandlerFunc)
// MethodNotAllowed defines a handler to respond whenever a method is
// not allowed.
MethodNotAllowed(h http.HandlerFunc)
}
// Routes interface adds two methods for router traversal, which is also
// used by the github.com/go-chi/docgen package to generate documentation for Routers.
type Routes interface {
// Routes returns the routing tree in an easily traversable structure.
Routes() []Route
// Middlewares returns the list of middlewares in use by the router.
Middlewares() Middlewares
// Match searches the routing tree for a handler that matches
// the method/path - similar to routing a http request, but without
// executing the handler thereafter.
Match(rctx *Context, method, path string) bool
}
```
Each routing method accepts a URL `pattern` and chain of `handlers`. The URL pattern
supports named params (ie. `/users/{userID}`) and wildcards (ie. `/admin/*`). URL parameters
can be fetched at runtime by calling `chi.URLParam(r, "userID")` for named parameters
and `chi.URLParam(r, "*")` for a wildcard parameter.
### Middleware handlers
chi's middlewares are just stdlib net/http middleware handlers. There is nothing special
about them, which means the router and all the tooling is designed to be compatible and
friendly with any middleware in the community. This offers much better extensibility and reuse
of packages and is at the heart of chi's purpose.
Here is an example of a standard net/http middleware where we assign a context key `"user"`
the value of `"123"`. This middleware sets a hypothetical user identifier on the request
context and calls the next handler in the chain.
```go
// HTTP middleware setting a value on the request context
func MyMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// create new context from `r` request context, and assign key `"user"`
// to value of `"123"`
ctx := context.WithValue(r.Context(), "user", "123")
// call the next handler in the chain, passing the response writer and
// the updated request object with the new context value.
//
// note: context.Context values are nested, so any previously set
// values will be accessible as well, and the new `"user"` key
// will be accessible from this point forward.
next.ServeHTTP(w, r.WithContext(ctx))
})
}
```
### Request handlers
chi uses standard net/http request handlers. This little snippet is an example of a http.Handler
func that reads a user identifier from the request context - hypothetically, identifying
the user sending an authenticated request, validated+set by a previous middleware handler.
```go
// HTTP handler accessing data from the request context.
func MyRequestHandler(w http.ResponseWriter, r *http.Request) {
// here we read from the request context and fetch out `"user"` key set in
// the MyMiddleware example above.
user := r.Context().Value("user").(string)
// respond to the client
w.Write([]byte(fmt.Sprintf("hi %s", user)))
}
```
### URL parameters
chi's router parses and stores URL parameters right onto the request context. Here is
an example of how to access URL params in your net/http handlers. And of course, middlewares
are able to access the same information.
```go
// HTTP handler accessing the url routing parameters.
func MyRequestHandler(w http.ResponseWriter, r *http.Request) {
// fetch the url parameter `"userID"` from the request of a matching
// routing pattern. An example routing pattern could be: /users/{userID}
userID := chi.URLParam(r, "userID")
// fetch `"key"` from the request context
ctx := r.Context()
key := ctx.Value("key").(string)
// respond to the client
w.Write([]byte(fmt.Sprintf("hi %v, %v", userID, key)))
}
```
## Middlewares
chi comes equipped with an optional `middleware` package, providing a suite of standard
`net/http` middlewares. Please note, any middleware in the ecosystem that is also compatible
with `net/http` can be used with chi's mux.
### Core middlewares
----------------------------------------------------------------------------------------------------
| chi/middleware Handler | description |
| :--------------------- | :---------------------------------------------------------------------- |
| [AllowContentEncoding] | Enforces a whitelist of request Content-Encoding headers |
| [AllowContentType] | Explicit whitelist of accepted request Content-Types |
| [BasicAuth] | Basic HTTP authentication |
| [Compress] | Gzip compression for clients that accept compressed responses |
| [ContentCharset] | Ensure charset for Content-Type request headers |
| [CleanPath] | Clean double slashes from request path |
| [GetHead] | Automatically route undefined HEAD requests to GET handlers |
| [Heartbeat] | Monitoring endpoint to check the servers pulse |
| [Logger] | Logs the start and end of each request with the elapsed processing time |
| [NoCache] | Sets response headers to prevent clients from caching |
| [Profiler] | Easily attach net/http/pprof to your routers |
| [RealIP] | Sets a http.Request's RemoteAddr to either X-Real-IP or X-Forwarded-For |
| [Recoverer] | Gracefully absorb panics and prints the stack trace |
| [RequestID] | Injects a request ID into the context of each request |
| [RedirectSlashes] | Redirect slashes on routing paths |
| [RouteHeaders] | Route handling for request headers |
| [SetHeader] | Short-hand middleware to set a response header key/value |
| [StripSlashes] | Strip slashes on routing paths |
| [Sunset] | Sunset set Deprecation/Sunset header to response |
| [Throttle] | Puts a ceiling on the number of concurrent requests |
| [Timeout] | Signals to the request context when the timeout deadline is reached |
| [URLFormat] | Parse extension from url and put it on request context |
| [WithValue] | Short-hand middleware to set a key/value on the request context |
----------------------------------------------------------------------------------------------------
[AllowContentEncoding]: https://pkg.go.dev/github.com/go-chi/chi/middleware#AllowContentEncoding
[AllowContentType]: https://pkg.go.dev/github.com/go-chi/chi/middleware#AllowContentType
[BasicAuth]: https://pkg.go.dev/github.com/go-chi/chi/middleware#BasicAuth
[Compress]: https://pkg.go.dev/github.com/go-chi/chi/middleware#Compress
[ContentCharset]: https://pkg.go.dev/github.com/go-chi/chi/middleware#ContentCharset
[CleanPath]: https://pkg.go.dev/github.com/go-chi/chi/middleware#CleanPath
[GetHead]: https://pkg.go.dev/github.com/go-chi/chi/middleware#GetHead
[GetReqID]: https://pkg.go.dev/github.com/go-chi/chi/middleware#GetReqID
[Heartbeat]: https://pkg.go.dev/github.com/go-chi/chi/middleware#Heartbeat
[Logger]: https://pkg.go.dev/github.com/go-chi/chi/middleware#Logger
[NoCache]: https://pkg.go.dev/github.com/go-chi/chi/middleware#NoCache
[Profiler]: https://pkg.go.dev/github.com/go-chi/chi/middleware#Profiler
[RealIP]: https://pkg.go.dev/github.com/go-chi/chi/middleware#RealIP
[Recoverer]: https://pkg.go.dev/github.com/go-chi/chi/middleware#Recoverer
[RedirectSlashes]: https://pkg.go.dev/github.com/go-chi/chi/middleware#RedirectSlashes
[RequestLogger]: https://pkg.go.dev/github.com/go-chi/chi/middleware#RequestLogger
[RequestID]: https://pkg.go.dev/github.com/go-chi/chi/middleware#RequestID
[RouteHeaders]: https://pkg.go.dev/github.com/go-chi/chi/middleware#RouteHeaders
[SetHeader]: https://pkg.go.dev/github.com/go-chi/chi/middleware#SetHeader
[StripSlashes]: https://pkg.go.dev/github.com/go-chi/chi/middleware#StripSlashes
[Sunset]: https://pkg.go.dev/github.com/go-chi/chi/v5/middleware#Sunset
[Throttle]: https://pkg.go.dev/github.com/go-chi/chi/middleware#Throttle
[ThrottleBacklog]: https://pkg.go.dev/github.com/go-chi/chi/middleware#ThrottleBacklog
[ThrottleWithOpts]: https://pkg.go.dev/github.com/go-chi/chi/middleware#ThrottleWithOpts
[Timeout]: https://pkg.go.dev/github.com/go-chi/chi/middleware#Timeout
[URLFormat]: https://pkg.go.dev/github.com/go-chi/chi/middleware#URLFormat
[WithLogEntry]: https://pkg.go.dev/github.com/go-chi/chi/middleware#WithLogEntry
[WithValue]: https://pkg.go.dev/github.com/go-chi/chi/middleware#WithValue
[Compressor]: https://pkg.go.dev/github.com/go-chi/chi/middleware#Compressor
[DefaultLogFormatter]: https://pkg.go.dev/github.com/go-chi/chi/middleware#DefaultLogFormatter
[EncoderFunc]: https://pkg.go.dev/github.com/go-chi/chi/middleware#EncoderFunc
[HeaderRoute]: https://pkg.go.dev/github.com/go-chi/chi/middleware#HeaderRoute
[HeaderRouter]: https://pkg.go.dev/github.com/go-chi/chi/middleware#HeaderRouter
[LogEntry]: https://pkg.go.dev/github.com/go-chi/chi/middleware#LogEntry
[LogFormatter]: https://pkg.go.dev/github.com/go-chi/chi/middleware#LogFormatter
[LoggerInterface]: https://pkg.go.dev/github.com/go-chi/chi/middleware#LoggerInterface
[ThrottleOpts]: https://pkg.go.dev/github.com/go-chi/chi/middleware#ThrottleOpts
[WrapResponseWriter]: https://pkg.go.dev/github.com/go-chi/chi/middleware#WrapResponseWriter
### Extra middlewares & packages
Please see https://github.com/go-chi for additional packages.
--------------------------------------------------------------------------------------------------------------------
| package | description |
|:---------------------------------------------------|:-------------------------------------------------------------
| [cors](https://github.com/go-chi/cors) | Cross-origin resource sharing (CORS) |
| [docgen](https://github.com/go-chi/docgen) | Print chi.Router routes at runtime |
| [jwtauth](https://github.com/go-chi/jwtauth) | JWT authentication |
| [hostrouter](https://github.com/go-chi/hostrouter) | Domain/host based request routing |
| [httplog](https://github.com/go-chi/httplog) | Small but powerful structured HTTP request logging |
| [httprate](https://github.com/go-chi/httprate) | HTTP request rate limiter |
| [httptracer](https://github.com/go-chi/httptracer) | HTTP request performance tracing library |
| [httpvcr](https://github.com/go-chi/httpvcr) | Write deterministic tests for external sources |
| [stampede](https://github.com/go-chi/stampede) | HTTP request coalescer |
--------------------------------------------------------------------------------------------------------------------
## context?
`context` is a tiny pkg that provides simple interface to signal context across call stacks
and goroutines. It was originally written by [Sameer Ajmani](https://github.com/Sajmani)
and is available in stdlib since go1.7.
Learn more at https://blog.golang.org/context
and..
* Docs: https://golang.org/pkg/context
* Source: https://github.com/golang/go/tree/master/src/context
## Benchmarks
The benchmark suite: https://github.com/pkieltyka/go-http-routing-benchmark
Results as of Nov 29, 2020 with Go 1.15.5 on Linux AMD 3950x
```shell
BenchmarkChi_Param 3075895 384 ns/op 400 B/op 2 allocs/op
BenchmarkChi_Param5 2116603 566 ns/op 400 B/op 2 allocs/op
BenchmarkChi_Param20 964117 1227 ns/op 400 B/op 2 allocs/op
BenchmarkChi_ParamWrite 2863413 420 ns/op 400 B/op 2 allocs/op
BenchmarkChi_GithubStatic 3045488 395 ns/op 400 B/op 2 allocs/op
BenchmarkChi_GithubParam 2204115 540 ns/op 400 B/op 2 allocs/op
BenchmarkChi_GithubAll 10000 113811 ns/op 81203 B/op 406 allocs/op
BenchmarkChi_GPlusStatic 3337485 359 ns/op 400 B/op 2 allocs/op
BenchmarkChi_GPlusParam 2825853 423 ns/op 400 B/op 2 allocs/op
BenchmarkChi_GPlus2Params 2471697 483 ns/op 400 B/op 2 allocs/op
BenchmarkChi_GPlusAll 194220 5950 ns/op 5200 B/op 26 allocs/op
BenchmarkChi_ParseStatic 3365324 356 ns/op 400 B/op 2 allocs/op
BenchmarkChi_ParseParam 2976614 404 ns/op 400 B/op 2 allocs/op
BenchmarkChi_Parse2Params 2638084 439 ns/op 400 B/op 2 allocs/op
BenchmarkChi_ParseAll 109567 11295 ns/op 10400 B/op 52 allocs/op
BenchmarkChi_StaticAll 16846 71308 ns/op 62802 B/op 314 allocs/op
```
Comparison with other routers: https://gist.github.com/pkieltyka/123032f12052520aaccab752bd3e78cc
NOTE: the allocs in the benchmark above are from the calls to http.Request's
`WithContext(context.Context)` method that clones the http.Request, sets the `Context()`
on the duplicated (alloc'd) request and returns it the new request object. This is just
how setting context on a request in Go works.
## Credits
* Carl Jackson for https://github.com/zenazn/goji
* Parts of chi's thinking comes from goji, and chi's middleware package
sources from [goji](https://github.com/zenazn/goji/tree/master/web/middleware).
* Please see goji's [LICENSE](https://github.com/zenazn/goji/blob/master/LICENSE) (MIT)
* Armon Dadgar for https://github.com/armon/go-radix
* Contributions: [@VojtechVitek](https://github.com/VojtechVitek)
We'll be more than happy to see [your contributions](./CONTRIBUTING.md)!
## Beyond REST
chi is just a http router that lets you decompose request handling into many smaller layers.
Many companies use chi to write REST services for their public APIs. But, REST is just a convention
for managing state via HTTP, and there's a lot of other pieces required to write a complete client-server
system or network of microservices.
Looking beyond REST, I also recommend some newer works in the field:
* [webrpc](https://github.com/webrpc/webrpc) - Web-focused RPC client+server framework with code-gen
* [gRPC](https://github.com/grpc/grpc-go) - Google's RPC framework via protobufs
* [graphql](https://github.com/99designs/gqlgen) - Declarative query language
* [NATS](https://nats.io) - lightweight pub-sub
## License
Copyright (c) 2015-present [Peter Kieltyka](https://github.com/pkieltyka)
Licensed under [MIT License](./LICENSE)
[GoDoc]: https://pkg.go.dev/github.com/go-chi/chi/v5
[GoDoc Widget]: https://godoc.org/github.com/go-chi/chi?status.svg
[Travis]: https://travis-ci.org/go-chi/chi
[Travis Widget]: https://travis-ci.org/go-chi/chi.svg?branch=master
================================================
FILE: SECURITY.md
================================================
# Reporting Security Issues
We appreciate your efforts to responsibly disclose your findings, and will make every effort to acknowledge your contributions.
To report a security issue, please use the GitHub Security Advisory ["Report a Vulnerability"](https://github.com/go-chi/chi/security/advisories/new) tab.
================================================
FILE: _examples/README.md
================================================
chi examples
============
* [custom-handler](https://github.com/go-chi/chi/blob/master/_examples/custom-handler/main.go) - Use a custom handler function signature
* [custom-method](https://github.com/go-chi/chi/blob/master/_examples/custom-method/main.go) - Add a custom HTTP method
* [fileserver](https://github.com/go-chi/chi/blob/master/_examples/fileserver/main.go) - Easily serve static files
* [graceful](https://github.com/go-chi/chi/blob/master/_examples/graceful/main.go) - Graceful context signaling and server shutdown
* [hello-world](https://github.com/go-chi/chi/blob/master/_examples/hello-world/main.go) - Hello World!
* [limits](https://github.com/go-chi/chi/blob/master/_examples/limits/main.go) - Timeouts and Throttling
* [logging](https://github.com/go-chi/chi/blob/master/_examples/logging/main.go) - Easy structured logging for any backend
* [rest](https://github.com/go-chi/chi/blob/master/_examples/rest/main.go) - REST APIs made easy, productive and maintainable
* [router-walk](https://github.com/go-chi/chi/blob/master/_examples/router-walk/main.go) - Print to stdout a router's routes
* [todos-resource](https://github.com/go-chi/chi/blob/master/_examples/todos-resource/main.go) - Struct routers/handlers, an example of another code layout style
* [versions](https://github.com/go-chi/chi/blob/master/_examples/versions/main.go) - Demo of `chi/render` subpkg
* [pathvalue](https://github.com/go-chi/chi/blob/master/_examples/pathvalue/main.go) - Demonstrates `PathValue` usage for retrieving URL parameters
## Usage
1. `go get -v -d -u ./...` - fetch example deps
2. `cd <example>/` ie. `cd rest/`
3. `go run *.go` - note, example services run on port 3333
4. Open another terminal and use curl to send some requests to your example service,
`curl -v http://localhost:3333/`
5. Read <example>/main.go source to learn how service works and read comments for usage
================================================
FILE: _examples/custom-handler/main.go
================================================
package main
import (
"errors"
"net/http"
"github.com/go-chi/chi/v5"
)
type Handler func(w http.ResponseWriter, r *http.Request) error
func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if err := h(w, r); err != nil {
// handle returned error here.
w.WriteHeader(503)
w.Write([]byte("bad"))
}
}
func main() {
r := chi.NewRouter()
r.Method("GET", "/", Handler(customHandler))
http.ListenAndServe(":3333", r)
}
func customHandler(w http.ResponseWriter, r *http.Request) error {
q := r.URL.Query().Get("err")
if q != "" {
return errors.New(q)
}
w.Write([]byte("foo"))
return nil
}
================================================
FILE: _examples/custom-method/main.go
================================================
package main
import (
"net/http"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
)
func init() {
chi.RegisterMethod("LINK")
chi.RegisterMethod("UNLINK")
chi.RegisterMethod("WOOHOO")
}
func main() {
r := chi.NewRouter()
r.Use(middleware.RequestID)
r.Use(middleware.Logger)
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("hello world"))
})
r.MethodFunc("LINK", "/link", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("custom link method"))
})
r.MethodFunc("WOOHOO", "/woo", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("custom woohoo method"))
})
r.HandleFunc("/everything", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("capturing all standard http methods, as well as LINK, UNLINK and WOOHOO"))
})
http.ListenAndServe(":3333", r)
}
================================================
FILE: _examples/fileserver/data/notes.txt
================================================
Notessszzz
================================================
FILE: _examples/fileserver/main.go
================================================
// This example demonstrates how to serve static files from your filesystem.
//
// Boot the server:
//
// $ go run main.go
//
// Client requests:
//
// $ curl http://localhost:3333/files/
// <pre>
// <a href="notes.txt">notes.txt</a>
// </pre>
//
// $ curl http://localhost:3333/files/notes.txt
// Notessszzz
package main
import (
"net/http"
"os"
"path/filepath"
"strings"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
)
func main() {
r := chi.NewRouter()
r.Use(middleware.Logger)
// Index handler
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("hi"))
})
// Create a route along /files that will serve contents from
// the ./data/ folder.
workDir, _ := os.Getwd()
filesDir := http.Dir(filepath.Join(workDir, "data"))
FileServer(r, "/files", filesDir)
http.ListenAndServe(":3333", r)
}
// FileServer conveniently sets up a http.FileServer handler to serve
// static files from a http.FileSystem.
func FileServer(r chi.Router, path string, root http.FileSystem) {
if strings.ContainsAny(path, "{}*") {
panic("FileServer does not permit any URL parameters.")
}
if path != "/" && path[len(path)-1] != '/' {
r.Get(path, http.RedirectHandler(path+"/", 301).ServeHTTP)
path += "/"
}
path += "*"
r.Get(path, func(w http.ResponseWriter, r *http.Request) {
rctx := chi.RouteContext(r.Context())
pathPrefix := strings.TrimSuffix(rctx.RoutePattern(), "/*")
fs := http.StripPrefix(pathPrefix, http.FileServer(root))
fs.ServeHTTP(w, r)
})
}
================================================
FILE: _examples/graceful/main.go
================================================
package main
import (
"context"
"errors"
"fmt"
"log"
"net/http"
"os/signal"
"syscall"
"time"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
)
func main() {
// The HTTP Server
server := &http.Server{Addr: "0.0.0.0:3333", Handler: service()}
// Create context that listens for the interrupt signal
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()
// Run server in the background
go func() {
if err := server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Fatal(err)
}
}()
// Listen for the interrupt signal
<-ctx.Done()
// Create shutdown context with 30-second timeout
shutdownCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
// Trigger graceful shutdown
if err := server.Shutdown(shutdownCtx); err != nil {
log.Fatal(err)
}
}
func service() http.Handler {
r := chi.NewRouter()
r.Use(middleware.RequestID)
r.Use(middleware.Logger)
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("sup"))
})
r.Get("/slow", func(w http.ResponseWriter, r *http.Request) {
// Simulates some hard work.
//
// We want this handler to complete successfully during a shutdown signal,
// so consider the work here as some background routine to fetch a long-running
// search query to find as many results as possible, but, instead we cut it short
// and respond with what we have so far. How a shutdown is handled is entirely
// up to the developer, as some code blocks are preemptible, and others are not.
time.Sleep(5 * time.Second)
w.Write([]byte(fmt.Sprintf("all done.\n")))
})
return r
}
================================================
FILE: _examples/hello-world/main.go
================================================
package main
import (
"net/http"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
)
func main() {
r := chi.NewRouter()
r.Use(middleware.RequestID)
r.Use(middleware.Logger)
r.Use(middleware.Recoverer)
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("hello world"))
})
http.ListenAndServe(":3333", r)
}
================================================
FILE: _examples/limits/main.go
================================================
// This example demonstrates the use of Timeout, and Throttle middlewares.
//
// Timeout: cancel a request if processing takes longer than 2.5 seconds,
// server will respond with a http.StatusGatewayTimeout.
//
// Throttle: limit the number of in-flight requests along a particular
// routing path and backlog the others.
package main
import (
"context"
"fmt"
"math/rand"
"net/http"
"time"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
)
func main() {
r := chi.NewRouter()
r.Use(middleware.RequestID)
r.Use(middleware.Logger)
r.Use(middleware.Recoverer)
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("root."))
})
r.Get("/ping", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("pong"))
})
r.Get("/panic", func(w http.ResponseWriter, r *http.Request) {
panic("test")
})
// Slow handlers/operations.
r.Group(func(r chi.Router) {
// Stop processing after 2.5 seconds.
r.Use(middleware.Timeout(2500 * time.Millisecond))
r.Get("/slow", func(w http.ResponseWriter, r *http.Request) {
rand.Seed(time.Now().Unix())
// Processing will take 1-5 seconds.
processTime := time.Duration(rand.Intn(4)+1) * time.Second
select {
case <-r.Context().Done():
return
case <-time.After(processTime):
// The above channel simulates some hard work.
}
w.Write([]byte(fmt.Sprintf("Processed in %v seconds\n", processTime)))
})
})
// Throttle very expensive handlers/operations.
r.Group(func(r chi.Router) {
// Stop processing after 30 seconds.
r.Use(middleware.Timeout(30 * time.Second))
// Only one request will be processed at a time.
r.Use(middleware.Throttle(1))
r.Get("/throttled", func(w http.ResponseWriter, r *http.Request) {
select {
case <-r.Context().Done():
switch r.Context().Err() {
case context.DeadlineExceeded:
w.WriteHeader(504)
w.Write([]byte("Processing too slow\n"))
default:
w.Write([]byte("Canceled\n"))
}
return
case <-time.After(5 * time.Second):
// The above channel simulates some hard work.
}
w.Write([]byte("Processed\n"))
})
})
http.ListenAndServe(":3333", r)
}
================================================
FILE: _examples/logging/main.go
================================================
package main
// Please see https://github.com/go-chi/httplog for a complete package
// and example for writing a structured logger on chi built on
// the Go 1.21+ "log/slog" package.
func main() {
// See https://github.com/go-chi/httplog/blob/master/_example/main.go
}
================================================
FILE: _examples/pathvalue/main.go
================================================
package main
import (
"fmt"
"net/http"
"github.com/go-chi/chi/v5"
)
func main() {
r := chi.NewRouter()
// Registering a handler that retrieves a path parameter using PathValue
r.Get("/users/{userID}", pathValueHandler)
http.ListenAndServe(":3333", r)
}
// pathValueHandler retrieves a URL parameter using PathValue and writes it to the response.
func pathValueHandler(w http.ResponseWriter, r *http.Request) {
userID := r.PathValue("userID")
// Respond with the extracted userID
w.Write([]byte(fmt.Sprintf("User ID: %s", userID)))
}
================================================
FILE: _examples/rest/go.mod
================================================
module rest-example
go 1.16
require (
github.com/go-chi/chi/v5 v5.0.1
github.com/go-chi/docgen v1.2.0
github.com/go-chi/render v1.0.1
)
================================================
FILE: _examples/rest/go.sum
================================================
github.com/go-chi/chi/v5 v5.0.1 h1:ALxjCrTf1aflOlkhMnCUP86MubbWFrzB3gkRPReLpTo=
github.com/go-chi/chi/v5 v5.0.1/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-chi/docgen v1.2.0 h1:da0Nq2PKU9W9pSOTUfVrKI1vIgTGpauo9cfh4Iwivek=
github.com/go-chi/docgen v1.2.0/go.mod h1:G9W0G551cs2BFMSn/cnGwX+JBHEloAgo17MBhyrnhPI=
github.com/go-chi/render v1.0.1 h1:4/5tis2cKaNdnv9zFLfXzcquC9HbeZgCnxGnKrltBS8=
github.com/go-chi/render v1.0.1/go.mod h1:pq4Rr7HbnsdaeHagklXub+p6Wd16Af5l9koip1OvJns=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
================================================
FILE: _examples/rest/main.go
================================================
// This example demonstrates a HTTP REST web service with some fixture data.
// Follow along the example and patterns.
//
// Also check routes.json for the generated docs from passing the -routes flag,
// to run yourself do: `go run . -routes`
//
// Boot the server:
//
// $ go run main.go
//
// Client requests:
//
// $ curl http://localhost:3333/
// root.
//
// $ curl http://localhost:3333/articles
// [{"id":"1","title":"Hi"},{"id":"2","title":"sup"}]
//
// $ curl http://localhost:3333/articles/1
// {"id":"1","title":"Hi"}
//
// $ curl -X DELETE http://localhost:3333/articles/1
// {"id":"1","title":"Hi"}
//
// $ curl http://localhost:3333/articles/1
// "Not Found"
//
// $ curl -X POST -d '{"id":"will-be-omitted","title":"awesomeness"}' http://localhost:3333/articles
// {"id":"97","title":"awesomeness"}
//
// $ curl http://localhost:3333/articles/97
// {"id":"97","title":"awesomeness"}
//
// $ curl http://localhost:3333/articles
// [{"id":"2","title":"sup"},{"id":"97","title":"awesomeness"}]
package main
import (
"context"
"errors"
"flag"
"fmt"
"math/rand"
"net/http"
"strings"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"github.com/go-chi/docgen"
"github.com/go-chi/render"
)
var routes = flag.Bool("routes", false, "Generate router documentation")
func main() {
flag.Parse()
r := chi.NewRouter()
r.Use(middleware.RequestID)
r.Use(middleware.Logger)
r.Use(middleware.Recoverer)
r.Use(middleware.URLFormat)
r.Use(render.SetContentType(render.ContentTypeJSON))
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("root."))
})
r.Get("/ping", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("pong"))
})
r.Get("/panic", func(w http.ResponseWriter, r *http.Request) {
panic("test")
})
// RESTy routes for "articles" resource
r.Route("/articles", func(r chi.Router) {
r.With(paginate).Get("/", ListArticles)
r.Post("/", CreateArticle) // POST /articles
r.Get("/search", SearchArticles) // GET /articles/search
r.Route("/{articleID}", func(r chi.Router) {
r.Use(ArticleCtx) // Load the *Article on the request context
r.Get("/", GetArticle) // GET /articles/123
r.Put("/", UpdateArticle) // PUT /articles/123
r.Delete("/", DeleteArticle) // DELETE /articles/123
})
// GET /articles/whats-up
r.With(ArticleCtx).Get("/{articleSlug:[a-z-]+}", GetArticle)
})
// Mount the admin sub-router, which btw is the same as:
// r.Route("/admin", func(r chi.Router) { admin routes here })
r.Mount("/admin", adminRouter())
// Passing -routes to the program will generate docs for the above
// router definition. See the `routes.json` file in this folder for
// the output.
if *routes {
// fmt.Println(docgen.JSONRoutesDoc(r))
fmt.Println(docgen.MarkdownRoutesDoc(r, docgen.MarkdownOpts{
ProjectPath: "github.com/go-chi/chi/v5",
Intro: "Welcome to the chi/_examples/rest generated docs.",
}))
return
}
http.ListenAndServe(":3333", r)
}
func ListArticles(w http.ResponseWriter, r *http.Request) {
if err := render.RenderList(w, r, NewArticleListResponse(articles)); err != nil {
render.Render(w, r, ErrRender(err))
return
}
}
// ArticleCtx middleware is used to load an Article object from
// the URL parameters passed through as the request. In case
// the Article could not be found, we stop here and return a 404.
func ArticleCtx(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var article *Article
var err error
if articleID := chi.URLParam(r, "articleID"); articleID != "" {
article, err = dbGetArticle(articleID)
} else if articleSlug := chi.URLParam(r, "articleSlug"); articleSlug != "" {
article, err = dbGetArticleBySlug(articleSlug)
} else {
render.Render(w, r, ErrNotFound)
return
}
if err != nil {
render.Render(w, r, ErrNotFound)
return
}
ctx := context.WithValue(r.Context(), "article", article)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
// SearchArticles searches the Articles data for a matching article.
// It's just a stub, but you get the idea.
func SearchArticles(w http.ResponseWriter, r *http.Request) {
render.RenderList(w, r, NewArticleListResponse(articles))
}
// CreateArticle persists the posted Article and returns it
// back to the client as an acknowledgement.
func CreateArticle(w http.ResponseWriter, r *http.Request) {
data := &ArticleRequest{}
if err := render.Bind(r, data); err != nil {
render.Render(w, r, ErrInvalidRequest(err))
return
}
article := data.Article
dbNewArticle(article)
render.Status(r, http.StatusCreated)
render.Render(w, r, NewArticleResponse(article))
}
// GetArticle returns the specific Article. You'll notice it just
// fetches the Article right off the context, as its understood that
// if we made it this far, the Article must be on the context. In case
// its not due to a bug, then it will panic, and our Recoverer will save us.
func GetArticle(w http.ResponseWriter, r *http.Request) {
// Assume if we've reach this far, we can access the article
// context because this handler is a child of the ArticleCtx
// middleware. The worst case, the recoverer middleware will save us.
article := r.Context().Value("article").(*Article)
if err := render.Render(w, r, NewArticleResponse(article)); err != nil {
render.Render(w, r, ErrRender(err))
return
}
}
// UpdateArticle updates an existing Article in our persistent store.
func UpdateArticle(w http.ResponseWriter, r *http.Request) {
article := r.Context().Value("article").(*Article)
data := &ArticleRequest{Article: article}
if err := render.Bind(r, data); err != nil {
render.Render(w, r, ErrInvalidRequest(err))
return
}
article = data.Article
dbUpdateArticle(article.ID, article)
render.Render(w, r, NewArticleResponse(article))
}
// DeleteArticle removes an existing Article from our persistent store.
func DeleteArticle(w http.ResponseWriter, r *http.Request) {
var err error
// Assume if we've reach this far, we can access the article
// context because this handler is a child of the ArticleCtx
// middleware. The worst case, the recoverer middleware will save us.
article := r.Context().Value("article").(*Article)
article, err = dbRemoveArticle(article.ID)
if err != nil {
render.Render(w, r, ErrInvalidRequest(err))
return
}
render.Render(w, r, NewArticleResponse(article))
}
// A completely separate router for administrator routes
func adminRouter() chi.Router {
r := chi.NewRouter()
r.Use(AdminOnly)
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("admin: index"))
})
r.Get("/accounts", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("admin: list accounts.."))
})
r.Get("/users/{userId}", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(fmt.Sprintf("admin: view user id %v", chi.URLParam(r, "userId"))))
})
return r
}
// AdminOnly middleware restricts access to just administrators.
func AdminOnly(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
isAdmin, ok := r.Context().Value("acl.admin").(bool)
if !ok || !isAdmin {
http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
return
}
next.ServeHTTP(w, r)
})
}
// paginate is a stub, but very possible to implement middleware logic
// to handle the request params for handling a paginated request.
func paginate(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// just a stub.. some ideas are to look at URL query params for something like
// the page number, or the limit, and send a query cursor down the chain
next.ServeHTTP(w, r)
})
}
// This is entirely optional, but I wanted to demonstrate how you could easily
// add your own logic to the render.Respond method.
func init() {
render.Respond = func(w http.ResponseWriter, r *http.Request, v interface{}) {
if err, ok := v.(error); ok {
// We set a default error status response code if one hasn't been set.
if _, ok := r.Context().Value(render.StatusCtxKey).(int); !ok {
w.WriteHeader(400)
}
// We log the error
fmt.Printf("Logging err: %s\n", err.Error())
// We change the response to not reveal the actual error message,
// instead we can transform the message something more friendly or mapped
// to some code / language, etc.
render.DefaultResponder(w, r, render.M{"status": "error"})
return
}
render.DefaultResponder(w, r, v)
}
}
//--
// Request and Response payloads for the REST api.
//
// The payloads embed the data model objects an
//
// In a real-world project, it would make sense to put these payloads
// in another file, or another sub-package.
//--
type UserPayload struct {
*User
Role string `json:"role"`
}
func NewUserPayloadResponse(user *User) *UserPayload {
return &UserPayload{User: user}
}
// Bind on UserPayload will run after the unmarshalling is complete, its
// a good time to focus some post-processing after a decoding.
func (u *UserPayload) Bind(r *http.Request) error {
return nil
}
func (u *UserPayload) Render(w http.ResponseWriter, r *http.Request) error {
u.Role = "collaborator"
return nil
}
// ArticleRequest is the request payload for Article data model.
//
// NOTE: It's good practice to have well defined request and response payloads
// so you can manage the specific inputs and outputs for clients, and also gives
// you the opportunity to transform data on input or output, for example
// on request, we'd like to protect certain fields and on output perhaps
// we'd like to include a computed field based on other values that aren't
// in the data model. Also, check out this awesome blog post on struct composition:
// http://attilaolah.eu/2014/09/10/json-and-struct-composition-in-go/
type ArticleRequest struct {
*Article
User *UserPayload `json:"user,omitempty"`
ProtectedID string `json:"id"` // override 'id' json to have more control
}
func (a *ArticleRequest) Bind(r *http.Request) error {
// a.Article is nil if no Article fields are sent in the request. Return an
// error to avoid a nil pointer dereference.
if a.Article == nil {
return errors.New("missing required Article fields.")
}
// a.User is nil if no Userpayload fields are sent in the request. In this app
// this won't cause a panic, but checks in this Bind method may be required if
// a.User or further nested fields like a.User.Name are accessed elsewhere.
// just a post-process after a decode..
a.ProtectedID = "" // unset the protected ID
a.Article.Title = strings.ToLower(a.Article.Title) // as an example, we down-case
return nil
}
// ArticleResponse is the response payload for the Article data model.
// See NOTE above in ArticleRequest as well.
//
// In the ArticleResponse object, first a Render() is called on itself,
// then the next field, and so on, all the way down the tree.
// Render is called in top-down order, like a http handler middleware chain.
type ArticleResponse struct {
*Article
User *UserPayload `json:"user,omitempty"`
// We add an additional field to the response here.. such as this
// elapsed computed property
Elapsed int64 `json:"elapsed"`
}
func NewArticleResponse(article *Article) *ArticleResponse {
resp := &ArticleResponse{Article: article}
if resp.User == nil {
if user, _ := dbGetUser(resp.UserID); user != nil {
resp.User = NewUserPayloadResponse(user)
}
}
return resp
}
func (rd *ArticleResponse) Render(w http.ResponseWriter, r *http.Request) error {
// Pre-processing before a response is marshalled and sent across the wire
rd.Elapsed = 10
return nil
}
func NewArticleListResponse(articles []*Article) []render.Renderer {
list := []render.Renderer{}
for _, article := range articles {
list = append(list, NewArticleResponse(article))
}
return list
}
// NOTE: as a thought, the request and response payloads for an Article could be the
// same payload type, perhaps will do an example with it as well.
// type ArticlePayload struct {
// *Article
// }
//--
// Error response payloads & renderers
//--
// ErrResponse renderer type for handling all sorts of errors.
//
// In the best case scenario, the excellent github.com/pkg/errors package
// helps reveal information on the error, setting it on Err, and in the Render()
// method, using it to set the application-specific error code in AppCode.
type ErrResponse struct {
Err error `json:"-"` // low-level runtime error
HTTPStatusCode int `json:"-"` // http response status code
StatusText string `json:"status"` // user-level status message
AppCode int64 `json:"code,omitempty"` // application-specific error code
ErrorText string `json:"error,omitempty"` // application-level error message, for debugging
}
func (e *ErrResponse) Render(w http.ResponseWriter, r *http.Request) error {
render.Status(r, e.HTTPStatusCode)
return nil
}
func ErrInvalidRequest(err error) render.Renderer {
return &ErrResponse{
Err: err,
HTTPStatusCode: 400,
StatusText: "Invalid request.",
ErrorText: err.Error(),
}
}
func ErrRender(err error) render.Renderer {
return &ErrResponse{
Err: err,
HTTPStatusCode: 422,
StatusText: "Error rendering response.",
ErrorText: err.Error(),
}
}
var ErrNotFound = &ErrResponse{HTTPStatusCode: 404, StatusText: "Resource not found."}
//--
// Data model objects and persistence mocks:
//--
// User data model
type User struct {
ID int64 `json:"id"`
Name string `json:"name"`
}
// Article data model. I suggest looking at https://upper.io for an easy
// and powerful data persistence adapter.
type Article struct {
ID string `json:"id"`
UserID int64 `json:"user_id"` // the author
Title string `json:"title"`
Slug string `json:"slug"`
}
// Article fixture data
var articles = []*Article{
{ID: "1", UserID: 100, Title: "Hi", Slug: "hi"},
{ID: "2", UserID: 200, Title: "sup", Slug: "sup"},
{ID: "3", UserID: 300, Title: "alo", Slug: "alo"},
{ID: "4", UserID: 400, Title: "bonjour", Slug: "bonjour"},
{ID: "5", UserID: 500, Title: "whats up", Slug: "whats-up"},
}
// User fixture data
var users = []*User{
{ID: 100, Name: "Peter"},
{ID: 200, Name: "Julia"},
}
func dbNewArticle(article *Article) (string, error) {
article.ID = fmt.Sprintf("%d", rand.Intn(100)+10)
articles = append(articles, article)
return article.ID, nil
}
func dbGetArticle(id string) (*Article, error) {
for _, a := range articles {
if a.ID == id {
return a, nil
}
}
return nil, errors.New("article not found.")
}
func dbGetArticleBySlug(slug string) (*Article, error) {
for _, a := range articles {
if a.Slug == slug {
return a, nil
}
}
return nil, errors.New("article not found.")
}
func dbUpdateArticle(id string, article *Article) (*Article, error) {
for i, a := range articles {
if a.ID == id {
articles[i] = article
return article, nil
}
}
return nil, errors.New("article not found.")
}
func dbRemoveArticle(id string) (*Article, error) {
for i, a := range articles {
if a.ID == id {
articles = append((articles)[:i], (articles)[i+1:]...)
return a, nil
}
}
return nil, errors.New("article not found.")
}
func dbGetUser(id int64) (*User, error) {
for _, u := range users {
if u.ID == id {
return u, nil
}
}
return nil, errors.New("user not found.")
}
================================================
FILE: _examples/rest/routes.json
================================================
{
"router": {
"middlewares": [
{
"pkg": "github.com/go-chi/chi/v5/middleware",
"func": "RequestID",
"comment": "RequestID is a middleware that injects a request ID into the context of each\nrequest. A request ID is a string of the form \"host.example.com/random-0001\",\nwhere \"random\" is a base62 random string that uniquely identifies this go\nprocess, and where the last number is an atomically incremented request\ncounter.\n",
"file": "github.com/go-chi/chi/middleware/request_id.go",
"line": 63
},
{
"pkg": "github.com/go-chi/chi/v5/middleware",
"func": "Logger",
"comment": "Logger is a middleware that logs the start and end of each request, along\nwith some useful data about what was requested, what the response status was,\nand how long it took to return. When standard output is a TTY, Logger will\nprint in color, otherwise it will print in black and white. Logger prints a\nrequest ID if one is provided.\n\nAlternatively, look at https://github.com/pressly/lg and the `lg.RequestLogger`\nmiddleware pkg.\n",
"file": "github.com/go-chi/chi/middleware/logger.go",
"line": 26
},
{
"pkg": "github.com/go-chi/chi/v5/middleware",
"func": "Recoverer",
"comment": "Recoverer is a middleware that recovers from panics, logs the panic (and a\nbacktrace), and returns a HTTP 500 (Internal Server Error) status if\npossible. Recoverer prints a request ID if one is provided.\n\nAlternatively, look at https://github.com/pressly/lg middleware pkgs.\n",
"file": "github.com/go-chi/chi/middleware/recoverer.go",
"line": 18
},
{
"pkg": "github.com/go-chi/chi/v5/middleware",
"func": "URLFormat",
"comment": "URLFormat is a middleware that parses the url extension from a request path and stores it\non the context as a string under the key `middleware.URLFormatCtxKey`. The middleware will\ntrim the suffix from the routing path and continue routing.\n\nRouters should not include a url parameter for the suffix when using this middleware.\n\nSample usage.. for url paths: `/articles/1`, `/articles/1.json` and `/articles/1.xml`\n\n func routes() http.Handler {\n r := chi.NewRouter()\n r.Use(middleware.URLFormat)\n\n r.Get(\"/articles/{id}\", ListArticles)\n\n return r\n }\n\n func ListArticles(w http.ResponseWriter, r *http.Request) {\n\t urlFormat, _ := r.Context().Value(middleware.URLFormatCtxKey).(string)\n\n\t switch urlFormat {\n\t case \"json\":\n\t \trender.JSON(w, r, articles)\n\t case \"xml:\"\n\t \trender.XML(w, r, articles)\n\t default:\n\t \trender.JSON(w, r, articles)\n\t }\n}\n",
"file": "github.com/go-chi/chi/middleware/url_format.go",
"line": 45
},
{
"pkg": "github.com/go-chi/render",
"func": "SetContentType.func1",
"comment": "",
"file": "github.com/go-chi/render/content_type.go",
"line": 49,
"anonymous": true
}
],
"routes": {
"/": {
"handlers": {
"GET": {
"middlewares": [],
"method": "GET",
"pkg": "",
"func": "main.main.func1",
"comment": "",
"file": "github.com/go-chi/chi/_examples/rest/main.go",
"line": 69,
"anonymous": true
}
}
},
"/admin/*": {
"router": {
"middlewares": [
{
"pkg": "",
"func": "main.AdminOnly",
"comment": "AdminOnly middleware restricts access to just administrators.\n",
"file": "github.com/go-chi/chi/_examples/rest/main.go",
"line": 238
}
],
"routes": {
"/": {
"handlers": {
"GET": {
"middlewares": [],
"method": "GET",
"pkg": "",
"func": "main.adminRouter.func1",
"comment": "",
"file": "github.com/go-chi/chi/_examples/rest/main.go",
"line": 225,
"anonymous": true
}
}
},
"/accounts": {
"handlers": {
"GET": {
"middlewares": [],
"method": "GET",
"pkg": "",
"func": "main.adminRouter.func2",
"comment": "",
"file": "github.com/go-chi/chi/_examples/rest/main.go",
"line": 228,
"anonymous": true
}
}
},
"/users/{userId}": {
"handlers": {
"GET": {
"middlewares": [],
"method": "GET",
"pkg": "",
"func": "main.adminRouter.func3",
"comment": "",
"file": "github.com/go-chi/chi/_examples/rest/main.go",
"line": 231,
"anonymous": true
}
}
}
}
}
},
"/articles/*": {
"router": {
"middlewares": [],
"routes": {
"/": {
"handlers": {
"GET": {
"middlewares": [
{
"pkg": "",
"func": "main.paginate",
"comment": "paginate is a stub, but very possible to implement middleware logic\nto handle the request params for handling a paginated request.\n",
"file": "github.com/go-chi/chi/_examples/rest/main.go",
"line": 251
}
],
"method": "GET",
"pkg": "",
"func": "main.ListArticles",
"comment": "",
"file": "github.com/go-chi/chi/_examples/rest/main.go",
"line": 117
},
"POST": {
"middlewares": [],
"method": "POST",
"pkg": "",
"func": "main.CreateArticle",
"comment": "CreateArticle persists the posted Article and returns it\nback to the client as an acknowledgement.\n",
"file": "github.com/go-chi/chi/_examples/rest/main.go",
"line": 158
}
}
},
"/search": {
"handlers": {
"GET": {
"middlewares": [],
"method": "GET",
"pkg": "",
"func": "main.SearchArticles",
"comment": "SearchArticles searches the Articles data for a matching article.\nIt's just a stub, but you get the idea.\n",
"file": "github.com/go-chi/chi/_examples/rest/main.go",
"line": 152
}
}
},
"/{articleID}/*": {
"router": {
"middlewares": [
{
"pkg": "",
"func": "main.ArticleCtx",
"comment": "ArticleCtx middleware is used to load an Article object from\nthe URL parameters passed through as the request. In case\nthe Article could not be found, we stop here and return a 404.\n",
"file": "github.com/go-chi/chi/_examples/rest/main.go",
"line": 127
}
],
"routes": {
"/": {
"handlers": {
"DELETE": {
"middlewares": [],
"method": "DELETE",
"pkg": "",
"func": "main.DeleteArticle",
"comment": "DeleteArticle removes an existing Article from our persistent store.\n",
"file": "github.com/go-chi/chi/_examples/rest/main.go",
"line": 204
},
"GET": {
"middlewares": [],
"method": "GET",
"pkg": "",
"func": "main.GetArticle",
"comment": "GetArticle returns the specific Article. You'll notice it just\nfetches the Article right off the context, as its understood that\nif we made it this far, the Article must be on the context. In case\nits not due to a bug, then it will panic, and our Recoverer will save us.\n",
"file": "github.com/go-chi/chi/_examples/rest/main.go",
"line": 176
},
"PUT": {
"middlewares": [],
"method": "PUT",
"pkg": "",
"func": "main.UpdateArticle",
"comment": "UpdateArticle updates an existing Article in our persistent store.\n",
"file": "github.com/go-chi/chi/_examples/rest/main.go",
"line": 189
}
}
}
}
}
},
"/{articleSlug:[a-z-]+}": {
"handlers": {
"GET": {
"middlewares": [
{
"pkg": "",
"func": "main.ArticleCtx",
"comment": "ArticleCtx middleware is used to load an Article object from\nthe URL parameters passed through as the request. In case\nthe Article could not be found, we stop here and return a 404.\n",
"file": "github.com/go-chi/chi/_examples/rest/main.go",
"line": 127
}
],
"method": "GET",
"pkg": "",
"func": "main.GetArticle",
"comment": "GetArticle returns the specific Article. You'll notice it just\nfetches the Article right off the context, as its understood that\nif we made it this far, the Article must be on the context. In case\nits not due to a bug, then it will panic, and our Recoverer will save us.\n",
"file": "github.com/go-chi/chi/_examples/rest/main.go",
"line": 176
}
}
}
}
}
},
"/panic": {
"handlers": {
"GET": {
"middlewares": [],
"method": "GET",
"pkg": "",
"func": "main.main.func3",
"comment": "",
"file": "github.com/go-chi/chi/_examples/rest/main.go",
"line": 77,
"anonymous": true
}
}
},
"/ping": {
"handlers": {
"GET": {
"middlewares": [],
"method": "GET",
"pkg": "",
"func": "main.main.func2",
"comment": "",
"file": "github.com/go-chi/chi/_examples/rest/main.go",
"line": 73,
"anonymous": true
}
}
}
}
}
}
================================================
FILE: _examples/rest/routes.md
================================================
# github.com/go-chi/chi
Welcome to the chi/_examples/rest generated docs.
## Routes
<details>
<summary>`/`</summary>
- [RequestID](/middleware/request_id.go#L63)
- [Logger](/middleware/logger.go#L26)
- [Recoverer](/middleware/recoverer.go#L18)
- [URLFormat](/middleware/url_format.go#L45)
- [SetContentType.func1](https://github.com/go-chi/render/content_type.go#L49)
- **/**
- _GET_
- [main.main.func1](/_examples/rest/main.go#L69)
</details>
<details>
<summary>`/admin/*`</summary>
- [RequestID](/middleware/request_id.go#L63)
- [Logger](/middleware/logger.go#L26)
- [Recoverer](/middleware/recoverer.go#L18)
- [URLFormat](/middleware/url_format.go#L45)
- [SetContentType.func1](https://github.com/go-chi/render/content_type.go#L49)
- **/admin/***
- [main.AdminOnly](/_examples/rest/main.go#L238)
- **/**
- _GET_
- [main.adminRouter.func1](/_examples/rest/main.go#L225)
</details>
<details>
<summary>`/admin/*/accounts`</summary>
- [RequestID](/middleware/request_id.go#L63)
- [Logger](/middleware/logger.go#L26)
- [Recoverer](/middleware/recoverer.go#L18)
- [URLFormat](/middleware/url_format.go#L45)
- [SetContentType.func1](https://github.com/go-chi/render/content_type.go#L49)
- **/admin/***
- [main.AdminOnly](/_examples/rest/main.go#L238)
- **/accounts**
- _GET_
- [main.adminRouter.func2](/_examples/rest/main.go#L228)
</details>
<details>
<summary>`/admin/*/users/{userId}`</summary>
- [RequestID](/middleware/request_id.go#L63)
- [Logger](/middleware/logger.go#L26)
- [Recoverer](/middleware/recoverer.go#L18)
- [URLFormat](/middleware/url_format.go#L45)
- [SetContentType.func1](https://github.com/go-chi/render/content_type.go#L49)
- **/admin/***
- [main.AdminOnly](/_examples/rest/main.go#L238)
- **/users/{userId}**
- _GET_
- [main.adminRouter.func3](/_examples/rest/main.go#L231)
</details>
<details>
<summary>`/articles/*`</summary>
- [RequestID](/middleware/request_id.go#L63)
- [Logger](/middleware/logger.go#L26)
- [Recoverer](/middleware/recoverer.go#L18)
- [URLFormat](/middleware/url_format.go#L45)
- [SetContentType.func1](https://github.com/go-chi/render/content_type.go#L49)
- **/articles/***
- **/**
- _GET_
- [main.paginate](/_examples/rest/main.go#L251)
- [main.ListArticles](/_examples/rest/main.go#L117)
- _POST_
- [main.CreateArticle](/_examples/rest/main.go#L158)
</details>
<details>
<summary>`/articles/*/search`</summary>
- [RequestID](/middleware/request_id.go#L63)
- [Logger](/middleware/logger.go#L26)
- [Recoverer](/middleware/recoverer.go#L18)
- [URLFormat](/middleware/url_format.go#L45)
- [SetContentType.func1](https://github.com/go-chi/render/content_type.go#L49)
- **/articles/***
- **/search**
- _GET_
- [main.SearchArticles](/_examples/rest/main.go#L152)
</details>
<details>
<summary>`/articles/*/{articleID}/*`</summary>
- [RequestID](/middleware/request_id.go#L63)
- [Logger](/middleware/logger.go#L26)
- [Recoverer](/middleware/recoverer.go#L18)
- [URLFormat](/middleware/url_format.go#L45)
- [SetContentType.func1](https://github.com/go-chi/render/content_type.go#L49)
- **/articles/***
- **/{articleID}/***
- [main.ArticleCtx](/_examples/rest/main.go#L127)
- **/**
- _DELETE_
- [main.DeleteArticle](/_examples/rest/main.go#L204)
- _GET_
- [main.GetArticle](/_examples/rest/main.go#L176)
- _PUT_
- [main.UpdateArticle](/_examples/rest/main.go#L189)
</details>
<details>
<summary>`/articles/*/{articleSlug:[a-z-]+}`</summary>
- [RequestID](/middleware/request_id.go#L63)
- [Logger](/middleware/logger.go#L26)
- [Recoverer](/middleware/recoverer.go#L18)
- [URLFormat](/middleware/url_format.go#L45)
- [SetContentType.func1](https://github.com/go-chi/render/content_type.go#L49)
- **/articles/***
- **/{articleSlug:[a-z-]+}**
- _GET_
- [main.ArticleCtx](/_examples/rest/main.go#L127)
- [main.GetArticle](/_examples/rest/main.go#L176)
</details>
<details>
<summary>`/panic`</summary>
- [RequestID](/middleware/request_id.go#L63)
- [Logger](/middleware/logger.go#L26)
- [Recoverer](/middleware/recoverer.go#L18)
- [URLFormat](/middleware/url_format.go#L45)
- [SetContentType.func1](https://github.com/go-chi/render/content_type.go#L49)
- **/panic**
- _GET_
- [main.main.func3](/_examples/rest/main.go#L77)
</details>
<details>
<summary>`/ping`</summary>
- [RequestID](/middleware/request_id.go#L63)
- [Logger](/middleware/logger.go#L26)
- [Recoverer](/middleware/recoverer.go#L18)
- [URLFormat](/middleware/url_format.go#L45)
- [SetContentType.func1](https://github.com/go-chi/render/content_type.go#L49)
- **/ping**
- _GET_
- [main.main.func2](/_examples/rest/main.go#L73)
</details>
Total # of routes: 10
================================================
FILE: _examples/router-walk/main.go
================================================
package main
import (
"fmt"
"net/http"
"strings"
"github.com/go-chi/chi/v5"
)
func main() {
r := chi.NewRouter()
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("root."))
})
r.Route("/road", func(r chi.Router) {
r.Get("/left", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("left road"))
})
r.Post("/right", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("right road"))
})
})
r.Put("/ping", Ping)
walkFunc := func(method string, route string, handler http.Handler, middlewares ...func(http.Handler) http.Handler) error {
route = strings.ReplaceAll(route, "/*/", "/")
fmt.Printf("%s %s\n", method, route)
return nil
}
if err := chi.Walk(r, walkFunc); err != nil {
fmt.Printf("Logging err: %s\n", err.Error())
}
}
// Ping returns pong
func Ping(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("pong"))
}
================================================
FILE: _examples/todos-resource/main.go
================================================
// This example demonstrates a project structure that defines a subrouter and its
// handlers on a struct, and mounting them as subrouters to a parent router.
// See also _examples/rest for an in-depth example of a REST service, and apply
// those same patterns to this structure.
package main
import (
"net/http"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
)
func main() {
r := chi.NewRouter()
r.Use(middleware.RequestID)
r.Use(middleware.RealIP)
r.Use(middleware.Logger)
r.Use(middleware.Recoverer)
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("."))
})
r.Mount("/users", usersResource{}.Routes())
r.Mount("/todos", todosResource{}.Routes())
http.ListenAndServe(":3333", r)
}
================================================
FILE: _examples/todos-resource/todos.go
================================================
package main
import (
"net/http"
"github.com/go-chi/chi/v5"
)
type todosResource struct{}
// Routes creates a REST router for the todos resource
func (rs todosResource) Routes() chi.Router {
r := chi.NewRouter()
// r.Use() // some middleware..
r.Get("/", rs.List) // GET /todos - read a list of todos
r.Post("/", rs.Create) // POST /todos - create a new todo and persist it
r.Put("/", rs.Delete)
r.Route("/{id}", func(r chi.Router) {
// r.Use(rs.TodoCtx) // lets have a todos map, and lets actually load/manipulate
r.Get("/", rs.Get) // GET /todos/{id} - read a single todo by :id
r.Put("/", rs.Update) // PUT /todos/{id} - update a single todo by :id
r.Delete("/", rs.Delete) // DELETE /todos/{id} - delete a single todo by :id
r.Get("/sync", rs.Sync)
})
return r
}
func (rs todosResource) List(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("todos list of stuff.."))
}
func (rs todosResource) Create(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("todos create"))
}
func (rs todosResource) Get(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("todo get"))
}
func (rs todosResource) Update(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("todo update"))
}
func (rs todosResource) Delete(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("todo delete"))
}
func (rs todosResource) Sync(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("todo sync"))
}
================================================
FILE: _examples/todos-resource/users.go
================================================
package main
import (
"net/http"
"github.com/go-chi/chi/v5"
)
type usersResource struct{}
// Routes creates a REST router for the todos resource
func (rs usersResource) Routes() chi.Router {
r := chi.NewRouter()
// r.Use() // some middleware..
r.Get("/", rs.List) // GET /users - read a list of users
r.Post("/", rs.Create) // POST /users - create a new user and persist it
r.Put("/", rs.Delete)
r.Route("/{id}", func(r chi.Router) {
// r.Use(rs.TodoCtx) // lets have a users map, and lets actually load/manipulate
r.Get("/", rs.Get) // GET /users/{id} - read a single user by :id
r.Put("/", rs.Update) // PUT /users/{id} - update a single user by :id
r.Delete("/", rs.Delete) // DELETE /users/{id} - delete a single user by :id
})
return r
}
func (rs usersResource) List(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("users list of stuff.."))
}
func (rs usersResource) Create(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("users create"))
}
func (rs usersResource) Get(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("user get"))
}
func (rs usersResource) Update(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("user update"))
}
func (rs usersResource) Delete(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("user delete"))
}
================================================
FILE: _examples/versions/data/article.go
================================================
package data
// Article is runtime object, that's not meant to be sent via REST.
type Article struct {
ID int `db:"id" json:"id" xml:"id"`
Title string `db:"title" json:"title" xml:"title"`
Data []string `db:"data,stringarray" json:"data" xml:"data"`
CustomDataForAuthUsers string `db:"custom_data" json:"-" xml:"-"`
}
================================================
FILE: _examples/versions/data/errors.go
================================================
package data
import (
"errors"
"net/http"
"github.com/go-chi/render"
)
var (
ErrUnauthorized = errors.New("Unauthorized")
ErrForbidden = errors.New("Forbidden")
ErrNotFound = errors.New("Resource not found")
)
func PresentError(r *http.Request, err error) (*http.Request, interface{}) {
switch err {
case ErrUnauthorized:
render.Status(r, 401)
case ErrForbidden:
render.Status(r, 403)
case ErrNotFound:
render.Status(r, 404)
default:
render.Status(r, 500)
}
return r, map[string]string{"error": err.Error()}
}
================================================
FILE: _examples/versions/go.mod
================================================
module versions
go 1.16
require (
github.com/go-chi/chi/v5 v5.1.0
github.com/go-chi/render v1.0.3
)
require github.com/ajg/form v1.5.1 // indirect
================================================
FILE: _examples/versions/go.sum
================================================
github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=
github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw=
github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4=
github.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0=
================================================
FILE: _examples/versions/main.go
================================================
// This example demonstrates the use of the render subpackage, with
// a quick concept for how to support multiple api versions.
package main
import (
"context"
"errors"
"fmt"
"math/rand"
"net/http"
"time"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/_examples/versions/data"
v1 "github.com/go-chi/chi/v5/_examples/versions/presenter/v1"
v2 "github.com/go-chi/chi/v5/_examples/versions/presenter/v2"
v3 "github.com/go-chi/chi/v5/_examples/versions/presenter/v3"
"github.com/go-chi/chi/v5/middleware"
"github.com/go-chi/render"
)
func main() {
r := chi.NewRouter()
r.Use(middleware.RequestID)
r.Use(middleware.Logger)
r.Use(middleware.Recoverer)
// API version 3.
r.Route("/v3", func(r chi.Router) {
r.Use(apiVersionCtx("v3"))
r.Mount("/articles", articleRouter())
})
// API version 2.
r.Route("/v2", func(r chi.Router) {
r.Use(apiVersionCtx("v2"))
r.Mount("/articles", articleRouter())
})
// API version 1.
r.Route("/v1", func(r chi.Router) {
r.Use(randomErrorMiddleware) // Simulate random error, ie. version 1 is buggy.
r.Use(apiVersionCtx("v1"))
r.Mount("/articles", articleRouter())
})
http.ListenAndServe(":3333", r)
}
func apiVersionCtx(version string) func(next http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
r = r.WithContext(context.WithValue(r.Context(), "api.version", version))
next.ServeHTTP(w, r)
})
}
}
func articleRouter() http.Handler {
r := chi.NewRouter()
r.Get("/", listArticles)
r.Route("/{articleID}", func(r chi.Router) {
r.Get("/", getArticle)
// r.Put("/", updateArticle)
// r.Delete("/", deleteArticle)
})
return r
}
func listArticles(w http.ResponseWriter, r *http.Request) {
articles := make(chan render.Renderer, 5)
// Load data asynchronously into the channel (simulate slow storage):
go func() {
for i := 1; i <= 10; i++ {
article := &data.Article{
ID: i,
Title: fmt.Sprintf("Article #%v", i),
Data: []string{"one", "two", "three", "four"},
CustomDataForAuthUsers: "secret data for auth'd users only",
}
apiVersion := r.Context().Value("api.version").(string)
switch apiVersion {
case "v1":
articles <- v1.NewArticleResponse(article)
case "v2":
articles <- v2.NewArticleResponse(article)
default:
articles <- v3.NewArticleResponse(article)
}
time.Sleep(100 * time.Millisecond)
}
close(articles)
}()
// Start streaming data from the channel.
render.Respond(w, r, articles)
}
func getArticle(w http.ResponseWriter, r *http.Request) {
// Load article.
if chi.URLParam(r, "articleID") != "1" {
render.Respond(w, r, data.ErrNotFound)
return
}
article := &data.Article{
ID: 1,
Title: "Article #1",
Data: []string{"one", "two", "three", "four"},
CustomDataForAuthUsers: "secret data for auth'd users only",
}
// Simulate some context values:
// 1. ?auth=true simulates authenticated session/user.
// 2. ?error=true simulates random error.
if r.URL.Query().Get("auth") != "" {
r = r.WithContext(context.WithValue(r.Context(), "auth", true))
}
if r.URL.Query().Get("error") != "" {
render.Respond(w, r, errors.New("error"))
return
}
var payload render.Renderer
apiVersion := r.Context().Value("api.version").(string)
switch apiVersion {
case "v1":
payload = v1.NewArticleResponse(article)
case "v2":
payload = v2.NewArticleResponse(article)
default:
payload = v3.NewArticleResponse(article)
}
render.Render(w, r, payload)
}
func randomErrorMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
rand.Seed(time.Now().Unix())
// One in three chance of random error.
if rand.Int31n(3) == 0 {
errors := []error{data.ErrUnauthorized, data.ErrForbidden, data.ErrNotFound}
render.Respond(w, r, errors[rand.Intn(len(errors))])
return
}
next.ServeHTTP(w, r)
})
}
================================================
FILE: _examples/versions/presenter/v1/article.go
================================================
package v1
import (
"net/http"
"github.com/go-chi/chi/v5/_examples/versions/data"
)
// Article presented in API version 1.
type Article struct {
*data.Article
Data map[string]bool `json:"data" xml:"data"`
}
func (a *Article) Render(w http.ResponseWriter, r *http.Request) error {
return nil
}
func NewArticleResponse(article *data.Article) *Article {
return &Article{Article: article}
}
================================================
FILE: _examples/versions/presenter/v2/article.go
================================================
package v2
import (
"fmt"
"net/http"
"github.com/go-chi/chi/v5/_examples/versions/data"
)
// Article presented in API version 2.
type Article struct {
// *v3.Article `json:",inline" xml:",inline"`
*data.Article
// Additional fields.
SelfURL string `json:"self_url" xml:"self_url"`
// Omitted fields.
URL interface{} `json:"url,omitempty" xml:"url,omitempty"`
}
func (a *Article) Render(w http.ResponseWriter, r *http.Request) error {
a.SelfURL = fmt.Sprintf("http://localhost:3333/v2?id=%v", a.ID)
return nil
}
func NewArticleResponse(article *data.Article) *Article {
return &Article{Article: article}
}
================================================
FILE: _examples/versions/presenter/v3/article.go
================================================
package v3
import (
"fmt"
"math/rand"
"net/http"
"github.com/go-chi/chi/v5/_examples/versions/data"
)
// Article presented in API version 2.
type Article struct {
*data.Article `json:",inline" xml:",inline"`
// Additional fields.
URL string `json:"url" xml:"url"`
ViewsCount int64 `json:"views_count" xml:"views_count"`
APIVersion string `json:"api_version" xml:"api_version"`
// Omitted fields.
// Show custom_data explicitly for auth'd users only.
CustomDataForAuthUsers interface{} `json:"custom_data,omitempty" xml:"custom_data,omitempty"`
}
func (a *Article) Render(w http.ResponseWriter, r *http.Request) error {
a.ViewsCount = rand.Int63n(100000)
a.URL = fmt.Sprintf("http://localhost:3333/v3/?id=%v", a.ID)
// Only show to auth'd user.
if _, ok := r.Context().Value("auth").(bool); ok {
a.CustomDataForAuthUsers = a.Article.CustomDataForAuthUsers
}
return nil
}
func NewArticleResponse(article *data.Article) *Article {
return &Article{Article: article}
}
================================================
FILE: chain.go
================================================
package chi
import "net/http"
// Chain returns a Middlewares type from a slice of middleware handlers.
func Chain(middlewares ...func(http.Handler) http.Handler) Middlewares {
return Middlewares(middlewares)
}
// Handler builds and returns a http.Handler from the chain of middlewares,
// with `h http.Handler` as the final handler.
func (mws Middlewares) Handler(h http.Handler) http.Handler {
return &ChainHandler{h, chain(mws, h), mws}
}
// HandlerFunc builds and returns a http.Handler from the chain of middlewares,
// with `h http.Handler` as the final handler.
func (mws Middlewares) HandlerFunc(h http.HandlerFunc) http.Handler {
return &ChainHandler{h, chain(mws, h), mws}
}
// ChainHandler is a http.Handler with support for handler composition and
// execution.
type ChainHandler struct {
Endpoint http.Handler
chain http.Handler
Middlewares Middlewares
}
func (c *ChainHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
c.chain.ServeHTTP(w, r)
}
// chain builds a http.Handler composed of an inline middleware stack and endpoint
// handler in the order they are passed.
func chain(middlewares []func(http.Handler) http.Handler, endpoint http.Handler) http.Handler {
// Return ahead of time if there aren't any middlewares for the chain
if len(middlewares) == 0 {
return endpoint
}
// Wrap the end handler with the middleware chain
h := middlewares[len(middlewares)-1](endpoint)
for i := len(middlewares) - 2; i >= 0; i-- {
h = middlewares[i](h)
}
return h
}
================================================
FILE: chi.go
================================================
// Package chi is a small, idiomatic and composable router for building HTTP services.
//
// chi supports the four most recent major versions of Go.
//
// Example:
//
// package main
//
// import (
// "net/http"
//
// "github.com/go-chi/chi/v5"
// "github.com/go-chi/chi/v5/middleware"
// )
//
// func main() {
// r := chi.NewRouter()
// r.Use(middleware.Logger)
// r.Use(middleware.Recoverer)
//
// r.Get("/", func(w http.ResponseWriter, r *http.Request) {
// w.Write([]byte("root."))
// })
//
// http.ListenAndServe(":3333", r)
// }
//
// See github.com/go-chi/chi/_examples/ for more in-depth examples.
//
// URL patterns allow for easy matching of path components in HTTP
// requests. The matching components can then be accessed using
// chi.URLParam(). All patterns must begin with a slash.
//
// A simple named placeholder {name} matches any sequence of characters
// up to the next / or the end of the URL. Trailing slashes on paths must
// be handled explicitly.
//
// A placeholder with a name followed by a colon allows a regular
// expression match, for example {number:\\d+}. The regular expression
// syntax is Go's normal regexp RE2 syntax, except that / will never be
// matched. An anonymous regexp pattern is allowed, using an empty string
// before the colon in the placeholder, such as {:\\d+}
//
// The special placeholder of asterisk matches the rest of the requested
// URL. Any trailing characters in the pattern are ignored. This is the only
// placeholder which will match / characters.
//
// Examples:
//
// "/user/{name}" matches "/user/jsmith" but not "/user/jsmith/info" or "/user/jsmith/"
// "/user/{name}/info" matches "/user/jsmith/info"
// "/page/*" matches "/page/intro/latest"
// "/page/{other}/latest" also matches "/page/intro/latest"
// "/date/{yyyy:\\d\\d\\d\\d}/{mm:\\d\\d}/{dd:\\d\\d}" matches "/date/2017/04/01"
package chi
import "net/http"
// NewRouter returns a new Mux object that implements the Router interface.
func NewRouter() *Mux {
return NewMux()
}
// Router consisting of the core routing methods used by chi's Mux,
// using only the standard net/http.
type Router interface {
http.Handler
Routes
// Use appends one or more middlewares onto the Router stack.
Use(middlewares ...func(http.Handler) http.Handler)
// With adds inline middlewares for an endpoint handler.
With(middlewares ...func(http.Handler) http.Handler) Router
// Group adds a new inline-Router along the current routing
// path, with a fresh middleware stack for the inline-Router.
Group(fn func(r Router)) Router
// Route mounts a sub-Router along a `pattern`` string.
Route(pattern string, fn func(r Router)) Router
// Mount attaches another http.Handler along ./pattern/*
Mount(pattern string, h http.Handler)
// Handle and HandleFunc adds routes for `pattern` that matches
// all HTTP methods.
Handle(pattern string, h http.Handler)
HandleFunc(pattern string, h http.HandlerFunc)
// Method and MethodFunc adds routes for `pattern` that matches
// the `method` HTTP method.
Method(method, pattern string, h http.Handler)
MethodFunc(method, pattern string, h http.HandlerFunc)
// HTTP-method routing along `pattern`
Connect(pattern string, h http.HandlerFunc)
Delete(pattern string, h http.HandlerFunc)
Get(pattern string, h http.HandlerFunc)
Head(pattern string, h http.HandlerFunc)
Options(pattern string, h http.HandlerFunc)
Patch(pattern string, h http.HandlerFunc)
Post(pattern string, h http.HandlerFunc)
Put(pattern string, h http.HandlerFunc)
Trace(pattern string, h http.HandlerFunc)
// NotFound defines a handler to respond whenever a route could
// not be found.
NotFound(h http.HandlerFunc)
// MethodNotAllowed defines a handler to respond whenever a method is
// not allowed.
MethodNotAllowed(h http.HandlerFunc)
}
// Routes interface adds two methods for router traversal, which is also
// used by the `docgen` subpackage to generation documentation for Routers.
type Routes interface {
// Routes returns the routing tree in an easily traversable structure.
Routes() []Route
// Middlewares returns the list of middlewares in use by the router.
Middlewares() Middlewares
// Match searches the routing tree for a handler that matches
// the method/path - similar to routing a http request, but without
// executing the handler thereafter.
Match(rctx *Context, method, path string) bool
// Find searches the routing tree for the pattern that matches
// the method/path.
Find(rctx *Context, method, path string) string
}
// Middlewares type is a slice of standard middleware handlers with methods
// to compose middleware chains and http.Handler's.
type Middlewares []func(http.Handler) http.Handler
================================================
FILE: context.go
================================================
package chi
import (
"context"
"net/http"
"strings"
)
// URLParam returns the url parameter from a http.Request object.
func URLParam(r *http.Request, key string) string {
if rctx := RouteContext(r.Context()); rctx != nil {
return rctx.URLParam(key)
}
return ""
}
// URLParamFromCtx returns the url parameter from a http.Request Context.
func URLParamFromCtx(ctx context.Context, key string) string {
if rctx := RouteContext(ctx); rctx != nil {
return rctx.URLParam(key)
}
return ""
}
// RouteContext returns chi's routing Context object from a
// http.Request Context.
func RouteContext(ctx context.Context) *Context {
val, _ := ctx.Value(RouteCtxKey).(*Context)
return val
}
// NewRouteContext returns a new routing Context object.
func NewRouteContext() *Context {
return &Context{}
}
var (
// RouteCtxKey is the context.Context key to store the request context.
RouteCtxKey = &contextKey{"RouteContext"}
)
// Context is the default routing context set on the root node of a
// request context to track route patterns, URL parameters and
// an optional routing path.
type Context struct {
Routes Routes
// parentCtx is the parent of this one, for using Context as a
// context.Context directly. This is an optimization that saves
// 1 allocation.
parentCtx context.Context
// Routing path/method override used during the route search.
// See Mux#routeHTTP method.
RoutePath string
RouteMethod string
// URLParams are the stack of routeParams captured during the
// routing lifecycle across a stack of sub-routers.
URLParams RouteParams
// Route parameters matched for the current sub-router. It is
// intentionally unexported so it can't be tampered.
routeParams RouteParams
// The endpoint routing pattern that matched the request URI path
// or `RoutePath` of the current sub-router. This value will update
// during the lifecycle of a request passing through a stack of
// sub-routers.
routePattern string
// Routing pattern stack throughout the lifecycle of the request,
// across all connected routers. It is a record of all matching
// patterns across a stack of sub-routers.
RoutePatterns []string
methodsAllowed []methodTyp // allowed methods in case of a 405
methodNotAllowed bool
}
// Reset a routing context to its initial state.
func (x *Context) Reset() {
x.Routes = nil
x.RoutePath = ""
x.RouteMethod = ""
x.RoutePatterns = x.RoutePatterns[:0]
x.URLParams.Keys = x.URLParams.Keys[:0]
x.URLParams.Values = x.URLParams.Values[:0]
x.routePattern = ""
x.routeParams.Keys = x.routeParams.Keys[:0]
x.routeParams.Values = x.routeParams.Values[:0]
x.methodNotAllowed = false
x.methodsAllowed = x.methodsAllowed[:0]
x.parentCtx = nil
}
// URLParam returns the corresponding URL parameter value from the request
// routing context.
func (x *Context) URLParam(key string) string {
for k := len(x.URLParams.Keys) - 1; k >= 0; k-- {
if x.URLParams.Keys[k] == key {
return x.URLParams.Values[k]
}
}
return ""
}
// RoutePattern builds the routing pattern string for the particular
// request, at the particular point during routing. This means, the value
// will change throughout the execution of a request in a router. That is
// why it's advised to only use this value after calling the next handler.
//
// For example,
//
// func Instrument(next http.Handler) http.Handler {
// return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// next.ServeHTTP(w, r)
// routePattern := chi.RouteContext(r.Context()).RoutePattern()
// measure(w, r, routePattern)
// })
// }
func (x *Context) RoutePattern() string {
if x == nil {
return ""
}
routePattern := strings.Join(x.RoutePatterns, "")
routePattern = replaceWildcards(routePattern)
if routePattern != "/" {
routePattern = strings.TrimSuffix(routePattern, "//")
routePattern = strings.TrimSuffix(routePattern, "/")
}
return routePattern
}
// replaceWildcards takes a route pattern and replaces all occurrences of
// "/*/" with "/". It iteratively runs until no wildcards remain to
// correctly handle consecutive wildcards.
func replaceWildcards(p string) string {
for strings.Contains(p, "/*/") {
p = strings.ReplaceAll(p, "/*/", "/")
}
return p
}
// RouteParams is a structure to track URL routing parameters efficiently.
type RouteParams struct {
Keys, Values []string
}
// Add will append a URL parameter to the end of the route param
func (s *RouteParams) Add(key, value string) {
s.Keys = append(s.Keys, key)
s.Values = append(s.Values, value)
}
// contextKey is a value for use with context.WithValue. It's used as
// a pointer so it fits in an interface{} without allocation. This technique
// for defining context keys was copied from Go 1.7's new use of context in net/http.
type contextKey struct {
name string
}
func (k *contextKey) String() string {
return "chi context value " + k.name
}
================================================
FILE: context_test.go
================================================
package chi
import "testing"
// TestRoutePattern tests correct in-the-middle wildcard removals.
// If user organizes a router like this:
//
// (router.go)
//
// r.Route("/v1", func(r chi.Router) {
// r.Mount("/resources", resourcesController{}.Router())
// }
//
// (resources_controller.go)
//
// r.Route("/", func(r chi.Router) {
// r.Get("/{resource_id}", getResource())
// // other routes...
// }
//
// This test checks how the route pattern is calculated
// "/v1/resources/{resource_id}" (right)
// "/v1/resources/*/{resource_id}" (wrong)
func TestRoutePattern(t *testing.T) {
routePatterns := []string{
"/v1/*",
"/resources/*",
"/{resource_id}",
}
x := &Context{
RoutePatterns: routePatterns,
}
if p := x.RoutePattern(); p != "/v1/resources/{resource_id}" {
t.Fatal("unexpected route pattern: " + p)
}
x.RoutePatterns = []string{
"/v1/*",
"/resources/*",
// Additional wildcard, depending on the router structure of the user
"/*",
"/{resource_id}",
}
// Correctly removes in-the-middle wildcards instead of "/v1/resources/*/{resource_id}"
if p := x.RoutePattern(); p != "/v1/resources/{resource_id}" {
t.Fatal("unexpected route pattern: " + p)
}
x.RoutePatterns = []string{
"/v1/*",
"/resources/*",
// Even with many wildcards
"/*",
"/*",
"/*",
"/{resource_id}/*", // Keeping trailing wildcard
}
// Correctly removes in-the-middle wildcards instead of "/v1/resources/*/*/{resource_id}/*"
if p := x.RoutePattern(); p != "/v1/resources/{resource_id}/*" {
t.Fatal("unexpected route pattern: " + p)
}
x.RoutePatterns = []string{
"/v1/*",
"/resources/*",
// And respects asterisks as part of the paths
"/*special_path/*",
"/with_asterisks*/*",
"/{resource_id}",
}
// Correctly removes in-the-middle wildcards instead of "/v1/resourcesspecial_path/with_asterisks{resource_id}"
if p := x.RoutePattern(); p != "/v1/resources/*special_path/with_asterisks*/{resource_id}" {
t.Fatal("unexpected route pattern: " + p)
}
// Testing for the root route pattern
x.RoutePatterns = []string{"/"}
// It should just return "/" as the pattern
if p := x.RoutePattern(); p != "/" {
t.Fatal("unexpected route pattern for root: " + p)
}
// Testing empty route pattern for nil context
var nilContext *Context
if p := nilContext.RoutePattern(); p != "" {
t.Fatalf("unexpected non-empty route pattern for nil context: %q", p)
}
}
// TestReplaceWildcardsConsecutive ensures multiple consecutive wildcards are
// collapsed into a single slash.
func TestReplaceWildcardsConsecutive(t *testing.T) {
if p := replaceWildcards("/foo/*/*/*/bar"); p != "/foo/bar" {
t.Fatalf("unexpected wildcard replacement: %s", p)
}
if p := replaceWildcards("/foo/*/*/*/bar/*"); p != "/foo/bar/*" {
t.Fatalf("unexpected trailing wildcard behavior: %s", p)
}
}
================================================
FILE: go.mod
================================================
module github.com/go-chi/chi/v5
// Chi supports the four most recent major versions of Go.
// See https://github.com/go-chi/chi/issues/963.
go 1.23
================================================
FILE: middleware/basic_auth.go
================================================
package middleware
import (
"crypto/subtle"
"fmt"
"net/http"
)
// BasicAuth implements a simple middleware handler for adding basic http auth to a route.
func BasicAuth(realm string, creds map[string]string) func(next http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
user, pass, ok := r.BasicAuth()
if !ok {
basicAuthFailed(w, realm)
return
}
credPass, credUserOk := creds[user]
if !credUserOk || subtle.ConstantTimeCompare([]byte(pass), []byte(credPass)) != 1 {
basicAuthFailed(w, realm)
return
}
next.ServeHTTP(w, r)
})
}
}
func basicAuthFailed(w http.ResponseWriter, realm string) {
w.Header().Add("WWW-Authenticate", fmt.Sprintf(`Basic realm="%s"`, realm))
w.WriteHeader(http.StatusUnauthorized)
}
================================================
FILE: middleware/clean_path.go
================================================
package middleware
import (
"net/http"
"path"
"github.com/go-chi/chi/v5"
)
// CleanPath middleware will clean out double slash mistakes from a user's request path.
// For example, if a user requests /users//1 or //users////1 will both be treated as: /users/1
func CleanPath(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
rctx := chi.RouteContext(r.Context())
routePath := rctx.RoutePath
if routePath == "" {
if r.URL.RawPath != "" {
routePath = r.URL.RawPath
} else {
routePath = r.URL.Path
}
rctx.RoutePath = path.Clean(routePath)
}
next.ServeHTTP(w, r)
})
}
================================================
FILE: middleware/compress.go
================================================
package middleware
import (
"bufio"
"compress/flate"
"compress/gzip"
"errors"
"fmt"
"io"
"net"
"net/http"
"strings"
"sync"
)
var defaultCompressibleContentTypes = []string{
"text/html",
"text/css",
"text/plain",
"text/javascript",
"application/javascript",
"application/x-javascript",
"application/json",
"application/atom+xml",
"application/rss+xml",
"image/svg+xml",
}
// Compress is a middleware that compresses response
// body of a given content types to a data format based
// on Accept-Encoding request header. It uses a given
// compression level.
//
// NOTE: make sure to set the Content-Type header on your response
// otherwise this middleware will not compress the response body. For ex, in
// your handler you should set w.Header().Set("Content-Type", http.DetectContentType(yourBody))
// or set it manually.
//
// Passing a compression level of 5 is sensible value
func Compress(level int, types ...string) func(next http.Handler) http.Handler {
compressor := NewCompressor(level, types...)
return compressor.Handler
}
// Compressor represents a set of encoding configurations.
type Compressor struct {
// The mapping of encoder names to encoder functions.
encoders map[string]EncoderFunc
// The mapping of pooled encoders to pools.
pooledEncoders map[string]*sync.Pool
// The set of content types allowed to be compressed.
allowedTypes map[string]struct{}
allowedWildcards map[string]struct{}
// The list of encoders in order of decreasing precedence.
encodingPrecedence []string
level int // The compression level.
}
// NewCompressor creates a new Compressor that will handle encoding responses.
//
// The level should be one of the ones defined in the flate package.
// The types are the content types that are allowed to be compressed.
func NewCompressor(level int, types ...string) *Compressor {
// If types are provided, set those as the allowed types. If none are
// provided, use the default list.
allowedTypes := make(map[string]struct{})
allowedWildcards := make(map[string]struct{})
if len(types) > 0 {
for _, t := range types {
if strings.Contains(strings.TrimSuffix(t, "/*"), "*") {
panic(fmt.Sprintf("middleware/compress: Unsupported content-type wildcard pattern '%s'. Only '/*' supported", t))
}
if before, ok := strings.CutSuffix(t, "/*"); ok {
allowedWildcards[before] = struct{}{}
} else {
allowedTypes[t] = struct{}{}
}
}
} else {
for _, t := range defaultCompressibleContentTypes {
allowedTypes[t] = struct{}{}
}
}
c := &Compressor{
level: level,
encoders: make(map[string]EncoderFunc),
pooledEncoders: make(map[string]*sync.Pool),
allowedTypes: allowedTypes,
allowedWildcards: allowedWildcards,
}
// Set the default encoders. The precedence order uses the reverse
// ordering that the encoders were added. This means adding new encoders
// will move them to the front of the order.
//
// TODO:
// lzma: Opera.
// sdch: Chrome, Android. Gzip output + dictionary header.
// br: Brotli, see https://github.com/go-chi/chi/pull/326
// HTTP 1.1 "deflate" (RFC 2616) stands for DEFLATE data (RFC 1951)
// wrapped with zlib (RFC 1950). The zlib wrapper uses Adler-32
// checksum compared to CRC-32 used in "gzip" and thus is faster.
//
// But.. some old browsers (MSIE, Safari 5.1) incorrectly expect
// raw DEFLATE data only, without the mentioned zlib wrapper.
// Because of this major confusion, most modern browsers try it
// both ways, first looking for zlib headers.
// Quote by Mark Adler: http://stackoverflow.com/a/9186091/385548
//
// The list of browsers having problems is quite big, see:
// http://zoompf.com/blog/2012/02/lose-the-wait-http-compression
// https://web.archive.org/web/20120321182910/http://www.vervestudios.co/projects/compression-tests/results
//
// That's why we prefer gzip over deflate. It's just more reliable
// and not significantly slower than deflate.
c.SetEncoder("deflate", encoderDeflate)
// TODO: Exception for old MSIE browsers that can't handle non-HTML?
// https://zoompf.com/blog/2012/02/lose-the-wait-http-compression
c.SetEncoder("gzip", encoderGzip)
// NOTE: Not implemented, intentionally:
// case "compress": // LZW. Deprecated.
// case "bzip2": // Too slow on-the-fly.
// case "zopfli": // Too slow on-the-fly.
// case "xz": // Too slow on-the-fly.
return c
}
// SetEncoder can be used to set the implementation of a compression algorithm.
//
// The encoding should be a standardised identifier. See:
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Encoding
//
// For example, add the Brotli algorithm:
//
// import brotli_enc "gopkg.in/kothar/brotli-go.v0/enc"
//
// compressor := middleware.NewCompressor(5, "text/html")
// compressor.SetEncoder("br", func(w io.Writer, level int) io.Writer {
// params := brotli_enc.NewBrotliParams()
// params.SetQuality(level)
// return brotli_enc.NewBrotliWriter(params, w)
// })
func (c *Compressor) SetEncoder(encoding string, fn EncoderFunc) {
encoding = strings.ToLower(encoding)
if encoding == "" {
panic("the encoding can not be empty")
}
if fn == nil {
panic("attempted to set a nil encoder function")
}
// If we are adding a new encoder that is already registered, we have to
// clear that one out first.
delete(c.pooledEncoders, encoding)
delete(c.encoders, encoding)
// If the encoder supports Resetting (IoReseterWriter), then it can be pooled.
encoder := fn(io.Discard, c.level)
if _, ok := encoder.(ioResetterWriter); ok {
pool := &sync.Pool{
New: func() interface{} {
return fn(io.Discard, c.level)
},
}
c.pooledEncoders[encoding] = pool
}
// If the encoder is not in the pooledEncoders, add it to the normal encoders.
if _, ok := c.pooledEncoders[encoding]; !ok {
c.encoders[encoding] = fn
}
for i, v := range c.encodingPrecedence {
if v == encoding {
c.encodingPrecedence = append(c.encodingPrecedence[:i], c.encodingPrecedence[i+1:]...)
}
}
c.encodingPrecedence = append([]string{encoding}, c.encodingPrecedence...)
}
// Handler returns a new middleware that will compress the response based on the
// current Compressor.
func (c *Compressor) Handler(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
encoder, encoding, cleanup := c.selectEncoder(r.Header, w)
cw := &compressResponseWriter{
ResponseWriter: w,
w: w,
contentTypes: c.allowedTypes,
contentWildcards: c.allowedWildcards,
encoding: encoding,
compressible: false, // determined in post-handler
}
if encoder != nil {
cw.w = encoder
}
// Re-add the encoder to the pool if applicable.
defer cleanup()
defer cw.Close()
next.ServeHTTP(cw, r)
})
}
// selectEncoder returns the encoder, the name of the encoder, and a closer function.
func (c *Compressor) selectEncoder(h http.Header, w io.Writer) (io.Writer, string, func()) {
header := h.Get("Accept-Encoding")
// Parse the names of all accepted algorithms from the header.
accepted := strings.Split(strings.ToLower(header), ",")
// Find supported encoder by accepted list by precedence
for _, name := range c.encodingPrecedence {
if matchAcceptEncoding(accepted, name) {
if pool, ok := c.pooledEncoders[name]; ok {
encoder := pool.Get().(ioResetterWriter)
cleanup := func() {
pool.Put(encoder)
}
encoder.Reset(w)
return encoder, name, cleanup
}
if fn, ok := c.encoders[name]; ok {
return fn(w, c.level), name, func() {}
}
}
}
// No encoder found to match the accepted encoding
return nil, "", func() {}
}
func matchAcceptEncoding(accepted []string, encoding string) bool {
for _, v := range accepted {
if strings.Contains(v, encoding) {
return true
}
}
return false
}
// An EncoderFunc is a function that wraps the provided io.Writer with a
// streaming compression algorithm and returns it.
//
// In case of failure, the function should return nil.
type EncoderFunc func(w io.Writer, level int) io.Writer
// Interface for types that allow resetting io.Writers.
type ioResetterWriter interface {
io.Writer
Reset(w io.Writer)
}
type compressResponseWriter struct {
http.ResponseWriter
// The streaming encoder writer to be used if there is one. Otherwise,
// this is just the normal writer.
w io.Writer
contentTypes map[string]struct{}
contentWildcards map[string]struct{}
encoding string
wroteHeader bool
compressible bool
}
func (cw *compressResponseWriter) isCompressible() bool {
// Parse the first part of the Content-Type response header.
contentType := cw.Header().Get("Content-Type")
contentType, _, _ = strings.Cut(contentType, ";")
// Is the content type compressible?
if _, ok := cw.contentTypes[contentType]; ok {
return true
}
if contentType, _, hadSlash := strings.Cut(contentType, "/"); hadSlash {
_, ok := cw.contentWildcards[contentType]
return ok
}
return false
}
func (cw *compressResponseWriter) WriteHeader(code int) {
if cw.wroteHeader {
cw.ResponseWriter.WriteHeader(code) // Allow multiple calls to propagate.
return
}
cw.wroteHeader = true
defer cw.ResponseWriter.WriteHeader(code)
// Already compressed data?
if cw.Header().Get("Content-Encoding") != "" {
return
}
if !cw.isCompressible() {
cw.compressible = false
return
}
if cw.encoding != "" {
cw.compressible = true
cw.Header().Set("Content-Encoding", cw.encoding)
cw.Header().Add("Vary", "Accept-Encoding")
// The content-length after compression is unknown
cw.Header().Del("Content-Length")
}
}
func (cw *compressResponseWriter) Write(p []byte) (int, error) {
if !cw.wroteHeader {
cw.WriteHeader(http.StatusOK)
}
return cw.writer().Write(p)
}
func (cw *compressResponseWriter) writer() io.Writer {
if cw.compressible {
return cw.w
}
return cw.ResponseWriter
}
type compressFlusher interface {
Flush() error
}
func (cw *compressResponseWriter) Flush() {
if f, ok := cw.writer().(http.Flusher); ok {
f.Flush()
}
// If the underlying writer has a compression flush signature,
// call this Flush() method instead
if f, ok := cw.writer().(compressFlusher); ok {
f.Flush()
// Also flush the underlying response writer
if f, ok := cw.ResponseWriter.(http.Flusher); ok {
f.Flush()
}
}
}
func (cw *compressResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
if hj, ok := cw.writer().(http.Hijacker); ok {
return hj.Hijack()
}
return nil, nil, errors.New("chi/middleware: http.Hijacker is unavailable on the writer")
}
func (cw *compressResponseWriter) Push(target string, opts *http.PushOptions) error {
if ps, ok := cw.writer().(http.Pusher); ok {
return ps.Push(target, opts)
}
return errors.New("chi/middleware: http.Pusher is unavailable on the writer")
}
func (cw *compressResponseWriter) Close() error {
if c, ok := cw.writer().(io.WriteCloser); ok {
return c.Close()
}
return errors.New("chi/middleware: io.WriteCloser is unavailable on the writer")
}
func (cw *compressResponseWriter) Unwrap() http.ResponseWriter {
return cw.ResponseWriter
}
func encoderGzip(w io.Writer, level int) io.Writer {
gw, err := gzip.NewWriterLevel(w, level)
if err != nil {
return nil
}
return gw
}
func encoderDeflate(w io.Writer, level int) io.Writer {
dw, err := flate.NewWriter(w, level)
if err != nil {
return nil
}
return dw
}
================================================
FILE: middleware/compress_test.go
================================================
package middleware
import (
"compress/flate"
"compress/gzip"
"fmt"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/go-chi/chi/v5"
)
func TestCompressor(t *testing.T) {
r := chi.NewRouter()
compressor := NewCompressor(5, "text/html", "text/css")
if len(compressor.encoders) != 0 || len(compressor.pooledEncoders) != 2 {
t.Errorf("gzip and deflate should be pooled")
}
compressor.SetEncoder("nop", func(w io.Writer, _ int) io.Writer {
return w
})
if len(compressor.encoders) != 1 {
t.Errorf("nop encoder should be stored in the encoders map")
}
r.Use(compressor.Handler)
r.Get("/gethtml", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html")
w.Write([]byte("textstring"))
})
r.Get("/getcss", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html")
w.Write([]byte("textstring"))
})
r.Get("/getplain", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html")
w.Write([]byte("textstring"))
})
ts := httptest.NewServer(r)
defer ts.Close()
tests := []struct {
name string
path string
expectedEncoding string
acceptedEncodings []string
}{
{
name: "no expected encodings due to no accepted encodings",
path: "/gethtml",
acceptedEncodings: nil,
expectedEncoding: "",
},
{
name: "no expected encodings due to content type",
path: "/getplain",
acceptedEncodings: nil,
expectedEncoding: "",
},
{
name: "gzip is only encoding",
path: "/gethtml",
acceptedEncodings: []string{"gzip"},
expectedEncoding: "gzip",
},
{
name: "gzip is preferred over deflate",
path: "/getcss",
acceptedEncodings: []string{"gzip", "deflate"},
expectedEncoding: "gzip",
},
{
name: "deflate is used",
path: "/getcss",
acceptedEncodings: []string{"deflate"},
expectedEncoding: "deflate",
},
{
name: "nop is preferred",
path: "/getcss",
acceptedEncodings: []string{"nop, gzip, deflate"},
expectedEncoding: "nop",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
resp, respString := testRequestWithAcceptedEncodings(t, ts, "GET", tc.path, tc.acceptedEncodings...)
if respString != "textstring" {
t.Errorf("response text doesn't match; expected:%q, got:%q", "textstring", respString)
}
if got := resp.Header.Get("Content-Encoding"); got != tc.expectedEncoding {
t.Errorf("expected encoding %q but got %q", tc.expectedEncoding, got)
}
})
}
}
func TestCompressorWildcards(t *testing.T) {
tests := []struct {
name string
recover string
types []string
typesCount int
wcCount int
}{
{
name: "defaults",
typesCount: 10,
},
{
name: "no wildcard",
types: []string{"text/plain", "text/html"},
typesCount: 2,
},
{
name: "invalid wildcard #1",
types: []string{"audio/*wav"},
recover: "middleware/compress: Unsupported content-type wildcard pattern 'audio/*wav'. Only '/*' supported",
},
{
name: "invalid wildcard #2",
types: []string{"application*/*"},
recover: "middleware/compress: Unsupported content-type wildcard pattern 'application*/*'. Only '/*' supported",
},
{
name: "valid wildcard",
types: []string{"text/*"},
wcCount: 1,
},
{
name: "mixed",
types: []string{"audio/wav", "text/*"},
typesCount: 1,
wcCount: 1,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
defer func() {
if tt.recover == "" {
tt.recover = "<nil>"
}
if r := recover(); tt.recover != fmt.Sprintf("%v", r) {
t.Errorf("Unexpected value recovered: %v", r)
}
}()
compressor := NewCompressor(5, tt.types...)
if len(compressor.allowedTypes) != tt.typesCount {
t.Errorf("expected %d allowedTypes, got %d", tt.typesCount, len(compressor.allowedTypes))
}
if len(compressor.allowedWildcards) != tt.wcCount {
t.Errorf("expected %d allowedWildcards, got %d", tt.wcCount, len(compressor.allowedWildcards))
}
})
}
}
func testRequestWithAcceptedEncodings(t *testing.T, ts *httptest.Server, method, path string, encodings ...string) (*http.Response, string) {
req, err := http.NewRequest(method, ts.URL+path, nil)
if err != nil {
t.Fatal(err)
return nil, ""
}
if len(encodings) > 0 {
encodingsString := strings.Join(encodings, ",")
req.Header.Set("Accept-Encoding", encodingsString)
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatal(err)
return nil, ""
}
respBody := decodeResponseBody(t, resp)
defer resp.Body.Close()
return resp, respBody
}
func decodeResponseBody(t *testing.T, resp *http.Response) string {
var reader io.ReadCloser
switch resp.Header.Get("Content-Encoding") {
case "gzip":
var err error
reader, err = gzip.NewReader(resp.Body)
if err != nil {
t.Fatal(err)
}
case "deflate":
reader = flate.NewReader(resp.Body)
default:
reader = resp.Body
}
respBody, err := io.ReadAll(reader)
if err != nil {
t.Fatal(err)
return ""
}
reader.Close()
return string(respBody)
}
================================================
FILE: middleware/content_charset.go
================================================
package middleware
import (
"net/http"
"slices"
"strings"
)
// ContentCharset generates a handler that writes a 415 Unsupported Media Type response if none of the charsets match.
// An empty charset will allow requests with no Content-Type header or no specified charset.
func ContentCharset(charsets ...string) func(next http.Handler) http.Handler {
for i, c := range charsets {
charsets[i] = strings.ToLower(c)
}
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !contentEncoding(r.Header.Get("Content-Type"), charsets...) {
w.WriteHeader(http.StatusUnsupportedMediaType)
return
}
next.ServeHTTP(w, r)
})
}
}
// Check the content encoding against a list of acceptable values.
func contentEncoding(ce string, charsets ...string) bool {
_, ce = split(strings.ToLower(ce), ";")
_, ce = split(ce, "charset=")
ce, _ = split(ce, ";")
return slices.Contains(charsets, ce)
}
// Split a string in two parts, cleaning any whitespace.
func split(str, sep string) (string, string) {
a, b, found := strings.Cut(str, sep)
a = strings.TrimSpace(a)
if found {
b = strings.TrimSpace(b)
}
return a, b
}
================================================
FILE: middleware/content_charset_test.go
================================================
package middleware
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/go-chi/chi/v5"
)
func TestContentCharset(t *testing.T) {
t.Parallel()
var tests = []struct {
name string
inputValue string
inputContentCharset []string
want int
}{
{
"should accept requests with a matching charset",
"application/json; charset=UTF-8",
[]string{"UTF-8"},
http.StatusOK,
},
{
"should be case-insensitive",
"application/json; charset=utf-8",
[]string{"UTF-8"},
http.StatusOK,
},
{
"should accept requests with a matching charset with extra values",
"application/json; foo=bar; charset=UTF-8; spam=eggs",
[]string{"UTF-8"},
http.StatusOK,
},
{
"should accept requests with a matching charset when multiple charsets are supported",
"text/xml; charset=UTF-8",
[]string{"UTF-8", "Latin-1"},
http.StatusOK,
},
{
"should accept requests with no charset if empty charset headers are allowed",
"text/xml",
[]string{"UTF-8", ""},
http.StatusOK,
},
{
"should not accept requests with no charset if empty charset headers are not allowed",
"text/xml",
[]string{"UTF-8"},
http.StatusUnsupportedMediaType,
},
{
"should not accept requests with a mismatching charset",
"text/plain; charset=Latin-1",
[]string{"UTF-8"},
http.StatusUnsupportedMediaType,
},
{
"should not accept requests with a mismatching charset even if empty charsets are allowed",
"text/plain; charset=Latin-1",
[]string{"UTF-8", ""},
http.StatusUnsupportedMediaType,
},
}
for _, tt := range tests {
var tt = tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
var recorder = httptest.NewRecorder()
var r = chi.NewRouter()
r.Use(ContentCharset(tt.inputContentCharset...))
r.Get("/", func(w http.ResponseWriter, r *http.Request) {})
var req, _ = http.NewRequest("GET", "/", nil)
req.Header.Set("Content-Type", tt.inputValue)
r.ServeHTTP(recorder, req)
var res = recorder.Result()
if res.StatusCode != tt.want {
t.Errorf("response is incorrect, got %d, want %d", recorder.Code, tt.want)
}
})
}
}
func TestSplit(t *testing.T) {
t.Parallel()
var s1, s2 = split(" type1;type2 ", ";")
if s1 != "type1" || s2 != "type2" {
t.Errorf("Want type1, type2 got %s, %s", s1, s2)
}
s1, s2 = split("type1 ", ";")
if s1 != "type1" {
t.Errorf("Want \"type1\" got \"%s\"", s1)
}
if s2 != "" {
t.Errorf("Want empty string got \"%s\"", s2)
}
}
func TestContentEncoding(t *testing.T) {
t.Parallel()
if !contentEncoding("application/json; foo=bar; charset=utf-8; spam=eggs", []string{"utf-8"}...) {
t.Error("Want true, got false")
}
if contentEncoding("text/plain; charset=latin-1", []string{"utf-8"}...) {
t.Error("Want false, got true")
}
if !contentEncoding("text/xml; charset=UTF-8", []string{"latin-1", "utf-8"}...) {
t.Error("Want true, got false")
}
}
================================================
FILE: middleware/content_encoding.go
================================================
package middleware
import (
"net/http"
"strings"
)
// AllowContentEncoding enforces a whitelist of request Content-Encoding otherwise responds
// with a 415 Unsupported Media Type status.
func AllowContentEncoding(contentEncoding ...string) func(next http.Handler) http.Handler {
allowedEncodings := make(map[string]struct{}, len(contentEncoding))
for _, encoding := range contentEncoding {
allowedEncodings[strings.TrimSpace(strings.ToLower(encoding))] = struct{}{}
}
return func(next http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
requestEncodings := r.Header["Content-Encoding"]
// skip check for empty content body or no Content-Encoding
if r.ContentLength == 0 {
next.ServeHTTP(w, r)
return
}
// All encodings in the request must be allowed
for _, encoding := range requestEncodings {
if _, ok := allowedEncodings[strings.TrimSpace(strings.ToLower(encoding))]; !ok {
w.WriteHeader(http.StatusUnsupportedMediaType)
return
}
}
next.ServeHTTP(w, r)
}
return http.HandlerFunc(fn)
}
}
================================================
FILE: middleware/content_encoding_test.go
================================================
package middleware
import (
"bytes"
"net/http"
"net/http/httptest"
"testing"
"github.com/go-chi/chi/v5"
)
func TestContentEncodingMiddleware(t *testing.T) {
t.Parallel()
// support for:
// Content-Encoding: gzip
// Content-Encoding: deflate
// Content-Encoding: gzip, deflate
// Content-Encoding: deflate, gzip
middleware := AllowContentEncoding("deflate", "gzip")
tests := []struct {
name string
encodings []string
expectedStatus int
}{
{
name: "Support no encoding",
encodings: []string{},
expectedStatus: 200,
},
{
name: "Support gzip encoding",
encodings: []string{"gzip"},
expectedStatus: 200,
},
{
name: "No support for br encoding",
encodings: []string{"br"},
expectedStatus: 415,
},
{
name: "Support for gzip and deflate encoding",
encodings: []string{"gzip", "deflate"},
expectedStatus: 200,
},
{
name: "Support for deflate and gzip encoding",
encodings: []string{"deflate", "gzip"},
expectedStatus: 200,
},
{
name: "No support for deflate and br encoding",
encodings: []string{"deflate", "br"},
expectedStatus: 415,
},
}
for _, tt := range tests {
var tt = tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
body := []byte("This is my content. There are many like this but this one is mine")
r := httptest.NewRequest("POST", "/", bytes.NewReader(body))
for _, encoding := range tt.encodings {
r.Header.Set("Content-Encoding", encoding)
}
w := httptest.NewRecorder()
router := chi.NewRouter()
router.Use(middleware)
router.Post("/", func(w http.ResponseWriter, r *http.Request) {})
router.ServeHTTP(w, r)
res := w.Result()
if res.StatusCode != tt.expectedStatus {
t.Errorf("response is incorrect, got %d, want %d", w.Code, tt.expectedStatus)
}
})
}
}
================================================
FILE: middleware/content_type.go
================================================
package middleware
import (
"net/http"
"strings"
)
// SetHeader is a convenience handler to set a response header key/value
func SetHeader(key, value string) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set(key, value)
next.ServeHTTP(w, r)
})
}
}
// AllowContentType enforces a whitelist of request Content-Types otherwise responds
// with a 415 Unsupported Media Type status.
func AllowContentType(contentTypes ...string) func(http.Handler) http.Handler {
allowedContentTypes := make(map[string]struct{}, len(contentTypes))
for _, ctype := range contentTypes {
allowedContentTypes[strings.TrimSpace(strings.ToLower(ctype))] = struct{}{}
}
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.ContentLength == 0 {
// Skip check for empty content body
next.ServeHTTP(w, r)
return
}
s, _, _ := strings.Cut(r.Header.Get("Content-Type"), ";")
s = strings.ToLower(strings.TrimSpace(s))
if _, ok := allowedContentTypes[s]; ok {
next.ServeHTTP(w, r)
return
}
w.WriteHeader(http.StatusUnsupportedMediaType)
})
}
}
================================================
FILE: middleware/content_type_test.go
================================================
package middleware
import (
"bytes"
"net/http"
"net/http/httptest"
"testing"
"github.com/go-chi/chi/v5"
)
func TestContentType(t *testing.T) {
t.Parallel()
var tests = []struct {
name string
inputValue string
allowedContentTypes []string
want int
}{
{
"should accept requests with a matching content type",
"application/json; charset=UTF-8",
[]string{"application/json"},
http.StatusOK,
},
{
"should accept requests with a matching content type no charset",
"application/json",
[]string{"application/json"},
http.StatusOK,
},
{
"should accept requests with a matching content-type with extra values",
"application/json; foo=bar; charset=UTF-8; spam=eggs",
[]string{"application/json"},
http.StatusOK,
},
{
"should accept requests with a matching content type when multiple content types are supported",
"text/xml; charset=UTF-8",
[]string{"application/json", "text/xml"},
http.StatusOK,
},
{
"should not accept requests with a mismatching content type",
"text/plain; charset=latin-1",
[]string{"application/json"},
http.StatusUnsupportedMediaType,
},
{
"should not accept requests with a mismatching content type even if multiple content types are allowed",
"text/plain; charset=Latin-1",
[]string{"application/json", "text/xml"},
http.StatusUnsupportedMediaType,
},
}
for _, tt := range tests {
var tt = tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
recorder := httptest.NewRecorder()
r := chi.NewRouter()
r.Use(AllowContentType(tt.allowedContentTypes...))
r.Post("/", func(w http.ResponseWriter, r *http.Request) {})
body := []byte("This is my content. There are many like this but this one is mine")
req := httptest.NewRequest("POST", "/", bytes.NewReader(body))
req.Header.Set("Content-Type", tt.inputValue)
r.ServeHTTP(recorder, req)
res := recorder.Result()
if res.StatusCode != tt.want {
t.Errorf("response is incorrect, got %d, want %d", recorder.Code, tt.want)
}
})
}
}
================================================
FILE: middleware/get_head.go
================================================
package middleware
import (
"net/http"
"github.com/go-chi/chi/v5"
)
// GetHead automatically route undefined HEAD requests to GET handlers.
func GetHead(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method == "HEAD" {
rctx := chi.RouteContext(r.Context())
routePath := rctx.RoutePath
if routePath == "" {
if r.URL.RawPath != "" {
routePath = r.URL.RawPath
} else {
routePath = r.URL.Path
}
}
// Temporary routing context to look-ahead before routing the request
tctx := chi.NewRouteContext()
// Attempt to find a HEAD handler for the routing path, if not found, traverse
// the router as through its a GET route, but proceed with the request
// with the HEAD method.
if !rctx.Routes.Match(tctx, "HEAD", routePath) {
rctx.RouteMethod = "GET"
rctx.RoutePath = routePath
next.ServeHTTP(w, r)
return
}
}
next.ServeHTTP(w, r)
})
}
================================================
FILE: middleware/get_head_test.go
================================================
package middleware
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/go-chi/chi/v5"
)
func TestGetHead(t *testing.T) {
r := chi.NewRouter()
r.Use(GetHead)
r.Get("/hi", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("X-Test", "yes")
w.Write([]byte("bye"))
})
r.Route("/articles", func(r chi.Router) {
r.Get("/{id}", func(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "id")
w.Header().Set("X-Article", id)
w.Write([]byte("article:" + id))
})
})
r.Route("/users", func(r chi.Router) {
r.Head("/{id}", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("X-User", "-")
w.Write([]byte("user"))
})
r.Get("/{id}", func(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "id")
w.Header().Set("X-User", id)
w.Write([]byte("user:" + id))
})
})
ts := httptest.NewServer(r)
defer ts.Close()
if _, body := testRequest(t, ts, "GET", "/hi", nil); body != "bye" {
t.Fatal(body)
}
if req, body := testRequest(t, ts, "HEAD", "/hi", nil); body != "" || req.Header.Get("X-Test") != "yes" {
t.Fatal(body)
}
if _, body := testRequest(t, ts, "GET", "/", nil); body != "404 page not found\n" {
t.Fatal(body)
}
if req, body := testRequest(t, ts, "HEAD", "/", nil); body != "" || req.StatusCode != 404 {
t.Fatal(body)
}
if _, body := testRequest(t, ts, "GET", "/articles/5", nil); body != "article:5" {
t.Fatal(body)
}
if req, body := testRequest(t, ts, "HEAD", "/articles/5", nil); body != "" || req.Header.Get("X-Article") != "5" {
t.Fatalf("expecting X-Article header '5' but got '%s'", req.Header.Get("X-Article"))
}
if _, body := testRequest(t, ts, "GET", "/users/1", nil); body != "user:1" {
t.Fatal(body)
}
if req, body := testRequest(t, ts, "HEAD", "/users/1", nil); body != "" || req.Header.Get("X-User") != "-" {
t.Fatalf("expecting X-User header '-' but got '%s'", req.Header.Get("X-User"))
}
}
================================================
FILE: middleware/heartbeat.go
================================================
package middleware
import (
"net/http"
"strings"
)
// Heartbeat endpoint middleware useful to setting up a path like
// `/ping` that load balancers or uptime testing external services
// can make a request before hitting any routes. It's also convenient
// to place this above ACL middlewares as well.
func Heartbeat(endpoint string) func(http.Handler) http.Handler {
f := func(h http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
if (r.Method == "GET" || r.Method == "HEAD") && strings.EqualFold(r.URL.Path, endpoint) {
w.Header().Set("Content-Type", "text/plain")
w.WriteHeader(http.StatusOK)
w.Write([]byte("."))
return
}
h.ServeHTTP(w, r)
}
return http.HandlerFunc(fn)
}
return f
}
================================================
FILE: middleware/logger.go
================================================
package middleware
import (
"bytes"
"context"
"log"
"net/http"
"os"
"runtime"
"time"
)
var (
// LogEntryCtxKey is the context.Context key to store the request log entry.
LogEntryCtxKey = &contextKey{"LogEntry"}
// DefaultLogger is called by the Logger middleware handler to log each request.
// Its made a package-level variable so that it can be reconfigured for custom
// logging configurations.
DefaultLogger func(next http.Handler) http.Handler
)
// Logger is a middleware that logs the start and end of each request, along
// with some useful data about what was requested, what the response status was,
// and how long it took to return. When standard output is a TTY, Logger will
// print in color, otherwise it will print in black and white. Logger prints a
// request ID if one is provided.
//
// Alternatively, look at https://github.com/goware/httplog for a more in-depth
// http logger with structured logging support.
//
// IMPORTANT NOTE: Logger should go before any other middleware that may change
// the response, such as middleware.Recoverer. Example:
//
// r := chi.NewRouter()
// r.Use(middleware.Logger) // <--<< Logger should come before Recoverer
// r.Use(middleware.Recoverer)
// r.Get("/", handler)
func Logger(next http.Handler) http.Handler {
return DefaultLogger(next)
}
// RequestLogger returns a logger handler using a custom LogFormatter.
func RequestLogger(f LogFormatter) func(next http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
entry := f.NewLogEntry(r)
ww := NewWrapResponseWriter(w, r.ProtoMajor)
t1 := time.Now()
defer func() {
entry.Write(ww.Status(), ww.BytesWritten(), ww.Header(), time.Since(t1), nil)
}()
next.ServeHTTP(ww, WithLogEntry(r, entry))
}
return http.HandlerFunc(fn)
}
}
// LogFormatter initiates the beginning of a new LogEntry per request.
// See DefaultLogFormatter for an example implementation.
type LogFormatter interface {
NewLogEntry(r *http.Request) LogEntry
}
// LogEntry records the final log when a request completes.
// See defaultLogEntry for an example implementation.
type LogEntry interface {
Write(status, bytes int, header http.Header, elapsed time.Duration, extra interface{})
Panic(v interface{}, stack []byte)
}
// GetLogEntry returns the in-context LogEntry for a request.
func GetLogEntry(r *http.Request) LogEntry {
entry, _ := r.Context().Value(LogEntryCtxKey).(LogEntry)
return entry
}
// WithLogEntry sets the in-context LogEntry for a request.
func WithLogEntry(r *http.Request, entry LogEntry) *http.Request {
r = r.WithContext(context.WithValue(r.Context(), LogEntryCtxKey, entry))
return r
}
// LoggerInterface accepts printing to stdlib logger or compatible logger.
type LoggerInterface interface {
Print(v ...interface{})
}
// DefaultLogFormatter is a simple logger that implements a LogFormatter.
type DefaultLogFormatter struct {
Logger LoggerInterface
NoColor bool
}
// NewLogEntry creates a new LogEntry for the request.
func (l *DefaultLogFormatter) NewLogEntry(r *http.Request) LogEntry {
useColor := !l.NoColor
entry := &defaultLogEntry{
DefaultLogFormatter: l,
request: r,
buf: &bytes.Buffer{},
useColor: useColor,
}
reqID := GetReqID(r.Context())
if reqID != "" {
cW(entry.buf, useColor, nYellow, "[%s] ", reqID)
}
cW(entry.buf, useColor, nCyan, "\"")
cW(entry.buf, useColor, bMagenta, "%s ", r.Method)
scheme := "http"
if r.TLS != nil {
scheme = "https"
}
cW(entry.buf, useColor, nCyan, "%s://%s%s %s\" ", scheme, r.Host, r.RequestURI, r.Proto)
entry.buf.WriteString("from ")
entry.buf.WriteString(r.RemoteAddr)
entry.buf.WriteString(" - ")
return entry
}
type defaultLogEntry struct {
*DefaultLogFormatter
request *http.Request
buf *bytes.Buffer
useColor bool
}
func (l *defaultLogEntry) Write(status, bytes int, header http.Header, elapsed time.Duration, extra interface{}) {
switch {
case status < 200:
cW(l.buf, l.useColor, bBlue, "%03d", status)
case status < 300:
cW(l.buf, l.useColor, bGreen, "%03d", status)
case status < 400:
cW(l.buf, l.useColor, bCyan, "%03d", status)
case status < 500:
cW(l.buf, l.useColor, bYellow, "%03d", status)
default:
cW(l.buf, l.useColor, bRed, "%03d", status)
}
cW(l.buf, l.useColor, bBlue, " %dB", bytes)
l.buf.WriteString(" in ")
if elapsed < 500*time.Millisecond {
cW(l.buf, l.useColor, nGreen, "%s", elapsed)
} else if elapsed < 5*time.Second {
cW(l.buf, l.useColor, nYellow, "%s", elapsed)
} else {
cW(l.buf, l.useColor, nRed, "%s", elapsed)
}
l.Logger.Print(l.buf.String())
}
func (l *defaultLogEntry) Panic(v interface{}, stack []byte) {
PrintPrettyStack(v)
}
func init() {
color := true
if runtime.GOOS == "windows" {
color = false
}
DefaultLogger = RequestLogger(&DefaultLogFormatter{Logger: log.New(os.Stdout, "", log.LstdFlags), NoColor: !color})
}
================================================
FILE: middleware/logger_test.go
================================================
package middleware
import (
"bufio"
"bytes"
"net"
"net/http"
"net/http/httptest"
"testing"
"time"
)
type testLoggerWriter struct {
*httptest.ResponseRecorder
}
func (cw testLoggerWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
return nil, nil, nil
}
func TestRequestLogger(t *testing.T) {
testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, ok := w.(http.Hijacker)
if !ok {
t.Errorf("http.Hijacker is unavailable on the writer. add the interface methods.")
}
})
r := httptest.NewRequest("GET", "/", nil)
w := testLoggerWriter{
ResponseRecorder: httptest.NewRecorder(),
}
handler := DefaultLogger(testHandler)
handler.ServeHTTP(w, r)
}
func TestRequestLoggerReadFrom(t *testing.T) {
data := []byte("file data")
testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.ServeContent(w, r, "file", time.Time{}, bytes.NewReader(data))
})
r := httptest.NewRequest("GET", "/", nil)
w := httptest.NewRecorder()
handler := DefaultLogger(testHandler)
handler.ServeHTTP(w, r)
assertEqual(t, data, w.Body.Bytes())
}
================================================
FILE: middleware/maybe.go
================================================
package middleware
import "net/http"
// Maybe middleware will allow you to change the flow of the middleware stack execution depending on return
// value of maybeFn(request). This is useful for example if you'd like to skip a middleware handler if
// a request does not satisfy the maybeFn logic.
func Maybe(mw func(http.Handler) http.Handler, maybeFn func(r *http.Request) bool) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if maybeFn(r) {
mw(next).ServeHTTP(w, r)
} else {
next.ServeHTTP(w, r)
}
})
}
}
================================================
FILE: middleware/middleware.go
================================================
package middleware
import "net/http"
// New will create a new middleware handler from a http.Handler.
func New(h http.Handler) func(next http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
h.ServeHTTP(w, r)
})
}
}
// contextKey is a value for use with context.WithValue. It's used as
// a pointer so it fits in an interface{} without allocation. This technique
// for defining context keys was copied from Go 1.7's new use of context in net/http.
type contextKey struct {
name string
}
func (k *contextKey) String() string {
return "chi/middleware context value " + k.name
}
================================================
FILE: middleware/middleware_test.go
================================================
package middleware
import (
"crypto/tls"
"io"
"net/http"
"net/http/httptest"
"path"
"reflect"
"runtime"
"testing"
"time"
)
var testdataDir string
func init() {
_, filename, _, _ := runtime.Caller(0)
testdataDir = path.Join(path.Dir(filename), "/../testdata")
}
func TestWrapWriterHTTP2(t *testing.T) {
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Proto != "HTTP/2.0" {
t.Fatalf("request proto should be HTTP/2.0 but was %s", r.Proto)
}
_, fl := w.(http.Flusher)
if !fl {
t.Fatal("request should have been a http.Flusher")
}
_, hj := w.(http.Hijacker)
if hj {
t.Fatal("request should not have been a http.Hijacker")
}
_, rf := w.(io.ReaderFrom)
if rf {
t.Fatal("request should not have been an io.ReaderFrom")
}
_, ps := w.(http.Pusher)
if !ps {
t.Fatal("request should have been a http.Pusher")
}
w.Write([]byte("OK"))
})
wmw := func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
next.ServeHTTP(NewWrapResponseWriter(w, r.ProtoMajor), r)
})
}
server := http.Server{
Addr: ":7072",
Handler: wmw(handler),
}
// By serving over TLS, we get HTTP2 requests
go server.ListenAndServeTLS(testdataDir+"/cert.pem", testdataDir+"/key.pem")
defer server.Close()
// We need the server to start before making the request
time.Sleep(100 * time.Millisecond)
client := &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
// The certificates we are using are self signed
InsecureSkipVerify: true,
},
ForceAttemptHTTP2: true,
},
}
resp, err := client.Get("https://localhost:7072")
if err != nil {
t.Fatalf("could not get server: %v", err)
}
if resp.StatusCode != 200 {
t.Fatalf("non 200 response: %v", resp.StatusCode)
}
}
func testRequest(t *testing.T, ts *httptest.Server, method, path string, body io.Reader) (*http.Response, string) {
req, err := http.NewRequest(method, ts.URL+path, body)
if err != nil {
t.Fatal(err)
return nil, ""
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatal(err)
return nil, ""
}
respBody, err := io.ReadAll(resp.Body)
if err != nil {
t.Fatal(err)
return nil, ""
}
defer resp.Body.Close()
return resp, string(respBody)
}
func testRequestNoRedirect(t *testing.T, ts *httptest.Server, method, path string, body io.Reader) (*http.Response, string) {
req, err := http.NewRequest(method, ts.URL+path, body)
if err != nil {
t.Fatal(err)
return nil, ""
}
// http client that doesn't redirect
httpClient := &http.Client{
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
}
resp, err := httpClient.Do(req)
if err != nil {
t.Fatal(err)
return nil, ""
}
respBody, err := io.ReadAll(resp.Body)
if err != nil {
t.Fatal(err)
return nil, ""
}
defer resp.Body.Close()
return resp, string(respBody)
}
func assertNoError(t *testing.T, err error) {
t.Helper()
if err != nil {
t.Fatalf("expecting no error")
}
}
func assertError(t *testing.T, err error) {
t.Helper()
if err == nil {
t.Fatalf("expecting error")
}
}
func assertEqual(t *testing.T, a, b interface{}) {
t.Helper()
if !reflect.DeepEqual(a, b) {
t.Fatalf("expecting values to be equal but got: '%v' and '%v'", a, b)
}
}
================================================
FILE: middleware/nocache.go
================================================
package middleware
// Ported from Goji's middleware, source:
// https://github.com/zenazn/goji/tree/master/web/middleware
import (
"net/http"
"time"
)
// Unix epoch time
var epoch = time.Unix(0, 0).UTC().Format(http.TimeFormat)
// Taken from https://github.com/mytrile/nocache
var noCacheHeaders = map[string]string{
"Expires": epoch,
"Cache-Control": "no-cache, no-store, no-transform, must-revalidate, private, max-age=0",
"Pragma": "no-cache",
"X-Accel-Expires": "0",
}
var etagHeaders = []string{
"ETag",
"If-Modified-Since",
"If-Match",
"If-None-Match",
"If-Range",
"If-Unmodified-Since",
}
// NoCache is a simple piece of middleware that sets a number of HTTP headers to prevent
// a router (or subrouter) from being cached by an upstream proxy and/or client.
//
// As per http://wiki.nginx.org/HttpProxyModule - NoCache sets:
//
// Expires: Thu, 01 Jan 1970 00:00:00 UTC
// Cache-Control: no-cache, private, max-age=0
// X-Accel-Expires: 0
// Pragma: no-cache (for HTTP/1.0 proxies/clients)
func NoCache(h http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
// Delete any ETag headers that may have been set
for _, v := range etagHeaders {
if r.Header.Get(v) != "" {
r.Header.Del(v)
}
}
// Set our NoCache headers
for k, v := range noCacheHeaders {
w.Header().Set(k, v)
}
h.ServeHTTP(w, r)
}
return http.HandlerFunc(fn)
}
================================================
FILE: middleware/page_route.go
================================================
package middleware
import (
"net/http"
"strings"
)
// PageRoute is a simple middleware which allows you to route a static GET request
// at the middleware stack level.
func PageRoute(path string, handler http.Handler) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method == "GET" && strings.EqualFold(r.URL.Path, path) {
handler.ServeHTTP(w, r)
return
}
next.ServeHTTP(w, r)
})
}
}
================================================
FILE: middleware/path_rewrite.go
================================================
package middleware
import (
"net/http"
"strings"
)
// PathRewrite is a simple middleware which allows you to rewrite the request URL path.
func PathRewrite(old, new string) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
r.URL.Path = strings.Replace(r.URL.Path, old, new, 1)
next.ServeHTTP(w, r)
})
}
}
================================================
FILE: middleware/profiler.go
================================================
//go:build !tinygo
// +build !tinygo
package middleware
import (
"expvar"
"net/http"
"net/http/pprof"
"github.com/go-chi/chi/v5"
)
// Profiler is a convenient subrouter used for mounting net/http/pprof. ie.
//
// func MyService() http.Handler {
// r := chi.NewRouter()
// // ..middlewares
// r.Mount("/debug", middleware.Profiler())
// // ..routes
// return r
// }
func Profiler() http.Handler {
r := chi.NewRouter()
r.Use(NoCache)
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, r.RequestURI+"/pprof/", http.StatusMovedPermanently)
})
r.HandleFunc("/pprof", func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, r.RequestURI+"/", http.StatusMovedPermanently)
})
r.HandleFunc("/pprof/*", pprof.Index)
r.HandleFunc("/pprof/cmdline", pprof.Cmdline)
r.HandleFunc("/pprof/profile", pprof.Profile)
r.HandleFunc("/pprof/symbol", pprof.Symbol)
r.HandleFunc("/pprof/trace", pprof.Trace)
r.Handle("/vars", expvar.Handler())
r.Handle("/pprof/goroutine", pprof.Handler("goroutine"))
r.Handle("/pprof/threadcreate", pprof.Handler("threadcreate"))
r.Handle("/pprof/mutex", pprof.Handler("mutex"))
r.Handle("/pprof/heap", pprof.Handler("heap"))
r.Handle("/pprof/block", pprof.Handler("block"))
r.Handle("/pprof/allocs", pprof.Handler("allocs"))
return r
}
================================================
FILE: middleware/realip.go
================================================
package middleware
// Ported from Goji's middleware, source:
// https://github.com/zenazn/goji/tree/master/web/middleware
import (
"net"
"net/http"
"strings"
)
var trueClientIP = http.CanonicalHeaderKey("True-Client-IP")
var xForwardedFor = http.CanonicalHeaderKey("X-Forwarded-For")
var xRealIP = http.CanonicalHeaderKey("X-Real-IP")
// RealIP is a middleware that sets a http.Request's RemoteAddr to the results
// of parsing either the True-Client-IP, X-Real-IP or the X-Forwarded-For headers
// (in that order).
//
// This middleware should be inserted fairly early in the middleware stack to
// ensure that subsequent layers (e.g., request loggers) which examine the
// RemoteAddr will see the intended value.
//
// You should only use this middleware if you can trust the headers passed to
// you (in particular, the three headers this middleware uses), for example
// because you have placed a reverse proxy like HAProxy or nginx in front of
// chi. If your reverse proxies are configured to pass along arbitrary header
// values from the client, or if you use this middleware without a reverse
// proxy, malicious clients will be able to make you very sad (or, depending on
// how you're using RemoteAddr, vulnerable to an attack of some sort).
func RealIP(h http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
if rip := realIP(r); rip != "" {
r.RemoteAddr = rip
}
h.ServeHTTP(w, r)
}
return http.HandlerFunc(fn)
}
func realIP(r *http.Request) string {
var ip string
if tcip := r.Header.Get(trueClientIP); tcip != "" {
ip = tcip
} else if xrip := r.Header.Get(xRealIP); xrip != "" {
ip = xrip
} else if xff := r.Header.Get(xForwardedFor); xff != "" {
ip, _, _ = strings.Cut(xff, ",")
}
if ip == "" || net.ParseIP(ip) == nil {
return ""
}
return ip
}
================================================
FILE: middleware/realip_test.go
================================================
package middleware
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/go-chi/chi/v5"
)
func TestXRealIP(t *testing.T) {
req, _ := http.NewRequest("GET", "/", nil)
req.Header.Add("X-Real-IP", "100.100.100.100")
w := httptest.NewRecorder()
r := chi.NewRouter()
r.Use(RealIP)
realIP := ""
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
realIP = r.RemoteAddr
w.Write([]byte("Hello World"))
})
r.ServeHTTP(w, req)
if w.Code != 200 {
t.Fatal("Response Code should be 200")
}
if realIP != "100.100.100.100" {
t.Fatal("Test get real IP error.")
}
}
func TestXForwardForIP(t *testing.T) {
xForwardedForIPs := []string{
"100.100.100.100",
"100.100.100.100, 200.200.200.200",
"100.100.100.100,200.200.200.200",
}
r := chi.NewRouter()
r.Use(RealIP)
for _, v := range xForwardedForIPs {
req, _ := http.NewRequest("GET", "/", nil)
req.Header.Add("X-Forwarded-For", v)
w := httptest.NewRecorder()
realIP := ""
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
realIP = r.RemoteAddr
w.Write([]byte("Hello World"))
})
r.ServeHTTP(w, req)
if w.Code != 200 {
t.Fatal("Response Code should be 200")
}
if realIP != "100.100.100.100" {
t.Fatal("Test get real IP error.")
}
}
}
func TestXForwardForXRealIPPrecedence(t *testing.T) {
req, _ := http.NewRequest("GET", "/", nil)
req.Header.Add("X-Forwarded-For", "0.0.0.0")
req.Header.Add("X-Real-IP", "100.100.100.100")
w := httptest.NewRecorder()
r := chi.NewRouter()
r.Use(RealIP)
realIP := ""
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
realIP = r.RemoteAddr
w.Write([]byte("Hello World"))
})
r.ServeHTTP(w, req)
if w.Code != 200 {
t.Fatal("Response Code should be 200")
}
if realIP != "100.100.100.100" {
t.Fatal("Test get real IP precedence error.")
}
}
func TestInvalidIP(t *testing.T) {
req, _ := http.NewRequest("GET", "/", nil)
req.Header.Add("X-Real-IP", "100.100.100.1000")
w := httptest.NewRecorder()
r := chi.NewRouter()
r.Use(RealIP)
realIP := ""
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
realIP = r.RemoteAddr
w.Write([]byte("Hello World"))
})
r.ServeHTTP(w, req)
if w.Code != 200 {
t.Fatal("Response Code should be 200")
}
if realIP != "" {
t.Fatal("Invalid IP used.")
}
}
================================================
FILE: middleware/recoverer.go
================================================
package middleware
// The original work was derived from Goji's middleware, source:
// https://github.com/zenazn/goji/tree/master/web/middleware
import (
"bytes"
"errors"
"fmt"
"io"
"net/http"
"os"
"runtime/debug"
"strings"
)
// Recoverer is a middleware that recovers from panics, logs the panic (and a
// backtrace), and returns a HTTP 500 (Internal Server Error) status if
// possible. Recoverer prints a request ID if one is provided.
//
// Alternatively, look at https://github.com/go-chi/httplog middleware pkgs.
func Recoverer(next http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
defer func() {
if rvr := recover(); rvr != nil {
if rvr == http.ErrAbortHandler {
// we don't recover http.ErrAbortHandler so the response
// to the client is aborted, this should not be logged
panic(rvr)
}
logEntry := GetLogEntry(r)
if logEntry != nil {
logEntry.Panic(rvr, debug.Stack())
} else {
PrintPrettyStack(rvr)
}
if r.Header.Get("Connection") != "Upgrade" {
w.WriteHeader(http.StatusInternalServerError)
}
}
}()
next.ServeHTTP(w, r)
}
return http.HandlerFunc(fn)
}
// for ability to test the PrintPrettyStack function
var recovererErrorWriter io.Writer = os.Stderr
func PrintPrettyStack(rvr interface{}) {
debugStack := debug.Stack()
s := prettyStack{}
out, err := s.parse(debugStack, rvr)
if err == nil {
recovererErrorWriter.Write(out)
} else {
// print stdlib output as a fallback
os.Stderr.Write(debugStack)
}
}
type prettyStack struct {
}
func (s prettyStack) parse(debugStack []byte, rvr interface{}) ([]byte, error) {
var err error
useColor := true
buf := &bytes.Buffer{}
cW(buf, false, bRed, "\n")
cW(buf, useColor, bCyan, " panic: ")
cW(buf, useColor, bBlue, "%v", rvr)
cW(buf, false, bWhite, "\n \n")
// process debug stack info
stack := strings.Split(string(debugStack), "\n")
lines := []string{}
// locate panic line, as we may have nested panics
for i := len(stack) - 1; i > 0; i-- {
lines = append(lines, stack[i])
if strings.HasPrefix(stack[i], "panic(") {
lines = lines[0 : len(lines)-2] // remove boilerplate
break
}
}
// reverse
for i := len(lines)/2 - 1; i >= 0; i-- {
opp := len(lines) - 1 - i
lines[i], lines[opp] = lines[opp], lines[i]
}
// decorate
for i, line := range lines {
lines[i], err = s.decorateLine(line, useColor, i)
if err != nil {
return nil, err
}
}
for _, l := range lines {
fmt.Fprintf(buf, "%s", l)
}
return buf.Bytes(), nil
}
func (s prettyStack) decorateLine(line string, useColor bool, num int) (string, error) {
line = strings.TrimSpace(line)
if strings.HasPrefix(line, "\t") || strings.Contains(line, ".go:") {
return s.decorateSourceLine(line, useColor, num)
}
if strings.HasSuffix(line, ")") {
return s.decorateFuncCallLine(line, useColor, num)
}
if strings.HasPrefix(line, "\t") {
return strings.Replace(line, "\t", " ", 1), nil
}
return fmt.Sprintf(" %s\n", line), nil
}
func (s prettyStack) decorateFuncCallLine(line string, useColor bool, num int) (string, error) {
idx := strings.LastIndex(line, "(")
if idx < 0 {
return "", errors.New("not a func call line")
}
buf := &bytes.Buffer{}
pkg := line[0:idx]
// addr := line[idx:]
method := ""
if idx := strings.LastIndex(pkg, string(os.PathSeparator)); idx < 0 {
if idx := strings.Index(pkg, "."); idx > 0 {
method = pkg[idx:]
pkg = pkg[0:idx]
}
} else {
method = pkg[idx+1:]
pkg = pkg[0 : idx+1]
if idx := strings.Index(method, "."); idx > 0 {
pkg += method[0:idx]
method = method[idx:]
}
}
pkgColor := nYellow
methodColor := bGreen
if num == 0 {
cW(buf, useColor, bRed, " -> ")
pkgColor = bMagenta
methodColor = bRed
} else {
cW(buf, useColor, bWhite, " ")
}
cW(buf, useColor, pkgColor, "%s", pkg)
cW(buf, useColor, methodColor, "%s\n", method)
// cW(buf, useColor, nBlack, "%s", addr)
return buf.String(), nil
}
func (s prettyStack) decorateSourceLine(line string, useColor bool, num int) (string, error) {
idx := strings.LastIndex(line, ".go:")
if idx < 0 {
return "", errors.New("not a source line")
}
buf := &bytes.Buffer{}
path := line[0 : idx+3]
lineno := line[idx+3:]
idx = strings.LastIndex(path, string(os.PathSeparator))
dir := path[0 : idx+1]
file := path[idx+1:]
idx = strings.Index(lineno, " ")
if idx > 0 {
lineno = lineno[0:idx]
}
fileColor := bCyan
lineColor := bGreen
if num == 1 {
cW(buf, useColor, bRed, " -> ")
fileColor = bRed
lineColor = bMagenta
} else {
cW(buf, false, bWhite, " ")
}
cW(buf, useColor, bWhite, "%s", dir)
cW(buf, useColor, fileColor, "%s", file)
cW(buf, useColor, lineColor, "%s", lineno)
if num == 1 {
cW(buf, false, bWhite, "\n")
}
cW(buf, false, bWhite, "\n")
return buf.String(), nil
}
================================================
FILE: middleware/recoverer_test.go
================================================
package middleware
import (
"bytes"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/go-chi/chi/v5"
)
func panickingHandler(http.ResponseWriter, *http.Request) { panic("foo") }
func TestRecoverer(t *testing.T) {
r := chi.NewRouter()
oldRecovererErrorWriter := recovererErrorWriter
defer func() { recovererErrorWriter = oldRecovererErrorWriter }()
buf := &bytes.Buffer{}
recovererErrorWriter = buf
r.Use(Recoverer)
r.Get("/", panickingHandler)
ts := httptest.NewServer(r)
defer ts.Close()
res, _ := testRequest(t, ts, "GET", "/", nil)
assertEqual(t, res.StatusCode, http.StatusInternalServerError)
lines := strings.Split(buf.String(), "\n")
for _, line := range lines {
if strings.HasPrefix(strings.TrimSpace(line), "->") {
if !strings.Contains(line, "panickingHandler") {
t.Fatalf("First func call line should refer to panickingHandler, but actual line:\n%v\n", line)
}
return
}
}
t.Fatal("First func call line should start with ->.")
}
func TestRecovererAbortHandler(t *testing.T) {
defer func() {
rcv := recover()
if rcv != http.ErrAbortHandler {
t.Fatalf("http.ErrAbortHandler should not be recovered")
}
}()
w := httptest.NewRecorder()
r := chi.NewRouter()
r.Use(Recoverer)
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
panic(http.ErrAbortHandler)
})
req, err := http.NewRequest("GET", "/", nil)
if err != nil {
t.Fatal(err)
}
r.ServeHTTP(w, req)
}
================================================
FILE: middleware/request_id.go
================================================
package middleware
// Ported from Goji's middleware, source:
// https://github.com/zenazn/goji/tree/master/web/middleware
import (
"context"
"crypto/rand"
"encoding/base64"
"fmt"
"net/http"
"os"
"strings"
"sync/atomic"
)
// Key to use when setting the request ID.
type ctxKeyRequestID int
// RequestIDKey is the key that holds the unique request ID in a request context.
const RequestIDKey ctxKeyRequestID = 0
// RequestIDHeader is the name of the HTTP Header which contains the request id.
// Exported so that it can be changed by developers
var RequestIDHeader = "X-Request-Id"
var prefix string
var reqid atomic.Uint64
// A quick note on the statistics here: we're trying to calculate the chance that
// two randomly generated base62 prefixes will collide. We use the formula from
// http://en.wikipedia.org/wiki/Birthday_problem
//
// P[m, n] \approx 1 - e^{-m^2/2n}
//
// We ballpark an upper bound for $m$ by imagining (for whatever reason) a server
// that restarts every second over 10 years, for $m = 86400 * 365 * 10 = 315360000$
//
// For a $k$ character base-62 identifier, we have $n(k) = 62^k$
//
// Plugging this in, we find $P[m, n(10)] \approx 5.75%$, which is good enough for
// our purposes, and is surely more than anyone would ever need in practice -- a
// process that is rebooted a handful of times a day for a hundred years has less
// than a millionth of a percent chance of generating two colliding IDs.
func init() {
hostname, err := os.Hostname()
if hostname == "" || err != nil {
hostname = "localhost"
}
var buf [12]byte
var b64 string
for len(b64) < 10 {
rand.Read(buf[:])
b64 = base64.StdEncoding.EncodeToString(buf[:])
b64 = strings.NewReplacer("+", "", "/", "").Replace(b64)
}
prefix = fmt.Sprintf("%s/%s", hostname, b64[0:10])
}
// RequestID is a middleware that injects a request ID into the context of each
// request. A request ID is a string of the form "host.example.com/random-0001",
// where "random" is a base62 random string that uniquely identifies this go
// process, and where the last number is an atomically incremented request
// counter.
func RequestID(next http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
requestID := r.Header.Get(RequestIDHeader)
if requestID == "" {
myid := reqid.Add(1)
requestID = fmt.Sprintf("%s-%06d", prefix, myid)
}
ctx = context.WithValue(ctx, RequestIDKey, requestID)
next.ServeHTTP(w, r.WithContext(ctx))
}
return http.HandlerFunc(fn)
}
// GetReqID returns a request ID from the given context if one is present.
// Returns the empty string if a request ID cannot be found.
func GetReqID(ctx context.Context) string {
if ctx == nil {
return ""
}
if reqID, ok := ctx.Value(RequestIDKey).(string); ok {
return reqID
}
return ""
}
// NextRequestID generates the next request ID in the sequence.
func NextRequestID() uint64 {
return reqid.Add(1)
}
================================================
FILE: middleware/request_id_test.go
================================================
package middleware
import (
"fmt"
"net/http"
"net/http/httptest"
"testing"
"github.com/go-chi/chi/v5"
)
func maintainDefaultRequestID() func() {
original := RequestIDHeader
return func() {
RequestIDHeader = original
}
}
func TestRequestID(t *testing.T) {
tests := map[string]struct {
requestIDHeader string
request func() *http.Request
expectedResponse string
}{
"Retrieves Request Id from default header": {
"X-Request-Id",
func() *http.Request {
req, _ := http.NewRequest("GET", "/", nil)
req.Header.Add("X-Request-Id", "req-123456")
return req
},
"RequestID: req-123456",
},
"Retrieves Request Id from custom header": {
"X-Trace-Id",
func() *http.Request {
req, _ := http.NewRequest("GET", "/", nil)
req.Header.Add("X-Trace-Id", "trace:abc123")
return req
},
"RequestID: trace:abc123",
},
}
defer maintainDefaultRequestID()()
for _, test := range tests {
w := httptest.NewRecorder()
r := chi.NewRouter()
RequestIDHeader = test.requestIDHeader
r.Use(RequestID)
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
requestID := GetReqID(r.Context())
response := fmt.Sprintf("RequestID: %s", requestID)
w.Write([]byte(response))
})
r.ServeHTTP(w, test.request())
if w.Body.String() != test.expectedResponse {
t.Fatalf("RequestID was not the expected value")
}
}
}
================================================
FILE: middleware/request_size.go
================================================
package middleware
import (
"net/http"
)
// RequestSize is a middleware that will limit request sizes to a specified
// number of bytes. It uses MaxBytesReader to do so.
func RequestSize(bytes int64) func(http.Handler) http.Handler {
f := func(h http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, bytes)
h.ServeHTTP(w, r)
}
return http.HandlerFunc(fn)
}
return f
}
================================================
FILE: middleware/route_headers.go
================================================
package middleware
import (
"net/http"
"strings"
)
// RouteHeaders is a neat little header-based router that allows you to direct
// the flow of a request through a middleware stack based on a request header.
//
// For example, lets say you'd like to setup multiple routers depending on the
// request Host header, you could then do something as so:
//
// r := chi.NewRouter()
// rSubdomain := chi.NewRouter()
// r.Use(middleware.RouteHeaders().
// Route("Host", "example.com", middleware.New(r)).
// Route("Host", "*.example.com", middleware.New(rSubdomain)).
// Handler)
// r.Get("/", h)
// rSubdomain.Get("/", h2)
//
// Another example, imagine you want to setup multiple CORS handlers, where for
// your origin servers you allow authorized requests, but for third-party public
// requests, authorization is disabled.
//
// r := chi.NewRouter()
// r.Use(middleware.RouteHeaders().
// Route("Origin", "https://app.skyweaver.net", cors.Handler(cors.Options{
// AllowedOrigins: []string{"https://api.skyweaver.net"},
// AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
// AllowedHeaders: []string{"Accept", "Authorization", "Content-Type"},
// AllowCredentials: true, // <----------<<< allow credentials
// })).
// Route("Origin", "*", cors.Handler(cors.Options{
// AllowedOrigins: []string{"*"},
// AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
// AllowedHeaders: []string{"Accept", "Content-Type"},
// AllowCredentials: false, // <----------<<< do not allow credentials
// })).
// Handler)
func RouteHeaders() HeaderRouter {
return HeaderRouter{}
}
type HeaderRouter map[string][]HeaderRoute
func (hr HeaderRouter) Route(header, match string, middlewareHandler func(next http.Handler) http.Handler) HeaderRouter {
header = strings.ToLower(header)
k := hr[header]
if k == nil {
hr[header] = []HeaderRoute{}
}
hr[header] = append(hr[header], HeaderRoute{MatchOne: NewPattern(match), Middleware: middlewareHandler})
return hr
}
func (hr HeaderRouter) RouteAny(header string, match []string, middlewareHandler func(next http.Handler) http.Handler) HeaderRouter {
header = strings.ToLower(header)
k := hr[header]
if k == nil {
hr[header] = []HeaderRoute{}
}
patterns := []Pattern{}
for _, m := range match {
patterns = append(patterns, NewPattern(m))
}
hr[header] = append(hr[header], HeaderRoute{MatchAny: patterns, Middleware: middlewareHandler})
return hr
}
func (hr HeaderRouter) RouteDefault(handler func(next http.Handler) http.Handler) HeaderRouter {
hr["*"] = []HeaderRoute{{Middleware: handler}}
return hr
}
func (hr HeaderRouter) Handler(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if len(hr) == 0 {
// skip if no routes set
next.ServeHTTP(w, r)
return
}
// find first matching header route, and continue
for header, matchers := range hr {
headerValue := r.Header.Get(header)
if headerValue == "" {
continue
}
headerValue = strings.ToLower(headerValue)
for _, matcher := range matchers {
if matcher.IsMatch(headerValue) {
matcher.Middleware(next).ServeHTTP(w, r)
return
}
}
}
// if no match, check for "*" default route
matcher, ok := hr["*"]
if !ok || matcher[0].Middleware == nil {
next.ServeHTTP(w, r)
return
}
matcher[0].Middleware(next).ServeHTTP(w, r)
})
}
type HeaderRoute struct {
Middleware func(next http.Handler) http.Handler
MatchOne Pattern
MatchAny []Pattern
}
func (r HeaderRoute) IsMatch(value string) bool {
if len(r.MatchAny) > 0 {
for _, m := range r.MatchAny {
if m.Match(value) {
return true
}
}
} else if r.MatchOne.Match(value) {
return true
}
return false
}
type Pattern struct {
prefix string
suffix string
wildcard bool
}
func NewPattern(value string) Pattern {
p := Pattern{}
p.prefix, p.suffix, p.wildcard = strings.Cut(value, "*")
return p
}
func (p Pattern) Match(v string) bool {
if !p.wildcard {
return p.prefix == v
}
return len(v) >= len(p.prefix+p.suffix) && strings.HasPrefix(v, p.prefix) && strings.HasSuffix(v, p.suffix)
}
================================================
FILE: middleware/route_headers_test.go
================================================
package middleware
import (
"net/http"
"net/http/httptest"
"sync/atomic"
"testing"
)
func TestRouteHeaders(t *testing.T) {
t.Run("empty router should call next handler exactly once", func(t *testing.T) {
var callCount atomic.Int32
hr := RouteHeaders()
handler := hr.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
callCount.Add(1)
w.WriteHeader(http.StatusOK)
}))
req := httptest.NewRequest("GET", "/", nil)
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if callCount.Load() != 1 {
t.Errorf("expected next handler to be called exactly once, but was called %d times", callCount.Load())
}
})
t.Run("matching header should route to correct middleware", func(t *testing.T) {
var matchedRoute string
hr := RouteHeaders().
Route("Host", "example.com", func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
matchedRoute = "example.com"
next.ServeHTTP(w, r)
})
}).
Route("Host", "other.com", func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
matchedRoute = "other.com"
next.ServeHTTP(w, r)
})
})
handler := hr.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}))
req := httptest.NewRequest("GET", "/", nil)
req.Host = "example.com"
req.Header.Set("Host", "example.com")
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if matchedRoute != "example.com" {
t.Errorf("expected matched route to be 'example.com', got '%s'", matchedRoute)
}
})
t.Run("wildcard pattern should match", func(t *testing.T) {
var matched bool
hr := RouteHeaders().
Route("Host", "*.example.com", func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
matched = true
next.ServeHTTP(w, r)
})
})
handler := hr.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}))
req := httptest.NewRequest("GET", "/", nil)
req.Header.Set("Host", "api.example.com")
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if !matched {
t.Error("expected wildcard pattern to match")
}
})
t.Run("default route should be used when no match", func(t *testing.T) {
var usedDefault bool
hr := RouteHeaders().
Route("Host", "example.com", func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
next.ServeHTTP(w, r)
})
}).
RouteDefault(func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
usedDefault = true
next.ServeHTTP(w, r)
})
})
handler := hr.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}))
req := httptest.NewRequest("GET", "/", nil)
req.Header.Set("Host", "other.com")
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if !usedDefault {
t.Error("expected default route to be used when no match")
}
})
t.Run("RouteAny should match any of the provided patterns", func(t *testing.T) {
var matched bool
hr := RouteHeaders().
RouteAny("Content-Type", []string{"application/json", "application/xml"}, func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
matched = true
next.ServeHTTP(w, r)
})
})
handler := hr.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}))
// Test with application/json
req := httptest.NewRequest("POST", "/", nil)
req.Header.Set("Content-Type", "application/json")
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if !matched {
t.Error("expected RouteAny to match 'application/json'")
}
// Reset and test with application/xml
matched = false
req = httptest.NewRequest("POST", "/", nil)
req.Header.Set("Content-Type", "application/xml")
rec = httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if !matched {
t.Error("expected RouteAny to match 'application/xml'")
}
})
t.Run("no match and no default should call next handler", func(t *testing.T) {
var nextCalled bool
hr := RouteHeaders().
Route("Host", "example.com", func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
next.ServeHTTP(w, r)
})
})
handler := hr.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
nextCalled = true
w.WriteHeader(http.StatusOK)
}))
req := httptest.NewRequest("GET", "/", nil)
req.Header.Set("Host", "other.com")
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if !nextCalled {
t.Error("expected next handler to be called when no match and no default")
}
})
}
func TestPattern(t *testing.T) {
tests := []struct {
pattern string
value string
expected bool
}{
{"example.com", "example.com", true},
{"example.com", "other.com", false},
{"*.example.com", "api.example.com", true},
{"*.example.com", "example.com", false},
{"api.*", "api.example.com", true},
{"*", "anything", true},
{"prefix*suffix", "prefixmiddlesuffix", true},
{"prefix*suffix", "prefixsuffix", true},
{"prefix*suffix", "wrongmiddlesuffix", false},
}
for _, tt := range tests {
t.Run(tt.pattern+"_"+tt.value, func(t *testing.T) {
p := NewPattern(tt.pattern)
if got := p.Match(tt.value); got != tt.expected {
t.Errorf("Pattern(%q).Match(%q) = %v, want %v", tt.pattern, tt.value, got, tt.expected)
}
})
}
}
================================================
FILE: middleware/strip.go
================================================
package middleware
import (
"fmt"
"net/http"
"strings"
"github.com/go-chi/chi/v5"
)
// StripSlashes is a middleware that will match request paths with a trailing
// slash, strip it from the path and continue routing through the mux, if a route
// matches, then it will serve the handler.
func StripSlashes(next http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
var path string
rctx := chi.RouteContext(r.Context())
if rctx != nil && rctx.RoutePath != "" {
path = rctx.RoutePath
} else {
path = r.URL.Path
}
if len(path) > 1 && path[len(path)-1] == '/' {
newPath := path[:len(path)-1]
if rctx == nil {
r.URL.Path = newPath
} else {
rctx.RoutePath = newPath
}
}
next.ServeHTTP(w, r)
}
return http.HandlerFunc(fn)
}
// RedirectSlashes is a middleware that will match request paths with a trailing
// slash and redirect to the same path, less the trailing slash.
//
// NOTE: RedirectSlashes middleware is *incompatible* with http.FileServer,
// see https://github.com/go-chi/chi/issues/343
func RedirectSlashes(next http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
var path string
rctx := chi.RouteContext(r.Context())
if rctx != nil && rctx.RoutePath != "" {
path = rctx.RoutePath
} else {
path = r.URL.Path
}
if len(path) > 1 && path[len(path)-1] == '/' {
// Normalize backslashes to forward slashes to prevent "/\evil.com" style redirects
// that some clients may interpret as protocol-relative.
path = strings.ReplaceAll(path, `\`, `/`)
// Collapse leading/trailing slashes and force a single leading slash.
path := "/" + strings.Trim(path, "/")
if r.URL.RawQuery != "" {
path = fmt.Sprintf("%s?%s", path, r.URL.RawQuery)
}
http.Redirect(w, r, path, 301)
return
}
next.ServeHTTP(w, r)
}
return http.HandlerFunc(fn)
}
// StripPrefix is a middleware that will strip the provided prefix from the
// request path before handing the request over to the next handler.
func StripPrefix(prefix string) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.StripPrefix(prefix, next)
}
}
================================================
FILE: middleware/strip_test.go
================================================
package middleware
import (
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
"github.com/go-chi/chi/v5"
)
func TestStripSlashes(t *testing.T) {
r := chi.NewRouter()
// This middleware must be mounted at the top level of the router, not at the end-handler
// because then it'll be too late and will end up in a 404
r.Use(StripSlashes)
r.NotFound(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(404)
w.Write([]byte("nothing here"))
})
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("root"))
})
r.Route("/accounts/{accountID}", func(r chi.Router) {
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
accountID := chi.URLParam(r, "accountID")
w.Write([]byte(accountID))
})
})
ts := httptest.NewServer(r)
defer ts.Close()
if _, resp := testRequest(t, ts, "GET", "/", nil); resp != "root" {
t.Fatal(resp)
}
if _, resp := testRequest(t, ts, "GET", "//", nil); resp != "root" {
t.Fatal(resp)
}
if _, resp := testRequest(t, ts, "GET", "/accounts/admin", nil); resp != "admin" {
t.Fatal(resp)
}
if _, resp := testRequest(t, ts, "GET", "/accounts/admin/", nil); resp != "admin" {
t.Fatal(resp)
}
if _, resp := testRequest(t, ts, "GET", "/nothing-here", nil); resp != "nothing here" {
t.Fatal(resp)
}
}
func TestStripSlashesInRoute(t *testing.T) {
r := chi.NewRouter()
r.NotFound(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(404)
w.Write([]byte("nothing here"))
})
r.Get("/hi", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("hi"))
})
r.Route("/accounts/{accountID}", func(r chi.Router) {
r.Use(StripSlashes)
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("accounts index"))
})
r.Get("/query", func(w http.ResponseWriter, r *http.Request) {
accountID := chi.URLParam(r, "accountID")
w.Write([]byte(accountID))
})
})
ts := httptest.NewServer(r)
defer ts.Close()
if _, resp := testRequest(t, ts, "GET", "/hi", nil); resp != "hi" {
t.Fatal(resp)
}
if _, resp := testRequest(t, ts, "GET", "/hi/", nil); resp != "nothing here" {
t.Fatal(resp)
}
if _, resp := testRequest(t, ts, "GET", "/accounts/admin", nil); resp != "accounts index" {
t.Fatal(resp)
}
if _, resp := testRequest(t, ts, "GET", "/accounts/admin/", nil); resp != "accounts index" {
t.Fatal(resp)
}
if _, resp := testRequest(t, ts, "GET", "/accounts/admin/query", nil); resp != "admin" {
t.Fatal(resp)
}
if _, resp := testRequest(t, ts, "GET", "/accounts/admin/query/", nil); resp != "admin" {
t.Fatal(resp)
}
}
func TestRedirectSlashes(t *testing.T) {
r := chi.NewRouter()
// This middleware must be mounted at the top level of the router, not at the end-handler
// because then it'll be too late and will end up in a 404
r.Use(RedirectSlashes)
r.NotFound(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(404)
w.Write([]byte("nothing here"))
})
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("root"))
})
r.Route("/accounts/{accountID}", func(r chi.Router) {
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
accountID := chi.URLParam(r, "accountID")
w.Write([]byte(accountID))
})
})
ts := httptest.NewServer(r)
defer ts.Close()
if resp, body := testRequest(t, ts, "GET", "/", nil); body != "root" || resp.StatusCode != 200 {
t.Fatal(body, resp.StatusCode)
}
// NOTE: the testRequest client will follow the redirection.
gitextract_uo24njyf/ ├── .github/ │ ├── FUNDING.yml │ └── workflows/ │ └── ci.yml ├── .gitignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── README.md ├── SECURITY.md ├── _examples/ │ ├── README.md │ ├── custom-handler/ │ │ └── main.go │ ├── custom-method/ │ │ └── main.go │ ├── fileserver/ │ │ ├── data/ │ │ │ └── notes.txt │ │ └── main.go │ ├── graceful/ │ │ └── main.go │ ├── hello-world/ │ │ └── main.go │ ├── limits/ │ │ └── main.go │ ├── logging/ │ │ └── main.go │ ├── pathvalue/ │ │ └── main.go │ ├── rest/ │ │ ├── go.mod │ │ ├── go.sum │ │ ├── main.go │ │ ├── routes.json │ │ └── routes.md │ ├── router-walk/ │ │ └── main.go │ ├── todos-resource/ │ │ ├── main.go │ │ ├── todos.go │ │ └── users.go │ └── versions/ │ ├── data/ │ │ ├── article.go │ │ └── errors.go │ ├── go.mod │ ├── go.sum │ ├── main.go │ └── presenter/ │ ├── v1/ │ │ └── article.go │ ├── v2/ │ │ └── article.go │ └── v3/ │ └── article.go ├── chain.go ├── chi.go ├── context.go ├── context_test.go ├── go.mod ├── middleware/ │ ├── basic_auth.go │ ├── clean_path.go │ ├── compress.go │ ├── compress_test.go │ ├── content_charset.go │ ├── content_charset_test.go │ ├── content_encoding.go │ ├── content_encoding_test.go │ ├── content_type.go │ ├── content_type_test.go │ ├── get_head.go │ ├── get_head_test.go │ ├── heartbeat.go │ ├── logger.go │ ├── logger_test.go │ ├── maybe.go │ ├── middleware.go │ ├── middleware_test.go │ ├── nocache.go │ ├── page_route.go │ ├── path_rewrite.go │ ├── profiler.go │ ├── realip.go │ ├── realip_test.go │ ├── recoverer.go │ ├── recoverer_test.go │ ├── request_id.go │ ├── request_id_test.go │ ├── request_size.go │ ├── route_headers.go │ ├── route_headers_test.go │ ├── strip.go │ ├── strip_test.go │ ├── sunset.go │ ├── sunset_test.go │ ├── supress_notfound.go │ ├── terminal.go │ ├── throttle.go │ ├── throttle_test.go │ ├── timeout.go │ ├── url_format.go │ ├── url_format_test.go │ ├── value.go │ ├── wrap_writer.go │ └── wrap_writer_test.go ├── mux.go ├── mux_test.go ├── path_value_test.go ├── pattern_test.go ├── testdata/ │ ├── cert.pem │ └── key.pem ├── tree.go └── tree_test.go
SYMBOL INDEX (427 symbols across 74 files)
FILE: _examples/custom-handler/main.go
type Handler (line 10) | type Handler
method ServeHTTP (line 12) | func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
function main (line 20) | func main() {
function customHandler (line 26) | func customHandler(w http.ResponseWriter, r *http.Request) error {
FILE: _examples/custom-method/main.go
function init (line 10) | func init() {
function main (line 16) | func main() {
FILE: _examples/fileserver/main.go
function main (line 28) | func main() {
function FileServer (line 48) | func FileServer(r chi.Router, path string, root http.FileSystem) {
FILE: _examples/graceful/main.go
function main (line 17) | func main() {
function service (line 45) | func service() http.Handler {
FILE: _examples/hello-world/main.go
function main (line 10) | func main() {
FILE: _examples/limits/main.go
function main (line 21) | func main() {
FILE: _examples/logging/main.go
function main (line 7) | func main() {
FILE: _examples/pathvalue/main.go
function main (line 10) | func main() {
function pathValueHandler (line 20) | func pathValueHandler(w http.ResponseWriter, r *http.Request) {
FILE: _examples/rest/main.go
function main (line 55) | func main() {
function ListArticles (line 114) | func ListArticles(w http.ResponseWriter, r *http.Request) {
function ArticleCtx (line 124) | func ArticleCtx(next http.Handler) http.Handler {
function SearchArticles (line 149) | func SearchArticles(w http.ResponseWriter, r *http.Request) {
function CreateArticle (line 155) | func CreateArticle(w http.ResponseWriter, r *http.Request) {
function GetArticle (line 173) | func GetArticle(w http.ResponseWriter, r *http.Request) {
function UpdateArticle (line 186) | func UpdateArticle(w http.ResponseWriter, r *http.Request) {
function DeleteArticle (line 201) | func DeleteArticle(w http.ResponseWriter, r *http.Request) {
function adminRouter (line 219) | func adminRouter() chi.Router {
function AdminOnly (line 235) | func AdminOnly(next http.Handler) http.Handler {
function paginate (line 248) | func paginate(next http.Handler) http.Handler {
function init (line 258) | func init() {
type UserPayload (line 290) | type UserPayload struct
method Bind (line 301) | func (u *UserPayload) Bind(r *http.Request) error {
method Render (line 305) | func (u *UserPayload) Render(w http.ResponseWriter, r *http.Request) e...
function NewUserPayloadResponse (line 295) | func NewUserPayloadResponse(user *User) *UserPayload {
type ArticleRequest (line 319) | type ArticleRequest struct
method Bind (line 327) | func (a *ArticleRequest) Bind(r *http.Request) error {
type ArticleResponse (line 350) | type ArticleResponse struct
method Render (line 372) | func (rd *ArticleResponse) Render(w http.ResponseWriter, r *http.Reque...
function NewArticleResponse (line 360) | func NewArticleResponse(article *Article) *ArticleResponse {
function NewArticleListResponse (line 378) | func NewArticleListResponse(articles []*Article) []render.Renderer {
type ErrResponse (line 401) | type ErrResponse struct
method Render (line 410) | func (e *ErrResponse) Render(w http.ResponseWriter, r *http.Request) e...
function ErrInvalidRequest (line 415) | func ErrInvalidRequest(err error) render.Renderer {
function ErrRender (line 424) | func ErrRender(err error) render.Renderer {
type User (line 440) | type User struct
type Article (line 447) | type Article struct
function dbNewArticle (line 469) | func dbNewArticle(article *Article) (string, error) {
function dbGetArticle (line 475) | func dbGetArticle(id string) (*Article, error) {
function dbGetArticleBySlug (line 484) | func dbGetArticleBySlug(slug string) (*Article, error) {
function dbUpdateArticle (line 493) | func dbUpdateArticle(id string, article *Article) (*Article, error) {
function dbRemoveArticle (line 503) | func dbRemoveArticle(id string) (*Article, error) {
function dbGetUser (line 513) | func dbGetUser(id int64) (*User, error) {
FILE: _examples/router-walk/main.go
function main (line 11) | func main() {
function Ping (line 40) | func Ping(w http.ResponseWriter, r *http.Request) {
FILE: _examples/todos-resource/main.go
function main (line 14) | func main() {
FILE: _examples/todos-resource/todos.go
type todosResource (line 9) | type todosResource struct
method Routes (line 12) | func (rs todosResource) Routes() chi.Router {
method List (line 31) | func (rs todosResource) List(w http.ResponseWriter, r *http.Request) {
method Create (line 35) | func (rs todosResource) Create(w http.ResponseWriter, r *http.Request) {
method Get (line 39) | func (rs todosResource) Get(w http.ResponseWriter, r *http.Request) {
method Update (line 43) | func (rs todosResource) Update(w http.ResponseWriter, r *http.Request) {
method Delete (line 47) | func (rs todosResource) Delete(w http.ResponseWriter, r *http.Request) {
method Sync (line 51) | func (rs todosResource) Sync(w http.ResponseWriter, r *http.Request) {
FILE: _examples/todos-resource/users.go
type usersResource (line 9) | type usersResource struct
method Routes (line 12) | func (rs usersResource) Routes() chi.Router {
method List (line 30) | func (rs usersResource) List(w http.ResponseWriter, r *http.Request) {
method Create (line 34) | func (rs usersResource) Create(w http.ResponseWriter, r *http.Request) {
method Get (line 38) | func (rs usersResource) Get(w http.ResponseWriter, r *http.Request) {
method Update (line 42) | func (rs usersResource) Update(w http.ResponseWriter, r *http.Request) {
method Delete (line 46) | func (rs usersResource) Delete(w http.ResponseWriter, r *http.Request) {
FILE: _examples/versions/data/article.go
type Article (line 4) | type Article struct
FILE: _examples/versions/data/errors.go
function PresentError (line 16) | func PresentError(r *http.Request, err error) (*http.Request, interface{...
FILE: _examples/versions/main.go
function main (line 22) | func main() {
function apiVersionCtx (line 51) | func apiVersionCtx(version string) func(next http.Handler) http.Handler {
function articleRouter (line 60) | func articleRouter() http.Handler {
function listArticles (line 71) | func listArticles(w http.ResponseWriter, r *http.Request) {
function getArticle (line 103) | func getArticle(w http.ResponseWriter, r *http.Request) {
function randomErrorMiddleware (line 142) | func randomErrorMiddleware(next http.Handler) http.Handler {
FILE: _examples/versions/presenter/v1/article.go
type Article (line 10) | type Article struct
method Render (line 16) | func (a *Article) Render(w http.ResponseWriter, r *http.Request) error {
function NewArticleResponse (line 20) | func NewArticleResponse(article *data.Article) *Article {
FILE: _examples/versions/presenter/v2/article.go
type Article (line 11) | type Article struct
method Render (line 23) | func (a *Article) Render(w http.ResponseWriter, r *http.Request) error {
function NewArticleResponse (line 28) | func NewArticleResponse(article *data.Article) *Article {
FILE: _examples/versions/presenter/v3/article.go
type Article (line 12) | type Article struct
method Render (line 25) | func (a *Article) Render(w http.ResponseWriter, r *http.Request) error {
function NewArticleResponse (line 37) | func NewArticleResponse(article *data.Article) *Article {
FILE: chain.go
function Chain (line 6) | func Chain(middlewares ...func(http.Handler) http.Handler) Middlewares {
method Handler (line 12) | func (mws Middlewares) Handler(h http.Handler) http.Handler {
method HandlerFunc (line 18) | func (mws Middlewares) HandlerFunc(h http.HandlerFunc) http.Handler {
type ChainHandler (line 24) | type ChainHandler struct
method ServeHTTP (line 30) | func (c *ChainHandler) ServeHTTP(w http.ResponseWriter, r *http.Reques...
function chain (line 36) | func chain(middlewares []func(http.Handler) http.Handler, endpoint http....
FILE: chi.go
function NewRouter (line 60) | func NewRouter() *Mux {
type Router (line 66) | type Router interface
type Routes (line 118) | type Routes interface
type Middlewares (line 137) | type Middlewares
FILE: context.go
function URLParam (line 10) | func URLParam(r *http.Request, key string) string {
function URLParamFromCtx (line 18) | func URLParamFromCtx(ctx context.Context, key string) string {
function RouteContext (line 27) | func RouteContext(ctx context.Context) *Context {
function NewRouteContext (line 33) | func NewRouteContext() *Context {
type Context (line 45) | type Context struct
method Reset (line 82) | func (x *Context) Reset() {
method URLParam (line 100) | func (x *Context) URLParam(key string) string {
method RoutePattern (line 123) | func (x *Context) RoutePattern() string {
function replaceWildcards (line 139) | func replaceWildcards(p string) string {
type RouteParams (line 147) | type RouteParams struct
method Add (line 152) | func (s *RouteParams) Add(key, value string) {
type contextKey (line 160) | type contextKey struct
method String (line 164) | func (k *contextKey) String() string {
FILE: context_test.go
function TestRoutePattern (line 24) | func TestRoutePattern(t *testing.T) {
function TestReplaceWildcardsConsecutive (line 97) | func TestReplaceWildcardsConsecutive(t *testing.T) {
FILE: middleware/basic_auth.go
function BasicAuth (line 10) | func BasicAuth(realm string, creds map[string]string) func(next http.Han...
function basicAuthFailed (line 30) | func basicAuthFailed(w http.ResponseWriter, realm string) {
FILE: middleware/clean_path.go
function CleanPath (line 12) | func CleanPath(next http.Handler) http.Handler {
FILE: middleware/compress.go
function Compress (line 40) | func Compress(level int, types ...string) func(next http.Handler) http.H...
type Compressor (line 46) | type Compressor struct
method SetEncoder (line 147) | func (c *Compressor) SetEncoder(encoding string, fn EncoderFunc) {
method Handler (line 187) | func (c *Compressor) Handler(next http.Handler) http.Handler {
method selectEncoder (line 211) | func (c *Compressor) selectEncoder(h http.Header, w io.Writer) (io.Wri...
function NewCompressor (line 63) | func NewCompressor(level int, types ...string) *Compressor {
function matchAcceptEncoding (line 240) | func matchAcceptEncoding(accepted []string, encoding string) bool {
type EncoderFunc (line 253) | type EncoderFunc
type ioResetterWriter (line 256) | type ioResetterWriter interface
type compressResponseWriter (line 261) | type compressResponseWriter struct
method isCompressible (line 274) | func (cw *compressResponseWriter) isCompressible() bool {
method WriteHeader (line 290) | func (cw *compressResponseWriter) WriteHeader(code int) {
method Write (line 318) | func (cw *compressResponseWriter) Write(p []byte) (int, error) {
method writer (line 326) | func (cw *compressResponseWriter) writer() io.Writer {
method Flush (line 337) | func (cw *compressResponseWriter) Flush() {
method Hijack (line 353) | func (cw *compressResponseWriter) Hijack() (net.Conn, *bufio.ReadWrite...
method Push (line 360) | func (cw *compressResponseWriter) Push(target string, opts *http.PushO...
method Close (line 367) | func (cw *compressResponseWriter) Close() error {
method Unwrap (line 374) | func (cw *compressResponseWriter) Unwrap() http.ResponseWriter {
type compressFlusher (line 333) | type compressFlusher interface
function encoderGzip (line 378) | func encoderGzip(w io.Writer, level int) io.Writer {
function encoderDeflate (line 386) | func encoderDeflate(w io.Writer, level int) io.Writer {
FILE: middleware/compress_test.go
function TestCompressor (line 16) | func TestCompressor(t *testing.T) {
function TestCompressorWildcards (line 112) | func TestCompressorWildcards(t *testing.T) {
function testRequestWithAcceptedEncodings (line 172) | func testRequestWithAcceptedEncodings(t *testing.T, ts *httptest.Server,...
function decodeResponseBody (line 195) | func decodeResponseBody(t *testing.T, resp *http.Response) string {
FILE: middleware/content_charset.go
function ContentCharset (line 11) | func ContentCharset(charsets ...string) func(next http.Handler) http.Han...
function contentEncoding (line 29) | func contentEncoding(ce string, charsets ...string) bool {
function split (line 37) | func split(str, sep string) (string, string) {
FILE: middleware/content_charset_test.go
function TestContentCharset (line 11) | func TestContentCharset(t *testing.T) {
function TestSplit (line 94) | func TestSplit(t *testing.T) {
function TestContentEncoding (line 113) | func TestContentEncoding(t *testing.T) {
FILE: middleware/content_encoding.go
function AllowContentEncoding (line 10) | func AllowContentEncoding(contentEncoding ...string) func(next http.Hand...
FILE: middleware/content_encoding_test.go
function TestContentEncodingMiddleware (line 12) | func TestContentEncodingMiddleware(t *testing.T) {
FILE: middleware/content_type.go
function SetHeader (line 9) | func SetHeader(key, value string) func(http.Handler) http.Handler {
function AllowContentType (line 20) | func AllowContentType(contentTypes ...string) func(http.Handler) http.Ha...
FILE: middleware/content_type_test.go
function TestContentType (line 12) | func TestContentType(t *testing.T) {
FILE: middleware/get_head.go
function GetHead (line 10) | func GetHead(next http.Handler) http.Handler {
FILE: middleware/get_head_test.go
function TestGetHead (line 11) | func TestGetHead(t *testing.T) {
FILE: middleware/heartbeat.go
function Heartbeat (line 12) | func Heartbeat(endpoint string) func(http.Handler) http.Handler {
FILE: middleware/logger.go
function Logger (line 39) | func Logger(next http.Handler) http.Handler {
function RequestLogger (line 44) | func RequestLogger(f LogFormatter) func(next http.Handler) http.Handler {
type LogFormatter (line 63) | type LogFormatter interface
type LogEntry (line 69) | type LogEntry interface
function GetLogEntry (line 75) | func GetLogEntry(r *http.Request) LogEntry {
function WithLogEntry (line 81) | func WithLogEntry(r *http.Request, entry LogEntry) *http.Request {
type LoggerInterface (line 87) | type LoggerInterface interface
type DefaultLogFormatter (line 92) | type DefaultLogFormatter struct
method NewLogEntry (line 98) | func (l *DefaultLogFormatter) NewLogEntry(r *http.Request) LogEntry {
type defaultLogEntry (line 127) | type defaultLogEntry struct
method Write (line 134) | func (l *defaultLogEntry) Write(status, bytes int, header http.Header,...
method Panic (line 162) | func (l *defaultLogEntry) Panic(v interface{}, stack []byte) {
function init (line 166) | func init() {
FILE: middleware/logger_test.go
type testLoggerWriter (line 13) | type testLoggerWriter struct
method Hijack (line 17) | func (cw testLoggerWriter) Hijack() (net.Conn, *bufio.ReadWriter, erro...
function TestRequestLogger (line 21) | func TestRequestLogger(t *testing.T) {
function TestRequestLoggerReadFrom (line 38) | func TestRequestLoggerReadFrom(t *testing.T) {
FILE: middleware/maybe.go
function Maybe (line 8) | func Maybe(mw func(http.Handler) http.Handler, maybeFn func(r *http.Requ...
FILE: middleware/middleware.go
function New (line 6) | func New(h http.Handler) func(next http.Handler) http.Handler {
type contextKey (line 17) | type contextKey struct
method String (line 21) | func (k *contextKey) String() string {
FILE: middleware/middleware_test.go
function init (line 17) | func init() {
function TestWrapWriterHTTP2 (line 22) | func TestWrapWriterHTTP2(t *testing.T) {
function testRequest (line 82) | func testRequest(t *testing.T, ts *httptest.Server, method, path string,...
function testRequestNoRedirect (line 105) | func testRequestNoRedirect(t *testing.T, ts *httptest.Server, method, pa...
function assertNoError (line 135) | func assertNoError(t *testing.T, err error) {
function assertError (line 142) | func assertError(t *testing.T, err error) {
function assertEqual (line 149) | func assertEqual(t *testing.T, a, b interface{}) {
FILE: middleware/nocache.go
function NoCache (line 40) | func NoCache(h http.Handler) http.Handler {
FILE: middleware/page_route.go
function PageRoute (line 10) | func PageRoute(path string, handler http.Handler) func(http.Handler) htt...
FILE: middleware/path_rewrite.go
function PathRewrite (line 9) | func PathRewrite(old, new string) func(http.Handler) http.Handler {
FILE: middleware/profiler.go
function Profiler (line 23) | func Profiler() http.Handler {
FILE: middleware/realip.go
function RealIP (line 31) | func RealIP(h http.Handler) http.Handler {
function realIP (line 42) | func realIP(r *http.Request) string {
FILE: middleware/realip_test.go
function TestXRealIP (line 11) | func TestXRealIP(t *testing.T) {
function TestXForwardForIP (line 35) | func TestXForwardForIP(t *testing.T) {
function TestXForwardForXRealIPPrecedence (line 68) | func TestXForwardForXRealIPPrecedence(t *testing.T) {
function TestInvalidIP (line 93) | func TestInvalidIP(t *testing.T) {
FILE: middleware/recoverer.go
function Recoverer (line 22) | func Recoverer(next http.Handler) http.Handler {
function PrintPrettyStack (line 54) | func PrintPrettyStack(rvr interface{}) {
type prettyStack (line 66) | type prettyStack struct
method parse (line 69) | func (s prettyStack) parse(debugStack []byte, rvr interface{}) ([]byte...
method decorateLine (line 112) | func (s prettyStack) decorateLine(line string, useColor bool, num int)...
method decorateFuncCallLine (line 126) | func (s prettyStack) decorateFuncCallLine(line string, useColor bool, ...
method decorateSourceLine (line 166) | func (s prettyStack) decorateSourceLine(line string, useColor bool, nu...
FILE: middleware/recoverer_test.go
function panickingHandler (line 13) | func panickingHandler(http.ResponseWriter, *http.Request) { panic("foo") }
function TestRecoverer (line 15) | func TestRecoverer(t *testing.T) {
function TestRecovererAbortHandler (line 44) | func TestRecovererAbortHandler(t *testing.T) {
FILE: middleware/request_id.go
type ctxKeyRequestID (line 18) | type ctxKeyRequestID
constant RequestIDKey (line 21) | RequestIDKey ctxKeyRequestID = 0
function init (line 46) | func init() {
function RequestID (line 67) | func RequestID(next http.Handler) http.Handler {
function GetReqID (line 83) | func GetReqID(ctx context.Context) string {
function NextRequestID (line 94) | func NextRequestID() uint64 {
FILE: middleware/request_id_test.go
function maintainDefaultRequestID (line 12) | func maintainDefaultRequestID() func() {
function TestRequestID (line 20) | func TestRequestID(t *testing.T) {
FILE: middleware/request_size.go
function RequestSize (line 9) | func RequestSize(bytes int64) func(http.Handler) http.Handler {
FILE: middleware/route_headers.go
function RouteHeaders (line 42) | func RouteHeaders() HeaderRouter {
type HeaderRouter (line 46) | type HeaderRouter
method Route (line 48) | func (hr HeaderRouter) Route(header, match string, middlewareHandler f...
method RouteAny (line 58) | func (hr HeaderRouter) RouteAny(header string, match []string, middlew...
method RouteDefault (line 72) | func (hr HeaderRouter) RouteDefault(handler func(next http.Handler) ht...
method Handler (line 77) | func (hr HeaderRouter) Handler(next http.Handler) http.Handler {
type HeaderRoute (line 110) | type HeaderRoute struct
method IsMatch (line 116) | func (r HeaderRoute) IsMatch(value string) bool {
type Pattern (line 129) | type Pattern struct
method Match (line 141) | func (p Pattern) Match(v string) bool {
function NewPattern (line 135) | func NewPattern(value string) Pattern {
FILE: middleware/route_headers_test.go
function TestRouteHeaders (line 10) | func TestRouteHeaders(t *testing.T) {
function TestPattern (line 187) | func TestPattern(t *testing.T) {
FILE: middleware/strip.go
function StripSlashes (line 14) | func StripSlashes(next http.Handler) http.Handler {
function RedirectSlashes (line 41) | func RedirectSlashes(next http.Handler) http.Handler {
function StripPrefix (line 73) | func StripPrefix(prefix string) func(http.Handler) http.Handler {
FILE: middleware/strip_test.go
function TestStripSlashes (line 13) | func TestStripSlashes(t *testing.T) {
function TestStripSlashesInRoute (line 56) | func TestStripSlashesInRoute(t *testing.T) {
function TestRedirectSlashes (line 102) | func TestRedirectSlashes(t *testing.T) {
function TestStripSlashesWithNilContext (line 203) | func TestStripSlashesWithNilContext(t *testing.T) {
function TestStripPrefix (line 241) | func TestStripPrefix(t *testing.T) {
function TestRedirectSlashes_PreventBackslashRelativeOpenRedirect (line 276) | func TestRedirectSlashes_PreventBackslashRelativeOpenRedirect(t *testing...
FILE: middleware/sunset.go
function Sunset (line 11) | func Sunset(sunsetAt time.Time, links ...string) func(http.Handler) http...
FILE: middleware/sunset_test.go
function TestSunset (line 12) | func TestSunset(t *testing.T) {
FILE: middleware/supress_notfound.go
function SupressNotFound (line 15) | func SupressNotFound(router *chi.Mux) func(next http.Handler) http.Handl...
FILE: middleware/terminal.go
function init (line 37) | func init() {
function cW (line 55) | func cW(w io.Writer, useColor bool, color []byte, s string, args ...inte...
FILE: middleware/throttle.go
constant errCapacityExceeded (line 10) | errCapacityExceeded = "Server capacity exceeded."
constant errTimedOut (line 11) | errTimedOut = "Timed out while waiting for a pending request to ...
constant errContextCanceled (line 12) | errContextCanceled = "Context was canceled."
type ThrottleOpts (line 20) | type ThrottleOpts struct
function Throttle (line 32) | func Throttle(limit int) func(http.Handler) http.Handler {
function ThrottleBacklog (line 39) | func ThrottleBacklog(limit, backlogLimit int, backlogTimeout time.Durati...
function ThrottleWithOpts (line 44) | func ThrottleWithOpts(opts ThrottleOpts) func(http.Handler) http.Handler {
type token (line 134) | type token struct
type throttler (line 137) | type throttler struct
method setRetryAfterHeaderIfNeeded (line 146) | func (t throttler) setRetryAfterHeaderIfNeeded(w http.ResponseWriter, ...
FILE: middleware/throttle_test.go
function TestThrottleBacklog (line 17) | func TestThrottleBacklog(t *testing.T) {
function TestThrottleClientTimeout (line 58) | func TestThrottleClientTimeout(t *testing.T) {
function TestThrottleTriggerGatewayTimeout (line 90) | func TestThrottleTriggerGatewayTimeout(t *testing.T) {
function TestThrottleMaximum (line 144) | func TestThrottleMaximum(t *testing.T) {
function TestThrottleRetryAfter (line 202) | func TestThrottleRetryAfter(t *testing.T) {
function TestThrottleCustomStatusCode (line 264) | func TestThrottleCustomStatusCode(t *testing.T) {
function BenchmarkThrottle (line 315) | func BenchmarkThrottle(b *testing.B) {
FILE: middleware/timeout.go
function Timeout (line 32) | func Timeout(timeout time.Duration) func(next http.Handler) http.Handler {
FILE: middleware/url_format.go
function URLFormat (line 46) | func URLFormat(next http.Handler) http.Handler {
FILE: middleware/url_format_test.go
function TestURLFormat (line 11) | func TestURLFormat(t *testing.T) {
function TestURLFormatInSubRouter (line 52) | func TestURLFormatInSubRouter(t *testing.T) {
FILE: middleware/value.go
function WithValue (line 9) | func WithValue(key, val interface{}) func(next http.Handler) http.Handler {
FILE: middleware/wrap_writer.go
function NewWrapResponseWriter (line 15) | func NewWrapResponseWriter(w http.ResponseWriter, protoMajor int) WrapRe...
type WrapResponseWriter (line 48) | type WrapResponseWriter interface
type basicWriter (line 73) | type basicWriter struct
method WriteHeader (line 82) | func (b *basicWriter) WriteHeader(code int) {
method Write (line 96) | func (b *basicWriter) Write(buf []byte) (n int, err error) {
method maybeWriteHeader (line 116) | func (b *basicWriter) maybeWriteHeader() {
method Status (line 122) | func (b *basicWriter) Status() int {
method BytesWritten (line 126) | func (b *basicWriter) BytesWritten() int {
method Tee (line 130) | func (b *basicWriter) Tee(w io.Writer) {
method Unwrap (line 134) | func (b *basicWriter) Unwrap() http.ResponseWriter {
method Discard (line 138) | func (b *basicWriter) Discard() {
type flushWriter (line 143) | type flushWriter struct
method Flush (line 147) | func (f *flushWriter) Flush() {
type hijackWriter (line 156) | type hijackWriter struct
method Hijack (line 160) | func (f *hijackWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
type flushHijackWriter (line 168) | type flushHijackWriter struct
method Flush (line 172) | func (f *flushHijackWriter) Flush() {
method Hijack (line 178) | func (f *flushHijackWriter) Hijack() (net.Conn, *bufio.ReadWriter, err...
type httpFancyWriter (line 190) | type httpFancyWriter struct
method Flush (line 194) | func (f *httpFancyWriter) Flush() {
method Hijack (line 200) | func (f *httpFancyWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
method ReadFrom (line 209) | func (f *httpFancyWriter) ReadFrom(r io.Reader) (int64, error) {
type http2FancyWriter (line 231) | type http2FancyWriter struct
method Push (line 205) | func (f *http2FancyWriter) Push(target string, opts *http.PushOptions)...
method Flush (line 235) | func (f *http2FancyWriter) Flush() {
FILE: middleware/wrap_writer_test.go
function TestHttpFancyWriterRemembersWroteHeaderWhenFlushed (line 10) | func TestHttpFancyWriterRemembersWroteHeaderWhenFlushed(t *testing.T) {
function TestHttp2FancyWriterRemembersWroteHeaderWhenFlushed (line 19) | func TestHttp2FancyWriterRemembersWroteHeaderWhenFlushed(t *testing.T) {
function TestBasicWritesTeesWritesWithoutDiscard (line 28) | func TestBasicWritesTeesWritesWithoutDiscard(t *testing.T) {
function TestBasicWriterDiscardsWritesToOriginalResponseWriter (line 48) | func TestBasicWriterDiscardsWritesToOriginalResponseWriter(t *testing.T) {
FILE: mux.go
type Mux (line 21) | type Mux struct
method ServeHTTP (line 63) | func (mx *Mux) ServeHTTP(w http.ResponseWriter, r *http.Request) {
method Use (line 100) | func (mx *Mux) Use(middlewares ...func(http.Handler) http.Handler) {
method Handle (line 109) | func (mx *Mux) Handle(pattern string, handler http.Handler) {
method HandleFunc (line 121) | func (mx *Mux) HandleFunc(pattern string, handlerFn http.HandlerFunc) {
method Method (line 127) | func (mx *Mux) Method(method, pattern string, handler http.Handler) {
method MethodFunc (line 137) | func (mx *Mux) MethodFunc(method, pattern string, handlerFn http.Handl...
method Connect (line 143) | func (mx *Mux) Connect(pattern string, handlerFn http.HandlerFunc) {
method Delete (line 149) | func (mx *Mux) Delete(pattern string, handlerFn http.HandlerFunc) {
method Get (line 155) | func (mx *Mux) Get(pattern string, handlerFn http.HandlerFunc) {
method Head (line 161) | func (mx *Mux) Head(pattern string, handlerFn http.HandlerFunc) {
method Options (line 167) | func (mx *Mux) Options(pattern string, handlerFn http.HandlerFunc) {
method Patch (line 173) | func (mx *Mux) Patch(pattern string, handlerFn http.HandlerFunc) {
method Post (line 179) | func (mx *Mux) Post(pattern string, handlerFn http.HandlerFunc) {
method Put (line 185) | func (mx *Mux) Put(pattern string, handlerFn http.HandlerFunc) {
method Trace (line 191) | func (mx *Mux) Trace(pattern string, handlerFn http.HandlerFunc) {
method NotFound (line 197) | func (mx *Mux) NotFound(handlerFn http.HandlerFunc) {
method MethodNotAllowed (line 217) | func (mx *Mux) MethodNotAllowed(handlerFn http.HandlerFunc) {
method With (line 236) | func (mx *Mux) With(middlewares ...func(http.Handler) http.Handler) Ro...
method Group (line 262) | func (mx *Mux) Group(fn func(r Router)) Router {
method Route (line 272) | func (mx *Mux) Route(pattern string, fn func(r Router)) Router {
method Mount (line 289) | func (mx *Mux) Mount(pattern string, handler http.Handler) {
method Routes (line 344) | func (mx *Mux) Routes() []Route {
method Middlewares (line 349) | func (mx *Mux) Middlewares() Middlewares {
method Match (line 359) | func (mx *Mux) Match(rctx *Context, method, path string) bool {
method Find (line 368) | func (mx *Mux) Find(rctx *Context, method, path string) string {
method NotFoundHandler (line 398) | func (mx *Mux) NotFoundHandler() http.HandlerFunc {
method MethodNotAllowedHandler (line 407) | func (mx *Mux) MethodNotAllowedHandler(methodsAllowed ...methodTyp) ht...
method handle (line 416) | func (mx *Mux) handle(method methodTyp, pattern string, handler http.H...
method routeHTTP (line 441) | func (mx *Mux) routeHTTP(w http.ResponseWriter, r *http.Request) {
method nextRoutePath (line 487) | func (mx *Mux) nextRoutePath(rctx *Context) string {
method updateSubRoutes (line 497) | func (mx *Mux) updateSubRoutes(fn func(subMux *Mux)) {
method updateRouteHandler (line 511) | func (mx *Mux) updateRouteHandler() {
function NewMux (line 52) | func NewMux() *Mux {
function methodNotAllowedHandler (line 518) | func methodNotAllowedHandler(methodsAllowed ...methodTyp) func(w http.Re...
FILE: mux_test.go
function TestMuxBasic (line 16) | func TestMuxBasic(t *testing.T) {
function TestMuxMounts (line 208) | func TestMuxMounts(t *testing.T) {
function TestMuxPlain (line 245) | func TestMuxPlain(t *testing.T) {
function TestMuxEmptyRoutes (line 266) | func TestMuxEmptyRoutes(t *testing.T) {
function TestMuxTrailingSlash (line 285) | func TestMuxTrailingSlash(t *testing.T) {
function TestMuxNestedNotFound (line 316) | func TestMuxNestedNotFound(t *testing.T) {
function TestMethodNotAllowed (line 394) | func TestMethodNotAllowed(t *testing.T) {
function TestMuxNestedMethodNotAllowed (line 431) | func TestMuxNestedMethodNotAllowed(t *testing.T) {
function TestMuxComplicatedNotFound (line 497) | func TestMuxComplicatedNotFound(t *testing.T) {
function TestMuxWith (line 589) | func TestMuxWith(t *testing.T) {
function TestMuxHandlePatternValidation (line 642) | func TestMuxHandlePatternValidation(t *testing.T) {
function TestRouterFromMuxWith (line 734) | func TestRouterFromMuxWith(t *testing.T) {
function TestMuxMiddlewareStack (line 754) | func TestMuxMiddlewareStack(t *testing.T) {
function TestMuxRouteGroups (line 829) | func TestMuxRouteGroups(t *testing.T) {
function TestMuxBig (line 885) | func TestMuxBig(t *testing.T) {
function bigMux (line 956) | func bigMux() Router {
function TestMuxSubroutesBasic (line 1096) | func TestMuxSubroutesBasic(t *testing.T) {
function TestMuxSubroutes (line 1177) | func TestMuxSubroutes(t *testing.T) {
function TestSingleHandler (line 1292) | func TestSingleHandler(t *testing.T) {
function TestServeHTTPExistingContext (line 1336) | func TestServeHTTPExistingContext(t *testing.T) {
function TestNestedGroups (line 1392) | func TestNestedGroups(t *testing.T) {
function TestMiddlewarePanicOnLateUse (line 1448) | func TestMiddlewarePanicOnLateUse(t *testing.T) {
function TestMountingExistingPath (line 1470) | func TestMountingExistingPath(t *testing.T) {
function TestMountingSimilarPattern (line 1485) | func TestMountingSimilarPattern(t *testing.T) {
function TestMuxEmptyParams (line 1512) | func TestMuxEmptyParams(t *testing.T) {
function TestMuxMissingParams (line 1532) | func TestMuxMissingParams(t *testing.T) {
function TestMuxWildcardRoute (line 1554) | func TestMuxWildcardRoute(t *testing.T) {
function TestMuxWildcardRouteCheckTwo (line 1567) | func TestMuxWildcardRouteCheckTwo(t *testing.T) {
function TestMuxRegexp (line 1580) | func TestMuxRegexp(t *testing.T) {
function TestMuxRegexp2 (line 1596) | func TestMuxRegexp2(t *testing.T) {
function TestMuxRegexp3 (line 1612) | func TestMuxRegexp3(t *testing.T) {
function TestMuxSubrouterWildcardParam (line 1653) | func TestMuxSubrouterWildcardParam(t *testing.T) {
function TestMuxContextIsThreadSafe (line 1685) | func TestMuxContextIsThreadSafe(t *testing.T) {
function TestEscapedURLParams (line 1721) | func TestEscapedURLParams(t *testing.T) {
function TestCustomHTTPMethod (line 1761) | func TestCustomHTTPMethod(t *testing.T) {
function TestMuxMatch (line 1807) | func TestMuxMatch(t *testing.T) {
function TestMuxMatch_HasBasePath (line 1845) | func TestMuxMatch_HasBasePath(t *testing.T) {
function TestMuxMatch_DoesNotHaveBasePath (line 1860) | func TestMuxMatch_DoesNotHaveBasePath(t *testing.T) {
function TestMuxFind (line 1871) | func TestMuxFind(t *testing.T) {
function TestServerBaseContext (line 1955) | func TestServerBaseContext(t *testing.T) {
function testRequest (line 1983) | func testRequest(t *testing.T, ts *httptest.Server, method, path string,...
function testHandler (line 2006) | func testHandler(t *testing.T, h http.Handler, method, path string, body...
type ctxKey (line 2013) | type ctxKey struct
method String (line 2017) | func (k ctxKey) String() string {
function BenchmarkMux (line 2021) | func BenchmarkMux(b *testing.B) {
FILE: path_value_test.go
function TestPathValue (line 10) | func TestPathValue(t *testing.T) {
FILE: pattern_test.go
function TestPattern (line 12) | func TestPattern(t *testing.T) {
FILE: tree.go
type methodTyp (line 17) | type methodTyp
constant mSTUB (line 20) | mSTUB methodTyp = 1 << iota
constant mCONNECT (line 21) | mCONNECT
constant mDELETE (line 22) | mDELETE
constant mGET (line 23) | mGET
constant mHEAD (line 24) | mHEAD
constant mOPTIONS (line 25) | mOPTIONS
constant mPATCH (line 26) | mPATCH
constant mPOST (line 27) | mPOST
constant mPUT (line 28) | mPUT
constant mTRACE (line 29) | mTRACE
function RegisterMethod (line 61) | func RegisterMethod(method string) {
type nodeTyp (line 79) | type nodeTyp
constant ntStatic (line 82) | ntStatic nodeTyp = iota
constant ntRegexp (line 83) | ntRegexp
constant ntParam (line 84) | ntParam
constant ntCatchAll (line 85) | ntCatchAll
type node (line 88) | type node struct
method InsertRoute (line 139) | func (n *node) InsertRoute(method methodTyp, pattern string, handler h...
method addChild (line 235) | func (n *node) addChild(child *node, prefix string) *node {
method replaceChild (line 319) | func (n *node) replaceChild(label, tail byte, child *node) {
method getEdge (line 331) | func (n *node) getEdge(ntyp nodeTyp, label, tail byte, prefix string) ...
method setEndpoint (line 344) | func (n *node) setEndpoint(method methodTyp, handler http.Handler, pat...
method FindRoute (line 374) | func (n *node) FindRoute(rctx *Context, method methodTyp, path string)...
method findRoute (line 401) | func (n *node) findRoute(rctx *Context, method methodTyp, path string)...
method findEdge (line 546) | func (n *node) findEdge(ntyp nodeTyp, label byte) *node {
method isLeaf (line 574) | func (n *node) isLeaf() bool {
method findPattern (line 578) | func (n *node) findPattern(pattern string) bool {
method routes (line 620) | func (n *node) routes() []Route {
method walk (line 668) | func (n *node) walk(fn func(eps endpoints, subroutes Routes) bool) bool {
type endpoints (line 117) | type endpoints
method Value (line 130) | func (s endpoints) Value(method methodTyp) *endpoint {
type endpoint (line 119) | type endpoint struct
function patNextSegment (line 687) | func patNextSegment(pattern string) (nodeTyp, string, string, byte, int,...
function patParamKeys (line 755) | func patParamKeys(pattern string) []string {
function longestPrefix (line 774) | func longestPrefix(k1, k2 string) (i int) {
type nodes (line 783) | type nodes
method Sort (line 786) | func (ns nodes) Sort() { sort.Sort(ns); ns.tailSort() }
method Len (line 787) | func (ns nodes) Len() int { return len(ns) }
method Swap (line 788) | func (ns nodes) Swap(i, j int) { ns[i], ns[j] = ns[j], ns[i] }
method Less (line 789) | func (ns nodes) Less(i, j int) bool { return ns[i].label < ns[j].label }
method tailSort (line 793) | func (ns nodes) tailSort() {
method findEdge (line 802) | func (ns nodes) findEdge(label byte) *node {
type Route (line 824) | type Route struct
type WalkFunc (line 831) | type WalkFunc
function Walk (line 834) | func Walk(r Routes, walkFn WalkFunc) error {
function walk (line 838) | func walk(r Routes, walkFn WalkFunc, parentRoute string, parentMw ...fun...
FILE: tree_test.go
function TestTree (line 10) | func TestTree(t *testing.T) {
function TestTreeMoar (line 154) | func TestTreeMoar(t *testing.T) {
function TestTreeRegexp (line 270) | func TestTreeRegexp(t *testing.T) {
function TestTreeRegexpRecursive (line 336) | func TestTreeRegexpRecursive(t *testing.T) {
function TestTreeRegexMatchWholeParam (line 387) | func TestTreeRegexMatchWholeParam(t *testing.T) {
function TestTreeFindPattern (line 417) | func TestTreeFindPattern(t *testing.T) {
function debugPrintTree (line 447) | func debugPrintTree(parent int, i int, n *node, label byte) bool {
function stringSliceEqual (line 475) | func stringSliceEqual(a, b []string) bool {
function BenchmarkTreeGet (line 487) | func BenchmarkTreeGet(b *testing.B) {
function TestWalker (line 512) | func TestWalker(t *testing.T) {
function TestWalkInlineMiddlewaresAcrossSubrouter (line 525) | func TestWalkInlineMiddlewaresAcrossSubrouter(t *testing.T) {
Condensed preview — 94 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (388K chars).
[
{
"path": ".github/FUNDING.yml",
"chars": 723,
"preview": "# These are supported funding model platforms\n\ngithub: [pkieltyka] # Replace with up to 4 GitHub Sponsors-enabled userna"
},
{
"path": ".github/workflows/ci.yml",
"chars": 896,
"preview": "on:\n push:\n branches: \"**\"\n paths-ignore:\n - \"docs/**\"\n pull_request:\n branches: \"**\"\n paths-ignore:\n"
},
{
"path": ".gitignore",
"chars": 20,
"preview": ".idea\n*.sw?\n.vscode\n"
},
{
"path": "CHANGELOG.md",
"chars": 15854,
"preview": "# Changelog\n\n## v5.0.12 (2024-02-16)\n\n- History of changes: see https://github.com/go-chi/chi/compare/v5.0.11...v5.0.12\n"
},
{
"path": "CONTRIBUTING.md",
"chars": 1187,
"preview": "# Contributing\n\n## Prerequisites\n\n1. [Install Go][go-install].\n2. Download the sources and switch the working directory:"
},
{
"path": "LICENSE",
"chars": 1123,
"preview": "Copyright (c) 2015-present Peter Kieltyka (https://github.com/pkieltyka), Google Inc.\n\nMIT License\n\nPermission is hereby"
},
{
"path": "Makefile",
"chars": 490,
"preview": ".PHONY: all\nall:\n\t@echo \"**********************************************************\"\n\t@echo \"** chi b"
},
{
"path": "README.md",
"chars": 23313,
"preview": "# <img alt=\"chi\" src=\"https://cdn.rawgit.com/go-chi/chi/master/_examples/chi.svg\" width=\"220\" />\n\n\n[![GoDoc Widget]][GoD"
},
{
"path": "SECURITY.md",
"chars": 313,
"preview": "# Reporting Security Issues\n\nWe appreciate your efforts to responsibly disclose your findings, and will make every effor"
},
{
"path": "_examples/README.md",
"chars": 1899,
"preview": "chi examples\n============\n\n* [custom-handler](https://github.com/go-chi/chi/blob/master/_examples/custom-handler/main.go"
},
{
"path": "_examples/custom-handler/main.go",
"chars": 625,
"preview": "package main\n\nimport (\n\t\"errors\"\n\t\"net/http\"\n\n\t\"github.com/go-chi/chi/v5\"\n)\n\ntype Handler func(w http.ResponseWriter, r "
},
{
"path": "_examples/custom-method/main.go",
"chars": 853,
"preview": "package main\n\nimport (\n\t\"net/http\"\n\n\t\"github.com/go-chi/chi/v5\"\n\t\"github.com/go-chi/chi/v5/middleware\"\n)\n\nfunc init() {\n"
},
{
"path": "_examples/fileserver/data/notes.txt",
"chars": 11,
"preview": "Notessszzz\n"
},
{
"path": "_examples/fileserver/main.go",
"chars": 1521,
"preview": "// This example demonstrates how to serve static files from your filesystem.\n//\n// Boot the server:\n//\n//\t$ go run main."
},
{
"path": "_examples/graceful/main.go",
"chars": 1707,
"preview": "package main\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log\"\n\t\"net/http\"\n\t\"os/signal\"\n\t\"syscall\"\n\t\"time\"\n\n\t\"github.com/go-c"
},
{
"path": "_examples/hello-world/main.go",
"chars": 359,
"preview": "package main\n\nimport (\n\t\"net/http\"\n\n\t\"github.com/go-chi/chi/v5\"\n\t\"github.com/go-chi/chi/v5/middleware\"\n)\n\nfunc main() {\n"
},
{
"path": "_examples/limits/main.go",
"chars": 2181,
"preview": "// This example demonstrates the use of Timeout, and Throttle middlewares.\n//\n// Timeout: cancel a request if processing"
},
{
"path": "_examples/logging/main.go",
"chars": 272,
"preview": "package main\n\n// Please see https://github.com/go-chi/httplog for a complete package\n// and example for writing a struct"
},
{
"path": "_examples/pathvalue/main.go",
"chars": 550,
"preview": "package main\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\n\t\"github.com/go-chi/chi/v5\"\n)\n\nfunc main() {\n\tr := chi.NewRouter()\n\n\t// Regis"
},
{
"path": "_examples/rest/go.mod",
"chars": 141,
"preview": "module rest-example\n\ngo 1.16\n\nrequire (\n\tgithub.com/go-chi/chi/v5 v5.0.1\n\tgithub.com/go-chi/docgen v1.2.0\n\tgithub.com/go"
},
{
"path": "_examples/rest/go.sum",
"chars": 688,
"preview": "github.com/go-chi/chi/v5 v5.0.1 h1:ALxjCrTf1aflOlkhMnCUP86MubbWFrzB3gkRPReLpTo=\ngithub.com/go-chi/chi/v5 v5.0.1/go.mod h"
},
{
"path": "_examples/rest/main.go",
"chars": 15501,
"preview": "// This example demonstrates a HTTP REST web service with some fixture data.\n// Follow along the example and patterns.\n/"
},
{
"path": "_examples/rest/routes.json",
"chars": 11317,
"preview": "{\n \"router\": {\n \"middlewares\": [\n {\n \"pkg\": \"github.com/go-chi/chi/v5/middleware\",\n \"func\": \"Requ"
},
{
"path": "_examples/rest/routes.md",
"chars": 4665,
"preview": "# github.com/go-chi/chi\n\nWelcome to the chi/_examples/rest generated docs.\n\n## Routes\n\n<details>\n<summary>`/`</summary>\n"
},
{
"path": "_examples/router-walk/main.go",
"chars": 913,
"preview": "package main\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\t\"strings\"\n\n\t\"github.com/go-chi/chi/v5\"\n)\n\nfunc main() {\n\tr := chi.NewRouter()"
},
{
"path": "_examples/todos-resource/main.go",
"chars": 748,
"preview": "// This example demonstrates a project structure that defines a subrouter and its\n// handlers on a struct, and mounting "
},
{
"path": "_examples/todos-resource/todos.go",
"chars": 1453,
"preview": "package main\n\nimport (\n\t\"net/http\"\n\n\t\"github.com/go-chi/chi/v5\"\n)\n\ntype todosResource struct{}\n\n// Routes creates a REST"
},
{
"path": "_examples/todos-resource/users.go",
"chars": 1323,
"preview": "package main\n\nimport (\n\t\"net/http\"\n\n\t\"github.com/go-chi/chi/v5\"\n)\n\ntype usersResource struct{}\n\n// Routes creates a REST"
},
{
"path": "_examples/versions/data/article.go",
"chars": 388,
"preview": "package data\n\n// Article is runtime object, that's not meant to be sent via REST.\ntype Article struct {\n\tID "
},
{
"path": "_examples/versions/data/errors.go",
"chars": 542,
"preview": "package data\n\nimport (\n\t\"errors\"\n\t\"net/http\"\n\n\t\"github.com/go-chi/render\"\n)\n\nvar (\n\tErrUnauthorized = errors.New(\"Unauth"
},
{
"path": "_examples/versions/go.mod",
"chars": 152,
"preview": "module versions\n\ngo 1.16\n\nrequire (\n\tgithub.com/go-chi/chi/v5 v5.1.0\n\tgithub.com/go-chi/render v1.0.3\n)\n\nrequire github."
},
{
"path": "_examples/versions/go.sum",
"chars": 491,
"preview": "github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=\ngithub.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h"
},
{
"path": "_examples/versions/main.go",
"chars": 4072,
"preview": "// This example demonstrates the use of the render subpackage, with\n// a quick concept for how to support multiple api v"
},
{
"path": "_examples/versions/presenter/v1/article.go",
"chars": 399,
"preview": "package v1\n\nimport (\n\t\"net/http\"\n\n\t\"github.com/go-chi/chi/v5/_examples/versions/data\"\n)\n\n// Article presented in API ver"
},
{
"path": "_examples/versions/presenter/v2/article.go",
"chars": 625,
"preview": "package v2\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\n\t\"github.com/go-chi/chi/v5/_examples/versions/data\"\n)\n\n// Article presented in "
},
{
"path": "_examples/versions/presenter/v3/article.go",
"chars": 1002,
"preview": "package v3\n\nimport (\n\t\"fmt\"\n\t\"math/rand\"\n\t\"net/http\"\n\n\t\"github.com/go-chi/chi/v5/_examples/versions/data\"\n)\n\n// Article "
},
{
"path": "chain.go",
"chars": 1517,
"preview": "package chi\n\nimport \"net/http\"\n\n// Chain returns a Middlewares type from a slice of middleware handlers.\nfunc Chain(midd"
},
{
"path": "chi.go",
"chars": 4713,
"preview": "// Package chi is a small, idiomatic and composable router for building HTTP services.\n//\n// chi supports the four most "
},
{
"path": "context.go",
"chars": 4892,
"preview": "package chi\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\t\"strings\"\n)\n\n// URLParam returns the url parameter from a http.Request obj"
},
{
"path": "context_test.go",
"chars": 2824,
"preview": "package chi\n\nimport \"testing\"\n\n// TestRoutePattern tests correct in-the-middle wildcard removals.\n// If user organizes a"
},
{
"path": "go.mod",
"chars": 149,
"preview": "module github.com/go-chi/chi/v5\n\n// Chi supports the four most recent major versions of Go.\n// See https://github.com/go"
},
{
"path": "middleware/basic_auth.go",
"chars": 852,
"preview": "package middleware\n\nimport (\n\t\"crypto/subtle\"\n\t\"fmt\"\n\t\"net/http\"\n)\n\n// BasicAuth implements a simple middleware handler "
},
{
"path": "middleware/clean_path.go",
"chars": 658,
"preview": "package middleware\n\nimport (\n\t\"net/http\"\n\t\"path\"\n\n\t\"github.com/go-chi/chi/v5\"\n)\n\n// CleanPath middleware will clean out "
},
{
"path": "middleware/compress.go",
"chars": 11488,
"preview": "package middleware\n\nimport (\n\t\"bufio\"\n\t\"compress/flate\"\n\t\"compress/gzip\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net\"\n\t\"net/http\"\n\t\"str"
},
{
"path": "middleware/compress_test.go",
"chars": 5328,
"preview": "package middleware\n\nimport (\n\t\"compress/flate\"\n\t\"compress/gzip\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"strings\""
},
{
"path": "middleware/content_charset.go",
"chars": 1205,
"preview": "package middleware\n\nimport (\n\t\"net/http\"\n\t\"slices\"\n\t\"strings\"\n)\n\n// ContentCharset generates a handler that writes a 415"
},
{
"path": "middleware/content_charset_test.go",
"chars": 2960,
"preview": "package middleware\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/go-chi/chi/v5\"\n)\n\nfunc TestConten"
},
{
"path": "middleware/content_encoding.go",
"chars": 1087,
"preview": "package middleware\n\nimport (\n\t\"net/http\"\n\t\"strings\"\n)\n\n// AllowContentEncoding enforces a whitelist of request Content-E"
},
{
"path": "middleware/content_encoding_test.go",
"chars": 1929,
"preview": "package middleware\n\nimport (\n\t\"bytes\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/go-chi/chi/v5\"\n)\n\nfunc T"
},
{
"path": "middleware/content_type.go",
"chars": 1265,
"preview": "package middleware\n\nimport (\n\t\"net/http\"\n\t\"strings\"\n)\n\n// SetHeader is a convenience handler to set a response header ke"
},
{
"path": "middleware/content_type_test.go",
"chars": 2095,
"preview": "package middleware\n\nimport (\n\t\"bytes\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/go-chi/chi/v5\"\n)\n\nfunc T"
},
{
"path": "middleware/get_head.go",
"chars": 977,
"preview": "package middleware\n\nimport (\n\t\"net/http\"\n\n\t\"github.com/go-chi/chi/v5\"\n)\n\n// GetHead automatically route undefined HEAD r"
},
{
"path": "middleware/get_head_test.go",
"chars": 1941,
"preview": "package middleware\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/go-chi/chi/v5\"\n)\n\nfunc TestGetHea"
},
{
"path": "middleware/heartbeat.go",
"chars": 755,
"preview": "package middleware\n\nimport (\n\t\"net/http\"\n\t\"strings\"\n)\n\n// Heartbeat endpoint middleware useful to setting up a path like"
},
{
"path": "middleware/logger.go",
"chars": 4978,
"preview": "package middleware\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"log\"\n\t\"net/http\"\n\t\"os\"\n\t\"runtime\"\n\t\"time\"\n)\n\nvar (\n\t// LogEntryCtxKey"
},
{
"path": "middleware/logger_test.go",
"chars": 1116,
"preview": "package middleware\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\t\"time\"\n)\n\ntype testLo"
},
{
"path": "middleware/maybe.go",
"chars": 636,
"preview": "package middleware\n\nimport \"net/http\"\n\n// Maybe middleware will allow you to change the flow of the middleware stack exe"
},
{
"path": "middleware/middleware.go",
"chars": 684,
"preview": "package middleware\n\nimport \"net/http\"\n\n// New will create a new middleware handler from a http.Handler.\nfunc New(h http."
},
{
"path": "middleware/middleware_test.go",
"chars": 3359,
"preview": "package middleware\n\nimport (\n\t\"crypto/tls\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"path\"\n\t\"reflect\"\n\t\"runtime\"\n\t\"testin"
},
{
"path": "middleware/nocache.go",
"chars": 1430,
"preview": "package middleware\n\n// Ported from Goji's middleware, source:\n// https://github.com/zenazn/goji/tree/master/web/middlewa"
},
{
"path": "middleware/page_route.go",
"chars": 519,
"preview": "package middleware\n\nimport (\n\t\"net/http\"\n\t\"strings\"\n)\n\n// PageRoute is a simple middleware which allows you to route a s"
},
{
"path": "middleware/path_rewrite.go",
"chars": 422,
"preview": "package middleware\n\nimport (\n\t\"net/http\"\n\t\"strings\"\n)\n\n// PathRewrite is a simple middleware which allows you to rewrite"
},
{
"path": "middleware/profiler.go",
"chars": 1325,
"preview": "//go:build !tinygo\n// +build !tinygo\n\npackage middleware\n\nimport (\n\t\"expvar\"\n\t\"net/http\"\n\t\"net/http/pprof\"\n\n\t\"github.com"
},
{
"path": "middleware/realip.go",
"chars": 1826,
"preview": "package middleware\n\n// Ported from Goji's middleware, source:\n// https://github.com/zenazn/goji/tree/master/web/middlewa"
},
{
"path": "middleware/realip_test.go",
"chars": 2311,
"preview": "package middleware\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/go-chi/chi/v5\"\n)\n\nfunc TestXRealI"
},
{
"path": "middleware/recoverer.go",
"chars": 4858,
"preview": "package middleware\n\n// The original work was derived from Goji's middleware, source:\n// https://github.com/zenazn/goji/t"
},
{
"path": "middleware/recoverer_test.go",
"chars": 1456,
"preview": "package middleware\n\nimport (\n\t\"bytes\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/go-chi/chi/v5"
},
{
"path": "middleware/request_id.go",
"chars": 2943,
"preview": "package middleware\n\n// Ported from Goji's middleware, source:\n// https://github.com/zenazn/goji/tree/master/web/middlewa"
},
{
"path": "middleware/request_id_test.go",
"chars": 1402,
"preview": "package middleware\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/go-chi/chi/v5\"\n)\n\nfunc mai"
},
{
"path": "middleware/request_size.go",
"chars": 454,
"preview": "package middleware\n\nimport (\n\t\"net/http\"\n)\n\n// RequestSize is a middleware that will limit request sizes to a specified\n"
},
{
"path": "middleware/route_headers.go",
"chars": 4167,
"preview": "package middleware\n\nimport (\n\t\"net/http\"\n\t\"strings\"\n)\n\n// RouteHeaders is a neat little header-based router that allows "
},
{
"path": "middleware/route_headers_test.go",
"chars": 5766,
"preview": "package middleware\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"sync/atomic\"\n\t\"testing\"\n)\n\nfunc TestRouteHeaders(t *test"
},
{
"path": "middleware/strip.go",
"chars": 2199,
"preview": "package middleware\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\t\"strings\"\n\n\t\"github.com/go-chi/chi/v5\"\n)\n\n// StripSlashes is a middlewa"
},
{
"path": "middleware/strip_test.go",
"chars": 9524,
"preview": "package middleware\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/go-chi/chi/"
},
{
"path": "middleware/sunset.go",
"chars": 700,
"preview": "package middleware\n\nimport (\n\t\"net/http\"\n\t\"time\"\n)\n\n// Sunset set Deprecation/Sunset header to response\n// This can be u"
},
{
"path": "middleware/sunset_test.go",
"chars": 2459,
"preview": "package middleware\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-chi/chi/v5\"\n)\n\nfunc Te"
},
{
"path": "middleware/supress_notfound.go",
"chars": 852,
"preview": "package middleware\n\nimport (\n\t\"net/http\"\n\n\t\"github.com/go-chi/chi/v5\"\n)\n\n// SupressNotFound will quickly respond with a "
},
{
"path": "middleware/terminal.go",
"chars": 1885,
"preview": "package middleware\n\n// Ported from Goji's middleware, source:\n// https://github.com/zenazn/goji/tree/master/web/middlewa"
},
{
"path": "middleware/throttle.go",
"chars": 4199,
"preview": "package middleware\n\nimport (\n\t\"net/http\"\n\t\"strconv\"\n\t\"time\"\n)\n\nconst (\n\terrCapacityExceeded = \"Server capacity exceeded."
},
{
"path": "middleware/throttle_test.go",
"chars": 7136,
"preview": "package middleware\n\nimport (\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"strings\"\n\t\"sync\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/"
},
{
"path": "middleware/timeout.go",
"chars": 1222,
"preview": "package middleware\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\t\"time\"\n)\n\n// Timeout is a middleware that cancels ctx after a given"
},
{
"path": "middleware/url_format.go",
"chars": 1814,
"preview": "package middleware\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\t\"strings\"\n\n\t\"github.com/go-chi/chi/v5\"\n)\n\nvar (\n\t// URLFormatCtxKey"
},
{
"path": "middleware/url_format_test.go",
"chars": 1653,
"preview": "package middleware\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/go-chi/chi/v5\"\n)\n\nfunc TestURLFor"
},
{
"path": "middleware/value.go",
"chars": 436,
"preview": "package middleware\n\nimport (\n\t\"context\"\n\t\"net/http\"\n)\n\n// WithValue is a middleware that sets a given key/value in a con"
},
{
"path": "middleware/wrap_writer.go",
"chars": 6080,
"preview": "package middleware\n\n// The original work was derived from Goji's middleware, source:\n// https://github.com/zenazn/goji/t"
},
{
"path": "middleware/wrap_writer_test.go",
"chars": 2454,
"preview": "package middleware\n\nimport (\n\t\"bytes\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n)\n\nfunc TestHttpFancyWriterRemembersWr"
},
{
"path": "mux.go",
"chars": 16755,
"preview": "package chi\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"strings\"\n\t\"sync\"\n)\n\nvar _ Router = &Mux{}\n\n// Mux is a simple HTTP"
},
{
"path": "mux_test.go",
"chars": 57188,
"preview": "package chi\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"sync\"\n\t\"testing\"\n\t\"time"
},
{
"path": "path_value_test.go",
"chars": 1746,
"preview": "package chi\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"strings\"\n\t\"testing\"\n)\n\nfunc TestPathValue(t *testing.T) {\n\ttest"
},
{
"path": "pattern_test.go",
"chars": 1186,
"preview": "//go:build go1.23\n// +build go1.23\n\npackage chi\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n)\n\nfunc TestPatter"
},
{
"path": "testdata/cert.pem",
"chars": 1098,
"preview": "-----BEGIN CERTIFICATE-----\nMIIC/zCCAeegAwIBAgIRANioW0Re7DtpT4qZpJU1iK8wDQYJKoZIhvcNAQELBQAw\nEjEQMA4GA1UEChMHQWNtZSBDbzA"
},
{
"path": "testdata/key.pem",
"chars": 1678,
"preview": "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEA6RXzrGlw2JS/npLX7KhmK0gLKE23u86o6aMPZ6lFOD5i0btm\nDGmlvHwKJ3lJycbCcO6pd/P"
},
{
"path": "tree.go",
"chars": 20574,
"preview": "package chi\n\n// Radix tree implementation below is a based on the original work by\n// Armon Dadgar in https://github.com"
},
{
"path": "tree_test.go",
"chars": 23961,
"preview": "package chi\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\t\"net/http\"\n\t\"testing\"\n)\n\nfunc TestTree(t *testing.T) {\n\thStub := http.HandlerFunc(f"
}
]
About this extraction
This page contains the full source code of the go-chi/chi GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 94 files (343.2 KB), approximately 100.7k tokens, and a symbol index with 427 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.