feats:add custom headers override - #1644
Conversation
WalkthroughAdds end-to-end support for channel-specific request header overrides: new context key and model field, propagation into RelayInfo, validation and merging of header overrides into API/Form requests, a dedicated error code for invalid values, and a UI field to edit JSON header overrides. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
participant User
participant WebUI as Web UI
participant Server
participant MW as Middleware (Distributor)
participant Relay as RelayInfo
participant Req as Request Flow
participant Upstream
User->>WebUI: Edit channel (header_override JSON)
WebUI->>Server: Save channel (HeaderOverride persisted)
User->>Server: Invoke channel
Server->>MW: Select channel
MW->>Relay: Set context with header_override
Relay->>Req: Init ChannelMeta (HeadersOverride populated)
note over Req: Validate that each header value is a string
alt Non-string value
Req-->>User: Error (channel:header_override_invalid)
else All string values
Req->>Req: Merge HeadersOverride into request headers
Req->>Upstream: Send request with overridden headers
Upstream-->>User: Response
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Poem
Tip 🔌 Remote MCP (Model Context Protocol) integration is now available!Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats. 📜 Recent review detailsConfiguration used: CodeRabbit UI Review profile: CHILL Plan: Pro 💡 Knowledge Base configuration:
You can enable these sources in your CodeRabbit configuration. 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
✨ Finishing Touches
🧪 Generate unit tests
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. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
relay/channel/api_request.go (2)
111-126: Apply header overrides to WebSocket requests as wellRealtime channels often need custom Authorization or vendor-specific headers. DoWssRequest builds targetHeader and calls SetupRequestHeader but never applies Overrides. For consistency with API/Form flows, apply overrides after SetupRequestHeader.
Proposed diff:
targetHeader := http.Header{} -err = a.SetupRequestHeader(c, &targetHeader, info) +err = a.SetupRequestHeader(c, &targetHeader, info) if err != nil { return nil, fmt.Errorf("setup request header failed: %w", err) } -targetHeader.Set("Content-Type", c.Request.Header.Get("Content-Type")) +targetHeader.Set("Content-Type", c.Request.Header.Get("Content-Type")) +// Apply channel header overrides last so they take precedence +for k, v := range info.HeadersOverride { + s, ok := v.(string) + if !ok { + return nil, types.NewError( + fmt.Errorf("header_override %q must be string, got %T", k, v), + types.ErrorCodeChannelHeaderOverrideInvalid, + ) + } + targetHeader.Set(k, s) +}
1-23: Use NewErrorWithStatusCode for invalid header overridesInstead of letting header-validation failures bubble up as 500s, switch to the typed error helper that lets you control the HTTP status and error message. In relay/channel/api_request.go, replace calls like:
return types.NewError( fmt.Errorf("invalid header override: %s", headerKey), types.ErrorCodeInvalidRequest, )with:
- return types.NewError( - fmt.Errorf("invalid header override: %s", headerKey), - types.ErrorCodeInvalidRequest, - ) + return types.NewErrorWithStatusCode( + errors.New("invalid header override"), + types.ErrorCodeInvalidRequest, + http.StatusBadRequest, + types.ErrOptionWithSkipRetry(), + )• Call NewErrorWithStatusCode to set
http.StatusBadRequestfor client errors.
• Pass a simpleerrors.New("…")message for clarity.
• Includetypes.ErrOptionWithSkipRetry()to prevent retrying on bad input.
🧹 Nitpick comments (8)
constant/context_key.go (1)
30-30: New context key is fine; add a short doc comment for clarityThe name and value are consistent with adjacent keys. Consider adding a brief comment to document expected value shape (map[string]interface{}) and intended lifecycle (set by distributor; consumed by relay). Minor naming nit: ChannelMeta uses “HeadersOverride” (plural) while this is singular; optional to align.
web/src/components/table/channels/modals/EditChannelModal.jsx (1)
1702-1725: Validate header_override JSON and prefer JSONEditor for consistencyRight now
header_overrideaccepts free text without JSON validation, unlikemodel_mapping. Invalid JSON will be saved and only fail later at request time. Recommend:
- Use the existing JSONEditor component for in-form validation and better UX.
- Add submit-time validation with
verifyJSON.- Optional: warn or block sensitive headers (e.g., Authorization, Host, Content-Length) to avoid breaking auth/upstream routing.
Apply this UI refactor within this hunk:
- <Form.TextArea - field='header_override' - label={t('请求头覆盖')} - placeholder={ - t('此项可选,用于覆盖请求头参数') + - '\n' + t('格式示例:') + - '\n{\n "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36 Edg/139.0.0.0"\n}' - } - autosize - onChange={(value) => handleInputChange('header_override', value)} - extraText={ - <div className="flex gap-2 flex-wrap"> - <Text - className="!text-semi-color-primary cursor-pointer" - onClick={() => handleInputChange('header_override', JSON.stringify({ - "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36 Edg/139.0.0.0" - }, null, 2))} - > - {t('格式模板')} - </Text> - </div> - } - showClear - /> + <JSONEditor + key={`header_override-${isEdit ? channelId : 'new'}`} + field='header_override' + label={t('请求头覆盖')} + placeholder={ + t('此项可选,用于覆盖请求头参数') + + '\n' + t('格式示例:') + + `\n${JSON.stringify({ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36 Edg/139.0.0.0" }, null, 2)}` + } + value={inputs.header_override || ''} + onChange={(value) => handleInputChange('header_override', value)} + template={{ "User-Agent": "Mozilla/5.0 ..." }} + templateLabel={t('填入模板')} + editorType="keyValue" + formApi={formApiRef.current} + extraText={t('键为请求头名称,值为字符串;建议仅覆盖必要头,如 User-Agent')} + />Also add submit-time validation (outside this hunk) to prevent saving invalid JSON:
// inside submit() if (localInputs.header_override && localInputs.header_override !== '' && !verifyJSON(localInputs.header_override)) { showInfo(t('请求头覆盖必须是合法的 JSON 格式!')); return; }And when loading existing channels, pretty-print if present (optional, outside this hunk):
// after model_mapping pretty-print block if (data.header_override !== '') { try { data.header_override = JSON.stringify(JSON.parse(data.header_override), null, 2); } catch {} }model/channel.go (1)
879-888: Fix log message to reference header override (copy/paste typo)The error log currently says “param override”. Correct it to “header override” for accurate diagnostics.
Apply:
func (channel *Channel) GetHeaderOverride() map[string]interface{} { headerOverride := make(map[string]interface{}) if channel.HeaderOverride != nil && *channel.HeaderOverride != "" { err := common.Unmarshal([]byte(*channel.HeaderOverride), &headerOverride) if err != nil { - common.SysLog(fmt.Sprintf("failed to unmarshal param override: channel_id=%d, error=%v", channel.Id, err)) + common.SysLog(fmt.Sprintf("failed to unmarshal header override: channel_id=%d, error=%v", channel.Id, err)) } } return headerOverride }Optional: consider returning a second
boolto indicate “had invalid JSON” so callers can surface a user-facing warning instead of silently falling back to an empty map.middleware/distributor.go (1)
251-251: LGTM: Propagating header overrides via contextSetting
ContextKeyChannelHeaderOverridealongsideparam_overridekeeps the pattern consistent. Make sure the relay layer applies a denylist for sensitive headers (Authorization, Host, Content-Length, Accept-Encoding) when merging overrides to avoid breaking auth/routing.relay/common/relay_info.go (2)
66-72: Prefer a stronger type for HeadersOverride (map[string]string) + validate earlyStoring as map[string]interface{} defers validation downstream and forces repeated type assertions. Using map[string]string here (and converting once when initializing) would:
- Eliminate repeated conversions in request paths.
- Make invalid states unrepresentable and simplify call sites.
If changing the field type is too wide for this PR, consider at least normalizing to map[string]string during InitChannelMeta and dropping non-string entries with a logged warning.
Example (conversion in InitChannelMeta; see next comment for placement).
121-139: Normalize header overrides in InitChannelMeta (one-time conversion, with validation)Right now, each request path repeats conversion/validation from interface{} to string. Centralize this here:
- Convert the context map to map[string]string once.
- Optionally filter disallowed headers (Host, Content-Length, Transfer-Encoding, Connection, TE, Trailer, Upgrade, Expect) to avoid request corruption.
- Optionally reject values containing CR/LF to prevent invalid headers.
This keeps request code tight and avoids partial failures later.
Proposed sketch to apply within InitChannelMeta (outside the selected lines, for illustration only):
func normalizeHeaderOverride(m map[string]any) map[string]string { out := make(map[string]string, len(m)) for k, v := range m { if s, ok := v.(string); ok && !strings.ContainsAny(s, "\r\n") { switch textproto.CanonicalMIMEHeaderKey(k) { case "Host", "Content-Length", "Transfer-Encoding", "Connection", "Proxy-Connection", "TE", "Trailer", "Upgrade", "Expect": continue default: out[k] = s } } } return out }Then set
channelMeta.HeadersOverrideto the normalized map (and adjust its type accordingly if you pick map[string]string).relay/channel/api_request.go (2)
25-37: Do not unconditionally overwrite Content-Type/Accept inside SetupApiRequestHeaderSince header overrides are intended to be authoritative, ensure SetupApiRequestHeader only sets Content-Type/Accept when missing. Right now Content-Type is always set from the incoming request and may defeat an override unless you re-apply overrides after this step (as suggested above).
Two options:
- Make SetupApiRequestHeader idempotent: only set if empty.
- Keep current behavior but always apply overrides after SetupRequestHeader (preferred, already suggested).
51-63: Optional hardening: filter unsafe or invalid header overridesTo avoid malformed requests or request smuggling vectors, consider rejecting or skipping:
- Header keys: Host, Content-Length, Transfer-Encoding, Connection, Proxy-Connection, TE, Trailer, Upgrade, Expect.
- Values containing CR or LF.
You can add a tiny helper (outside of these hunks) and reuse in API/Form/WSS paths.
If you want, I can push a follow-up patch that adds a shared helper and updates the three call sites.
Also applies to: 86-101
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (7)
constant/context_key.go(1 hunks)middleware/distributor.go(1 hunks)model/channel.go(2 hunks)relay/channel/api_request.go(3 hunks)relay/common/relay_info.go(3 hunks)types/error.go(1 hunks)web/src/components/table/channels/modals/EditChannelModal.jsx(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (5)
middleware/distributor.go (2)
common/gin.go (1)
SetContextKey(53-55)constant/context_key.go (1)
ContextKeyChannelHeaderOverride(30-30)
model/channel.go (2)
common/json.go (1)
Unmarshal(8-10)common/sys_log.go (1)
SysLog(10-13)
web/src/components/table/channels/modals/EditChannelModal.jsx (1)
web/src/components/table/channels/modals/EditTagModal.jsx (1)
handleInputChange(77-129)
relay/common/relay_info.go (2)
common/gin.go (1)
GetContextKeyStringMap(77-79)constant/context_key.go (1)
ContextKeyChannelHeaderOverride(30-30)
relay/channel/api_request.go (1)
types/error.go (2)
NewError(182-194)ErrorCodeChannelHeaderOverrideInvalid(53-53)
🔇 Additional comments (1)
types/error.go (1)
51-57: LGTM: Added error code fits the existing namespace
channel:header_override_invalidfollows the “channel:” prefix convention and will be caught byIsChannelError. No further changes needed here.
| Tag *string `json:"tag" gorm:"index"` | ||
| Setting *string `json:"setting" gorm:"type:text"` // 渠道额外设置 | ||
| ParamOverride *string `json:"param_override" gorm:"type:text"` | ||
| HeaderOverride *string `json:"header_override" gorm:"type:text"` |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
Adding HeaderOverride column — ensure DB migration and add field comment
The new GORM field is fine. Please:
- Add a short comment explaining expected JSON structure (flat key->string map).
- Verify migration adds
header_overrideto the channels table in all supported DBs.
Run this script to confirm migrations/auto-migrate cover the new column and to locate usages:
🏁 Script executed:
#!/bin/bash
# Find AutoMigrate and any schema changes referencing Channel or header_override
rg -nP 'AutoMigrate\([^)]*Channel' -C2
rg -n --json 'header_override' -C2
rg -nP '(?i)(migrate|migration|schema|alter|add column|auto\s*migrate).*channel' -S -C2Length of output: 9291
Add DB migration for header_override and document its JSON structure
Please ensure the new HeaderOverride field in model/channel.go is fully supported:
-
Database migration
No existing migration orAutoMigratecall adds theheader_overridecolumn to thechannelstable. Add a migration (or update your auto-migrate logic) for all supported dialects. For example:ALTER TABLE channels ADD COLUMN header_override TEXT;
• If you use a versioned migrations folder (e.g.
migrations/), add a new up/down SQL file.
• If you rely on GORM’sAutoMigrate, locate whereAutoMigrate(&Channel{})is invoked and confirm it’s applied in your deployment flow. -
Field comment in Go model
Explain the expected JSON format (flat map of header names to values). For example, above the field inmodel/channel.go:// HeaderOverride holds a JSON-encoded map of HTTP header names to override values. // Example: {"User-Agent":"MyApp/1.0","Accept":"application/json"} HeaderOverride *string `json:"header_override" gorm:"type:text"`
Locations to update:
model/channel.go(line 49) – add the above Go doc comment.- Migration files or auto-migrate invocation – ensure
header_overrideis added to thechannelstable schema.
| headers := req.Header | ||
| headerOverride := make(map[string]string) | ||
| for k, v := range info.HeadersOverride { | ||
| if str, ok := v.(string); ok { | ||
| headerOverride[k] = str | ||
| } else { | ||
| return nil, types.NewError(err, types.ErrorCodeChannelHeaderOverrideInvalid) | ||
| } | ||
| } | ||
| for key, value := range headerOverride { | ||
| headers.Set(key, value) | ||
| } | ||
| err = a.SetupRequestHeader(c, &headers, info) |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Header overrides may be clobbered by SetupRequestHeader; also passing a nil err into types.NewError
- Order issue: You set overrides, then call a.SetupRequestHeader, which (via SetupApiRequestHeader) resets Content-Type/Accept (and many adaptors may set Authorization). This can undo the override. Expected behavior is “override wins,” so apply overrides after SetupRequestHeader.
- Error issue: On non-string override values you call types.NewError(err, ...), but err is nil here, losing the root cause and stack. Pass a constructed error, ideally with a 400 status.
Apply this diff:
-headers := req.Header
-headerOverride := make(map[string]string)
-for k, v := range info.HeadersOverride {
- if str, ok := v.(string); ok {
- headerOverride[k] = str
- } else {
- return nil, types.NewError(err, types.ErrorCodeChannelHeaderOverrideInvalid)
- }
-}
-for key, value := range headerOverride {
- headers.Set(key, value)
-}
-err = a.SetupRequestHeader(c, &headers, info)
+headers := req.Header
+// First, let adaptor set its defaults (auth, content-type, etc.).
+if err = a.SetupRequestHeader(c, &headers, info); err != nil {
+ return nil, fmt.Errorf("setup request header failed: %w", err)
+}
+// Then apply channel overrides so they take precedence.
+for k, v := range info.HeadersOverride {
+ s, ok := v.(string)
+ if !ok {
+ return nil, types.NewError(
+ fmt.Errorf("header_override %q must be string, got %T", k, v),
+ types.ErrorCodeChannelHeaderOverrideInvalid,
+ )
+ }
+ headers.Set(k, s)
+}Optional hardening: filter disallowed headers (Host, Content-Length, Transfer-Encoding, Connection, TE, Trailer, Upgrade, Expect) or CR/LF in values before Set.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| headers := req.Header | |
| headerOverride := make(map[string]string) | |
| for k, v := range info.HeadersOverride { | |
| if str, ok := v.(string); ok { | |
| headerOverride[k] = str | |
| } else { | |
| return nil, types.NewError(err, types.ErrorCodeChannelHeaderOverrideInvalid) | |
| } | |
| } | |
| for key, value := range headerOverride { | |
| headers.Set(key, value) | |
| } | |
| err = a.SetupRequestHeader(c, &headers, info) | |
| headers := req.Header | |
| // First, let adaptor set its defaults (auth, content-type, etc.). | |
| if err = a.SetupRequestHeader(c, &headers, info); err != nil { | |
| return nil, fmt.Errorf("setup request header failed: %w", err) | |
| } | |
| // Then apply channel overrides so they take precedence. | |
| for k, v := range info.HeadersOverride { | |
| s, ok := v.(string) | |
| if !ok { | |
| return nil, types.NewError( | |
| fmt.Errorf("header_override %q must be string, got %T", k, v), | |
| types.ErrorCodeChannelHeaderOverrideInvalid, | |
| ) | |
| } | |
| headers.Set(k, s) | |
| } |
🤖 Prompt for AI Agents
In relay/channel/api_request.go around lines 51-63, the header override handling
is done before calling a.SetupRequestHeader which may reset or overwrite those
values and the code also calls types.NewError(err, ...) with err == nil on
non-string values. Fix by first calling a.SetupRequestHeader(c, &req.Header,
info) to let adaptors set defaults, then apply info.HeadersOverride (casting
each value to string and returning types.NewError(errors.New("invalid header
override value"), types.ErrorCodeChannelHeaderOverrideInvalid) or a similar
400-level error when a value is not a string). When setting headers, skip
disallowed header names (Host, Content-Length, Transfer-Encoding, Connection,
TE, Trailer, Upgrade, Expect) and validate values to reject CR/LF before calling
headers.Set.
| // set form data | ||
| req.Header.Set("Content-Type", c.Request.Header.Get("Content-Type")) | ||
|
|
||
| err = a.SetupRequestHeader(c, &req.Header, info) | ||
| headers := req.Header | ||
| headerOverride := make(map[string]string) | ||
| for k, v := range info.HeadersOverride { | ||
| if str, ok := v.(string); ok { | ||
| headerOverride[k] = str | ||
| } else { | ||
| return nil, types.NewError(err, types.ErrorCodeChannelHeaderOverrideInvalid) | ||
| } | ||
| } | ||
| for key, value := range headerOverride { | ||
| headers.Set(key, value) | ||
| } | ||
| err = a.SetupRequestHeader(c, &headers, info) | ||
| if err != nil { |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Same two issues in DoFormRequest: override order and nil err passed to NewError
Replicate the fix used for DoApiRequest so that:
- Adaptor defaults are set first.
- Override headers take precedence and invalid entries return a clear 400-style error.
Apply this diff:
-// set form data
-req.Header.Set("Content-Type", c.Request.Header.Get("Content-Type"))
-headers := req.Header
-headerOverride := make(map[string]string)
-for k, v := range info.HeadersOverride {
- if str, ok := v.(string); ok {
- headerOverride[k] = str
- } else {
- return nil, types.NewError(err, types.ErrorCodeChannelHeaderOverrideInvalid)
- }
-}
-for key, value := range headerOverride {
- headers.Set(key, value)
-}
-err = a.SetupRequestHeader(c, &headers, info)
+// Preserve incoming content-type for form uploads
+req.Header.Set("Content-Type", c.Request.Header.Get("Content-Type"))
+headers := req.Header
+// First set adaptor defaults
+if err = a.SetupRequestHeader(c, &headers, info); err != nil {
+ return nil, fmt.Errorf("setup request header failed: %w", err)
+}
+// Then apply overrides so they win
+for k, v := range info.HeadersOverride {
+ s, ok := v.(string)
+ if !ok {
+ return nil, types.NewError(
+ fmt.Errorf("header_override %q must be string, got %T", k, v),
+ types.ErrorCodeChannelHeaderOverrideInvalid,
+ )
+ }
+ headers.Set(k, s)
+}If you add disallowed header filtering in DoApiRequest, mirror it here for consistency.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // set form data | |
| req.Header.Set("Content-Type", c.Request.Header.Get("Content-Type")) | |
| err = a.SetupRequestHeader(c, &req.Header, info) | |
| headers := req.Header | |
| headerOverride := make(map[string]string) | |
| for k, v := range info.HeadersOverride { | |
| if str, ok := v.(string); ok { | |
| headerOverride[k] = str | |
| } else { | |
| return nil, types.NewError(err, types.ErrorCodeChannelHeaderOverrideInvalid) | |
| } | |
| } | |
| for key, value := range headerOverride { | |
| headers.Set(key, value) | |
| } | |
| err = a.SetupRequestHeader(c, &headers, info) | |
| if err != nil { | |
| // Preserve incoming content-type for form uploads | |
| req.Header.Set("Content-Type", c.Request.Header.Get("Content-Type")) | |
| headers := req.Header | |
| // First set adaptor defaults | |
| if err = a.SetupRequestHeader(c, &headers, info); err != nil { | |
| return nil, fmt.Errorf("setup request header failed: %w", err) | |
| } | |
| // Then apply overrides so they win | |
| for k, v := range info.HeadersOverride { | |
| s, ok := v.(string) | |
| if !ok { | |
| return nil, types.NewError( | |
| fmt.Errorf("header_override %q must be string, got %T", k, v), | |
| types.ErrorCodeChannelHeaderOverrideInvalid, | |
| ) | |
| } | |
| headers.Set(k, s) | |
| } |
🤖 Prompt for AI Agents
In relay/channel/api_request.go around lines 86 to 101, DoFormRequest currently
applies adaptor defaults after applying header overrides and calls
types.NewError with a nil err; change the order so adaptor default headers are
set first (call a.SetupRequestHeader or equivalent to populate base headers),
then apply header overrides so they take precedence, validate each override
entry and return types.NewError(errors.New("invalid header override"),
types.ErrorCodeChannelHeaderOverrideInvalid) when a non-string value is found
(do not pass nil), and if you added disallowed header filtering in DoApiRequest,
apply the same filtering here so disallowed headers are removed/blocked
consistently before sending the request.
…headers feats:add custom headers override
Summary by CodeRabbit