Skip to content

Add per-target options to reverse proxy#576

Merged
lixmal merged 3 commits intomainfrom
feature/per-target-options
Mar 6, 2026
Merged

Add per-target options to reverse proxy#576
lixmal merged 3 commits intomainfrom
feature/per-target-options

Conversation

@lixmal
Copy link
Copy Markdown
Contributor

@lixmal lixmal commented Mar 5, 2026

Adds an Options tab to the reverse proxy target modal with skip TLS verification, request timeout, path rewrite, and custom headers settings per target.

See netbirdio/netbird#5501

CleanShot 2026-03-06 at 18 31 54 CleanShot 2026-03-06 at 18 31 31

Summary by CodeRabbit

  • New Features
    • Tabbed target configuration (Details & Options) with step/flow improvements and updated modal layout.
    • Manage custom HTTP headers with add/remove UI and per-entry validation.
    • TLS verification toggle, request timeout input with validation, and Preserve Full Path option for path forwarding.
    • Enhanced validation, inline help/tooltips, and Save disabled when option errors exist.
    • UI text tweak: Settings renamed to Advanced Settings; improved tooltip positioning and trigger behavior.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 5, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 89afc430-3a93-443b-a66f-f33b574b8909

📥 Commits

Reviewing files that changed from the base of the PR and between 0d526f3 and dbefdb7.

📒 Files selected for processing (1)
  • src/modules/reverse-proxy/targets/ReverseProxyTargetCustomHeaders.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/modules/reverse-proxy/targets/ReverseProxyTargetCustomHeaders.tsx

📝 Walkthrough

Walkthrough

Adds ServiceTargetOptions to reverse-proxy interfaces and extends ReverseProxyTarget. Introduces a tabbed ReverseProxyTarget modal with an Options tab, a useReverseProxyTargetOptions hook, custom header management, timeout and header validation, and HelpTooltip API enhancements.

Changes

Cohort / File(s) Summary
Interfaces
src/interfaces/ReverseProxy.ts
Add ServiceTargetOptionsPathRewrite = "preserve", add ServiceTargetOptions (skip_tls_verify, request_timeout, path_rewrite, custom_headers), and add optional options?: ServiceTargetOptions to ReverseProxyTarget.
Target modal & tabs
src/modules/reverse-proxy/targets/ReverseProxyTargetModal.tsx
Replace accordion UI with Details/Options tabs; integrate options flow (enable/disable Options tab by canAddTarget), merge options into target payload on save, adjust validation and modal button flows.
Options hook
src/modules/reverse-proxy/targets/useReverseProxyTargetOptions.ts
New hook managing target options state, timeout parsing/validation (max 5m), header integration, getTargetOptions(), and aggregated option errors.
Custom headers UI & utilities
src/modules/reverse-proxy/targets/ReverseProxyTargetCustomHeaders.tsx
New hook useCustomHeaders, header entry types/utilities (recordToHeaderEntries, headerEntriesToRecord), validation (name regex, blocked headers, CR/LF, duplicates), UI component for managing headers.
Tooltip component
src/components/HelpTooltip.tsx
Make children optional; add positioning/sizing/timing/variant props; change default interactive to false; pass new props through and render optional custom trigger.
Minor UI text
src/modules/reverse-proxy/ReverseProxyModal.tsx, package.json
Rename Settings tab label to "Advanced Settings" (text-only change).

Sequence Diagram(s)

sequenceDiagram
  participant User as User
  participant Modal as ReverseProxyTargetModal
  participant Hook as useReverseProxyTargetOptions
  participant Headers as ReverseProxyTargetCustomHeaders
  participant API as Save Handler

  User->>Modal: Open "Add/Edit Target"
  Modal->>Hook: initialize (initialOptions?)
  Modal->>Headers: render headerEntries UI
  User->>Headers: add/update/remove header
  Headers-->>Hook: update headerEntries
  User->>Modal: edit Details (host/port) or switch to Options
  User->>Hook: updateOption (timeout, path_rewrite, skip_tls_verify)
  Hook-->>Modal: provide errors / getTargetOptions
  User->>Modal: Click Save
  Modal->>Hook: getTargetOptions()
  Hook-->>Modal: combined options or undefined
  Modal->>API: submit target payload (includes options if present)
  API-->>Modal: persist / respond
  Modal-->>User: close modal / show result
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 Tabs and toggles, headers stacked neat,
I parse each timeout and guard every seat.
Preserve the path, skip TLS with a hop,
I hop, I nibble, then — save and stop.
Hooray for options — a carrot-shaped feat!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: adding per-target options functionality to the reverse proxy feature, which is well-supported by the changeset.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/per-target-options

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 and usage tips.

@lixmal lixmal changed the title [client] Add per-target options to reverse proxy Add per-target options to reverse proxy Mar 5, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/modules/reverse-proxy/targets/ReverseProxyTargetModal.tsx`:
- Around line 108-113: The validateTimeout function in ReverseProxyTargetModal
currently only checks format (DURATION_RE) but must also enforce the
UI-documented 5m cap; update validateTimeout to parse the duration string
(handle suffixes s, m) into seconds or milliseconds, compare against a 5-minute
limit (300s/300000ms), and return a validation error like "Duration must be <=
5m" when exceeded; apply the same cap enforcement to the other timeout validator
used elsewhere in this module (the same validation logic referenced in the
review) so client-side validation prevents values >5m before downstream
rejection.
- Around line 57-60: The BLOCKED_HEADERS set in ReverseProxyTargetModal.tsx
lists the hop-by-hop header as "trailers" (plural) which is incorrect; change
the entry in the BLOCKED_HEADERS Set from "trailers" to "trailer" (singular) so
the reserved Trailer header is correctly blocked by the validation logic that
uses BLOCKED_HEADERS.
- Around line 795-801: The remove-header button in ReverseProxyTargetModal is
icon-only (Button containing MinusCircleIcon) and lacks an accessible name;
update the Button (the element that calls removeHeader) to include an accessible
label such as aria-label="Remove header" or aria-labelledby (or add
visually-hidden text) so screen readers can identify it, ensuring the label
clearly describes the action (e.g., "Remove header") and keeping the onClick
handler removeHeader(entry.id) intact.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ac42def0-8d74-4c60-98cc-f7b8d1c7d804

📥 Commits

Reviewing files that changed from the base of the PR and between 9420214 and d76fcf2.

📒 Files selected for processing (2)
  • src/interfaces/ReverseProxy.ts
  • src/modules/reverse-proxy/targets/ReverseProxyTargetModal.tsx

Comment thread src/modules/reverse-proxy/targets/ReverseProxyTargetModal.tsx Outdated
Comment thread src/modules/reverse-proxy/targets/ReverseProxyTargetModal.tsx Outdated
Comment thread src/modules/reverse-proxy/targets/ReverseProxyTargetModal.tsx Outdated
@lixmal lixmal force-pushed the feature/per-target-options branch from d76fcf2 to 7b61201 Compare March 5, 2026 11:27
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
src/modules/reverse-proxy/targets/ReverseProxyTargetModal.tsx (1)

108-113: ⚠️ Potential issue | 🟡 Minor

Enforce the documented 5m timeout cap in validator.

Line 108-113 validates syntax but not the max duration promised in Line 726-727, so oversized values still pass client-side.

Proposed fix
 const DURATION_RE = /^(\d+(\.\d+)?(ns|us|µs|ms|s|m|h))+$/;
+const DURATION_PART_RE = /(\d+(\.\d+)?)(ns|us|µs|ms|s|m|h)/g;
+const MAX_TIMEOUT_MS = 5 * 60 * 1000;
+
+function durationToMs(input: string): number | null {
+  let total = 0;
+  let consumed = 0;
+  for (const match of input.matchAll(DURATION_PART_RE)) {
+    const value = Number(match[1]);
+    const unit = match[3];
+    consumed += match[0].length;
+    const factor =
+      unit === "h"
+        ? 3_600_000
+        : unit === "m"
+          ? 60_000
+          : unit === "s"
+            ? 1_000
+            : unit === "ms"
+              ? 1
+              : unit === "us" || unit === "µs"
+                ? 0.001
+                : 0.000001;
+    total += value * factor;
+  }
+  return consumed === input.length ? total : null;
+}
 
 function validateTimeout(timeout: string): string | undefined {
   if (!timeout) return undefined;
   if (!DURATION_RE.test(timeout))
     return 'Invalid duration, use e.g. "10s", "30s", "1m"';
+  const timeoutMs = durationToMs(timeout);
+  if (timeoutMs === null)
+    return 'Invalid duration, use e.g. "10s", "30s", "1m"';
+  if (timeoutMs > MAX_TIMEOUT_MS) return "Timeout must be 5m or less";
   return undefined;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/modules/reverse-proxy/targets/ReverseProxyTargetModal.tsx` around lines
108 - 113, The validateTimeout function currently only checks syntax via
DURATION_RE but must also enforce the documented 5m cap; update validateTimeout
to, after DURATION_RE.test(timeout) succeeds, parse the duration string
(supporting s, m units as used by DURATION_RE), convert to seconds or
milliseconds, and return an error if it exceeds 5 minutes (e.g., "Timeout must
be <= 5m"); keep the existing error for invalid format and return undefined only
when format is valid and value <= 5 minutes.
🧹 Nitpick comments (1)
src/modules/reverse-proxy/targets/ReverseProxyTargetModal.tsx (1)

160-161: Reset tab state when options are not selectable.

Consider forcing tab="details" when the modal opens or when hasTarget becomes false, to avoid landing on a disabled Options context.

Proposed refactor
-import React, { useCallback, useMemo, useRef, useState } from "react";
+import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";

   const [tab, setTab] = useState("details");
+  useEffect(() => {
+    if (!open || !hasTarget) setTab("details");
+  }, [open, hasTarget]);

Also applies to: 404-417

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/modules/reverse-proxy/targets/ReverseProxyTargetModal.tsx` around lines
160 - 161, The tab state (const [tab, setTab] = useState("details") in
ReverseProxyTargetModal) can leave the UI on a disabled "Options" tab when a
target is not selectable; add an effect that resets the tab to "details"
whenever the modal opens or whenever hasTarget becomes false—e.g. in a useEffect
that watches the modal open flag (isOpen/open) and hasTarget and calls
setTab("details") when the modal is opened or hasTarget is falsy so the user
never lands on a disabled Options context.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@src/modules/reverse-proxy/targets/ReverseProxyTargetModal.tsx`:
- Around line 108-113: The validateTimeout function currently only checks syntax
via DURATION_RE but must also enforce the documented 5m cap; update
validateTimeout to, after DURATION_RE.test(timeout) succeeds, parse the duration
string (supporting s, m units as used by DURATION_RE), convert to seconds or
milliseconds, and return an error if it exceeds 5 minutes (e.g., "Timeout must
be <= 5m"); keep the existing error for invalid format and return undefined only
when format is valid and value <= 5 minutes.

---

Nitpick comments:
In `@src/modules/reverse-proxy/targets/ReverseProxyTargetModal.tsx`:
- Around line 160-161: The tab state (const [tab, setTab] = useState("details")
in ReverseProxyTargetModal) can leave the UI on a disabled "Options" tab when a
target is not selectable; add an effect that resets the tab to "details"
whenever the modal opens or whenever hasTarget becomes false—e.g. in a useEffect
that watches the modal open flag (isOpen/open) and hasTarget and calls
setTab("details") when the modal is opened or hasTarget is falsy so the user
never lands on a disabled Options context.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ae4e5f07-798a-47b5-b1a6-895861c2f6eb

📥 Commits

Reviewing files that changed from the base of the PR and between d76fcf2 and 7b61201.

📒 Files selected for processing (2)
  • src/interfaces/ReverseProxy.ts
  • src/modules/reverse-proxy/targets/ReverseProxyTargetModal.tsx

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (2)
src/components/HelpTooltip.tsx (1)

35-67: Minor: Remove unnecessary fragment wrapper.

The empty fragment (<>...</>) wrapping the single FullTooltip child is redundant and can be removed for cleaner JSX.

♻️ Suggested cleanup
   return (
-    <>
-      <FullTooltip
-        interactive={interactive}
-        ...
-      </FullTooltip>
-    </>
+    <FullTooltip
+      interactive={interactive}
+      ...
+    </FullTooltip>
   );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/HelpTooltip.tsx` around lines 35 - 67, The top-level empty
fragment wrapping the FullTooltip is redundant; remove the surrounding <>...</>
so the component returns FullTooltip directly. Edit the JSX where FullTooltip
(and its children: the children conditional and the HelpCircle trigger) is the
sole root element and delete the fragment tokens to clean up the markup while
keeping props and className/children logic intact.
src/modules/reverse-proxy/targets/useReverseProxyTargetOptions.ts (1)

59-80: Narrow updateOption to the fields this hook actually owns.

updateOption currently accepts every keyof ServiceTargetOptions, but getTargetOptions() always rebuilds custom_headers from headerEntries. A caller can set custom_headers successfully and still lose that value on save.

Suggested fix
+type ManagedTargetOptionKey = Exclude<
+  keyof ServiceTargetOptions,
+  "custom_headers"
+>;
+
   const updateOption = useCallback(
-    <K extends keyof ServiceTargetOptions>(
+    <K extends ManagedTargetOptionKey>(
       key: K,
       value: ServiceTargetOptions[K],
     ) => {
       setTargetOptions((prev) => ({ ...prev, [key]: value }));
     },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/modules/reverse-proxy/targets/useReverseProxyTargetOptions.ts` around
lines 59 - 80, updateOption is typed to allow any keyof ServiceTargetOptions
which lets callers set custom_headers even though getTargetOptions always
rebuilds custom_headers from headerEntries; restrict updateOption so it cannot
accept 'custom_headers' by narrowing its generic to exclude that key (e.g.
define an OwnedKeys type = Exclude<keyof ServiceTargetOptions, 'custom_headers'>
and change the updateOption signature to use K extends OwnedKeys) and keep using
setTargetOptions as-is; this prevents callers from writing custom_headers that
will later be overwritten by getTargetOptions which reads headerEntries.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/modules/reverse-proxy/targets/ReverseProxyTargetCustomHeaders.tsx`:
- Around line 145-164: The header name and value Input components are currently
unlabeled for assistive tech; add programmatic accessible names to each Input
(the ones rendering entry.name and entry.value) by providing either an explicit
aria-label (e.g., "Header name for entry {entry.id}" and "Header value for entry
{entry.id}") or by associating a visible <label> with a unique id for those
Inputs (use entry.id to generate the id), ensuring the inputs remain tied to
updateHeaderEntry(entry.id, ...) and error display via headerErrors[index]; do
not change other behavior.
- Around line 98-104: Validation currently allows entries with an empty name and
a non-empty value (which headerEntriesToRecord later drops); update the
validation logic in the header validation block (the headerEntries map that
builds headerErrors using validateHeaderName and validateHeaderValue) to treat
any entry where name is blank but value is non-blank as a name error.
Concretely, augment the name validation for each entry (in the same place
validateHeaderName(entry.name, allHeaderNames) is called) to also return an
error when entry.name is empty/whitespace while entry.value is non-empty, so
those value-only rows surface a user-facing name error instead of being silently
discarded by headerEntriesToRecord.

In `@src/modules/reverse-proxy/targets/ReverseProxyTargetModal.tsx`:
- Around line 635-639: The help text under the Request Timeout field in
ReverseProxyTargetModal (Label "Request Timeout" and the HelpText element)
incorrectly says "Leave this field empty for no timeout" but the backend treats
empty as "use the default"; update the copy to match backend behavior (e.g.,
"Leave this field empty to use the default timeout") while preserving the "Max
time... (max 5m)" note so the UI message aligns with the actual fallback.

---

Nitpick comments:
In `@src/components/HelpTooltip.tsx`:
- Around line 35-67: The top-level empty fragment wrapping the FullTooltip is
redundant; remove the surrounding <>...</> so the component returns FullTooltip
directly. Edit the JSX where FullTooltip (and its children: the children
conditional and the HelpCircle trigger) is the sole root element and delete the
fragment tokens to clean up the markup while keeping props and
className/children logic intact.

In `@src/modules/reverse-proxy/targets/useReverseProxyTargetOptions.ts`:
- Around line 59-80: updateOption is typed to allow any keyof
ServiceTargetOptions which lets callers set custom_headers even though
getTargetOptions always rebuilds custom_headers from headerEntries; restrict
updateOption so it cannot accept 'custom_headers' by narrowing its generic to
exclude that key (e.g. define an OwnedKeys type = Exclude<keyof
ServiceTargetOptions, 'custom_headers'> and change the updateOption signature to
use K extends OwnedKeys) and keep using setTargetOptions as-is; this prevents
callers from writing custom_headers that will later be overwritten by
getTargetOptions which reads headerEntries.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 487f3cfd-cdf8-4a8d-b842-af28b3796713

📥 Commits

Reviewing files that changed from the base of the PR and between 7b61201 and 0d526f3.

📒 Files selected for processing (5)
  • src/components/HelpTooltip.tsx
  • src/modules/reverse-proxy/ReverseProxyModal.tsx
  • src/modules/reverse-proxy/targets/ReverseProxyTargetCustomHeaders.tsx
  • src/modules/reverse-proxy/targets/ReverseProxyTargetModal.tsx
  • src/modules/reverse-proxy/targets/useReverseProxyTargetOptions.ts

Comment thread src/modules/reverse-proxy/targets/ReverseProxyTargetModal.tsx
@lixmal lixmal merged commit 60e8394 into main Mar 6, 2026
4 checks passed
@lixmal lixmal deleted the feature/per-target-options branch March 6, 2026 17:55
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.

2 participants