Thanks to visit codestin.com
Credit goes to github.com

Skip to content

🔥 feat: Route Domain() func for host-based routing#4100

Draft
Copilot wants to merge 12 commits intomainfrom
copilot/add-route-domain-function
Draft

🔥 feat: Route Domain() func for host-based routing#4100
Copilot wants to merge 12 commits intomainfrom
copilot/add-route-domain-function

Conversation

Copy link
Contributor

Copilot AI commented Feb 21, 2026

Description

Adds Domain() to the Router interface for host-based routing with parameter support. Domain filtering is implemented via handler wrapping—zero changes to the core router—so there is no performance impact on routes that don't use it.

Domain parameters are kept separate from path parameters (accessed via DomainParam()) per maintainer guidance to not mix path and domain routing. All string operations use Fiber's utility packages (utils.TrimRight, utils.TrimSpace, utilsstrings.ToLower) for optimal performance. Domain parameter counts are bounded by the same maxParams limit used in the core path router to prevent OOM from unbounded allocations.

The domainLocalsKey uses an unexported typed struct key to prevent collisions with user or middleware locals. All handlers in a domain-scoped route are individually wrapped with domain checking, with the hostname match computed once per request per domainRouter and cached in c.Locals() via a directly allocated domainCheckResult to avoid redundant parsing for multi-handler routes. Static domain matches (no params) clear any previously stored domain params to prevent stale value leaks. Fully-qualified domain names with trailing dots (e.g., api.example.com.) are normalized via utils.TrimRight consistent with Fiber's Subdomains() host normalization (RFC 3986). Sub-app mounting via Domain(...).Use(*App) is explicitly unsupported with a clear panic message. The domain router is verified compatible with all 17 handler types defined in adapter.go.

Changes introduced

  • domain.godomainRouter implementing full Router interface, domain pattern parser/matcher using fiber utils (utils.TrimRight, utils.TrimSpace, utilsstrings.ToLower), trailing dot normalization (RFC 3986), maxParams bound check consistent with path router, DomainParam() accessor, domainRegistering for RouteChain support, typed struct key for Locals storage, per-request cached domain match result to avoid redundant hostname parsing, stale param clearing, explicit *App mount rejection
  • router.goDomain(host string) Router added to Router interface
  • app.go / group.goDomain() method on App and Group
  • docs/api/app.md — Detailed API documentation for Domain and DomainParam with full examples following the same formatting as Group, Route, and RouteChain sections
  • docs/whats_new.md — Domain routing section with examples
app := fiber.New()

// Static domain — only matches requests to api.example.com
app.Domain("api.example.com").Get("/users", listUsers)

// Domain with parameter
app.Domain(":user.blog.example.com").Get("/", func(c fiber.Ctx) error {
    user := fiber.DomainParam(c, "user")
    return c.SendString(user + "'s blog")
})

// Composable with groups and middleware
api := app.Domain("api.example.com")
api.Use(authMiddleware)
v1 := api.Group("/v1")
v1.Get("/posts", listPosts)

// Fallback for unmatched domains
app.Get("/", defaultHandler)
  • Benchmarks: Benchmark_Domain_Match, Benchmark_Domain_Route, Benchmark_Domain_NoImpact included to verify zero overhead on non-domain routes.
  • Documentation Update: docs/api/app.md updated with Domain and DomainParam sections with detailed examples; docs/whats_new.md updated with Domain routing section.
  • Changelog/What's New: Domain routing added under Router section.
  • API Alignment with Express: Pattern mirrors Express vhost style with :param syntax consistent with Fiber's path parameters.
  • API Longevity: Returns Router interface; DomainParam() is a standalone function avoiding Ctx interface bloat. Domain parameter count bounded by maxParams consistent with path router. Typed locals key prevents collisions. Per-request cached match avoids redundant work.
  • Examples: Usage examples in API docs and whats_new, plus comprehensive test cases including trailing dot, non-matching-host multi-handler assertions, stale param clearing, and compatibility tests for all 17 handler types from adapter.go.

Type of change

  • New feature (non-breaking change which adds functionality)
  • Documentation update (changes to documentation)
  • Performance improvement (non-breaking change which improves efficiency)
  • Code consistency (non-breaking change which improves code reliability and robustness)

Checklist

  • Followed the inspiration of the Express.js framework for new functionalities, making them similar in usage.
  • Conducted a self-review of the code and provided comments for complex or critical parts.
  • Updated the documentation in the /docs/ directory for Fiber's documentation.
  • Added or updated unit tests to validate the effectiveness of the changes or new features.
  • Ensured that new and existing unit tests pass locally with the changes.
  • Verified that any new dependencies are essential and have been agreed upon by the maintainers/community.
  • Aimed for optimal performance with minimal allocations in the new code.
  • Provided benchmarks for the new code to analyze and improve upon.

Commit formatting

Please use emojis in commit messages for an easy way to identify the purpose or intention of a commit. Check out the emoji cheatsheet here: CONTRIBUTING.md

Original prompt

This section details on the original issue you should resolve

<issue_title>🚀 [Feature]: Route Domain() func</issue_title>
<issue_description>### Feature Description

In projects using multiple domains, the easy-to-use Domain() function can be added.

Additional Context (optional)

I know there are manual solutions, but they are not easy to use.

Code Snippet (optional)

package main

import "github.com/gofiber/fiber/v2"
import "log"

func main() {
  app := fiber.New()

  app.Domain(":username.blog.example.com").Get("/*path", func(c *fiber.Ctx) error {
    username := c.Params("username")
    return c.SendString(username + "'s blog page")
  })

  log.Fatal(app.Listen(":3000"))
}

Checklist:

  • I agree to follow Fiber's Code of Conduct.
  • I have checked for existing issues that describe my suggestion prior to opening this one.
  • I understand that improperly formatted feature requests may be closed without explanation.</issue_description>

<agent_instructions>- Document the feature properly with examples

  • Add a section in whats_new.md
  • This can probably go in domain.go and tests in domain_test.go
  • Test coverage should be at least 90%
  • Router performance is critical</agent_instructions>

Comments on the Issue (you are @copilot in this section)

@ReneWerner87 nice idea related: https://github.com//issues/750 https://github.com//issues/1409 @ReneWerner87 would not want to mix the routing of the paths and this feature, more like in the proposal @gaby @joey1123455 Did you ever get anywhere with this? @gaby @vhespanha Give it a try, I'm not sure how will this be implemented without affecting router performance. @gaby @KeyzarRasya We looked at it, but it will impact router performance for everyone which is not ideal.

✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

Copilot AI and others added 4 commits February 21, 2026 21:19
- errcheck: handle errors from io.ReadAll(resp.Body) with require.NoError
- httpNoBody: use http.NoBody instead of nil in httptest.NewRequest
- fieldalignment: reorder struct fields to minimize padding
- errcheck: add error handling in benchmarks for app.Test

Co-authored-by: Copilot <[email protected]>
Copilot AI changed the title [WIP] Add Domain() function for multiple domains support 🔥 feat: Route Domain() func for host-based routing Feb 21, 2026
Copilot AI requested a review from gaby February 21, 2026 21:37
@ReneWerner87 ReneWerner87 added this to v3 Feb 21, 2026
@ReneWerner87 ReneWerner87 added this to the v3 milestone Feb 21, 2026
@codecov
Copy link

codecov bot commented Feb 21, 2026

Codecov Report

❌ Patch coverage is 94.63415% with 11 lines in your changes missing coverage. Please review.
✅ Project coverage is 91.17%. Comparing base (082e205) to head (1ffe491).

Files with missing lines Patch % Lines
domain.go 94.38% 6 Missing and 5 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #4100      +/-   ##
==========================================
+ Coverage   91.04%   91.17%   +0.13%     
==========================================
  Files         119      120       +1     
  Lines       11340    11545     +205     
==========================================
+ Hits        10324    10526     +202     
  Misses        644      644              
- Partials      372      375       +3     
Flag Coverage Δ
unittests 91.17% <94.63%> (+0.13%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Contributor

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Performance Alert ⚠️

Possible performance regression was detected for benchmark.
Benchmark result of this commit is worse than the previous benchmark result exceeding threshold 1.50.

Benchmark suite Current: 694cabf Previous: 92ada8a Ratio
BenchmarkDecoderedirectionMsgs - MB/s 114.43 MB/s 47.86 MB/s 2.39

This comment was automatically generated by workflow using github-action-benchmark.

@gaby
Copy link
Member

gaby commented Feb 21, 2026

@copilot

  • Add documentation for the new feature with detailed examples. Follow the same formatting as other docs.
  • The benchmark for Benchmark_NewError shows a regression from 42ns/op to 78ns/op
  • In domain.go make sure to use fiber utils. For example strings.ToLower should be using the fiber util which is more performant.

Copy link
Contributor Author

Copilot AI commented Feb 21, 2026

@copilot

  • Add documentation for the new feature with detailed examples. Follow the same formatting as other docs.
  • The benchmark for Benchmark_NewError shows a regression from 42ns/op to 78ns/op
    ...

Addressed in ed8cbb8:

  • Fiber utils: Replaced strings.ToLower with utilsstrings.ToLower from github.com/gofiber/utils/v2/strings in domain.go (both parseDomainPattern and match).
  • Documentation: Added Domain and DomainParam sections to docs/api/app.md following the same formatting as Group, Route, and RouteChain.
  • Benchmark_NewError: Confirmed no regression from our changes — the NewError function is untouched by this PR. Local benchmarks show ~28ns/op across 3 runs, consistent with baseline. The CI regression is likely environment noise.

@gaby
Copy link
Member

gaby commented Feb 22, 2026

@copilot

  • Replace strings.TrimSpace with utils.TrimSpace in domain.go
  • In domain.go function match() you call make using the len of params without any bound limits. This could cause a OOM in the future. Set a reasonable limit. Check the code in the regular router.go and app.go and use the same limit defined there.

Copy link
Contributor Author

Copilot AI commented Feb 22, 2026

@copilot

  • Replace strings.TrimSpace with utils.TrimSpace in domain.go
  • In domain.go function match() you call make using the len of params without any bound limits. This could cause a OOM in the ...

Addressed in 1f1dbd7:

  • Replaced strings.TrimSpace with utils.TrimSpace from github.com/gofiber/utils/v2.
  • Added maxParams (30) bound check in parseDomainPattern() — panics if the domain pattern exceeds the limit, consistent with the path parser in path.go:250. Since the check is at registration time, match() is already bounded by the validated m.paramIdx slice length.

@gaby
Copy link
Member

gaby commented Feb 22, 2026

/gemini review

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This is an excellent pull request that introduces host-based routing, a highly requested feature. The implementation is clean, non-intrusive, and aligns well with Fiber's existing API and performance goals. The "zero impact" on non-domain routes is a key achievement, and the comprehensive test suite, including benchmarks, provides strong confidence in the changes. The documentation is also very thorough. I have a couple of minor suggestions to improve code and documentation clarity.

@gaby gaby requested a review from Copilot February 22, 2026 21:12
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds host-based routing to Fiber via a new Domain(host string) Router API, implemented as a wrapper-based router (domainRouter) to avoid impacting non-domain routes while enabling :param capture via DomainParam().

Changes:

  • Extends the Router interface with Domain(host string) Router, and adds Domain() methods on App and Group.
  • Introduces domain.go with domain pattern parsing/matching, handler wrapping, and DomainParam() retrieval.
  • Adds comprehensive domain routing tests/benchmarks and updates API + “What’s New” documentation.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
router.go Adds Domain(host string) Router to the exported Router interface.
app.go Adds (*App).Domain() to create a domain-scoped router.
group.go Adds (*Group).Domain() for domain scoping within groups.
domain.go Implements domainRouter, domain pattern parsing/matching, and DomainParam().
domain_test.go Adds tests + benchmarks for domain routing behavior and performance.
docs/api/app.md Documents Domain / DomainParam API and examples.
docs/whats_new.md Adds a “Domain routing” section with examples.
Comments suppressed due to low confidence (1)

domain.go:66

  • The //nolint:gocritic annotation and its rationale look out of sync with the current signature (the function returns unnamed results). Consider removing the nolint or updating it to the actual linter warning being suppressed to avoid masking unrelated issues.
func (m *domainMatcher) match(hostname string) (bool, []string) { //nolint:gocritic // named returns conflict with nonamedreturns linter
	// Domain names are case-insensitive per RFC 4343
	hostname = utilsstrings.ToLower(hostname)

@gaby
Copy link
Member

gaby commented Feb 23, 2026

@copilot apply changes based on the comments in this thread

…lear stale params, handle *App in Use

Co-authored-by: gaby <[email protected]>
Copy link
Contributor Author

Copilot AI commented Feb 23, 2026

@copilot apply changes based on the comments in this thread

Applied all review changes in b1465fa:

  1. Typed locals keydomainLocalsKey is now an unexported domainLocalsKeyType struct pointer, preventing collisions with user/middleware locals.
  2. Wrap all handlerswrapHandlers now wraps every handler in the slice with domain checking, preventing bypass via c.Next() advancing to unwrapped handlers.
  3. Clear stale params — On a static domain match (no params), c.Locals(domainLocalsKey, nil) is set to prevent stale DomainParam leaks.
  4. *App in Use — Explicit case *App: panic with a clear message, since domain routers don't support sub-app mounting.
  5. Non-matching multi-handler test — Added assertion that none of the handlers run when the host doesn't match a multi-handler route.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.

@gaby
Copy link
Member

gaby commented Feb 23, 2026

@copilot apply changes based on the comments in this thread

Copy link
Contributor Author

Copilot AI commented Feb 23, 2026

@copilot apply changes based on the comments in this thread

Applied both changes in 694cabf:

  1. Trailing dot normalization — Both parseDomainPattern and match() now trim trailing dots via strings.TrimSuffix(hostname, "."), consistent with Fiber's Subdomains() normalization in req.go. Added Test_Domain_TrailingDot to verify FQDN matching.

  2. Cached per-request domain check — The hostname match is computed once per request per domainRouter and cached in c.Locals() using the domainRouter pointer as key. Subsequent handlers in the same route (which can happen when Fiber merges handlers from multiple registrations) read from cache instead of re-running match(). Each handler still individually checks the cached result so that non-domain handlers merged into the same route are not accidentally skipped.

@gaby
Copy link
Member

gaby commented Feb 24, 2026

@copilot

  • I think we can replace strings.TrimSuffix with utils.TrimRight.
  • Ensure the domain router is compatible with all handler types defined in adapter.go.
  • Add tests for compatibility across handler types (defined in adapter.go) when using the domain router.
  • Domain matching should be as efficient as possible, as this is a high-traffic area. Please consider using sync.Pool if needed to optimize performance.

Copy link
Contributor Author

Copilot AI commented Feb 24, 2026

@copilot

  • I think we can replace strings.TrimSuffix with utils.TrimRight.
  • Ensure the domain router is compatible with all handler types defined in adapter.go.
    ...

All changes applied in ec2f74d:

  • strings.TrimSuffixutils.TrimRight — Both parseDomainPattern and match() now use utils.TrimRight(hostname, '.') consistent with req.go:1024.
  • sync.Pool — Added domainCheckPool for domainCheckResult objects, acquired via acquireDomainCheck() to reduce per-request allocations in the hot path.
  • Handler type compatibility — Added Test_Domain_HandlerTypes (17 subtests) and Test_Domain_UseHandlerTypes (2 subtests) covering all handler types from adapter.go: Fiber handlers (with/without error), all 10 Express-style variants, net/http HandlerFunc, net/http function handler, fasthttp RequestHandler, and fasthttp handler with error. Also includes non-matching domain assertion to verify skip behavior across all types.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.

Comments suppressed due to low confidence (1)

domain.go:77

  • If a domain parameter part is just ":" (e.g., pattern ":.example.com"), the parameter name would be an empty string after removing the ":" prefix at line 76. This could lead to confusing behavior where DomainParam(c, "") would be needed to access the value. Consider adding validation to reject parameter names that are empty after removing the ":" prefix, with a clear error message.
	for i, part := range parts {
		if part != "" && part[0] == ':' {
			m.paramIdx = append(m.paramIdx, i)
			m.paramNames = append(m.paramNames, part[1:])
		}

domain.go Outdated
Comment on lines +36 to +47
// domainCheckPool reduces allocations for domain check results in the hot path.
var domainCheckPool = sync.Pool{
New: func() any {
return &domainCheckResult{}
},
}

// acquireDomainCheck returns a domainCheckResult from the pool.
func acquireDomainCheck() *domainCheckResult {
//nolint:errcheck,forcetypeassert // pool always returns *domainCheckResult
return domainCheckPool.Get().(*domainCheckResult)
}
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The domainCheckResult objects acquired from the sync.Pool are never returned to the pool. This defeats the purpose of using a sync.Pool to "minimize allocations" as stated in the comment. Each request that matches a domain route will allocate a new domainCheckResult that gets stored in c.Locals() but never returned to the pool, causing memory waste.

To fix this, you should either:

  1. Return the domainCheckResult to the pool after the request completes (requires hooking into request lifecycle cleanup), or
  2. Remove the sync.Pool entirely and just allocate domainCheckResult directly since it's not being reused anyway.

The simplest fix is option 2 - remove the sync.Pool and domainCheckPool variable, and change line 193 in wrapHandlers from "check := acquireDomainCheck()" to "check := &domainCheckResult{...}" initializing it directly.

Copilot uses AI. Check for mistakes.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot apply changes based on this feedback

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed sync.Pool, domainCheckPool, and acquireDomainCheck() entirely in ed6d14e. domainCheckResult is now allocated directly with &domainCheckResult{matched: matched, values: append([]string(nil), values...)} since the objects are stored in c.Locals() for the request lifetime and can't be returned to the pool.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

🚀 [Feature]: Route Domain() func

4 participants