Compare commits

...

38 commits
v1.0.1 ... main

Author SHA1 Message Date
0a0fc645fc
Merge branch 'beta'
All checks were successful
Release / Semantic Release (push) Successful in 58s
Build / Checks (push) Successful in 38s
Build / Code Coverage (push) Successful in 47s
Build / Build (push) Successful in 31s
2025-03-01 13:50:43 +01:00
107c873b4b
ci: merge dependency updates into develop branch
All checks were successful
Build / Checks (push) Successful in 33s
Build / Code Coverage (push) Successful in 48s
Build / Build (push) Successful in 38s
Release / Semantic Release (push) Successful in 47s
2025-03-01 13:26:56 +01:00
851ecb345a
chore: Configure Renovate (#1)
Reviewed-on: #1
2025-03-01 13:26:56 +01:00
f4ceca90ab
ci: merge dependency updates into develop branch 2025-03-01 13:26:00 +01:00
b4cff38dca chore: Configure Renovate (#1)
Reviewed-on: #1
2025-03-01 12:22:47 +00:00
7b5e1d0254
feat: new release of version 4
All checks were successful
Build / Checks (push) Successful in 34s
Build / Code Coverage (push) Successful in 45s
Build / Build (push) Successful in 33s
2025-02-28 15:46:06 +01:00
18a84b9b92
style: removed unnecessary braces and else if statements
All checks were successful
Build / Checks (push) Successful in 34s
Build / Code Coverage (push) Successful in 44s
Build / Build (push) Successful in 34s
2025-02-27 20:43:46 +01:00
8d8f8b0c99
style: fixed typo 2025-02-27 20:42:57 +01:00
203438dc8c
ci: new build pipeline 2025-02-27 20:41:08 +01:00
ceb7d04edd
ci: new release workflow 2025-02-27 20:39:15 +01:00
2fb1b35713
feat: renamed goHTTPRouter to httprouter
BREAKING CHANGE: new package name git.martin-riedl.de/golang/httprouter
2025-02-27 20:34:47 +01:00
c0e04495a4
docs: updated samples for v3 2024-07-02 02:08:39 +02:00
ac2d0bd827
fix: version number of new major release v3 2024-07-02 02:08:28 +02:00
1613a02b8e
test: fixed test for file system index check 2024-07-02 01:36:56 +02:00
ff36e5eb1d
renamed camel case index file names 2024-07-02 01:26:21 +02:00
a6d67cf60a
feat: new (optional) index file list for file system serve 2024-07-02 01:14:17 +02:00
aa0a11f3c5
fix: typo in FileSystem HeaderHandler
BREAKING CHANGE: rename HeaderHeandler to HeaderHandler if used
2024-07-02 00:49:34 +02:00
773209071c
fix: folders in FileSystems are now correctly returned as 404 instead of 200 without content 2024-07-02 00:40:11 +02:00
144d640578
ci: build on custom infrastructure 2024-07-01 23:46:13 +02:00
85ab140761
ci: new semantic release workflow 2024-07-01 23:24:01 +02:00
fda5e96ceb
increase go version to 1.19
BREAKING CHANGE: requires at least go 1.19 (instead of 1.16 as before)
2024-07-01 23:18:24 +02:00
f7dc11aabc
fix: ignore intellij project files 2024-07-01 22:55:50 +02:00
8225706f4c
ci: new build pipeline 2024-07-01 22:55:11 +02:00
e491412f07
fixed sources detection 2021-07-25 20:32:56 +02:00
78a018b4f6
fixed test files detection 2021-07-25 20:30:03 +02:00
5c80024022
upload coverage to sonarcloud 2021-07-25 20:17:02 +02:00
b7dc1d7f6e
new sonar-cloud integration 2021-07-24 19:34:04 +02:00
36efa38103
use new CI script 2021-07-24 19:22:47 +02:00
f3555a603d
use short name httprouter 2021-03-21 17:12:32 +01:00
8ecd10eff8
removed unused test 2021-03-21 17:09:21 +01:00
cdee276905
fixed build warnings 2021-03-18 19:02:42 +01:00
01030a52ba
enhanced prefix handling 2021-03-18 19:02:34 +01:00
dd14b201b1
enhanced CDN caching 2021-03-18 18:32:47 +01:00
b312a51c62
fixed sample code 2021-03-18 18:18:28 +01:00
50d3c1be28
updated readme for v2 release 2021-03-18 17:59:28 +01:00
d8985d5a74
prepared for new release v2 2021-03-18 17:44:00 +01:00
70865d5289
support virtual file system for static files 2021-03-17 20:45:32 +01:00
9bf43315b3
fixed path issue with url parameters 2019-08-22 11:23:25 +02:00
14 changed files with 720 additions and 79 deletions

View file

@ -0,0 +1,81 @@
name: Build
on:
push:
branches:
- main
- beta
- develop
pull_request:
jobs:
checks:
name: Checks
runs-on: docker
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: '1.23.x'
check-latest: true
- name: Run go fmt and go vet
run: |
go fmt $(go list ./...)
go vet $(go list ./...)
code-coverage:
name: Code Coverage
runs-on: docker
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: '1.23.x'
check-latest: true
- name: Run tests and generate coverage report
run: |
go test -covermode=count -coverprofile coverage.cov $(go list ./...)
go tool cover -func=coverage.cov
go tool cover -html=coverage.cov -o coverage.html
- name: Upload coverage artifacts
uses: https://code.forgejo.org/forgejo/upload-artifact@v4
with:
name: coverage-reports
path: |
coverage.cov
coverage.html
build:
name: Build
runs-on: docker
strategy:
matrix:
go:
- GOOS: darwin
GOARCH: amd64
- GOOS: darwin
GOARCH: arm64
- GOOS: linux
GOARCH: amd64
- GOOS: linux
GOARCH: arm64
- GOOS: windows
GOARCH: amd64
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: '1.23.x'
check-latest: true
- name: Set environment variables
run: |
echo "GOOS=${{ matrix.go.GOOS }}" >> $GITHUB_ENV
echo "GOARCH=${{ matrix.go.GOARCH }}" >> $GITHUB_ENV
- name: Build
run: go build .

View file

@ -0,0 +1,20 @@
name: Release
on:
push:
branches:
- main
- beta
jobs:
release:
name: Semantic Release
runs-on: docker
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Semantic Release
shell: bash
run: |
npm install -g semantic-release@23 conventional-changelog-conventionalcommits@7
semantic-release

1
.gitignore vendored
View file

@ -1,2 +1,3 @@
.DS_Store
.vscode
.idea

View file

@ -1,38 +0,0 @@
image: golang:latest
stages:
- test
validate lint:
stage: test
script:
- go get -u golang.org/x/lint/golint
- golint -set_exit_status ./...
execute tests:
stage: test
script:
- go test ./...
race detection:
stage: test
script:
- go test -race ./...
code coverage:
stage: test
script:
- go test -covermode=count -coverprofile coverage.cov ./...
- go tool cover -func=coverage.cov
- mkdir $CI_PROJECT_DIR/report
- go tool cover -html=coverage.cov -o $CI_PROJECT_DIR/report/coverage.html
coverage: '/\(statements\)\W+\d+\.\d+%/'
artifacts:
paths:
- report/
codecov.io:
stage: test
script:
- go test -race -coverprofile=coverage.txt -covermode=atomic ./...
- bash <(curl -s https://codecov.io/bash)

72
.releaserc Normal file
View file

@ -0,0 +1,72 @@
{
"branches": [
"main",
{
"name": "beta",
"prerelease": true
}
],
"plugins": [
[
"@semantic-release/commit-analyzer",
{
"preset": "conventionalcommits",
"releaseRules": [
{
"type": "chore",
"release": "patch"
},
{
"type": "build",
"release": "patch"
},
{
"type": "ci",
"release": "patch"
}
]
}
],
[
"@semantic-release/release-notes-generator",
{
"preset": "conventionalcommits",
"presetConfig": {
"types": [
{
"type": "feat",
"section": "Features"
},
{
"type": "feature",
"section": "Features"
},
{
"type": "fix",
"section": "Bug Fixes"
},
{
"type": "perf",
"section": "Performance Improvements"
},
{
"type": "revert",
"section": "Reverts"
},
{
"type": "chore",
"section": "Miscellaneous Chores"
}
]
}
}
],
[
"@semantic-release/github",
{
"successCommentCondition": false,
"failTitle": false
}
]
]
}

65
README.md Executable file → Normal file
View file

@ -1,36 +1,77 @@
# GoHTTPRouter
# httprouter
[![release](https://git.martin-riedl.de/golang/httprouter/badges/release.svg)](https://git.martin-riedl.de/golang/httprouter/tags)
[![pipeline status](https://git.martin-riedl.de/golang/httprouter/badges/workflows/build.yml/badge.svg)](https://git.martin-riedl.de/golang/httprouter/actions)
[![GoDoc](https://godoc.org/gitlab.com/martinr92/gohttprouter?status.svg)](https://godoc.org/gitlab.com/martinr92/gohttprouter)
[![pipeline status](https://gitlab.com/martinr92/gohttprouter/badges/master/pipeline.svg)](https://gitlab.com/martinr92/gohttprouter/commits/master)
[![coverage report](https://gitlab.com/martinr92/gohttprouter/badges/master/coverage.svg)](https://gitlab.com/martinr92/gohttprouter/commits/master)
[![codecov](https://codecov.io/gl/martinr92/gohttprouter/branch/master/graph/badge.svg)](https://codecov.io/gl/martinr92/gohttprouter)
[![Go Report Card](https://goreportcard.com/badge/gitlab.com/martinr92/gohttprouter)](https://goreportcard.com/report/gitlab.com/martinr92/gohttprouter)
GoHTTPRouter is a framework used for HTTP request routing.
httprouter is a framework used for HTTP request routing.
# Examples
## Simple Routing
Just replace the standard router of golang with this one:
```golang
router := gohttprouter.New()
router.HandleFunc(http.MethodGet, "/home", func(response http.ResponseWriter, request *http.Request, info gohttprouter.RoutingInfo) {
import "git.martin-riedl.de/golang/httprouter/v4"
```
```golang
router := httprouter.New()
router.HandleFunc(http.MethodGet, "/home", func(response http.ResponseWriter, request *http.Request, info httprouter.RoutingInfo) {
response.Write([]byte("Home"))
})
err := http.ListenAndServe("localhost:8080", router)
```
## Routing with placeholder
A path can also contain placeholder (like :id). If then a request is sent to the url "/user/123" the method gets executed and the URL part (in this case "123") is passed as parameter into your handler function.
```golang
router := gohttprouter.New()
router.HandleFunc(http.MethodGet, "/user/:id", func(response http.ResponseWriter, request *http.Request, info gohttprouter.RoutingInfo) {
import "git.martin-riedl.de/golang/httprouter/v4"
```
```golang
router := httprouter.New()
router.HandleFunc(http.MethodGet, "/user/:id", handleUserPages)
router.HandleFunc(http.MethodGet, "/user/:id/settings", handleUserPages)
func handleUserPages(response http.ResponseWriter, request *http.Request, info httprouter.RoutingInfo) {
id := info.Parameters["id"]
})
response.Write([]byte(id))
}
err := http.ListenAndServe("localhost:8080", router)
```
# License
## Serve Static Content
Static files (like JavaScript or CSS) can be served automatically (including caching header and MIME type). It uses the `embed.FS` (since go 1.16) to serve static content.
```golang
import "git.martin-riedl.de/golang/httprouter/v4"
//go:embed files/static/*
var staticFiles embed.FS
```
Copyright 2019 Martin Riedl
```golang
staticFS := httprouter.NewFS(&staticFiles)
router := httprouter.New()
router.Handle(http.MethodGet, "/static/*", staticFS)
```
For development purpose you can enable the local file serving additionally.
The framework checks first, if the file exists locally and serves it directly.
If not, the file is served from the `embed.FS`.
This helps you during local development so you can modify a CSS file without recompiling everything.
```golang
import "git.martin-riedl.de/golang/httprouter/v4"
```
```golang
staticFS := httprouter.NewFS(&staticFiles)
staticFS.UseLocalFolder = true
staticFS.LocalFolderPrefix = "some/folder" // optional
```
# License
```
Copyright 2018-2025 Martin Riedl
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.

185
fs.go Normal file
View file

@ -0,0 +1,185 @@
// Copyright 2021-2025 Martin Riedl
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package httprouter
import (
"embed"
"fmt"
"io"
"io/fs"
"mime"
"net/http"
"os"
"path"
"strings"
)
// FS file system for serving static content.
type FS struct {
StaticFiles *embed.FS
FolderPrefix string
UseLocalFolder bool
LocalFolderPrefix string
HeaderHandler HeaderHandler
IndexFileNames []string
}
// NewFS creates a new instance of the http file system used for serving static files.
func NewFS(staticFiles *embed.FS) *FS {
return &FS{
StaticFiles: staticFiles,
HeaderHandler: DefaultHeaderHandler,
}
}
func (fs *FS) ServeHTTP(w http.ResponseWriter, r *http.Request, info RoutingInfo) {
// use wildcard placeholder value for the path (if available)
url, found := info.Parameters["*"]
if !found {
url = r.URL.Path
}
// search for file in static file system
file, err := fs.findFile(url)
if err != nil {
http.NotFound(w, r)
return
}
// check, if the file is a folder
file, ok := fs.checkFolder(file, url)
if !ok {
http.NotFound(w, r)
return
}
defer file.Close()
// serve file content
fs.serve(w, r, info, file)
}
func (fs *FS) findFile(url string) (fs.File, error) {
// build file path
filePath := path.Join(fs.FolderPrefix, url)
filePath = strings.TrimPrefix(filePath, "/")
// check, if file exists in local folder
if fs.UseLocalFolder {
localFilePath := path.Join(fs.LocalFolderPrefix, filePath)
localFilePath = strings.TrimPrefix(localFilePath, "/")
if localFilePath == "" {
localFilePath = "."
}
file, err := os.Open(localFilePath)
if err == nil {
return file, nil
}
}
// use static file system
if filePath == "" {
filePath = "."
}
file, err := fs.StaticFiles.Open(filePath)
return file, err
}
func (fs *FS) checkFolder(file fs.File, url string) (fs.File, bool) {
fileInfo, err := file.Stat()
if err != nil {
_ = file.Close()
return file, false
}
// check for folder / root location
if fileInfo.IsDir() {
// close folder handler (we don't need it anymore)
_ = file.Close()
// check for index files
for _, indexFileName := range fs.IndexFileNames {
newFullPath := path.Join(url, indexFileName)
if newFile, err := fs.findFile(newFullPath); err == nil {
return newFile, true
}
}
return file, false
}
return file, true
}
func (fs *FS) serve(w http.ResponseWriter, r *http.Request, info RoutingInfo, file fs.File) {
// execute header handler
if fs.HeaderHandler != nil {
fs.HeaderHandler(w, r, info, file)
}
w.WriteHeader(http.StatusOK)
// check for head request
if r.Method == http.MethodHead {
// return no content
return
}
// send file content
io.Copy(w, file)
}
// HeaderHandler for custom http header handling
type HeaderHandler func(http.ResponseWriter, *http.Request, RoutingInfo, fs.File)
// DefaultHeaderHandler is the default implementation for file headers
func DefaultHeaderHandler(w http.ResponseWriter, r *http.Request, info RoutingInfo, file fs.File) {
ContentLengthHeaderHandler(w, r, info, file)
ContentTypeHeaderHandler(w, r, info, file)
CacheControlHeaderHandler(w, r, info, file)
ContentTypeOptionHeaderHandler(w, r, info, file)
}
// ContentLengthHeaderHandler sends the file size
func ContentLengthHeaderHandler(w http.ResponseWriter, r *http.Request, info RoutingInfo, file fs.File) {
// send file size as header
stat, err := file.Stat()
if err != nil {
return
}
w.Header().Set("Content-Length", fmt.Sprint(stat.Size()))
}
// ContentTypeHeaderHandler sends content type based on the file name extension
func ContentTypeHeaderHandler(w http.ResponseWriter, r *http.Request, info RoutingInfo, file fs.File) {
stat, err := file.Stat()
if err != nil {
return
}
mimeHeader := mime.TypeByExtension(path.Ext(stat.Name()))
if mimeHeader != "" {
w.Header().Set("Content-Type", mimeHeader)
}
}
// CacheControlHeaderHandler send cache control header of 7 days for the browser and 30 days for the CDN
func CacheControlHeaderHandler(w http.ResponseWriter, r *http.Request, info RoutingInfo, file fs.File) {
w.Header().Set("Cache-Control", fmt.Sprintf("public, s-maxage=%d, max-age=%d", 60*60*24*30, 60*60*24*7))
}
// ContentTypeOptionHeaderHandler for nosniff content type option
func ContentTypeOptionHeaderHandler(w http.ResponseWriter, r *http.Request, info RoutingInfo, file fs.File) {
w.Header().Set("X-Content-Type-Options", "nosniff")
}

212
fs_test.go Normal file
View file

@ -0,0 +1,212 @@
// Copyright 2025 Martin Riedl
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package httprouter
import (
"embed"
"net/http"
"net/url"
"testing"
)
//go:embed README.md LICENSE.txt
var staticFiles embed.FS
type testWriter struct {
header http.Header
written []byte
statusCode int
}
func (tw *testWriter) Header() http.Header {
if tw.header == nil {
tw.header = make(http.Header)
}
return tw.header
}
func (tw *testWriter) Write(bytes []byte) (int, error) {
tw.written = append(tw.written, bytes...)
return len(bytes), nil
}
func (tw *testWriter) WriteHeader(statusCode int) {
tw.statusCode = statusCode
}
func TestFSReadmeFile(t *testing.T) {
// create new FS
fs := NewFS(&staticFiles)
// serve the request
tw := &testWriter{}
tr := &http.Request{
Method: http.MethodGet,
URL: &url.URL{
Path: "/README.md",
},
}
fs.ServeHTTP(tw, tr, RoutingInfo{})
// check data
if tw.statusCode != http.StatusOK {
t.Error("received invalid http status code", tw.statusCode)
}
}
func TestFSReadmeFileLocal(t *testing.T) {
// create new FS
fs := NewFS(&staticFiles)
fs.UseLocalFolder = true
fs.LocalFolderPrefix = "/"
// serve the request
tw := &testWriter{}
tr := &http.Request{
Method: http.MethodGet,
URL: &url.URL{
Path: "/README.md",
},
}
fs.ServeHTTP(tw, tr, RoutingInfo{})
// check data
if tw.statusCode != http.StatusOK {
t.Error("received invalid http status code", tw.statusCode)
}
}
func TestFSReadmeFileHead(t *testing.T) {
// create new FS
fs := NewFS(&staticFiles)
// serve the request
tw := &testWriter{}
tr := &http.Request{
Method: http.MethodHead,
URL: &url.URL{
Path: "/README.md",
},
}
fs.ServeHTTP(tw, tr, RoutingInfo{})
// check data
if tw.statusCode != http.StatusOK {
t.Error("received invalid http status code", tw.statusCode)
}
}
func TestFSLicense(t *testing.T) {
// create new FS
fs := NewFS(&staticFiles)
// serve the request
tw := &testWriter{}
tr := &http.Request{
Method: http.MethodHead,
URL: &url.URL{
Path: "/LICENSE.txt",
},
}
fs.ServeHTTP(tw, tr, RoutingInfo{})
// check data
if tw.statusCode != http.StatusOK {
t.Error("received invalid http status code", tw.statusCode)
}
}
func TestFSFolder(t *testing.T) {
// create new FS
fs := NewFS(&staticFiles)
// serve the request
tw := &testWriter{}
tr := &http.Request{
Method: http.MethodHead,
URL: &url.URL{
Path: "/",
},
}
fs.ServeHTTP(tw, tr, RoutingInfo{})
// check data
if tw.statusCode != http.StatusNotFound {
t.Error("received invalid http status code", tw.statusCode)
}
}
func TestFSFolderLocal(t *testing.T) {
// create new FS
fs := NewFS(&staticFiles)
fs.UseLocalFolder = true
fs.LocalFolderPrefix = "/"
// serve the request
tw := &testWriter{}
tr := &http.Request{
Method: http.MethodHead,
URL: &url.URL{
Path: "/",
},
}
fs.ServeHTTP(tw, tr, RoutingInfo{})
// check data
if tw.statusCode != http.StatusNotFound {
t.Error("received invalid http status code", tw.statusCode)
}
}
func TestFSFolderIndex(t *testing.T) {
// create new FS
fs := NewFS(&staticFiles)
fs.IndexFileNames = []string{"README.md"}
// serve the request
tw := &testWriter{}
tr := &http.Request{
Method: http.MethodHead,
URL: &url.URL{
Path: "/",
},
}
fs.ServeHTTP(tw, tr, RoutingInfo{})
// check data
if tw.statusCode != http.StatusOK {
t.Error("received invalid http status code", tw.statusCode)
}
}
func TestFSNotFound(t *testing.T) {
// create new FS
fs := NewFS(&staticFiles)
// serve the request
tw := &testWriter{}
tr := &http.Request{
Method: http.MethodGet,
URL: &url.URL{
Path: "/not-exists.txt",
},
}
fs.ServeHTTP(tw, tr, RoutingInfo{})
// check data
if tw.statusCode != http.StatusNotFound {
t.Error("received invalid http status code", tw.statusCode)
}
}

4
go.mod
View file

@ -1 +1,3 @@
module gitlab.com/martinr92/gohttprouter
module git.martin-riedl.de/golang/httprouter/v4
go 1.19

9
renovate.json Normal file
View file

@ -0,0 +1,9 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"local>ci/renovate//configs/base"
],
"baseBranches": [
"develop"
]
}

33
route.go Executable file → Normal file
View file

@ -1,4 +1,4 @@
// Copyright 2018-2019 Martin Riedl
// Copyright 2018-2025 Martin Riedl
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package gohttprouter
package httprouter
import (
"strings"
@ -21,11 +21,15 @@ import (
type route struct {
name string
placeholderName string
isWildcard bool
handler Handler
routes map[string]*route
}
const parameterPrefix = ":"
const (
parameterPrefix = ":"
wildcard = "*"
)
func newRoute() *route {
return &route{routes: make(map[string]*route)}
@ -41,6 +45,13 @@ func (r *route) parsePath(path string, create bool) (*route, bool, map[string]st
parameters[r.placeholderName] = parts[0]
}
// check for wildcard match
if !create && r.isWildcard {
parameters = make(map[string]string)
parameters[wildcard] = path
return r, false, parameters
}
// more path is available
if len(parts) == 2 {
// parse next part element
@ -61,12 +72,17 @@ func (r *route) parsePath(path string, create bool) (*route, bool, map[string]st
subRoute, ok = r.routes[parameterPrefix]
}
// route still not found, try to use wildcard path
if !create && !ok {
subRoute, ok = r.routes[wildcard]
}
// check for creation
subRouteCreated := false
if !ok && !create {
// no route found
return nil, false, nil
} else if !ok && create {
} else if !ok {
subRoute = newRoute()
subRoute.parseName(subName)
r.routes[subRoute.name] = subRoute
@ -75,7 +91,7 @@ func (r *route) parsePath(path string, create bool) (*route, bool, map[string]st
// parse sub-route
matchingRoute, created, subParameters := subRoute.parsePath(parts[1], create)
return matchingRoute, (subRouteCreated || created), mergeParameterMaps(parameters, subParameters)
return matchingRoute, subRouteCreated || created, mergeParameterMaps(parameters, subParameters)
}
// last element in path
@ -86,6 +102,9 @@ func (r *route) parseName(name string) {
if isParameter(name) {
r.name = parameterPrefix
r.placeholderName = name[1:]
} else if isWildcard(name) {
r.name = wildcard
r.isWildcard = true
} else {
r.name = name
}
@ -108,3 +127,7 @@ func mergeParameterMaps(m1, m2 map[string]string) map[string]string {
func isParameter(s string) bool {
return strings.Index(s, parameterPrefix) == 0 && len(s) > 1
}
func isWildcard(s string) bool {
return s == wildcard
}

30
route_test.go Executable file → Normal file
View file

@ -1,4 +1,18 @@
package gohttprouter
// Copyright 2025 Martin Riedl
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package httprouter
import (
"testing"
@ -68,3 +82,17 @@ func TestRoutePlaceholder(t *testing.T) {
t.Error("wrong parameter value parsed")
}
}
func TestRouteWildcard(t *testing.T) {
route := newRoute()
route.parsePath("/static/*", true)
// check path
foundRoute, _, foundParameters := route.parsePath("/static/js/my.js", false)
if foundRoute == nil {
t.Error("no route found for wildcard path")
}
if foundParameters["*"] != "js/my.js" {
t.Error("wrong parameter value for wildcard parsed")
}
}

12
router.go Executable file → Normal file
View file

@ -1,4 +1,4 @@
// Copyright 2018-2019 Martin Riedl
// Copyright 2018-2025 Martin Riedl
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package gohttprouter
package httprouter
import (
"fmt"
@ -85,7 +85,7 @@ func (router *Router) findRoute(method string, path string, create bool) (route
methodRoute, ok := router.registry[methodUpper]
if !ok && !create {
return nil, false, nil
} else if !ok && create {
} else if !ok {
methodRoute = newRoute()
router.registry[methodUpper] = methodRoute
created = true
@ -113,9 +113,11 @@ func (router *Router) validateRoute(path string) error {
func (router *Router) ServeHTTP(response http.ResponseWriter, request *http.Request) {
// check for exact matching
route, _, parameter := router.findRoute(request.Method, request.RequestURI, false)
route, _, parameter := router.findRoute(request.Method, request.URL.Path, false)
if route != nil && route.handler != nil {
route.handler.ServeHTTP(response, request, RoutingInfo{Parameters: parameter})
route.handler.ServeHTTP(response, request, RoutingInfo{
Parameters: parameter,
})
return
}

View file

@ -1,4 +1,18 @@
package gohttprouter
// Copyright 2025 Martin Riedl
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package httprouter
import (
"net"
@ -7,17 +21,6 @@ import (
"testing"
)
func testWebServer(t *testing.T) {
router := New()
Must(router.HandleFunc(http.MethodGet, "/:name", func(response http.ResponseWriter, request *http.Request, info RoutingInfo) {
response.Write([]byte("name: " + info.Parameters["name"]))
}))
err := http.ListenAndServe("localhost:8080", router)
if err != nil {
t.Error(err)
}
}
func TestRouterInvalidPath(t *testing.T) {
router := New()
if err := router.HandleFunc(http.MethodGet, "missing/slash", emptyHandlerFunction); err == nil {
@ -74,7 +77,7 @@ func TestRouterServer(t *testing.T) {
}
// wait for completion
_ = <-quitChan
<-quitChan
listener.Close()
}
@ -90,13 +93,13 @@ func TestRouterServerHandler(t *testing.T) {
go http.Serve(listener, router)
// call method
_, err = http.Get("http://localhost:8081/")
_, err = http.Get("http://localhost:8081/?test=false")
if err != nil {
t.Error(err)
}
// wait for completion
_ = <-testHandler.CloseChannel
<-testHandler.CloseChannel
listener.Close()
}