From 409ecf2ce183e62359ff476e1f214855f58c59b4 Mon Sep 17 00:00:00 2001 From: Sotirios Mantziaris Date: Thu, 4 Apr 2024 23:47:23 +0300 Subject: [PATCH] Using new HTTP router in 1.22 and removing httprouter package (#684) ## Which problem is this PR solving? Resolves #672. ## Short description of the changes - Upgraded Go to 1.22 - removed httprouter - introduced new router - Changed to codecov for code coverage --- .github/workflows/ci.yml | 32 +- Makefile | 4 +- README.md | 8 +- client/es/elasticsearch_test.go | 2 +- client/http/http_test.go | 10 +- client/http/option_test.go | 2 +- client/sns/integration_test.go | 2 +- client/sns/publisher_test.go | 2 +- client/sqs/integration_test.go | 2 +- client/sqs/publisher_test.go | 2 +- codecov.yml | 6 + component/http/cache/cache.go | 4 +- component/http/cache/cache_test.go | 6 +- component/http/check.go | 8 +- component/http/check_test.go | 6 +- component/http/middleware/middleware.go | 14 +- component/http/middleware/middleware_test.go | 42 +- component/http/observability.go | 29 +- component/http/observability_test.go | 3 +- component/http/route.go | 59 +- component/http/route_option.go | 4 +- component/http/route_option_test.go | 10 +- component/http/route_test.go | 105 +-- .../http/router/{httprouter => }/route.go | 15 +- .../router/{httprouter => }/route_test.go | 9 +- .../http/router/{httprouter => }/router.go | 14 +- .../router/{httprouter => }/router_option.go | 2 +- .../{httprouter => }/router_option_test.go | 2 +- .../router/{httprouter => }/router_test.go | 6 +- .../{httprouter => }/testdata/index.html | 0 component/kafka/integration_test.go | 4 +- component/kafka/option_test.go | 2 +- component/sqs/integration_test.go | 2 +- examples/service/http.go | 6 +- go.mod | 3 +- go.sum | 2 - .../julienschmidt/httprouter/.travis.yml | 18 - .../julienschmidt/httprouter/LICENSE | 29 - .../julienschmidt/httprouter/README.md | 300 -------- .../julienschmidt/httprouter/path.go | 123 ---- .../julienschmidt/httprouter/router.go | 452 ------------ .../julienschmidt/httprouter/tree.go | 666 ------------------ vendor/modules.txt | 3 - 43 files changed, 131 insertions(+), 1889 deletions(-) create mode 100644 codecov.yml rename component/http/router/{httprouter => }/route.go (83%) rename component/http/router/{httprouter => }/route_test.go (88%) rename component/http/router/{httprouter => }/router.go (92%) rename component/http/router/{httprouter => }/router_option.go (99%) rename component/http/router/{httprouter => }/router_option_test.go (99%) rename component/http/router/{httprouter => }/router_test.go (90%) rename component/http/router/{httprouter => }/testdata/index.html (100%) delete mode 100644 vendor/github.com/julienschmidt/httprouter/.travis.yml delete mode 100644 vendor/github.com/julienschmidt/httprouter/LICENSE delete mode 100644 vendor/github.com/julienschmidt/httprouter/README.md delete mode 100644 vendor/github.com/julienschmidt/httprouter/path.go delete mode 100644 vendor/github.com/julienschmidt/httprouter/router.go delete mode 100644 vendor/github.com/julienschmidt/httprouter/tree.go diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index aa09cba820..aec5fe865f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,20 +22,17 @@ jobs: run: make lint build: - name: CI on Go ${{ matrix.go-image-version}} - strategy: - matrix: - go-image-version: ["1.21"] + name: CI on Go runs-on: self-hosted steps: - - name: Set up Go ${{ matrix.go-image-version }} - uses: actions/setup-go@v5 - with: - go-version: ${{ matrix.go-image-version }} - - name: Check out source code uses: actions/checkout@v4 + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version-file: "go.mod" + - name: Start dependencies run: make deps-start @@ -43,17 +40,14 @@ jobs: run: | sleep 30 make ci - - - name: Convert coverage file to lcov - run: | - GO111MODULE=off go get -u github.com/jandelgado/gcov2lcov - $(go env GOPATH)/bin/gcov2lcov -infile=coverage.txt -outfile=coverage.lcov - - - name: Coveralls - uses: coverallsapp/github-action@master + + - name: Codecov + uses: codecov/codecov-action@v4 with: - github-token: ${{ secrets.GITHUB_TOKEN }} - path-to-lcov: ./coverage.lcov + fail_ci_if_error: true # optional (default = false) + files: ./coverage.txt + name: codecov-umbrella # optional + token: ${{ secrets.CODECOV_TOKEN }} # required - name: e2e run: | diff --git a/Makefile b/Makefile index 0b6ffb667a..edacdf0769 100644 --- a/Makefile +++ b/Makefile @@ -25,10 +25,10 @@ fmtcheck: @sh -c "'$(CURDIR)/script/gofmtcheck.sh'" lint: fmtcheck - $(DOCKER) run --env=GOFLAGS=-mod=vendor --rm -v $(CURDIR):/app -w /app golangci/golangci-lint:v1.55.2 golangci-lint -v run + $(DOCKER) run --env=GOFLAGS=-mod=vendor --rm -v $(CURDIR):/app -w /app golangci/golangci-lint:v1.57.2 golangci-lint -v run deeplint: fmtcheck - $(DOCKER) run --env=GOFLAGS=-mod=vendor --rm -v $(CURDIR):/app -w /app golangci/golangci-lint:v1.55.2 golangci-lint run --exclude-use-default=false --enable-all -D dupl --build-tags integration + $(DOCKER) run --env=GOFLAGS=-mod=vendor --rm -v $(CURDIR):/app -w /app golangci/golangci-lint:v1.57.2 golangci-lint run --exclude-use-default=false --enable-all -D dupl --build-tags integration modsync: fmtcheck go mod tidy && go mod vendor diff --git a/README.md b/README.md index 4bedb28d3d..73dca3bb56 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# patron ![CI](https://github.com/beatlabs/patron/workflows/CI/badge.svg) [![Coverage Status](https://coveralls.io/repos/github/beatlabs/patron/badge.svg?branch=master)](https://coveralls.io/github/beatlabs/patron?branch=master) [![Go Report Card](https://goreportcard.com/badge/github.com/beatlabs/patron)](https://goreportcard.com/report/github.com/beatlabs/patron) [![GoDoc](https://godoc.org/github.com/beatlabs/patron?status.svg)](https://godoc.org/github.com/beatlabs/patron) ![GitHub release](https://img.shields.io/github/release/beatlabs/patron.svg)[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fbeatlabs%2Fpatron.svg?type=shield&issueType=license)](https://app.fossa.com/projects/git%2Bgithub.com%2Fbeatlabs%2Fpatron?ref=badge_shield&issueType=license) +# patron ![CI](https://github.com/beatlabs/patron/workflows/CI/badge.svg) [![codecov](https://codecov.io/gh/beatlabs/patron/graph/badge.svg?token=sxY15rXW1X)](https://codecov.io/gh/beatlabs/patron) [![Go Report Card](https://goreportcard.com/badge/github.com/beatlabs/patron)](https://goreportcard.com/report/github.com/beatlabs/patron) [![GoDoc](https://godoc.org/github.com/beatlabs/patron?status.svg)](https://godoc.org/github.com/beatlabs/patron) ![GitHub release](https://img.shields.io/github/release/beatlabs/patron.svg)[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fbeatlabs%2Fpatron.svg?type=shield&issueType=license)](https://app.fossa.com/projects/git%2Bgithub.com%2Fbeatlabs%2Fpatron?ref=badge_shield&issueType=license) Patron is a framework for creating microservices, originally created by Sotiris Mantzaris (). This fork is maintained by Beat Engineering () @@ -17,7 +17,11 @@ The entry point of the framework is the `Service`. The `Service` uses `Component - logging `Patron` provides the same defaults for making the usage as simple as possible. -`Patron` needs Go 1.21 as a minimum. +`Patron` needs Go 1.22 as a minimum. + +## Code coverage + +![Code coverage](https://codecov.io/gh/beatlabs/patron/graphs/icicle.svg?token=sxY15rXW1X) ## Table of Contents diff --git a/client/es/elasticsearch_test.go b/client/es/elasticsearch_test.go index 6ad48afcbb..b8510f6435 100644 --- a/client/es/elasticsearch_test.go +++ b/client/es/elasticsearch_test.go @@ -144,7 +144,7 @@ func TestEsQuery(t *testing.T) { responseMsg := `[{"acknowledged": true, "shards_acknowledged": true, "index": "test"}]` ctx, indexName := context.Background(), "test_index" - ts := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ts := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { _, err := w.Write([]byte(responseMsg)) assert.NoError(t, err) })) diff --git a/client/http/http_test.go b/client/http/http_test.go index 2157311eb4..1c6d863fc9 100644 --- a/client/http/http_test.go +++ b/client/http/http_test.go @@ -89,7 +89,7 @@ func TestTracedClient_Do_Redirect(t *testing.T) { http.Redirect(w, r, "https://google.com", http.StatusSeeOther) })) defer ts.Close() - c, err := New(WithCheckRedirect(func(req *http.Request, via []*http.Request) error { + c, err := New(WithCheckRedirect(func(_ *http.Request, _ []*http.Request) error { return errors.New("stop redirects") })) assert.NoError(t, err) @@ -116,7 +116,7 @@ func TestNew(t *testing.T) { WithTimeout(time.Second), WithCircuitBreaker("test", circuitbreaker.Setting{}), WithTransport(&http.Transport{}), - WithCheckRedirect(func(req *http.Request, via []*http.Request) error { return nil }), + WithCheckRedirect(func(_ *http.Request, _ []*http.Request) error { return nil }), }}, wantErr: false}, {name: "failure, invalid timeout", args: args{oo: []OptionFunc{WithTimeout(0 * time.Second)}}, wantErr: true}, {name: "failure, invalid circuit breaker", args: args{[]OptionFunc{WithCircuitBreaker("", circuitbreaker.Setting{})}}, wantErr: true}, @@ -139,12 +139,12 @@ func TestNew(t *testing.T) { func TestDecompress(t *testing.T) { const msg = "hello, client!" - ts1 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ts1 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { _, _ = fmt.Fprint(w, msg) })) defer ts1.Close() - ts2 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ts2 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { var b bytes.Buffer cw := gzip.NewWriter(&b) _, err := cw.Write([]byte(msg)) @@ -162,7 +162,7 @@ func TestDecompress(t *testing.T) { })) defer ts2.Close() - ts3 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ts3 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { var b bytes.Buffer cw, _ := flate.NewWriter(&b, 8) _, err := cw.Write([]byte(msg)) diff --git a/client/http/option_test.go b/client/http/option_test.go index 5f58aa1693..9fc3e8aa64 100644 --- a/client/http/option_test.go +++ b/client/http/option_test.go @@ -34,7 +34,7 @@ func TestCheckRedirect_Nil(t *testing.T) { } func TestCheckRedirect(t *testing.T) { - cr := func(req *http.Request, via []*http.Request) error { + cr := func(_ *http.Request, _ []*http.Request) error { return nil } diff --git a/client/sns/integration_test.go b/client/sns/integration_test.go index fbbc07209b..c9464239cf 100644 --- a/client/sns/integration_test.go +++ b/client/sns/integration_test.go @@ -83,7 +83,7 @@ type SNSAPI interface { } func createConfig(awsServiceID, awsRegion, awsEndpoint string) (aws.Config, error) { - customResolver := aws.EndpointResolverWithOptionsFunc(func(service, region string, options ...interface{}) (aws.Endpoint, error) { + customResolver := aws.EndpointResolverWithOptionsFunc(func(service, region string, _ ...interface{}) (aws.Endpoint, error) { if service == awsServiceID && region == awsRegion { return aws.Endpoint{ URL: awsEndpoint, diff --git a/client/sns/publisher_test.go b/client/sns/publisher_test.go index 940dbb5762..b3b14d64b0 100644 --- a/client/sns/publisher_test.go +++ b/client/sns/publisher_test.go @@ -104,7 +104,7 @@ func (s *stubSNSAPI) Publish(_ context.Context, _ *sns.PublishInput, _ ...func(* func ExamplePublisher() { // Create the SNS API with the required config, credentials, etc. - customResolver := aws.EndpointResolverWithOptionsFunc(func(service, region string, options ...interface{}) (aws.Endpoint, error) { + customResolver := aws.EndpointResolverWithOptionsFunc(func(service, region string, _ ...interface{}) (aws.Endpoint, error) { if service == sns.ServiceID && region == "eu-west-1" { return aws.Endpoint{ URL: "http://localhost:4575", diff --git a/client/sqs/integration_test.go b/client/sqs/integration_test.go index e5a2f7fcc2..6f6dce6e81 100644 --- a/client/sqs/integration_test.go +++ b/client/sqs/integration_test.go @@ -108,7 +108,7 @@ func createSQSAPI(region, endpoint string) (*sqs.Client, error) { } func createConfig(awsServiceID, awsRegion, awsEndpoint string) (aws.Config, error) { - customResolver := aws.EndpointResolverWithOptionsFunc(func(service, region string, options ...interface{}) (aws.Endpoint, error) { + customResolver := aws.EndpointResolverWithOptionsFunc(func(service, region string, _ ...interface{}) (aws.Endpoint, error) { if service == awsServiceID && region == awsRegion { return aws.Endpoint{ URL: awsEndpoint, diff --git a/client/sqs/publisher_test.go b/client/sqs/publisher_test.go index 0918ed4393..77905f64d3 100644 --- a/client/sqs/publisher_test.go +++ b/client/sqs/publisher_test.go @@ -210,7 +210,7 @@ func NewMockTracerInjector() MockTracerInjector { } func ExamplePublisher() { - customResolver := aws.EndpointResolverWithOptionsFunc(func(service, region string, options ...interface{}) (aws.Endpoint, error) { + customResolver := aws.EndpointResolverWithOptionsFunc(func(service, region string, _ ...interface{}) (aws.Endpoint, error) { if service == sqs.ServiceID && region == "eu-west-1" { return aws.Endpoint{ URL: "http://localhost:4576", diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 0000000000..1b6e6978ac --- /dev/null +++ b/codecov.yml @@ -0,0 +1,6 @@ +coverage: + status: + project: + default: + target: 80% # the required coverage value + threshold: 2% # the leniency in hitting the target \ No newline at end of file diff --git a/component/http/cache/cache.go b/component/http/cache/cache.go index 1104e718ce..f4f5e2cdd5 100644 --- a/component/http/cache/cache.go +++ b/component/http/cache/cache.go @@ -250,7 +250,7 @@ func extractRequestHeaders(header string, minAge, maxFresh int64) *control { if adjusted { wrn = append(wrn, fmt.Sprintf("max-age=%d", minAge)) } - cfg.validators = append(cfg.validators, func(age, maxAge int64) (bool, validationContext) { + cfg.validators = append(cfg.validators, func(age, _ int64) (bool, validationContext) { return age <= value, maxAgeValidation }) case controlMinFresh: @@ -286,7 +286,7 @@ func extractRequestHeaders(header string, minAge, maxFresh int64) *control { no storage whatsoever */ wrn = append(wrn, fmt.Sprintf("max-age=%d", minAge)) - cfg.validators = append(cfg.validators, func(age, maxAge int64) (bool, validationContext) { + cfg.validators = append(cfg.validators, func(age, _ int64) (bool, validationContext) { return age <= minAge, maxAgeValidation }) case controlOnlyIfCached: diff --git a/component/http/cache/cache_test.go b/component/http/cache/cache_test.go index 5d33b6e95d..ddc4f72c58 100644 --- a/component/http/cache/cache_test.go +++ b/component/http/cache/cache_test.go @@ -982,7 +982,7 @@ func TestCache_WithHandlerErrorWithoutHeaders(t *testing.T) { requestParams: newRequestAt(11), routeConfig: routeConfig{ path: rc.path, - hnd: func(now int64, key string) *response { + hnd: func(_ int64, _ string) *response { return &response{ Err: hndErr, } @@ -1010,7 +1010,7 @@ func TestCache_WithHandlerErr(t *testing.T) { rc := routeConfig{ path: "/", age: Age{Min: 10 * time.Second, Max: 10 * time.Second}, - hnd: func(now int64, key string) *response { + hnd: func(_ int64, _ string) *response { return &response{ Err: hndErr, } @@ -1587,7 +1587,7 @@ func assertCache(t *testing.T, args [][]testArgs) { // create a test request handler // that returns the current time instant times '10' multiplied by the VALUE parameter in the request exec := func(request requestParams) func(now int64, key string) *response { - return func(now int64, key string) *response { + return func(now int64, _ string) *response { i, err := strconv.Atoi(strings.Split(request.query, "=")[1]) if err != nil { return &response{ diff --git a/component/http/check.go b/component/http/check.go index 5b4080dc13..716a3dcbf4 100644 --- a/component/http/check.go +++ b/component/http/check.go @@ -25,9 +25,9 @@ const ( NotReady ReadyStatus = 2 // AlivePath of the component. - AlivePath = "/alive" + AlivePath = "GET /alive" // ReadyPath of the component. - ReadyPath = "/ready" + ReadyPath = "GET /ready" ) // ReadyCheckFunc defines a function type for implementing a readiness check. @@ -51,7 +51,7 @@ func LivenessCheckRoute(acf LivenessCheckFunc) (*Route, error) { } } - return NewRoute(http.MethodGet, AlivePath, f) + return NewRoute(AlivePath, f) } // ReadyCheckRoute returns a route for ready checks. @@ -69,5 +69,5 @@ func ReadyCheckRoute(rcf ReadyCheckFunc) (*Route, error) { } } - return NewRoute(http.MethodGet, ReadyPath, f) + return NewRoute(ReadyPath, f) } diff --git a/component/http/check_test.go b/component/http/check_test.go index 4057a07bd6..0c7264d2c4 100644 --- a/component/http/check_test.go +++ b/component/http/check_test.go @@ -25,8 +25,7 @@ func Test_aliveCheckRoute(t *testing.T) { t.Parallel() route, err := LivenessCheckRoute(tt.acf) assert.NoError(t, err) - assert.Equal(t, http.MethodGet, route.method) - assert.Equal(t, "/alive", route.path) + assert.Equal(t, "GET /alive", route.path) resp := httptest.NewRecorder() req, err := http.NewRequest(http.MethodGet, "/alive", nil) @@ -55,8 +54,7 @@ func Test_readyCheckRoute(t *testing.T) { t.Parallel() route, err := ReadyCheckRoute(tt.rcf) assert.NoError(t, err) - assert.Equal(t, http.MethodGet, route.method) - assert.Equal(t, "/ready", route.path) + assert.Equal(t, "GET /ready", route.path) resp := httptest.NewRecorder() req, err := http.NewRequest(http.MethodGet, "/ready", nil) diff --git a/component/http/middleware/middleware.go b/component/http/middleware/middleware.go index 918b3b69f9..ab55b18dca 100644 --- a/component/http/middleware/middleware.go +++ b/component/http/middleware/middleware.go @@ -226,7 +226,7 @@ func initHTTPServerMetrics() { Name: "handled_total", Help: "Total number of HTTP responses served by the server.", }, - []string{"method", "path", "status_code"}, + []string{"path", "status_code"}, ) prometheus.MustRegister(httpStatusTracingHandledMetric) httpStatusTracingLatencyMetric = prometheus.NewHistogramVec( @@ -236,18 +236,14 @@ func initHTTPServerMetrics() { Name: "handled_seconds", Help: "Latency of a completed HTTP response served by the server.", }, - []string{"method", "path", "status_code"}) + []string{"path", "status_code"}) prometheus.MustRegister(httpStatusTracingLatencyMetric) } // NewRequestObserver creates a Func that captures status code and duration metrics about the responses returned; // metrics are exposed via Prometheus. // This middleware is enabled by default. -func NewRequestObserver(method, path string) (Func, error) { - if method == "" { - return nil, errors.New("method cannot be empty") - } - +func NewRequestObserver(path string) (Func, error) { if path == "" { return nil, errors.New("path cannot be empty") } @@ -265,12 +261,12 @@ func NewRequestObserver(method, path string) (Func, error) { status := strconv.Itoa(lw.Status()) httpStatusCounter := trace.Counter{ - Counter: httpStatusTracingHandledMetric.WithLabelValues(method, path, status), + Counter: httpStatusTracingHandledMetric.WithLabelValues(path, status), } httpStatusCounter.Inc(r.Context()) httpLatencyMetricObserver := trace.Histogram{ - Observer: httpStatusTracingLatencyMetric.WithLabelValues(method, path, status), + Observer: httpStatusTracingLatencyMetric.WithLabelValues(path, status), } httpLatencyMetricObserver.Observe(r.Context(), time.Since(now).Seconds()) }) diff --git a/component/http/middleware/middleware_test.go b/component/http/middleware/middleware_test.go index 5be330145c..f8f4d430b2 100644 --- a/component/http/middleware/middleware_test.go +++ b/component/http/middleware/middleware_test.go @@ -41,8 +41,8 @@ func tagMiddleware(tag string) Func { // Panic middleware to test recovery middleware. func panicMiddleware(v interface{}) Func { - return func(h http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + return func(_ http.Handler) http.Handler { + return http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) { panic(v) }) } @@ -56,7 +56,7 @@ func getMockLimiter(allow bool) *rate.Limiter { } func TestMiddlewareChain(t *testing.T) { - handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + handler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(202) }) @@ -94,7 +94,7 @@ func TestMiddlewareChain(t *testing.T) { } func TestMiddlewares(t *testing.T) { - handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + handler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(202) }) @@ -174,10 +174,10 @@ func TestSpanLogError(t *testing.T) { mtr := mocktracer.New() opentracing.SetGlobalTracer(mtr) - successHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + successHandler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) }) - errorHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + errorHandler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusInternalServerError) _, err := w.Write([]byte("foo")) require.NoError(t, err) @@ -336,7 +336,7 @@ func TestNewCompressionMiddleware(t *testing.T) { return } - handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + handler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.Header().Add("Content-Length", "123") w.WriteHeader(202) }) @@ -431,7 +431,7 @@ func TestNewCompressionMiddlewareServer(t *testing.T) { for _, tt := range tests { tt := tt t.Run(fmt.Sprintf("%d - %s", tt.status, tt.expectedEncoding), func(t *testing.T) { - handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + handler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(tt.status) }) @@ -454,7 +454,7 @@ func TestNewCompressionMiddlewareServer(t *testing.T) { func TestNewCompressionMiddleware_Ignore(t *testing.T) { var ceh string // accept-encoding, content type - handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(202) }) + handler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(202) }) middleware, err := NewCompression(8, "/metrics") require.NoError(t, err) require.NotNil(t, middleware) @@ -485,7 +485,7 @@ func TestNewCompressionMiddleware_Ignore(t *testing.T) { } func TestNewCompressionMiddleware_Headers(t *testing.T) { - handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) }) + handler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) }) middleware, err := NewCompression(8, "/metrics") require.NoError(t, err) @@ -805,7 +805,7 @@ func TestSetResponseWriterStatusOnResponseFailWrite(t *testing.T) { } func TestNewInjectObservability(t *testing.T) { - handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(200) }) + handler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(200) }) middleware := NewInjectObservability() assert.NotNil(t, middleware) @@ -840,7 +840,7 @@ func TestNewCaching(t *testing.T) { assert.NoError(t, err) assert.NotNil(t, cachingMiddleware) - handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(200) }) + handler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(200) }) // check if the route is actually ignored req, err := http.NewRequest("GET", "/metrics", nil) @@ -856,22 +856,20 @@ func TestNewCaching(t *testing.T) { func TestNewRequestObserver(t *testing.T) { type args struct { - method string - path string + path string } tests := map[string]struct { args args expectedErr string }{ - "empty method should return error": {args: args{method: "", path: "path"}, expectedErr: "method cannot be empty"}, - "empty path should return error": {args: args{method: "method", path: ""}, expectedErr: "path cannot be empty"}, - "valid path and method should succeed without error": {args: args{method: http.MethodGet, path: "/api"}, expectedErr: ""}, + "empty path should return error": {args: args{path: ""}, expectedErr: "path cannot be empty"}, + "valid path and method should succeed without error": {args: args{path: "GET /api"}, expectedErr: ""}, } for name, test := range tests { tt := test t.Run(name, func(t *testing.T) { - appNameVersionMiddleware, err := NewRequestObserver(tt.args.method, tt.args.path) + appNameVersionMiddleware, err := NewRequestObserver(tt.args.path) if tt.expectedErr != "" { assert.EqualError(t, err, tt.expectedErr) assert.Nil(t, appNameVersionMiddleware) @@ -880,7 +878,7 @@ func TestNewRequestObserver(t *testing.T) { assert.NoError(t, err) assert.NotNil(t, appNameVersionMiddleware) - handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(200) }) + handler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(200) }) // check if the route actually ignored req, err := http.NewRequest("GET", "/api", nil) @@ -895,8 +893,8 @@ func TestNewRequestObserver(t *testing.T) { } func TestRequestObserver(t *testing.T) { - handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(200) }) - middleware, err := NewRequestObserver(http.MethodGet, "/api") + handler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(200) }) + middleware, err := NewRequestObserver("GET /api") require.NoError(t, err) assert.NotNil(t, middleware) @@ -936,7 +934,7 @@ func TestNewAppVersion(t *testing.T) { assert.NoError(t, err) assert.NotNil(t, appNameVersionMiddleware) - handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(200) }) + handler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(200) }) // check if the route actually ignored req, err := http.NewRequest("GET", "/api", nil) diff --git a/component/http/observability.go b/component/http/observability.go index 5aedb67edc..d4e7553c7b 100644 --- a/component/http/observability.go +++ b/component/http/observability.go @@ -11,13 +11,12 @@ import ( const ( // MetricsPath of the component. - MetricsPath = "/metrics" + MetricsPath = "GET /metrics" ) // MetricRoute creation. func MetricRoute() *Route { return &Route{ - method: http.MethodGet, path: MetricsPath, handler: promhttp.Handler().ServeHTTP, } @@ -27,23 +26,23 @@ func ProfilingRoutes(enableExpVar bool) []*Route { var routes []*Route routeFunc := func(path string, handler http.HandlerFunc) *Route { - route, _ := NewRoute(http.MethodGet, path, handler) + route, _ := NewRoute(path, handler) return route } - routes = append(routes, routeFunc("/debug/pprof/", pprof.Index)) - routes = append(routes, routeFunc("/debug/pprof/cmdline/", pprof.Cmdline)) - routes = append(routes, routeFunc("/debug/pprof/profile/", pprof.Profile)) - routes = append(routes, routeFunc("/debug/pprof/symbol/", pprof.Symbol)) - routes = append(routes, routeFunc("/debug/pprof/trace/", pprof.Trace)) - routes = append(routes, routeFunc("/debug/pprof/allocs/", pprof.Handler("allocs").ServeHTTP)) - routes = append(routes, routeFunc("/debug/pprof/heap/", pprof.Handler("heap").ServeHTTP)) - routes = append(routes, routeFunc("/debug/pprof/goroutine/", pprof.Handler("goroutine").ServeHTTP)) - routes = append(routes, routeFunc("/debug/pprof/block/", pprof.Handler("block").ServeHTTP)) - routes = append(routes, routeFunc("/debug/pprof/threadcreate/", pprof.Handler("threadcreate").ServeHTTP)) - routes = append(routes, routeFunc("/debug/pprof/mutex/", pprof.Handler("mutex").ServeHTTP)) + routes = append(routes, routeFunc("GET /debug/pprof/", pprof.Index)) + routes = append(routes, routeFunc("GET /debug/pprof/cmdline/", pprof.Cmdline)) + routes = append(routes, routeFunc("GET /debug/pprof/profile/", pprof.Profile)) + routes = append(routes, routeFunc("GET /debug/pprof/symbol/", pprof.Symbol)) + routes = append(routes, routeFunc("GET /debug/pprof/trace/", pprof.Trace)) + routes = append(routes, routeFunc("GET /debug/pprof/allocs/", pprof.Handler("allocs").ServeHTTP)) + routes = append(routes, routeFunc("GET /debug/pprof/heap/", pprof.Handler("heap").ServeHTTP)) + routes = append(routes, routeFunc("GET /debug/pprof/goroutine/", pprof.Handler("goroutine").ServeHTTP)) + routes = append(routes, routeFunc("GET /debug/pprof/block/", pprof.Handler("block").ServeHTTP)) + routes = append(routes, routeFunc("GET /debug/pprof/threadcreate/", pprof.Handler("threadcreate").ServeHTTP)) + routes = append(routes, routeFunc("GET /debug/pprof/mutex/", pprof.Handler("mutex").ServeHTTP)) if enableExpVar { - routes = append(routes, routeFunc("/debug/vars/", expVars)) + routes = append(routes, routeFunc("GET /debug/vars/", expVars)) } return routes diff --git a/component/http/observability_test.go b/component/http/observability_test.go index 3f30e5f304..3e0d6f4138 100644 --- a/component/http/observability_test.go +++ b/component/http/observability_test.go @@ -12,8 +12,7 @@ import ( func Test_metricRoute(t *testing.T) { route := MetricRoute() - assert.Equal(t, http.MethodGet, route.method) - assert.Equal(t, "/metrics", route.path) + assert.Equal(t, "GET /metrics", route.path) resp := httptest.NewRecorder() req, err := http.NewRequest(http.MethodGet, "/metrics", nil) diff --git a/component/http/route.go b/component/http/route.go index a253bbf320..33461c800b 100644 --- a/component/http/route.go +++ b/component/http/route.go @@ -13,16 +13,11 @@ type RouteOptionFunc func(route *Route) error // Route definition of an HTTP route. type Route struct { - method string path string handler http.HandlerFunc middlewares []patronhttp.Func } -func (r Route) Method() string { - return r.method -} - func (r Route) Path() string { return r.path } @@ -36,15 +31,11 @@ func (r Route) Middlewares() []patronhttp.Func { } func (r Route) String() string { - return r.method + " " + r.path + return r.path } // NewRoute creates a new raw route with functional configuration. -func NewRoute(method, path string, handler http.HandlerFunc, oo ...RouteOptionFunc) (*Route, error) { - if method == "" { - return nil, errors.New("method is empty") - } - +func NewRoute(path string, handler http.HandlerFunc, oo ...RouteOptionFunc) (*Route, error) { if path == "" { return nil, errors.New("path is empty") } @@ -54,7 +45,6 @@ func NewRoute(method, path string, handler http.HandlerFunc, oo ...RouteOptionFu } route := &Route{ - method: method, path: path, handler: handler, } @@ -69,51 +59,6 @@ func NewRoute(method, path string, handler http.HandlerFunc, oo ...RouteOptionFu return route, nil } -// NewGetRoute constructor. -func NewGetRoute(path string, handler http.HandlerFunc, oo ...RouteOptionFunc) (*Route, error) { - return NewRoute(http.MethodGet, path, handler, oo...) -} - -// NewHeadRoute constructor. -func NewHeadRoute(path string, handler http.HandlerFunc, oo ...RouteOptionFunc) (*Route, error) { - return NewRoute(http.MethodHead, path, handler, oo...) -} - -// NewPostRoute constructor. -func NewPostRoute(path string, handler http.HandlerFunc, oo ...RouteOptionFunc) (*Route, error) { - return NewRoute(http.MethodPost, path, handler, oo...) -} - -// NewPutRoute constructor. -func NewPutRoute(path string, handler http.HandlerFunc, oo ...RouteOptionFunc) (*Route, error) { - return NewRoute(http.MethodPut, path, handler, oo...) -} - -// NewPatchRoute constructor. -func NewPatchRoute(path string, handler http.HandlerFunc, oo ...RouteOptionFunc) (*Route, error) { - return NewRoute(http.MethodPatch, path, handler, oo...) -} - -// NewDeleteRoute constructor. -func NewDeleteRoute(path string, handler http.HandlerFunc, oo ...RouteOptionFunc) (*Route, error) { - return NewRoute(http.MethodDelete, path, handler, oo...) -} - -// NewConnectRoute constructor. -func NewConnectRoute(path string, handler http.HandlerFunc, oo ...RouteOptionFunc) (*Route, error) { - return NewRoute(http.MethodConnect, path, handler, oo...) -} - -// NewOptionsRoute constructor. -func NewOptionsRoute(path string, handler http.HandlerFunc, oo ...RouteOptionFunc) (*Route, error) { - return NewRoute(http.MethodOptions, path, handler, oo...) -} - -// NewTraceRoute constructor. -func NewTraceRoute(path string, handler http.HandlerFunc, oo ...RouteOptionFunc) (*Route, error) { - return NewRoute(http.MethodTrace, path, handler, oo...) -} - // Routes definition. type Routes struct { routes []*Route diff --git a/component/http/route_option.go b/component/http/route_option.go index 16c30faad0..a23c7fb939 100644 --- a/component/http/route_option.go +++ b/component/http/route_option.go @@ -2,7 +2,7 @@ package http import ( "errors" - "net/http" + "strings" "github.com/beatlabs/patron/cache" "github.com/beatlabs/patron/component/http/auth" @@ -57,7 +57,7 @@ func WithAuth(auth auth.Authenticator) RouteOptionFunc { // WithCache option for setting the route cache. func WithCache(cache cache.TTLCache, ageBounds httpcache.Age) RouteOptionFunc { return func(r *Route) error { - if r.method != http.MethodGet { + if !strings.HasPrefix(r.path, "GET") { return errors.New("cannot apply cache to a route with any method other than GET") } rc, ee := httpcache.NewRouteCache(cache, ageBounds) diff --git a/component/http/route_option_test.go b/component/http/route_option_test.go index a35dfe7c7f..5729c9c075 100644 --- a/component/http/route_option_test.go +++ b/component/http/route_option_test.go @@ -112,7 +112,7 @@ func TestAuth(t *testing.T) { func TestCache(t *testing.T) { t.Parallel() type fields struct { - httpMethod string + path string } type args struct { cache cache.TTLCache @@ -124,17 +124,17 @@ func TestCache(t *testing.T) { expectedErr string }{ "success": { - fields: fields{httpMethod: http.MethodGet}, + fields: fields{path: "GET /api"}, args: args{cache: &redis.Cache{}, ageBounds: httpcache.Age{}}, expectedErr: "", }, "fail with missing get": { - fields: fields{httpMethod: http.MethodDelete}, + fields: fields{path: "POST /api"}, args: args{cache: &redis.Cache{}, ageBounds: httpcache.Age{}}, expectedErr: "cannot apply cache to a route with any method other than GET", }, "fail with args": { - fields: fields{httpMethod: http.MethodGet}, + fields: fields{path: "GET /api"}, args: args{cache: nil, ageBounds: httpcache.Age{}}, expectedErr: "route cache is nil\n", }, @@ -143,7 +143,7 @@ func TestCache(t *testing.T) { tt := tt t.Run(name, func(t *testing.T) { t.Parallel() - route := &Route{method: tt.fields.httpMethod} + route := &Route{path: tt.fields.path} err := WithCache(tt.args.cache, tt.args.ageBounds)(route) if tt.expectedErr != "" { assert.EqualError(t, err, tt.expectedErr) diff --git a/component/http/route_test.go b/component/http/route_test.go index 2a78dec9ee..9a4a881fd1 100644 --- a/component/http/route_test.go +++ b/component/http/route_test.go @@ -17,7 +17,6 @@ func TestNewRoute(t *testing.T) { require.NoError(t, err) type args struct { - method string path string handler http.HandlerFunc optionFuncs []RouteOptionFunc @@ -28,32 +27,22 @@ func TestNewRoute(t *testing.T) { expectedErr string }{ "success": {args: args{ - method: http.MethodGet, - path: "/api", + path: "GET /api", handler: handler, optionFuncs: []RouteOptionFunc{rateLimiting}, }}, - "missing method": {args: args{ - method: "", - path: "/api", - handler: handler, - optionFuncs: []RouteOptionFunc{rateLimiting}, - }, expectedErr: "method is empty"}, "missing path": {args: args{ - method: http.MethodGet, path: "", handler: handler, optionFuncs: []RouteOptionFunc{rateLimiting}, }, expectedErr: "path is empty"}, "missing handler": {args: args{ - method: http.MethodGet, - path: "/api", + path: "GET /api", handler: nil, optionFuncs: []RouteOptionFunc{rateLimiting}, }, expectedErr: "handler is nil"}, "missing middlewares": {args: args{ - method: http.MethodGet, - path: "/api", + path: "GET /api", handler: handler, optionFuncs: []RouteOptionFunc{WithMiddlewares()}, }, expectedErr: "middlewares are empty"}, @@ -62,102 +51,20 @@ func TestNewRoute(t *testing.T) { tt := tt t.Run(name, func(t *testing.T) { t.Parallel() - got, err := NewRoute(tt.args.method, tt.args.path, tt.args.handler, tt.args.optionFuncs...) + got, err := NewRoute(tt.args.path, tt.args.handler, tt.args.optionFuncs...) if tt.expectedErr != "" { assert.EqualError(t, err, tt.expectedErr) assert.Nil(t, got) } else { assert.NoError(t, err) - assertRoute(t, tt.args.method, tt.args.path, got) + assertRoute(t, tt.args.path, got) assert.Equal(t, "GET /api", got.String()) } }) } } -func TestNewGetRoute(t *testing.T) { - rateLimiting, err := WithRateLimiting(1, 1) - require.NoError(t, err) - route, err := NewGetRoute("/api", func(writer http.ResponseWriter, request *http.Request) {}, - []RouteOptionFunc{rateLimiting}...) - require.NoError(t, err) - assertRoute(t, http.MethodGet, "/api", route) -} - -func TestNewHeadRoute(t *testing.T) { - rateLimiting, err := WithRateLimiting(1, 1) - require.NoError(t, err) - route, err := NewHeadRoute("/api", func(writer http.ResponseWriter, request *http.Request) {}, - []RouteOptionFunc{rateLimiting}...) - require.NoError(t, err) - assertRoute(t, http.MethodHead, "/api", route) -} - -func TestNewPostRoute(t *testing.T) { - rateLimiting, err := WithRateLimiting(1, 1) - require.NoError(t, err) - route, err := NewPostRoute("/api", func(writer http.ResponseWriter, request *http.Request) {}, - []RouteOptionFunc{rateLimiting}...) - require.NoError(t, err) - assertRoute(t, http.MethodPost, "/api", route) -} - -func TestNewPutRoute(t *testing.T) { - rateLimiting, err := WithRateLimiting(1, 1) - require.NoError(t, err) - route, err := NewPutRoute("/api", func(writer http.ResponseWriter, request *http.Request) {}, - []RouteOptionFunc{rateLimiting}...) - require.NoError(t, err) - assertRoute(t, http.MethodPut, "/api", route) -} - -func TestNewPatchRoute(t *testing.T) { - rateLimiting, err := WithRateLimiting(1, 1) - require.NoError(t, err) - route, err := NewPatchRoute("/api", func(writer http.ResponseWriter, request *http.Request) {}, - []RouteOptionFunc{rateLimiting}...) - require.NoError(t, err) - assertRoute(t, http.MethodPatch, "/api", route) -} - -func TestNewDeleteRoute(t *testing.T) { - rateLimiting, err := WithRateLimiting(1, 1) - require.NoError(t, err) - route, err := NewDeleteRoute("/api", func(writer http.ResponseWriter, request *http.Request) {}, - []RouteOptionFunc{rateLimiting}...) - require.NoError(t, err) - assertRoute(t, http.MethodDelete, "/api", route) -} - -func TestNewConnectRoute(t *testing.T) { - rateLimiting, err := WithRateLimiting(1, 1) - require.NoError(t, err) - route, err := NewConnectRoute("/api", func(writer http.ResponseWriter, request *http.Request) {}, - []RouteOptionFunc{rateLimiting}...) - require.NoError(t, err) - assertRoute(t, http.MethodConnect, "/api", route) -} - -func TestNewOptionsRoute(t *testing.T) { - rateLimiting, err := WithRateLimiting(1, 1) - require.NoError(t, err) - route, err := NewOptionsRoute("/api", func(writer http.ResponseWriter, request *http.Request) {}, - []RouteOptionFunc{rateLimiting}...) - require.NoError(t, err) - assertRoute(t, http.MethodOptions, "/api", route) -} - -func TestNewTraceRoute(t *testing.T) { - rateLimiting, err := WithRateLimiting(1, 1) - require.NoError(t, err) - route, err := NewTraceRoute("/api", func(writer http.ResponseWriter, request *http.Request) {}, - []RouteOptionFunc{rateLimiting}...) - require.NoError(t, err) - assertRoute(t, http.MethodTrace, "/api", route) -} - -func assertRoute(t *testing.T, method, path string, route *Route) { - assert.Equal(t, method, route.Method()) +func assertRoute(t *testing.T, path string, route *Route) { assert.Equal(t, path, route.Path()) assert.NotNil(t, route.Handler()) assert.Len(t, route.Middlewares(), 1) diff --git a/component/http/router/httprouter/route.go b/component/http/router/route.go similarity index 83% rename from component/http/router/httprouter/route.go rename to component/http/router/route.go index 7912d90456..58dddaf32d 100644 --- a/component/http/router/httprouter/route.go +++ b/component/http/router/route.go @@ -1,4 +1,4 @@ -package httprouter +package router import ( "errors" @@ -7,7 +7,6 @@ import ( "os" patronhttp "github.com/beatlabs/patron/component/http" - "github.com/julienschmidt/httprouter" ) // NewFileServerRoute returns a route that acts as a file server. @@ -39,16 +38,8 @@ func NewFileServerRoute(path string, assetsDir string, fallbackPath string) (*pa } handler := func(w http.ResponseWriter, r *http.Request) { - paramPath := "" - for _, param := range httprouter.ParamsFromContext(r.Context()) { - if param.Key == "path" { - paramPath = param.Value - break - } - } - // get the absolute path to prevent directory traversal - path := assetsDir + paramPath + path := assetsDir + r.PathValue("path") // check whether a file exists at the given path info, err := os.Stat(path) @@ -67,5 +58,5 @@ func NewFileServerRoute(path string, assetsDir string, fallbackPath string) (*pa http.ServeFile(w, r, path) } - return patronhttp.NewRoute(http.MethodGet, path, handler) + return patronhttp.NewRoute(path, handler) } diff --git a/component/http/router/httprouter/route_test.go b/component/http/router/route_test.go similarity index 88% rename from component/http/router/httprouter/route_test.go rename to component/http/router/route_test.go index 24b7056d58..1f41b5035e 100644 --- a/component/http/router/httprouter/route_test.go +++ b/component/http/router/route_test.go @@ -1,4 +1,4 @@ -package httprouter +package router import ( "net/http" @@ -20,7 +20,7 @@ func TestNewFileServerRoute(t *testing.T) { expectedErr string }{ "success": {args: args{ - path: "/frontend/*path", + path: "GET /frontend/*path", assetsDir: "testdata/", fallbackPath: "testdata/index.html", }}, @@ -50,8 +50,7 @@ func TestNewFileServerRoute(t *testing.T) { } else { assert.NoError(t, err) assert.NotNil(t, got) - assert.Equal(t, "/frontend/*path", got.Path()) - assert.Equal(t, http.MethodGet, got.Method()) + assert.Equal(t, "GET /frontend/*path", got.Path()) assert.NotNil(t, got.Handler()) assert.Len(t, got.Middlewares(), 0) } @@ -60,7 +59,7 @@ func TestNewFileServerRoute(t *testing.T) { } func TestFileServerRouteHandler(t *testing.T) { - handler, err := NewFileServerRoute("/frontend/*path", "testdata/", "testdata/index.html") + handler, err := NewFileServerRoute("GET /frontend/*path", "testdata/", "testdata/index.html") require.NoError(t, err) type args struct { diff --git a/component/http/router/httprouter/router.go b/component/http/router/router.go similarity index 92% rename from component/http/router/httprouter/router.go rename to component/http/router/router.go index b47f3d8a79..358158baf6 100644 --- a/component/http/router/httprouter/router.go +++ b/component/http/router/router.go @@ -1,13 +1,13 @@ -package httprouter +package router import ( "fmt" "log/slog" + "net/http" "os" patronhttp "github.com/beatlabs/patron/component/http" "github.com/beatlabs/patron/component/http/middleware" - "github.com/julienschmidt/httprouter" ) const defaultDeflateLevel = 6 @@ -27,7 +27,7 @@ type Config struct { } // New creates an http router with functional options. -func New(oo ...OptionFunc) (*httprouter.Router, error) { +func New(oo ...OptionFunc) (*http.ServeMux, error) { cfg := &Config{ aliveCheckFunc: func() patronhttp.AliveStatus { return patronhttp.Alive }, readyCheckFunc: func() patronhttp.ReadyStatus { return patronhttp.Ready }, @@ -43,7 +43,7 @@ func New(oo ...OptionFunc) (*httprouter.Router, error) { var stdRoutes []*patronhttp.Route - mux := httprouter.New() + mux := http.NewServeMux() stdRoutes = append(stdRoutes, patronhttp.MetricRoute()) stdRoutes = append(stdRoutes, patronhttp.ProfilingRoutes(cfg.enableProfilingExpVar)...) @@ -66,7 +66,7 @@ func New(oo ...OptionFunc) (*httprouter.Router, error) { for _, route := range stdRoutes { handler := middleware.Chain(route.Handler(), stdMiddlewares...) - mux.Handler(route.Method(), route.Path(), handler) + mux.Handle(route.Path(), handler) slog.Debug("added route", slog.Any("route", route)) } @@ -88,7 +88,7 @@ func New(oo ...OptionFunc) (*httprouter.Router, error) { return nil, err } middlewares = append(middlewares, loggingTracingMiddleware) - requestObserverMiddleware, err := middleware.NewRequestObserver(route.Method(), route.Path()) + requestObserverMiddleware, err := middleware.NewRequestObserver(route.Path()) if err != nil { return nil, err } @@ -105,7 +105,7 @@ func New(oo ...OptionFunc) (*httprouter.Router, error) { middlewares = append(middlewares, route.Middlewares()...) // chain all middlewares to the handler handler := middleware.Chain(route.Handler(), middlewares...) - mux.Handler(route.Method(), route.Path(), handler) + mux.Handle(route.Path(), handler) slog.Debug("added route with middlewares", slog.Any("route", route), slog.Int("middlewares", len(middlewares))) } diff --git a/component/http/router/httprouter/router_option.go b/component/http/router/router_option.go similarity index 99% rename from component/http/router/httprouter/router_option.go rename to component/http/router/router_option.go index 30117f55e6..0fa4d5e288 100644 --- a/component/http/router/httprouter/router_option.go +++ b/component/http/router/router_option.go @@ -1,4 +1,4 @@ -package httprouter +package router import ( "errors" diff --git a/component/http/router/httprouter/router_option_test.go b/component/http/router/router_option_test.go similarity index 99% rename from component/http/router/httprouter/router_option_test.go rename to component/http/router/router_option_test.go index 3ce9816146..c421f13a9d 100644 --- a/component/http/router/httprouter/router_option_test.go +++ b/component/http/router/router_option_test.go @@ -1,4 +1,4 @@ -package httprouter +package router import ( "net/http" diff --git a/component/http/router/httprouter/router_test.go b/component/http/router/router_test.go similarity index 90% rename from component/http/router/httprouter/router_test.go rename to component/http/router/router_test.go index 161cc88a21..2f25524efd 100644 --- a/component/http/router/httprouter/router_test.go +++ b/component/http/router/router_test.go @@ -1,4 +1,4 @@ -package httprouter +package router import ( "net/http" @@ -12,7 +12,7 @@ import ( func TestNew(t *testing.T) { t.Parallel() - route, err := patronhttp.NewRoute(http.MethodGet, "/api/", func(writer http.ResponseWriter, request *http.Request) { + route, err := patronhttp.NewRoute("GET /api/", func(writer http.ResponseWriter, _ *http.Request) { writer.WriteHeader(200) }) require.NoError(t, err) @@ -41,7 +41,7 @@ func TestNew(t *testing.T) { } func TestVerifyRouter(t *testing.T) { - route, err := patronhttp.NewRoute(http.MethodGet, "/api/", func(writer http.ResponseWriter, request *http.Request) { + route, err := patronhttp.NewRoute("GET /api/", func(writer http.ResponseWriter, _ *http.Request) { writer.WriteHeader(200) }) require.NoError(t, err) diff --git a/component/http/router/httprouter/testdata/index.html b/component/http/router/testdata/index.html similarity index 100% rename from component/http/router/httprouter/testdata/index.html rename to component/http/router/testdata/index.html diff --git a/component/kafka/integration_test.go b/component/kafka/integration_test.go index 6b9fa3e6af..4a16d85838 100644 --- a/component/kafka/integration_test.go +++ b/component/kafka/integration_test.go @@ -254,7 +254,7 @@ func TestKafkaComponent_FailOnceAndRetry(t *testing.T) { func TestGroupConsume_CheckTopicFailsDueToNonExistingTopic(t *testing.T) { // Test parameters - processorFunc := func(batch Batch) error { + processorFunc := func(_ Batch) error { return nil } invalidTopicName := "invalid-topic-name" @@ -265,7 +265,7 @@ func TestGroupConsume_CheckTopicFailsDueToNonExistingTopic(t *testing.T) { func TestGroupConsume_CheckTopicFailsDueToNonExistingBroker(t *testing.T) { // Test parameters - processorFunc := func(batch Batch) error { + processorFunc := func(_ Batch) error { return nil } _, err := New(successTopic2, successTopic2+groupSuffix, []string{"127.0.0.1:9999"}, diff --git a/component/kafka/option_test.go b/component/kafka/option_test.go index 6989e308e2..8c0d281258 100644 --- a/component/kafka/option_test.go +++ b/component/kafka/option_test.go @@ -160,7 +160,7 @@ func TestNewSessionCallback(t *testing.T) { expectedErr string }{ "success": { - args: args{sessionCallback: func(session sarama.ConsumerGroupSession) error { + args: args{sessionCallback: func(_ sarama.ConsumerGroupSession) error { return nil }}, }, diff --git a/component/sqs/integration_test.go b/component/sqs/integration_test.go index 4a2a539ddd..fcb4ff5489 100644 --- a/component/sqs/integration_test.go +++ b/component/sqs/integration_test.go @@ -154,7 +154,7 @@ func createSQSAPI(region, endpoint string) (*sqs.Client, error) { } func createConfig(awsServiceID, awsRegion, awsEndpoint string) (aws.Config, error) { - customResolver := aws.EndpointResolverWithOptionsFunc(func(service, region string, options ...interface{}) (aws.Endpoint, error) { + customResolver := aws.EndpointResolverWithOptionsFunc(func(service, region string, _ ...interface{}) (aws.Endpoint, error) { if service == awsServiceID && region == awsRegion { return aws.Endpoint{ URL: awsEndpoint, diff --git a/examples/service/http.go b/examples/service/http.go index e5fb789e64..3767e1775a 100644 --- a/examples/service/http.go +++ b/examples/service/http.go @@ -7,7 +7,7 @@ import ( "github.com/beatlabs/patron" patronhttp "github.com/beatlabs/patron/component/http" - "github.com/beatlabs/patron/component/http/router/httprouter" + "github.com/beatlabs/patron/component/http/router" "github.com/beatlabs/patron/log" ) @@ -26,13 +26,13 @@ func createHttpRouter() (patron.Component, error) { } var routes patronhttp.Routes - routes.Append(patronhttp.NewGetRoute("/", handler)) + routes.Append(patronhttp.NewRoute("GET /", handler)) rr, err := routes.Result() if err != nil { return nil, fmt.Errorf("failed to create routes: %w", err) } - router, err := httprouter.New(httprouter.WithRoutes(rr...)) + router, err := router.New(router.WithRoutes(rr...)) if err != nil { return nil, fmt.Errorf("failed to create http router: %w", err) } diff --git a/go.mod b/go.mod index f8109005c8..8d5666439e 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/beatlabs/patron -go 1.21 +go 1.22 require ( github.com/IBM/sarama v1.43.0 @@ -17,7 +17,6 @@ require ( github.com/go-sql-driver/mysql v1.8.0 github.com/google/uuid v1.6.0 github.com/hashicorp/golang-lru v1.0.2 - github.com/julienschmidt/httprouter v1.3.0 github.com/opentracing-contrib/go-stdlib v1.0.0 github.com/opentracing/opentracing-go v1.2.0 github.com/prometheus/client_golang v1.19.0 diff --git a/go.sum b/go.sum index 581afae679..12875b3686 100644 --- a/go.sum +++ b/go.sum @@ -120,8 +120,6 @@ github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh6 github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs= github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY= github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= -github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= -github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/klauspost/compress v1.17.7 h1:ehO88t2UGzQK66LMdE8tibEd1ErmzZjNEqWkjLAKQQg= github.com/klauspost/compress v1.17.7/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= diff --git a/vendor/github.com/julienschmidt/httprouter/.travis.yml b/vendor/github.com/julienschmidt/httprouter/.travis.yml deleted file mode 100644 index ffacfb5d05..0000000000 --- a/vendor/github.com/julienschmidt/httprouter/.travis.yml +++ /dev/null @@ -1,18 +0,0 @@ -sudo: false -language: go -go: - - 1.7.x - - 1.8.x - - 1.9.x - - 1.10.x - - 1.11.x - - 1.12.x - - 1.13.x - - master -before_install: - - go get github.com/mattn/goveralls -script: - - go test -v -covermode=count -coverprofile=coverage.out - - go vet ./... - - test -z "$(gofmt -d -s . | tee /dev/stderr)" - - $HOME/gopath/bin/goveralls -coverprofile=coverage.out -service=travis-ci diff --git a/vendor/github.com/julienschmidt/httprouter/LICENSE b/vendor/github.com/julienschmidt/httprouter/LICENSE deleted file mode 100644 index 875308f523..0000000000 --- a/vendor/github.com/julienschmidt/httprouter/LICENSE +++ /dev/null @@ -1,29 +0,0 @@ -BSD 3-Clause License - -Copyright (c) 2013, Julien Schmidt -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - -1. Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -2. Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -3. Neither the name of the copyright holder nor the names of its - contributors may be used to endorse or promote products derived from - this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/github.com/julienschmidt/httprouter/README.md b/vendor/github.com/julienschmidt/httprouter/README.md deleted file mode 100644 index d1e68b5e92..0000000000 --- a/vendor/github.com/julienschmidt/httprouter/README.md +++ /dev/null @@ -1,300 +0,0 @@ -# HttpRouter [![Build Status](https://travis-ci.org/julienschmidt/httprouter.svg?branch=master)](https://travis-ci.org/julienschmidt/httprouter) [![Coverage Status](https://coveralls.io/repos/github/julienschmidt/httprouter/badge.svg?branch=master)](https://coveralls.io/github/julienschmidt/httprouter?branch=master) [![GoDoc](https://godoc.org/github.com/julienschmidt/httprouter?status.svg)](http://godoc.org/github.com/julienschmidt/httprouter) - -HttpRouter is a lightweight high performance HTTP request router (also called *multiplexer* or just *mux* for short) for [Go](https://golang.org/). - -In contrast to the [default mux](https://golang.org/pkg/net/http/#ServeMux) of Go's `net/http` package, this router supports variables in the routing pattern and matches against the request method. It also scales better. - -The router is optimized for high performance and a small memory footprint. It scales well even with very long paths and a large number of routes. A compressing dynamic trie (radix tree) structure is used for efficient matching. - -## Features - -**Only explicit matches:** With other routers, like [`http.ServeMux`](https://golang.org/pkg/net/http/#ServeMux), a requested URL path could match multiple patterns. Therefore they have some awkward pattern priority rules, like *longest match* or *first registered, first matched*. By design of this router, a request can only match exactly one or no route. As a result, there are also no unintended matches, which makes it great for SEO and improves the user experience. - -**Stop caring about trailing slashes:** Choose the URL style you like, the router automatically redirects the client if a trailing slash is missing or if there is one extra. Of course it only does so, if the new path has a handler. If you don't like it, you can [turn off this behavior](https://godoc.org/github.com/julienschmidt/httprouter#Router.RedirectTrailingSlash). - -**Path auto-correction:** Besides detecting the missing or additional trailing slash at no extra cost, the router can also fix wrong cases and remove superfluous path elements (like `../` or `//`). Is [CAPTAIN CAPS LOCK](http://www.urbandictionary.com/define.php?term=Captain+Caps+Lock) one of your users? HttpRouter can help him by making a case-insensitive look-up and redirecting him to the correct URL. - -**Parameters in your routing pattern:** Stop parsing the requested URL path, just give the path segment a name and the router delivers the dynamic value to you. Because of the design of the router, path parameters are very cheap. - -**Zero Garbage:** The matching and dispatching process generates zero bytes of garbage. The only heap allocations that are made are building the slice of the key-value pairs for path parameters, and building new context and request objects (the latter only in the standard `Handler`/`HandlerFunc` API). In the 3-argument API, if the request path contains no parameters not a single heap allocation is necessary. - -**Best Performance:** [Benchmarks speak for themselves](https://github.com/julienschmidt/go-http-routing-benchmark). See below for technical details of the implementation. - -**No more server crashes:** You can set a [Panic handler](https://godoc.org/github.com/julienschmidt/httprouter#Router.PanicHandler) to deal with panics occurring during handling a HTTP request. The router then recovers and lets the `PanicHandler` log what happened and deliver a nice error page. - -**Perfect for APIs:** The router design encourages to build sensible, hierarchical RESTful APIs. Moreover it has built-in native support for [OPTIONS requests](http://zacstewart.com/2012/04/14/http-options-method.html) and `405 Method Not Allowed` replies. - -Of course you can also set **custom [`NotFound`](https://godoc.org/github.com/julienschmidt/httprouter#Router.NotFound) and [`MethodNotAllowed`](https://godoc.org/github.com/julienschmidt/httprouter#Router.MethodNotAllowed) handlers** and [**serve static files**](https://godoc.org/github.com/julienschmidt/httprouter#Router.ServeFiles). - -## Usage - -This is just a quick introduction, view the [GoDoc](http://godoc.org/github.com/julienschmidt/httprouter) for details. - -Let's start with a trivial example: - -```go -package main - -import ( - "fmt" - "net/http" - "log" - - "github.com/julienschmidt/httprouter" -) - -func Index(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { - fmt.Fprint(w, "Welcome!\n") -} - -func Hello(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { - fmt.Fprintf(w, "hello, %s!\n", ps.ByName("name")) -} - -func main() { - router := httprouter.New() - router.GET("/", Index) - router.GET("/hello/:name", Hello) - - log.Fatal(http.ListenAndServe(":8080", router)) -} -``` - -### Named parameters - -As you can see, `:name` is a *named parameter*. The values are accessible via `httprouter.Params`, which is just a slice of `httprouter.Param`s. You can get the value of a parameter either by its index in the slice, or by using the `ByName(name)` method: `:name` can be retrieved by `ByName("name")`. - -When using a `http.Handler` (using `router.Handler` or `http.HandlerFunc`) instead of HttpRouter's handle API using a 3rd function parameter, the named parameters are stored in the `request.Context`. See more below under [Why doesn't this work with http.Handler?](#why-doesnt-this-work-with-httphandler). - -Named parameters only match a single path segment: - -``` -Pattern: /user/:user - - /user/gordon match - /user/you match - /user/gordon/profile no match - /user/ no match -``` - -**Note:** Since this router has only explicit matches, you can not register static routes and parameters for the same path segment. For example you can not register the patterns `/user/new` and `/user/:user` for the same request method at the same time. The routing of different request methods is independent from each other. - -### Catch-All parameters - -The second type are *catch-all* parameters and have the form `*name`. Like the name suggests, they match everything. Therefore they must always be at the **end** of the pattern: - -``` -Pattern: /src/*filepath - - /src/ match - /src/somefile.go match - /src/subdir/somefile.go match -``` - -## How does it work? - -The router relies on a tree structure which makes heavy use of *common prefixes*, it is basically a *compact* [*prefix tree*](https://en.wikipedia.org/wiki/Trie) (or just [*Radix tree*](https://en.wikipedia.org/wiki/Radix_tree)). Nodes with a common prefix also share a common parent. Here is a short example what the routing tree for the `GET` request method could look like: - -``` -Priority Path Handle -9 \ *<1> -3 ├s nil -2 |├earch\ *<2> -1 |└upport\ *<3> -2 ├blog\ *<4> -1 | └:post nil -1 | └\ *<5> -2 ├about-us\ *<6> -1 | └team\ *<7> -1 └contact\ *<8> -``` - -Every `*` represents the memory address of a handler function (a pointer). If you follow a path trough the tree from the root to the leaf, you get the complete route path, e.g `\blog\:post\`, where `:post` is just a placeholder ([*parameter*](#named-parameters)) for an actual post name. Unlike hash-maps, a tree structure also allows us to use dynamic parts like the `:post` parameter, since we actually match against the routing patterns instead of just comparing hashes. [As benchmarks show](https://github.com/julienschmidt/go-http-routing-benchmark), this works very well and efficient. - -Since URL paths have a hierarchical structure and make use only of a limited set of characters (byte values), it is very likely that there are a lot of common prefixes. This allows us to easily reduce the routing into ever smaller problems. Moreover the router manages a separate tree for every request method. For one thing it is more space efficient than holding a method->handle map in every single node, it also allows us to greatly reduce the routing problem before even starting the look-up in the prefix-tree. - -For even better scalability, the child nodes on each tree level are ordered by priority, where the priority is just the number of handles registered in sub nodes (children, grandchildren, and so on..). This helps in two ways: - -1. Nodes which are part of the most routing paths are evaluated first. This helps to make as much routes as possible to be reachable as fast as possible. -2. It is some sort of cost compensation. The longest reachable path (highest cost) can always be evaluated first. The following scheme visualizes the tree structure. Nodes are evaluated from top to bottom and from left to right. - -``` -├------------ -├--------- -├----- -├---- -├-- -├-- -└- -``` - -## Why doesn't this work with `http.Handler`? - -**It does!** The router itself implements the `http.Handler` interface. Moreover the router provides convenient [adapters for `http.Handler`](https://godoc.org/github.com/julienschmidt/httprouter#Router.Handler)s and [`http.HandlerFunc`](https://godoc.org/github.com/julienschmidt/httprouter#Router.HandlerFunc)s which allows them to be used as a [`httprouter.Handle`](https://godoc.org/github.com/julienschmidt/httprouter#Router.Handle) when registering a route. - -Named parameters can be accessed `request.Context`: - -```go -func Hello(w http.ResponseWriter, r *http.Request) { - params := httprouter.ParamsFromContext(r.Context()) - - fmt.Fprintf(w, "hello, %s!\n", params.ByName("name")) -} -``` - -Alternatively, one can also use `params := r.Context().Value(httprouter.ParamsKey)` instead of the helper function. - -Just try it out for yourself, the usage of HttpRouter is very straightforward. The package is compact and minimalistic, but also probably one of the easiest routers to set up. - -## Automatic OPTIONS responses and CORS - -One might wish to modify automatic responses to OPTIONS requests, e.g. to support [CORS preflight requests](https://developer.mozilla.org/en-US/docs/Glossary/preflight_request) or to set other headers. -This can be achieved using the [`Router.GlobalOPTIONS`](https://godoc.org/github.com/julienschmidt/httprouter#Router.GlobalOPTIONS) handler: - -```go -router.GlobalOPTIONS = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.Header.Get("Access-Control-Request-Method") != "" { - // Set CORS headers - header := w.Header() - header.Set("Access-Control-Allow-Methods", r.Header.Get("Allow")) - header.Set("Access-Control-Allow-Origin", "*") - } - - // Adjust status code to 204 - w.WriteHeader(http.StatusNoContent) -}) -``` - -## Where can I find Middleware *X*? - -This package just provides a very efficient request router with a few extra features. The router is just a [`http.Handler`](https://golang.org/pkg/net/http/#Handler), you can chain any http.Handler compatible middleware before the router, for example the [Gorilla handlers](http://www.gorillatoolkit.org/pkg/handlers). Or you could [just write your own](https://justinas.org/writing-http-middleware-in-go/), it's very easy! - -Alternatively, you could try [a web framework based on HttpRouter](#web-frameworks-based-on-httprouter). - -### Multi-domain / Sub-domains - -Here is a quick example: Does your server serve multiple domains / hosts? -You want to use sub-domains? -Define a router per host! - -```go -// We need an object that implements the http.Handler interface. -// Therefore we need a type for which we implement the ServeHTTP method. -// We just use a map here, in which we map host names (with port) to http.Handlers -type HostSwitch map[string]http.Handler - -// Implement the ServeHTTP method on our new type -func (hs HostSwitch) ServeHTTP(w http.ResponseWriter, r *http.Request) { - // Check if a http.Handler is registered for the given host. - // If yes, use it to handle the request. - if handler := hs[r.Host]; handler != nil { - handler.ServeHTTP(w, r) - } else { - // Handle host names for which no handler is registered - http.Error(w, "Forbidden", 403) // Or Redirect? - } -} - -func main() { - // Initialize a router as usual - router := httprouter.New() - router.GET("/", Index) - router.GET("/hello/:name", Hello) - - // Make a new HostSwitch and insert the router (our http handler) - // for example.com and port 12345 - hs := make(HostSwitch) - hs["example.com:12345"] = router - - // Use the HostSwitch to listen and serve on port 12345 - log.Fatal(http.ListenAndServe(":12345", hs)) -} -``` - -### Basic Authentication - -Another quick example: Basic Authentication (RFC 2617) for handles: - -```go -package main - -import ( - "fmt" - "log" - "net/http" - - "github.com/julienschmidt/httprouter" -) - -func BasicAuth(h httprouter.Handle, requiredUser, requiredPassword string) httprouter.Handle { - return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { - // Get the Basic Authentication credentials - user, password, hasAuth := r.BasicAuth() - - if hasAuth && user == requiredUser && password == requiredPassword { - // Delegate request to the given handle - h(w, r, ps) - } else { - // Request Basic Authentication otherwise - w.Header().Set("WWW-Authenticate", "Basic realm=Restricted") - http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) - } - } -} - -func Index(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { - fmt.Fprint(w, "Not protected!\n") -} - -func Protected(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { - fmt.Fprint(w, "Protected!\n") -} - -func main() { - user := "gordon" - pass := "secret!" - - router := httprouter.New() - router.GET("/", Index) - router.GET("/protected/", BasicAuth(Protected, user, pass)) - - log.Fatal(http.ListenAndServe(":8080", router)) -} -``` - -## Chaining with the NotFound handler - -**NOTE: It might be required to set [`Router.HandleMethodNotAllowed`](https://godoc.org/github.com/julienschmidt/httprouter#Router.HandleMethodNotAllowed) to `false` to avoid problems.** - -You can use another [`http.Handler`](https://golang.org/pkg/net/http/#Handler), for example another router, to handle requests which could not be matched by this router by using the [`Router.NotFound`](https://godoc.org/github.com/julienschmidt/httprouter#Router.NotFound) handler. This allows chaining. - -### Static files - -The `NotFound` handler can for example be used to serve static files from the root path `/` (like an `index.html` file along with other assets): - -```go -// Serve static files from the ./public directory -router.NotFound = http.FileServer(http.Dir("public")) -``` - -But this approach sidesteps the strict core rules of this router to avoid routing problems. A cleaner approach is to use a distinct sub-path for serving files, like `/static/*filepath` or `/files/*filepath`. - -## Web Frameworks based on HttpRouter - -If the HttpRouter is a bit too minimalistic for you, you might try one of the following more high-level 3rd-party web frameworks building upon the HttpRouter package: - -* [Ace](https://github.com/plimble/ace): Blazing fast Go Web Framework -* [api2go](https://github.com/manyminds/api2go): A JSON API Implementation for Go -* [Gin](https://github.com/gin-gonic/gin): Features a martini-like API with much better performance -* [Goat](https://github.com/bahlo/goat): A minimalistic REST API server in Go -* [goMiddlewareChain](https://github.com/TobiEiss/goMiddlewareChain): An express.js-like-middleware-chain -* [Hikaru](https://github.com/najeira/hikaru): Supports standalone and Google AppEngine -* [Hitch](https://github.com/nbio/hitch): Hitch ties httprouter, [httpcontext](https://github.com/nbio/httpcontext), and middleware up in a bow -* [httpway](https://github.com/corneldamian/httpway): Simple middleware extension with context for httprouter and a server with gracefully shutdown support -* [kami](https://github.com/guregu/kami): A tiny web framework using x/net/context -* [Medeina](https://github.com/imdario/medeina): Inspired by Ruby's Roda and Cuba -* [Neko](https://github.com/rocwong/neko): A lightweight web application framework for Golang -* [pbgo](https://github.com/chai2010/pbgo): pbgo is a mini RPC/REST framework based on Protobuf -* [River](https://github.com/abiosoft/river): River is a simple and lightweight REST server -* [siesta](https://github.com/VividCortex/siesta): Composable HTTP handlers with contexts -* [xmux](https://github.com/rs/xmux): xmux is a httprouter fork on top of xhandler (net/context aware) diff --git a/vendor/github.com/julienschmidt/httprouter/path.go b/vendor/github.com/julienschmidt/httprouter/path.go deleted file mode 100644 index 0331c7ec6d..0000000000 --- a/vendor/github.com/julienschmidt/httprouter/path.go +++ /dev/null @@ -1,123 +0,0 @@ -// Copyright 2013 Julien Schmidt. All rights reserved. -// Based on the path package, Copyright 2009 The Go Authors. -// Use of this source code is governed by a BSD-style license that can be found -// in the LICENSE file. - -package httprouter - -// CleanPath is the URL version of path.Clean, it returns a canonical URL path -// for p, eliminating . and .. elements. -// -// The following rules are applied iteratively until no further processing can -// be done: -// 1. Replace multiple slashes with a single slash. -// 2. Eliminate each . path name element (the current directory). -// 3. Eliminate each inner .. path name element (the parent directory) -// along with the non-.. element that precedes it. -// 4. Eliminate .. elements that begin a rooted path: -// that is, replace "/.." by "/" at the beginning of a path. -// -// If the result of this process is an empty string, "/" is returned -func CleanPath(p string) string { - // Turn empty string into "/" - if p == "" { - return "/" - } - - n := len(p) - var buf []byte - - // Invariants: - // reading from path; r is index of next byte to process. - // writing to buf; w is index of next byte to write. - - // path must start with '/' - r := 1 - w := 1 - - if p[0] != '/' { - r = 0 - buf = make([]byte, n+1) - buf[0] = '/' - } - - trailing := n > 1 && p[n-1] == '/' - - // A bit more clunky without a 'lazybuf' like the path package, but the loop - // gets completely inlined (bufApp). So in contrast to the path package this - // loop has no expensive function calls (except 1x make) - - for r < n { - switch { - case p[r] == '/': - // empty path element, trailing slash is added after the end - r++ - - case p[r] == '.' && r+1 == n: - trailing = true - r++ - - case p[r] == '.' && p[r+1] == '/': - // . element - r += 2 - - case p[r] == '.' && p[r+1] == '.' && (r+2 == n || p[r+2] == '/'): - // .. element: remove to last / - r += 3 - - if w > 1 { - // can backtrack - w-- - - if buf == nil { - for w > 1 && p[w] != '/' { - w-- - } - } else { - for w > 1 && buf[w] != '/' { - w-- - } - } - } - - default: - // real path element. - // add slash if needed - if w > 1 { - bufApp(&buf, p, w, '/') - w++ - } - - // copy element - for r < n && p[r] != '/' { - bufApp(&buf, p, w, p[r]) - w++ - r++ - } - } - } - - // re-append trailing slash - if trailing && w > 1 { - bufApp(&buf, p, w, '/') - w++ - } - - if buf == nil { - return p[:w] - } - return string(buf[:w]) -} - -// internal helper to lazily create a buffer if necessary -func bufApp(buf *[]byte, s string, w int, c byte) { - if *buf == nil { - if s[w] == c { - return - } - - *buf = make([]byte, len(s)) - copy(*buf, s[:w]) - } - (*buf)[w] = c -} diff --git a/vendor/github.com/julienschmidt/httprouter/router.go b/vendor/github.com/julienschmidt/httprouter/router.go deleted file mode 100644 index 599529d198..0000000000 --- a/vendor/github.com/julienschmidt/httprouter/router.go +++ /dev/null @@ -1,452 +0,0 @@ -// Copyright 2013 Julien Schmidt. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be found -// in the LICENSE file. - -// Package httprouter is a trie based high performance HTTP request router. -// -// A trivial example is: -// -// package main -// -// import ( -// "fmt" -// "github.com/julienschmidt/httprouter" -// "net/http" -// "log" -// ) -// -// func Index(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { -// fmt.Fprint(w, "Welcome!\n") -// } -// -// func Hello(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { -// fmt.Fprintf(w, "hello, %s!\n", ps.ByName("name")) -// } -// -// func main() { -// router := httprouter.New() -// router.GET("/", Index) -// router.GET("/hello/:name", Hello) -// -// log.Fatal(http.ListenAndServe(":8080", router)) -// } -// -// The router matches incoming requests by the request method and the path. -// If a handle is registered for this path and method, the router delegates the -// request to that function. -// For the methods GET, POST, PUT, PATCH and DELETE shortcut functions exist to -// register handles, for all other methods router.Handle can be used. -// -// The registered path, against which the router matches incoming requests, can -// contain two types of parameters: -// Syntax Type -// :name named parameter -// *name catch-all parameter -// -// Named parameters are dynamic path segments. They match anything until the -// next '/' or the path end: -// Path: /blog/:category/:post -// -// Requests: -// /blog/go/request-routers match: category="go", post="request-routers" -// /blog/go/request-routers/ no match, but the router would redirect -// /blog/go/ no match -// /blog/go/request-routers/comments no match -// -// Catch-all parameters match anything until the path end, including the -// directory index (the '/' before the catch-all). Since they match anything -// until the end, catch-all parameters must always be the final path element. -// Path: /files/*filepath -// -// Requests: -// /files/ match: filepath="/" -// /files/LICENSE match: filepath="/LICENSE" -// /files/templates/article.html match: filepath="/templates/article.html" -// /files no match, but the router would redirect -// -// The value of parameters is saved as a slice of the Param struct, consisting -// each of a key and a value. The slice is passed to the Handle func as a third -// parameter. -// There are two ways to retrieve the value of a parameter: -// // by the name of the parameter -// user := ps.ByName("user") // defined by :user or *user -// -// // by the index of the parameter. This way you can also get the name (key) -// thirdKey := ps[2].Key // the name of the 3rd parameter -// thirdValue := ps[2].Value // the value of the 3rd parameter -package httprouter - -import ( - "context" - "net/http" - "strings" -) - -// Handle is a function that can be registered to a route to handle HTTP -// requests. Like http.HandlerFunc, but has a third parameter for the values of -// wildcards (variables). -type Handle func(http.ResponseWriter, *http.Request, Params) - -// Param is a single URL parameter, consisting of a key and a value. -type Param struct { - Key string - Value string -} - -// Params is a Param-slice, as returned by the router. -// The slice is ordered, the first URL parameter is also the first slice value. -// It is therefore safe to read values by the index. -type Params []Param - -// ByName returns the value of the first Param which key matches the given name. -// If no matching Param is found, an empty string is returned. -func (ps Params) ByName(name string) string { - for i := range ps { - if ps[i].Key == name { - return ps[i].Value - } - } - return "" -} - -type paramsKey struct{} - -// ParamsKey is the request context key under which URL params are stored. -var ParamsKey = paramsKey{} - -// ParamsFromContext pulls the URL parameters from a request context, -// or returns nil if none are present. -func ParamsFromContext(ctx context.Context) Params { - p, _ := ctx.Value(ParamsKey).(Params) - return p -} - -// Router is a http.Handler which can be used to dispatch requests to different -// handler functions via configurable routes -type Router struct { - trees map[string]*node - - // Enables automatic redirection if the current route can't be matched but a - // handler for the path with (without) the trailing slash exists. - // For example if /foo/ is requested but a route only exists for /foo, the - // client is redirected to /foo with http status code 301 for GET requests - // and 307 for all other request methods. - RedirectTrailingSlash bool - - // If enabled, the router tries to fix the current request path, if no - // handle is registered for it. - // First superfluous path elements like ../ or // are removed. - // Afterwards the router does a case-insensitive lookup of the cleaned path. - // If a handle can be found for this route, the router makes a redirection - // to the corrected path with status code 301 for GET requests and 307 for - // all other request methods. - // For example /FOO and /..//Foo could be redirected to /foo. - // RedirectTrailingSlash is independent of this option. - RedirectFixedPath bool - - // If enabled, the router checks if another method is allowed for the - // current route, if the current request can not be routed. - // If this is the case, the request is answered with 'Method Not Allowed' - // and HTTP status code 405. - // If no other Method is allowed, the request is delegated to the NotFound - // handler. - HandleMethodNotAllowed bool - - // If enabled, the router automatically replies to OPTIONS requests. - // Custom OPTIONS handlers take priority over automatic replies. - HandleOPTIONS bool - - // An optional http.Handler that is called on automatic OPTIONS requests. - // The handler is only called if HandleOPTIONS is true and no OPTIONS - // handler for the specific path was set. - // The "Allowed" header is set before calling the handler. - GlobalOPTIONS http.Handler - - // Cached value of global (*) allowed methods - globalAllowed string - - // Configurable http.Handler which is called when no matching route is - // found. If it is not set, http.NotFound is used. - NotFound http.Handler - - // Configurable http.Handler which is called when a request - // cannot be routed and HandleMethodNotAllowed is true. - // If it is not set, http.Error with http.StatusMethodNotAllowed is used. - // The "Allow" header with allowed request methods is set before the handler - // is called. - MethodNotAllowed http.Handler - - // Function to handle panics recovered from http handlers. - // It should be used to generate a error page and return the http error code - // 500 (Internal Server Error). - // The handler can be used to keep your server from crashing because of - // unrecovered panics. - PanicHandler func(http.ResponseWriter, *http.Request, interface{}) -} - -// Make sure the Router conforms with the http.Handler interface -var _ http.Handler = New() - -// New returns a new initialized Router. -// Path auto-correction, including trailing slashes, is enabled by default. -func New() *Router { - return &Router{ - RedirectTrailingSlash: true, - RedirectFixedPath: true, - HandleMethodNotAllowed: true, - HandleOPTIONS: true, - } -} - -// GET is a shortcut for router.Handle(http.MethodGet, path, handle) -func (r *Router) GET(path string, handle Handle) { - r.Handle(http.MethodGet, path, handle) -} - -// HEAD is a shortcut for router.Handle(http.MethodHead, path, handle) -func (r *Router) HEAD(path string, handle Handle) { - r.Handle(http.MethodHead, path, handle) -} - -// OPTIONS is a shortcut for router.Handle(http.MethodOptions, path, handle) -func (r *Router) OPTIONS(path string, handle Handle) { - r.Handle(http.MethodOptions, path, handle) -} - -// POST is a shortcut for router.Handle(http.MethodPost, path, handle) -func (r *Router) POST(path string, handle Handle) { - r.Handle(http.MethodPost, path, handle) -} - -// PUT is a shortcut for router.Handle(http.MethodPut, path, handle) -func (r *Router) PUT(path string, handle Handle) { - r.Handle(http.MethodPut, path, handle) -} - -// PATCH is a shortcut for router.Handle(http.MethodPatch, path, handle) -func (r *Router) PATCH(path string, handle Handle) { - r.Handle(http.MethodPatch, path, handle) -} - -// DELETE is a shortcut for router.Handle(http.MethodDelete, path, handle) -func (r *Router) DELETE(path string, handle Handle) { - r.Handle(http.MethodDelete, path, handle) -} - -// Handle registers a new request handle with the given path and method. -// -// For GET, POST, PUT, PATCH and DELETE requests the respective shortcut -// functions can be used. -// -// This function is intended for bulk loading and to allow the usage of less -// frequently used, non-standardized or custom methods (e.g. for internal -// communication with a proxy). -func (r *Router) Handle(method, path string, handle Handle) { - if len(path) < 1 || path[0] != '/' { - panic("path must begin with '/' in path '" + path + "'") - } - - if r.trees == nil { - r.trees = make(map[string]*node) - } - - root := r.trees[method] - if root == nil { - root = new(node) - r.trees[method] = root - - r.globalAllowed = r.allowed("*", "") - } - - root.addRoute(path, handle) -} - -// Handler is an adapter which allows the usage of an http.Handler as a -// request handle. -// The Params are available in the request context under ParamsKey. -func (r *Router) Handler(method, path string, handler http.Handler) { - r.Handle(method, path, - func(w http.ResponseWriter, req *http.Request, p Params) { - if len(p) > 0 { - ctx := req.Context() - ctx = context.WithValue(ctx, ParamsKey, p) - req = req.WithContext(ctx) - } - handler.ServeHTTP(w, req) - }, - ) -} - -// HandlerFunc is an adapter which allows the usage of an http.HandlerFunc as a -// request handle. -func (r *Router) HandlerFunc(method, path string, handler http.HandlerFunc) { - r.Handler(method, path, handler) -} - -// ServeFiles serves files from the given file system root. -// The path must end with "/*filepath", files are then served from the local -// path /defined/root/dir/*filepath. -// For example if root is "/etc" and *filepath is "passwd", the local file -// "/etc/passwd" would be served. -// Internally a http.FileServer is used, therefore http.NotFound is used instead -// of the Router's NotFound handler. -// To use the operating system's file system implementation, -// use http.Dir: -// router.ServeFiles("/src/*filepath", http.Dir("/var/www")) -func (r *Router) ServeFiles(path string, root http.FileSystem) { - if len(path) < 10 || path[len(path)-10:] != "/*filepath" { - panic("path must end with /*filepath in path '" + path + "'") - } - - fileServer := http.FileServer(root) - - r.GET(path, func(w http.ResponseWriter, req *http.Request, ps Params) { - req.URL.Path = ps.ByName("filepath") - fileServer.ServeHTTP(w, req) - }) -} - -func (r *Router) recv(w http.ResponseWriter, req *http.Request) { - if rcv := recover(); rcv != nil { - r.PanicHandler(w, req, rcv) - } -} - -// Lookup allows the manual lookup of a method + path combo. -// This is e.g. useful to build a framework around this router. -// If the path was found, it returns the handle function and the path parameter -// values. Otherwise the third return value indicates whether a redirection to -// the same path with an extra / without the trailing slash should be performed. -func (r *Router) Lookup(method, path string) (Handle, Params, bool) { - if root := r.trees[method]; root != nil { - return root.getValue(path) - } - return nil, nil, false -} - -func (r *Router) allowed(path, reqMethod string) (allow string) { - allowed := make([]string, 0, 9) - - if path == "*" { // server-wide - // empty method is used for internal calls to refresh the cache - if reqMethod == "" { - for method := range r.trees { - if method == http.MethodOptions { - continue - } - // Add request method to list of allowed methods - allowed = append(allowed, method) - } - } else { - return r.globalAllowed - } - } else { // specific path - for method := range r.trees { - // Skip the requested method - we already tried this one - if method == reqMethod || method == http.MethodOptions { - continue - } - - handle, _, _ := r.trees[method].getValue(path) - if handle != nil { - // Add request method to list of allowed methods - allowed = append(allowed, method) - } - } - } - - if len(allowed) > 0 { - // Add request method to list of allowed methods - allowed = append(allowed, http.MethodOptions) - - // Sort allowed methods. - // sort.Strings(allowed) unfortunately causes unnecessary allocations - // due to allowed being moved to the heap and interface conversion - for i, l := 1, len(allowed); i < l; i++ { - for j := i; j > 0 && allowed[j] < allowed[j-1]; j-- { - allowed[j], allowed[j-1] = allowed[j-1], allowed[j] - } - } - - // return as comma separated list - return strings.Join(allowed, ", ") - } - return -} - -// ServeHTTP makes the router implement the http.Handler interface. -func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) { - if r.PanicHandler != nil { - defer r.recv(w, req) - } - - path := req.URL.Path - - if root := r.trees[req.Method]; root != nil { - if handle, ps, tsr := root.getValue(path); handle != nil { - handle(w, req, ps) - return - } else if req.Method != http.MethodConnect && path != "/" { - code := 301 // Permanent redirect, request with GET method - if req.Method != http.MethodGet { - // Temporary redirect, request with same method - // As of Go 1.3, Go does not support status code 308. - code = 307 - } - - if tsr && r.RedirectTrailingSlash { - if len(path) > 1 && path[len(path)-1] == '/' { - req.URL.Path = path[:len(path)-1] - } else { - req.URL.Path = path + "/" - } - http.Redirect(w, req, req.URL.String(), code) - return - } - - // Try to fix the request path - if r.RedirectFixedPath { - fixedPath, found := root.findCaseInsensitivePath( - CleanPath(path), - r.RedirectTrailingSlash, - ) - if found { - req.URL.Path = string(fixedPath) - http.Redirect(w, req, req.URL.String(), code) - return - } - } - } - } - - if req.Method == http.MethodOptions && r.HandleOPTIONS { - // Handle OPTIONS requests - if allow := r.allowed(path, http.MethodOptions); allow != "" { - w.Header().Set("Allow", allow) - if r.GlobalOPTIONS != nil { - r.GlobalOPTIONS.ServeHTTP(w, req) - } - return - } - } else if r.HandleMethodNotAllowed { // Handle 405 - if allow := r.allowed(path, req.Method); allow != "" { - w.Header().Set("Allow", allow) - if r.MethodNotAllowed != nil { - r.MethodNotAllowed.ServeHTTP(w, req) - } else { - http.Error(w, - http.StatusText(http.StatusMethodNotAllowed), - http.StatusMethodNotAllowed, - ) - } - return - } - } - - // Handle 404 - if r.NotFound != nil { - r.NotFound.ServeHTTP(w, req) - } else { - http.NotFound(w, req) - } -} diff --git a/vendor/github.com/julienschmidt/httprouter/tree.go b/vendor/github.com/julienschmidt/httprouter/tree.go deleted file mode 100644 index c9fdf5b41d..0000000000 --- a/vendor/github.com/julienschmidt/httprouter/tree.go +++ /dev/null @@ -1,666 +0,0 @@ -// Copyright 2013 Julien Schmidt. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be found -// in the LICENSE file. - -package httprouter - -import ( - "strings" - "unicode" - "unicode/utf8" -) - -func min(a, b int) int { - if a <= b { - return a - } - return b -} - -const maxParamCount uint8 = ^uint8(0) - -func countParams(path string) uint8 { - var n uint - for i := 0; i < len(path); i++ { - if path[i] != ':' && path[i] != '*' { - continue - } - n++ - } - if n >= uint(maxParamCount) { - return maxParamCount - } - - return uint8(n) -} - -type nodeType uint8 - -const ( - static nodeType = iota // default - root - param - catchAll -) - -type node struct { - path string - wildChild bool - nType nodeType - maxParams uint8 - priority uint32 - indices string - children []*node - handle Handle -} - -// increments priority of the given child and reorders if necessary -func (n *node) incrementChildPrio(pos int) int { - n.children[pos].priority++ - prio := n.children[pos].priority - - // adjust position (move to front) - newPos := pos - for newPos > 0 && n.children[newPos-1].priority < prio { - // swap node positions - n.children[newPos-1], n.children[newPos] = n.children[newPos], n.children[newPos-1] - - newPos-- - } - - // build new index char string - if newPos != pos { - n.indices = n.indices[:newPos] + // unchanged prefix, might be empty - n.indices[pos:pos+1] + // the index char we move - n.indices[newPos:pos] + n.indices[pos+1:] // rest without char at 'pos' - } - - return newPos -} - -// addRoute adds a node with the given handle to the path. -// Not concurrency-safe! -func (n *node) addRoute(path string, handle Handle) { - fullPath := path - n.priority++ - numParams := countParams(path) - - // non-empty tree - if len(n.path) > 0 || len(n.children) > 0 { - walk: - for { - // Update maxParams of the current node - if numParams > n.maxParams { - n.maxParams = numParams - } - - // Find the longest common prefix. - // This also implies that the common prefix contains no ':' or '*' - // since the existing key can't contain those chars. - i := 0 - max := min(len(path), len(n.path)) - for i < max && path[i] == n.path[i] { - i++ - } - - // Split edge - if i < len(n.path) { - child := node{ - path: n.path[i:], - wildChild: n.wildChild, - nType: static, - indices: n.indices, - children: n.children, - handle: n.handle, - priority: n.priority - 1, - } - - // Update maxParams (max of all children) - for i := range child.children { - if child.children[i].maxParams > child.maxParams { - child.maxParams = child.children[i].maxParams - } - } - - n.children = []*node{&child} - // []byte for proper unicode char conversion, see #65 - n.indices = string([]byte{n.path[i]}) - n.path = path[:i] - n.handle = nil - n.wildChild = false - } - - // Make new node a child of this node - if i < len(path) { - path = path[i:] - - if n.wildChild { - n = n.children[0] - n.priority++ - - // Update maxParams of the child node - if numParams > n.maxParams { - n.maxParams = numParams - } - numParams-- - - // Check if the wildcard matches - if len(path) >= len(n.path) && n.path == path[:len(n.path)] && - // Adding a child to a catchAll is not possible - n.nType != catchAll && - // Check for longer wildcard, e.g. :name and :names - (len(n.path) >= len(path) || path[len(n.path)] == '/') { - continue walk - } else { - // Wildcard conflict - var pathSeg string - if n.nType == catchAll { - pathSeg = path - } else { - pathSeg = strings.SplitN(path, "/", 2)[0] - } - prefix := fullPath[:strings.Index(fullPath, pathSeg)] + n.path - panic("'" + pathSeg + - "' in new path '" + fullPath + - "' conflicts with existing wildcard '" + n.path + - "' in existing prefix '" + prefix + - "'") - } - } - - c := path[0] - - // slash after param - if n.nType == param && c == '/' && len(n.children) == 1 { - n = n.children[0] - n.priority++ - continue walk - } - - // Check if a child with the next path byte exists - for i := 0; i < len(n.indices); i++ { - if c == n.indices[i] { - i = n.incrementChildPrio(i) - n = n.children[i] - continue walk - } - } - - // Otherwise insert it - if c != ':' && c != '*' { - // []byte for proper unicode char conversion, see #65 - n.indices += string([]byte{c}) - child := &node{ - maxParams: numParams, - } - n.children = append(n.children, child) - n.incrementChildPrio(len(n.indices) - 1) - n = child - } - n.insertChild(numParams, path, fullPath, handle) - return - - } else if i == len(path) { // Make node a (in-path) leaf - if n.handle != nil { - panic("a handle is already registered for path '" + fullPath + "'") - } - n.handle = handle - } - return - } - } else { // Empty tree - n.insertChild(numParams, path, fullPath, handle) - n.nType = root - } -} - -func (n *node) insertChild(numParams uint8, path, fullPath string, handle Handle) { - var offset int // already handled bytes of the path - - // find prefix until first wildcard (beginning with ':'' or '*'') - for i, max := 0, len(path); numParams > 0; i++ { - c := path[i] - if c != ':' && c != '*' { - continue - } - - // find wildcard end (either '/' or path end) - end := i + 1 - for end < max && path[end] != '/' { - switch path[end] { - // the wildcard name must not contain ':' and '*' - case ':', '*': - panic("only one wildcard per path segment is allowed, has: '" + - path[i:] + "' in path '" + fullPath + "'") - default: - end++ - } - } - - // check if this Node existing children which would be - // unreachable if we insert the wildcard here - if len(n.children) > 0 { - panic("wildcard route '" + path[i:end] + - "' conflicts with existing children in path '" + fullPath + "'") - } - - // check if the wildcard has a name - if end-i < 2 { - panic("wildcards must be named with a non-empty name in path '" + fullPath + "'") - } - - if c == ':' { // param - // split path at the beginning of the wildcard - if i > 0 { - n.path = path[offset:i] - offset = i - } - - child := &node{ - nType: param, - maxParams: numParams, - } - n.children = []*node{child} - n.wildChild = true - n = child - n.priority++ - numParams-- - - // if the path doesn't end with the wildcard, then there - // will be another non-wildcard subpath starting with '/' - if end < max { - n.path = path[offset:end] - offset = end - - child := &node{ - maxParams: numParams, - priority: 1, - } - n.children = []*node{child} - n = child - } - - } else { // catchAll - if end != max || numParams > 1 { - panic("catch-all routes are only allowed at the end of the path in path '" + fullPath + "'") - } - - if len(n.path) > 0 && n.path[len(n.path)-1] == '/' { - panic("catch-all conflicts with existing handle for the path segment root in path '" + fullPath + "'") - } - - // currently fixed width 1 for '/' - i-- - if path[i] != '/' { - panic("no / before catch-all in path '" + fullPath + "'") - } - - n.path = path[offset:i] - - // first node: catchAll node with empty path - child := &node{ - wildChild: true, - nType: catchAll, - maxParams: 1, - } - // update maxParams of the parent node - if n.maxParams < 1 { - n.maxParams = 1 - } - n.children = []*node{child} - n.indices = string(path[i]) - n = child - n.priority++ - - // second node: node holding the variable - child = &node{ - path: path[i:], - nType: catchAll, - maxParams: 1, - handle: handle, - priority: 1, - } - n.children = []*node{child} - - return - } - } - - // insert remaining path part and handle to the leaf - n.path = path[offset:] - n.handle = handle -} - -// Returns the handle registered with the given path (key). The values of -// wildcards are saved to a map. -// If no handle can be found, a TSR (trailing slash redirect) recommendation is -// made if a handle exists with an extra (without the) trailing slash for the -// given path. -func (n *node) getValue(path string) (handle Handle, p Params, tsr bool) { -walk: // outer loop for walking the tree - for { - if len(path) > len(n.path) { - if path[:len(n.path)] == n.path { - path = path[len(n.path):] - // If this node does not have a wildcard (param or catchAll) - // child, we can just look up the next child node and continue - // to walk down the tree - if !n.wildChild { - c := path[0] - for i := 0; i < len(n.indices); i++ { - if c == n.indices[i] { - n = n.children[i] - continue walk - } - } - - // Nothing found. - // We can recommend to redirect to the same URL without a - // trailing slash if a leaf exists for that path. - tsr = (path == "/" && n.handle != nil) - return - - } - - // handle wildcard child - n = n.children[0] - switch n.nType { - case param: - // find param end (either '/' or path end) - end := 0 - for end < len(path) && path[end] != '/' { - end++ - } - - // save param value - if p == nil { - // lazy allocation - p = make(Params, 0, n.maxParams) - } - i := len(p) - p = p[:i+1] // expand slice within preallocated capacity - p[i].Key = n.path[1:] - p[i].Value = path[:end] - - // we need to go deeper! - if end < len(path) { - if len(n.children) > 0 { - path = path[end:] - n = n.children[0] - continue walk - } - - // ... but we can't - tsr = (len(path) == end+1) - return - } - - if handle = n.handle; handle != nil { - return - } else if len(n.children) == 1 { - // No handle found. Check if a handle for this path + a - // trailing slash exists for TSR recommendation - n = n.children[0] - tsr = (n.path == "/" && n.handle != nil) - } - - return - - case catchAll: - // save param value - if p == nil { - // lazy allocation - p = make(Params, 0, n.maxParams) - } - i := len(p) - p = p[:i+1] // expand slice within preallocated capacity - p[i].Key = n.path[2:] - p[i].Value = path - - handle = n.handle - return - - default: - panic("invalid node type") - } - } - } else if path == n.path { - // We should have reached the node containing the handle. - // Check if this node has a handle registered. - if handle = n.handle; handle != nil { - return - } - - if path == "/" && n.wildChild && n.nType != root { - tsr = true - return - } - - // No handle found. Check if a handle for this path + a - // trailing slash exists for trailing slash recommendation - for i := 0; i < len(n.indices); i++ { - if n.indices[i] == '/' { - n = n.children[i] - tsr = (len(n.path) == 1 && n.handle != nil) || - (n.nType == catchAll && n.children[0].handle != nil) - return - } - } - - return - } - - // Nothing found. We can recommend to redirect to the same URL with an - // extra trailing slash if a leaf exists for that path - tsr = (path == "/") || - (len(n.path) == len(path)+1 && n.path[len(path)] == '/' && - path == n.path[:len(n.path)-1] && n.handle != nil) - return - } -} - -// Makes a case-insensitive lookup of the given path and tries to find a handler. -// It can optionally also fix trailing slashes. -// It returns the case-corrected path and a bool indicating whether the lookup -// was successful. -func (n *node) findCaseInsensitivePath(path string, fixTrailingSlash bool) (ciPath []byte, found bool) { - return n.findCaseInsensitivePathRec( - path, - make([]byte, 0, len(path)+1), // preallocate enough memory for new path - [4]byte{}, // empty rune buffer - fixTrailingSlash, - ) -} - -// shift bytes in array by n bytes left -func shiftNRuneBytes(rb [4]byte, n int) [4]byte { - switch n { - case 0: - return rb - case 1: - return [4]byte{rb[1], rb[2], rb[3], 0} - case 2: - return [4]byte{rb[2], rb[3]} - case 3: - return [4]byte{rb[3]} - default: - return [4]byte{} - } -} - -// recursive case-insensitive lookup function used by n.findCaseInsensitivePath -func (n *node) findCaseInsensitivePathRec(path string, ciPath []byte, rb [4]byte, fixTrailingSlash bool) ([]byte, bool) { - npLen := len(n.path) - -walk: // outer loop for walking the tree - for len(path) >= npLen && (npLen == 0 || strings.EqualFold(path[1:npLen], n.path[1:])) { - // add common prefix to result - - oldPath := path - path = path[npLen:] - ciPath = append(ciPath, n.path...) - - if len(path) > 0 { - // If this node does not have a wildcard (param or catchAll) child, - // we can just look up the next child node and continue to walk down - // the tree - if !n.wildChild { - // skip rune bytes already processed - rb = shiftNRuneBytes(rb, npLen) - - if rb[0] != 0 { - // old rune not finished - for i := 0; i < len(n.indices); i++ { - if n.indices[i] == rb[0] { - // continue with child node - n = n.children[i] - npLen = len(n.path) - continue walk - } - } - } else { - // process a new rune - var rv rune - - // find rune start - // runes are up to 4 byte long, - // -4 would definitely be another rune - var off int - for max := min(npLen, 3); off < max; off++ { - if i := npLen - off; utf8.RuneStart(oldPath[i]) { - // read rune from cached path - rv, _ = utf8.DecodeRuneInString(oldPath[i:]) - break - } - } - - // calculate lowercase bytes of current rune - lo := unicode.ToLower(rv) - utf8.EncodeRune(rb[:], lo) - - // skip already processed bytes - rb = shiftNRuneBytes(rb, off) - - for i := 0; i < len(n.indices); i++ { - // lowercase matches - if n.indices[i] == rb[0] { - // must use a recursive approach since both the - // uppercase byte and the lowercase byte might exist - // as an index - if out, found := n.children[i].findCaseInsensitivePathRec( - path, ciPath, rb, fixTrailingSlash, - ); found { - return out, true - } - break - } - } - - // if we found no match, the same for the uppercase rune, - // if it differs - if up := unicode.ToUpper(rv); up != lo { - utf8.EncodeRune(rb[:], up) - rb = shiftNRuneBytes(rb, off) - - for i, c := 0, rb[0]; i < len(n.indices); i++ { - // uppercase matches - if n.indices[i] == c { - // continue with child node - n = n.children[i] - npLen = len(n.path) - continue walk - } - } - } - } - - // Nothing found. We can recommend to redirect to the same URL - // without a trailing slash if a leaf exists for that path - return ciPath, (fixTrailingSlash && path == "/" && n.handle != nil) - } - - n = n.children[0] - switch n.nType { - case param: - // find param end (either '/' or path end) - k := 0 - for k < len(path) && path[k] != '/' { - k++ - } - - // add param value to case insensitive path - ciPath = append(ciPath, path[:k]...) - - // we need to go deeper! - if k < len(path) { - if len(n.children) > 0 { - // continue with child node - n = n.children[0] - npLen = len(n.path) - path = path[k:] - continue - } - - // ... but we can't - if fixTrailingSlash && len(path) == k+1 { - return ciPath, true - } - return ciPath, false - } - - if n.handle != nil { - return ciPath, true - } else if fixTrailingSlash && len(n.children) == 1 { - // No handle found. Check if a handle for this path + a - // trailing slash exists - n = n.children[0] - if n.path == "/" && n.handle != nil { - return append(ciPath, '/'), true - } - } - return ciPath, false - - case catchAll: - return append(ciPath, path...), true - - default: - panic("invalid node type") - } - } else { - // We should have reached the node containing the handle. - // Check if this node has a handle registered. - if n.handle != nil { - return ciPath, true - } - - // No handle found. - // Try to fix the path by adding a trailing slash - if fixTrailingSlash { - for i := 0; i < len(n.indices); i++ { - if n.indices[i] == '/' { - n = n.children[i] - if (len(n.path) == 1 && n.handle != nil) || - (n.nType == catchAll && n.children[0].handle != nil) { - return append(ciPath, '/'), true - } - return ciPath, false - } - } - } - return ciPath, false - } - } - - // Nothing found. - // Try to fix the path by adding / removing a trailing slash - if fixTrailingSlash { - if path == "/" { - return ciPath, true - } - if len(path)+1 == npLen && n.path[len(path)] == '/' && - strings.EqualFold(path[1:], n.path[1:len(path)]) && n.handle != nil { - return append(ciPath, n.path...), true - } - } - return ciPath, false -} diff --git a/vendor/modules.txt b/vendor/modules.txt index b917bcf2da..f2c4a7a3db 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -893,9 +893,6 @@ github.com/jcmturner/gokrb5/v8/types ## explicit; go 1.13 github.com/jcmturner/rpc/v2/mstypes github.com/jcmturner/rpc/v2/ndr -# github.com/julienschmidt/httprouter v1.3.0 -## explicit; go 1.7 -github.com/julienschmidt/httprouter # github.com/klauspost/compress v1.17.7 ## explicit; go 1.20 github.com/klauspost/compress