From 00db3eef0431f8e981f5063151e2ac10bd8da79e Mon Sep 17 00:00:00 2001 From: Hulk Date: Fri, 9 Oct 2026 21:05:10 -0700 Subject: [PATCH] fix(openclaw): preserve custom headers when toggling User-Agent (#7847) The "Send User-Agent" toggle in the OpenClaw form replaced the whole headers object when turned on and deleted it when turned off, so every other custom header was lost on save. Update only the User-Agent entry: drop existing variants case-insensitively, add a canonical User-Agent when enabled, and remove the headers object only when it ends up empty. Detecting the toggle state is case-insensitive as well. Fixes #7846 --- .../forms/hooks/useOpenclawFormState.ts | 19 +- .../ProviderForm.openclawHeaders.test.tsx | 222 ++++++++++++++++++ tests/hooks/useOpenclawFormState.test.tsx | 173 ++++++++++++++ 3 files changed, 411 insertions(+), 3 deletions(-) create mode 100644 tests/components/ProviderForm.openclawHeaders.test.tsx create mode 100644 tests/hooks/useOpenclawFormState.test.tsx diff --git a/src/components/providers/forms/hooks/useOpenclawFormState.ts b/src/components/providers/forms/hooks/useOpenclawFormState.ts index 7f32424c5..32709b565 100644 --- a/src/components/providers/forms/hooks/useOpenclawFormState.ts +++ b/src/components/providers/forms/hooks/useOpenclawFormState.ts @@ -51,6 +51,12 @@ function parseOpenclawField( } } +function hasUserAgentHeader(headers?: Record): boolean { + return Object.keys(headers ?? {}).some( + (key) => key.toLowerCase() === "user-agent", + ); +} + export function useOpenclawFormState({ initialData, appId, @@ -99,7 +105,7 @@ export function useOpenclawFormState({ "headers", {}, ); - return "User-Agent" in headers; + return hasUserAgentHeader(headers); }); const updateOpenclawConfig = useCallback( @@ -161,8 +167,15 @@ export function useOpenclawFormState({ (enabled: boolean) => { setOpenclawUserAgent(enabled); updateOpenclawConfig((config) => { + const headers = { ...config.headers }; + for (const key of Object.keys(headers)) { + if (key.toLowerCase() === "user-agent") delete headers[key]; + } if (enabled) { - config.headers = { "User-Agent": OPENCLAW_DEFAULT_USER_AGENT }; + headers["User-Agent"] = OPENCLAW_DEFAULT_USER_AGENT; + } + if (Object.keys(headers).length > 0) { + config.headers = headers; } else { delete config.headers; } @@ -177,7 +190,7 @@ export function useOpenclawFormState({ setOpenclawApiKey(config?.apiKey || ""); setOpenclawApi(config?.api || "openai-completions"); setOpenclawModels(config?.models || []); - const ua = config?.headers ? "User-Agent" in config.headers : false; + const ua = hasUserAgentHeader(config?.headers); setOpenclawUserAgent(ua); }, []); diff --git a/tests/components/ProviderForm.openclawHeaders.test.tsx b/tests/components/ProviderForm.openclawHeaders.test.tsx new file mode 100644 index 000000000..44110d6be --- /dev/null +++ b/tests/components/ProviderForm.openclawHeaders.test.tsx @@ -0,0 +1,222 @@ +import { + fireEvent, + render, + screen, + waitFor, + within, +} from "@testing-library/react"; +import { QueryClientProvider } from "@tanstack/react-query"; +import { http, HttpResponse } from "msw"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { ProviderForm } from "@/components/providers/forms/ProviderForm"; +import { OPENCLAW_DEFAULT_USER_AGENT } from "@/components/providers/forms/hooks/useOpenclawFormState"; +import { server } from "../msw/server"; +import { createTestQueryClient } from "../utils/testQueryClient"; + +// Match existing provider-form tests: mock ancillary auth/editor dependencies only. +// The actual ProviderForm, switch component, hook and onSubmit path remain real. +vi.mock("sonner", () => ({ toast: { error: vi.fn(), success: vi.fn() } })); +vi.mock("@/components/JsonEditor", () => ({ + default: ({ value }: { value: string }) => ( +