Skip to content

fix(reverse-proxy): allow multi-label subdomains in service input - #728

Open
khannoussi-malek wants to merge 2 commits into
netbirdio:mainfrom
khannoussi-malek:fix/667-multi-label-subdomain
Open

fix(reverse-proxy): allow multi-label subdomains in service input#728
khannoussi-malek wants to merge 2 commits into
netbirdio:mainfrom
khannoussi-malek:fix/667-multi-label-subdomain

Conversation

@khannoussi-malek

@khannoussi-malek khannoussi-malek commented Jul 23, 2026

Copy link
Copy Markdown

Issue ticket number and link

Fixes #667#667

The subdomain input in the Reverse Proxy "Add Service" modal stripped dots on
every keystroke, making it impossible to enter multi-label subdomains (e.g.
dev.app.example.com) even though the management API already accepts and
provisions them correctly.

Changes

  • New src/modules/reverse-proxy/domain/subdomain.ts holding the two concerns
    as separate pure functions, so the live filter and the submit-time check
    can't drift apart:
    • sanitizeSubdomain — the live per-keystroke filter, now [^a-z0-9.-].
      Deliberately not anchored: users type incrementally, and an anchored
      pattern would reject valid in-progress input like dev. on the way to
      dev.app.
    • isValidSubdomain — anchored ^[a-z0-9-]+(\.[a-z0-9-]+)*$, so empty
      labels (leading/trailing dot, dev..app) still can't reach the API.
  • ReverseProxyDomainInput.tsx uses sanitizeSubdomain in onChange and
    surfaces an inline error for malformed input, matching how the existing
    domainAlreadyExists error already behaves in this field.
  • ReverseProxyModal.tsx gates canContinueToSettings on isValidSubdomain.
    Previously the only subdomain check was length > 0, which was sufficient
    only because the input made malformed values unreachable — allowing dots
    removes that guarantee, so the check now has to be explicit.
  • useReverseProxyDomain.ts uses the same helper when seeding the field from
    a resource name (initialSubdomain). This was the same dot-stripping bug on
    a sibling path: a resource named dev.app used to seed devapp.
  • subdomain.test.ts covers both functions, following the existing standalone
    script pattern in src/utils/ip.test.ts (run with
    npx tsx src/modules/reverse-proxy/domain/subdomain.test.ts) — the repo has
    no unit-test runner wired up, so I matched what's already there rather than
    introducing one.

Testing

  • npx tsx src/modules/reverse-proxy/domain/subdomain.test.ts — 19 cases pass,
    covering dev.app / a.b.c.d preserved, and .app, dev., dev..app,
    .. rejected.
  • npm run build — passes.
  • Not exercised end-to-end against a live management backend (I don't have one
    running locally), so the modal flow itself is unverified by me. The changed
    logic is pure and covered by the test above; fullDomain composition in
    useReverseProxyDomain is untouched, so dev.app + example.com still
    yields dev.app.example.com.

Note: npm run lint is currently broken on main (next lint was removed in
Next 16), so I verified formatting with the repo's pinned Prettier and
typechecking via next build instead.

Documentation

Select exactly one:

  • I added/updated documentation for this change
  • Documentation is not needed for this change (explain why)

This is a bug fix that brings the UI in line with what the management API
already accepts. No documented behavior changes — multi-label subdomains were
already supported server-side, they just couldn't be typed.

Docs PR URL (required if "docs added" is checked)

n/a

E2E tests

management-cloud-tag: main
reverse-proxy-tag: main


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

Summary by CodeRabbit

  • New Features

    • Added shared reverse-proxy subdomain sanitization and validation.
    • The reverse-proxy modal now blocks progression until the subdomain is valid.
    • Subdomain input is normalized as users type, with clear invalid-subdomain messaging.
  • Tests

    • Added a new TypeScript test suite covering subdomain sanitization and validity rules, including multi-label (dots) and hyphenated forms as well as invalid edge cases.

Copilot AI review requested due to automatic review settings July 23, 2026 22:53
@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 6f41ab8e-7d44-4b71-abb8-269e839f079a

📥 Commits

Reviewing files that changed from the base of the PR and between b0f3ea1 and ead8c19.

📒 Files selected for processing (2)
  • src/modules/reverse-proxy/domain/subdomain.test.ts
  • src/modules/reverse-proxy/domain/subdomain.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/modules/reverse-proxy/domain/subdomain.ts
  • src/modules/reverse-proxy/domain/subdomain.test.ts

📝 Walkthrough

Walkthrough

Reverse-proxy subdomain handling now uses shared sanitization and validation helpers across initialization, input errors, and modal progression. A standalone test script covers normalization and dot-separated subdomain validity cases.

Changes

Reverse-proxy subdomain validation

Layer / File(s) Summary
Subdomain helpers and validation cases
src/modules/reverse-proxy/domain/subdomain.ts, src/modules/reverse-proxy/domain/subdomain.test.ts
Adds shared sanitization and DNS-like validation helpers with coverage for normalization, empty values, hyphens, dots, and invalid label boundaries.
Domain input and initialization integration
src/modules/reverse-proxy/domain/useReverseProxyDomain.ts, src/modules/reverse-proxy/domain/ReverseProxyDomainInput.tsx
Uses shared sanitization for initial and typed values and reports an error when a subdomain fails validation.
Modal progression validation
src/modules/reverse-proxy/ReverseProxyModal.tsx
Includes shared subdomain validation in the gate for continuing to settings.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related issues

  • netbirdio/dashboard#667 — Addresses the same sanitization and dot-separated nested-subdomain validation behavior.

Poem

I’m a bunny guarding each dot,
Making subdomains neat on the spot.
Lowercase hops and labels align,
Invalid paths get a warning sign.
Through settings we safely go—
Validated fields in a tidy row.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately describes the main change: allowing multi-label subdomains in reverse proxy service input.
Description check ✅ Passed The description follows the template with issue link, documentation choice, and E2E tag fields filled in.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

src/modules/reverse-proxy/domain/subdomain.test.ts

Oops! Something went wrong! :(

ESLint: 9.39.3

TypeError: Converting circular structure to JSON
--> starting at object with constructor 'Object'
| property 'configs' -> object with constructor 'Object'
| property 'flat' -> object with constructor 'Object'
| ...
| property 'plugins' -> object with constructor 'Object'
--- property 'react' closes the circle
Referenced from: /.eslintrc.json
at JSON.stringify ()
at /node_modules/@eslint/eslintrc/dist/eslintrc.cjs:2255:45
at Array.map ()
at ConfigValidator.formatErrors (/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:2246:23)
at ConfigValidator.validateConfigSchema (/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:2277:84)
at ConfigArrayFactory._normalizeConfigData (/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:3123:19)
at ConfigArrayFactory._loadConfigData (/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:3088:21)
at ConfigArrayFactory._loadExtendedShareableConfig (/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:3393:21)
at ConfigArrayFactory._loadExtends (/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:3261:25)
at ConfigArrayFactory._normalizeObjectConfigDataBody (/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:3199:25)
(node:2) ESLintRCWarning: You are using an eslintrc configuration file, which is deprecated and support will be removed in v10.0.0. Please migrate to an eslint.config.js file. See https://eslint.org/docs/latest/use/configure/migration-guide for details. An eslintrc configuration file is used because you have the ESLINT_USE_FLAT_CONFIG environment variable set to false. If you want to use an eslint.config.js file, remove the environment variable. If you want to find the location of the eslintrc configuration file, use the --debug flag.
(Use node --trace-warnings ... to show where the warning was created)

src/modules/reverse-proxy/domain/subdomain.ts

Oops! Something went wrong! :(

ESLint: 9.39.3

TypeError: Converting circular structure to JSON
--> starting at object with constructor 'Object'
| property 'configs' -> object with constructor 'Object'
| property 'flat' -> object with constructor 'Object'
| ...
| property 'plugins' -> object with constructor 'Object'
--- property 'react' closes the circle
Referenced from: /.eslintrc.json
at JSON.stringify ()
at /node_modules/@eslint/eslintrc/dist/eslintrc.cjs:2255:45
at Array.map ()
at ConfigValidator.formatErrors (/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:2246:23)
at ConfigValidator.validateConfigSchema (/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:2277:84)
at ConfigArrayFactory._normalizeConfigData (/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:3123:19)
at ConfigArrayFactory._loadConfigData (/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:3088:21)
at ConfigArrayFactory._loadExtendedShareableConfig (/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:3393:21)
at ConfigArrayFactory._loadExtends (/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:3261:25)
at ConfigArrayFactory._normalizeObjectConfigDataBody (/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:3199:25)
(node:2) ESLintRCWarning: You are using an eslintrc configuration file, which is deprecated and support will be removed in v10.0.0. Please migrate to an eslint.config.js file. See https://eslint.org/docs/latest/use/configure/migration-guide for details. An eslintrc configuration file is used because you have the ESLINT_USE_FLAT_CONFIG environment variable set to false. If you want to use an eslint.config.js file, remove the environment variable. If you want to find the location of the eslintrc configuration file, use the --debug flag.
(Use node --trace-warnings ... to show where the warning was created)


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR fixes the Reverse Proxy “Add Service” modal so operators can enter multi-label subdomains (e.g. dev.app) without dots being stripped, aligning the UI with what the management API already supports.

Changes:

  • Introduces shared subdomain helpers (sanitizeSubdomain, isValidSubdomain) to separate live input filtering from submit/flow validation.
  • Updates the domain input to preserve dots during typing and show an inline validation error for malformed subdomains.
  • Tightens modal progression gating to require a syntactically valid subdomain (when present/required) before continuing.

Reviewed changes

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

Show a summary per file
File Description
src/modules/reverse-proxy/ReverseProxyModal.tsx Adds explicit subdomain validity gating for modal navigation/Continue behavior.
src/modules/reverse-proxy/domain/useReverseProxyDomain.ts Seeds initial subdomain using the shared sanitizer to avoid dot-stripping from resource names.
src/modules/reverse-proxy/domain/subdomain.ts Adds centralized sanitize/validate helpers for subdomain handling (including multi-label).
src/modules/reverse-proxy/domain/subdomain.test.ts Adds a standalone tsx-runnable test script covering sanitize/validate cases.
src/modules/reverse-proxy/domain/ReverseProxyDomainInput.tsx Uses shared sanitizer on change and shows inline error on invalid subdomain syntax.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 91 to 95
const [subdomain, setSubdomain] = useState(() => {
return (
parsed?.subdomain ||
initialSubdomain
?.toLowerCase()
.replace(/\s+/g, "-")
.replace(/[^a-z0-9-]/g, "") ||
""
sanitizeSubdomain(initialSubdomain?.replace(/\s+/g, "-") ?? "")
);

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Traced this and the bug is real, but I would rather not fix it in this PR.

Confirmed: domains comes from useFetchApi in ReverseProxiesProvider and is passed straight into the modal, which mounts on modalOpen with no loading guard. On a cold SWR cache, parseDomain runs with domains === undefined, falls through to the first-dot split, and dev.app.example.com initializes as subdomain dev + baseDomain app.example.com. The useState initializer never re-runs when domains arrive, so it stays wrong.

Two reasons to keep it separate:

  1. It is pre-existing on main. Both the first-dot fallback and the once-only initializer predate this PR, and nothing here changes that path — this PR only touches the sanitize/validate step. You are right that multi-label domains make it easier to hit, but that is a change in the data, not in the code path.
  2. It is the edit flow, not the input-stripping bug in Reverse Proxy service modal strips dots from the subdomain input, blocking nested subdomains the API accepts #667, and the fix needs a dirty-ref plus a sync effect — new state machinery that would not be covered by this PR title or its tests.

Happy to open a follow-up issue with the above repro, or send a separate PR if a maintainer would rather have it bundled.

Comment on lines +1 to +3
// Live input filter. Dots are kept so multi-label subdomains ("dev.app") can
// be typed at all — dot placement is only checked on submit, because an
// anchored check would reject valid in-progress input like "dev." mid-typing.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in ead8c19 — the comment was stale against my own final code. It now says the sanitizer deliberately skips dot-placement checks (so dev. survives mid-typing), and isValidSubdomain carries a note that it drives both the inline error and the modal gate.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/modules/reverse-proxy/domain/subdomain.ts`:
- Line 10: Update SUBDOMAIN_PATTERN so every dot-separated DNS label begins and
ends with an alphanumeric character while retaining lowercase letters, digits,
and internal hyphens. Add regression cases covering leading-hyphen,
trailing-hyphen, and single-hyphen labels, and ensure the existing input
validation and modal gating use the corrected pattern.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 3518a10e-3fe2-4b3f-8b81-71ce0279bde7

📥 Commits

Reviewing files that changed from the base of the PR and between 5a5e04a and b0f3ea1.

📒 Files selected for processing (5)
  • src/modules/reverse-proxy/ReverseProxyModal.tsx
  • src/modules/reverse-proxy/domain/ReverseProxyDomainInput.tsx
  • src/modules/reverse-proxy/domain/subdomain.test.ts
  • src/modules/reverse-proxy/domain/subdomain.ts
  • src/modules/reverse-proxy/domain/useReverseProxyDomain.ts

Comment thread src/modules/reverse-proxy/domain/subdomain.ts Outdated
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Reverse Proxy service modal strips dots from the subdomain input, blocking nested subdomains the API accepts

3 participants