Skip to content

feat(cloud): add ENGRAM_CLOUD_EXTRA_HEADERS support - #645

Open
4DRIAN0RTIZ wants to merge 2 commits into
Gentleman-Programming:mainfrom
4DRIAN0RTIZ:feat/extra-headers-support
Open

feat(cloud): add ENGRAM_CLOUD_EXTRA_HEADERS support#645
4DRIAN0RTIZ wants to merge 2 commits into
Gentleman-Programming:mainfrom
4DRIAN0RTIZ:feat/extra-headers-support

Conversation

@4DRIAN0RTIZ

@4DRIAN0RTIZ 4DRIAN0RTIZ commented Jul 20, 2026

Copy link
Copy Markdown

🔗 Linked Issue

Closes #355


🏷️ PR Type

  • type:feature — New feature

📝 Summary

  • Adds opt-in ENGRAM_CLOUD_EXTRA_HEADERS env var to inject custom HTTP headers into every outgoing cloud sync request (Cloudflare Access, reverse proxies, zero-trust gateways)
  • Authorization is rejected at parse time regardless of casing, preventing bearer-token shadowing; malformed pairs are skipped with a warning, values are never logged
  • Parsed once at construction on both RemoteTransport and MutationTransport; zero per-request overhead when unset

📂 Changes

File Change
internal/cloud/remote/transport.go Adds parseExtraHeaders / applyExtraHeaders, extraHeaders field on both transport structs, wired into ReadManifest, WriteChunk, ReadChunk, PullMutations
internal/cloud/remote/transport_extra_test.go Unit tests for parsing edge cases (empty, malformed, whitespace, Authorization rejection casing)
internal/cloud/remote/transport_headers_test.go Unit tests for header application on outgoing requests
cmd/engram/main.go Wires env var read-through
DOCS.md Documents ENGRAM_CLOUD_EXTRA_HEADERS in the autosync env var table

🧪 Test Plan

  • Unit tests pass locally: go test ./...
  • E2E tests pass locally: go test -tags e2e ./internal/server/...
  • Manually tested the affected functionality

Design was reviewed and approved by @Alan-TheGentleman on the issue thread, including the strings.TrimSpace condition on both key and value after splitting (closes a leading-space bypass of the Authorization guardrail) — confirmed present in parseExtraHeaders.


✅ Contributor Checklist

  • I linked an approved issue above (Closes #355)
  • I added exactly one type:* label to this PR
  • I ran unit tests locally: go test ./...
  • I ran e2e tests locally: go test -tags e2e ./internal/server/...
  • Docs updated (if behavior changed)
  • Commits follow conventional commits format
  • No Co-Authored-By trailers in commits

💬 Notes for Reviewers

Rebased onto latest main before opening (fork's main was ~65 commits behind upstream); resolved one trivial table-formatting conflict in DOCS.md.

Summary by CodeRabbit

  • New Features

    • Added support for injecting extra HTTP headers into cloud autosync requests via ENGRAM_CLOUD_EXTRA_HEADERS (comma-separated Key: Value pairs).
    • Applied to both manifest/chunk and mutation sync operations.
    • Rejects any attempt to provide an Authorization header; canonicalizes header names and uses last-value-wins for duplicates.
  • Documentation

    • Updated autosync documentation and CLI help with format, rules, and examples.
  • Tests

    • Added unit and HTTP-level coverage for parsing, propagation, and authorization precedence behavior.

Adds opt-in support for injecting arbitrary HTTP headers into every
outgoing cloud sync request via ENGRAM_CLOUD_EXTRA_HEADERS env var.

Comma-separated Key: Value pairs are parsed once at construction and
stored on both RemoteTransport and MutationTransport. Authorization
key is rejected at parse time to prevent token shadowing. Malformed
pairs are skipped with a warning log. Header values are never logged.

Enables compatibility with Cloudflare tunnels, reverse proxies, and
access systems that require custom headers beyond Authorization.
Copilot AI review requested due to automatic review settings July 20, 2026 23:29
@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds ENGRAM_CLOUD_EXTRA_HEADERS support for cloud autosync. Headers are parsed at transport construction, applied to all remote and mutation requests after bearer authorization, and documented with validation, logging, and precedence rules.

Changes

Cloud extra headers

Layer / File(s) Summary
Header parsing and transport construction
internal/cloud/remote/transport.go, internal/cloud/remote/transport_headers_test.go, internal/cloud/remote/transport_extra_test.go
Parses, canonicalizes, validates, and stores configured headers while rejecting Authorization overrides and avoiding secret logging.
Applying headers to cloud requests
internal/cloud/remote/transport.go, internal/cloud/remote/transport_extra_test.go
Applies extra headers to manifest, chunk, push, and pull requests after bearer authorization, with HTTP-level coverage.
Configuration and CLI documentation
DOCS.md, cmd/engram/main.go
Documents the environment variable, header format, parsing behavior, and authorization restriction.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant SyncCaller
  participant RemoteTransport
  participant CloudEndpoint
  SyncCaller->>RemoteTransport: Start cloud sync request
  RemoteTransport->>RemoteTransport: Set bearer Authorization and apply extra headers
  RemoteTransport->>CloudEndpoint: Send cloud request
  CloudEndpoint-->>RemoteTransport: Return response
Loading

Suggested labels: type:feature

Suggested reviewers: gentleman-programming

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and directly summarizes the main change: adding ENGRAM_CLOUD_EXTRA_HEADERS support.
Linked Issues check ✅ Passed The transport, docs, and tests implement #355 by adding opt-in extra headers, rejecting Authorization overrides, and skipping malformed pairs.
Out of Scope Changes check ✅ Passed The changes stay focused on cloud extra-header support, with only matching docs, CLI help, transport logic, and tests added.
Docstring Coverage ✅ Passed Docstring coverage is 80.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with 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.

❤️ 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 adds opt-in support for injecting additional HTTP headers into all outgoing cloud sync requests via ENGRAM_CLOUD_EXTRA_HEADERS, intended for environments like Cloudflare Access / zero-trust proxies, while preventing Authorization header shadowing.

Changes:

  • Parse ENGRAM_CLOUD_EXTRA_HEADERS once during RemoteTransport / MutationTransport construction and apply headers to all relevant requests.
  • Add unit tests covering header parsing edge cases and request header propagation.
  • Document the new env var in CLI help text and DOCS.md autosync documentation.

Reviewed changes

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

Show a summary per file
File Description
internal/cloud/remote/transport.go Adds parsing + application of extra headers, with Authorization guardrail, and wires header application into transports.
internal/cloud/remote/transport_headers_test.go Adds parsing-focused tests (including malformed input + Authorization rejection).
internal/cloud/remote/transport_extra_test.go Adds integration-style tests verifying headers are present on outgoing HTTP requests from both transports.
cmd/engram/main.go Updates CLI environment/help text to include ENGRAM_CLOUD_EXTRA_HEADERS.
DOCS.md Documents the new env var format/rules for autosync deployments.
Comments suppressed due to low confidence (1)

internal/cloud/remote/transport_headers_test.go:229

  • The custom contains / containsStr helpers plus the os.Stderr sentinel add noise and an inaccurate comment about “other tests”. Using strings.Contains is simpler and removes the need for the unused import workaround.
// contains is a helper to avoid importing strings in the test name collision.
func contains(s, substr string) bool {
	return len(s) >= len(substr) && (s == substr || len(substr) == 0 || containsStr(s, substr))
}

func containsStr(s, substr string) bool {
	for i := 0; i <= len(s)-len(substr); i++ {
		if s[i:i+len(substr)] == substr {
			return true
		}
	}
	return false
}

// Suppress unused import warning - os is used by other tests that may reference it.
var _ = os.Stderr


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

Comment thread internal/cloud/remote/transport.go Outdated
pair = strings.TrimSpace(pair)
idx := strings.IndexByte(pair, ':')
if idx < 0 {
log.Printf("[cloud] WARN ENGRAM_CLOUD_EXTRA_HEADERS: skipping malformed pair %q (missing colon)", pair)
Comment on lines +83 to +86
// The log should reference the malformed pair "badpair"
if !contains(logOut, "badpair") {
t.Fatalf("expected log to mention 'badpair', got: %q", logOut)
}
Comment on lines +3 to +8
import (
"bytes"
"log"
"os"
"testing"
)

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 4

🤖 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 `@internal/cloud/remote/transport_headers_test.go`:
- Around line 61-64: In internal/cloud/remote/transport_headers_test.go at lines
61-64, add a mutex-protected safeBuffer implementing Write and String, importing
sync as needed; replace the local bytes.Buffer with safeBuffer at lines 61-64,
91-94, 105-108, 122-125, 139-142, and 186-189, preserving the existing global
log interception and assertions.
- Around line 213-228: Replace the custom contains and containsStr helpers in
the test file with the standard strings.Contains function, importing strings or
an alias if needed for any naming collision. Update all call sites accordingly,
then remove the unused os import and the var _ = os.Stderr suppression.

In `@internal/cloud/remote/transport.go`:
- Around line 37-60: Update the extra-header parsing loop in transport.go to
accumulate comma-separated segments without a colon into the current header
value instead of discarding them or logging malformed-pair warnings, while
preserving key canonicalization and Authorization protection. Add an edge-case
test in internal/cloud/remote/transport_headers_test.go covering a header value
containing commas and asserting the complete value is retained.
- Around line 77-84: Replace the call to sortStrings with sort.Strings, add the
standard library sort import, and remove the custom sortStrings insertion-sort
function. Preserve the existing in-place sorting behavior for the string slice.
🪄 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: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 973ab9ca-b969-41e9-ba8b-2a8e825fbcc5

📥 Commits

Reviewing files that changed from the base of the PR and between 763a6ba and e5c6646.

📒 Files selected for processing (5)
  • DOCS.md
  • cmd/engram/main.go
  • internal/cloud/remote/transport.go
  • internal/cloud/remote/transport_extra_test.go
  • internal/cloud/remote/transport_headers_test.go

Comment on lines +61 to +64
orig := log.Writer()
var buf bytes.Buffer
log.SetOutput(&buf)
defer log.SetOutput(orig)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Data race hazard: bytes.Buffer is not thread-safe for global log interception.

These tests intercept the global log output using log.SetOutput(&buf) with a standard bytes.Buffer. If any background goroutine (such as those spun up by httptest.NewServer in other tests) or a concurrent test logs a message, it will race on the unprotected bytes.Buffer.Write, causing the test suite to panic.

  • internal/cloud/remote/transport_headers_test.go#L61-L64: Wrap the bytes.Buffer in a thread-safe struct with a sync.Mutex to safely intercept the global logger.
  • internal/cloud/remote/transport_headers_test.go#L91-L94: Use the thread-safe buffer here.
  • internal/cloud/remote/transport_headers_test.go#L105-L108: Use the thread-safe buffer here.
  • internal/cloud/remote/transport_headers_test.go#L122-L125: Use the thread-safe buffer here.
  • internal/cloud/remote/transport_headers_test.go#L139-L142: Use the thread-safe buffer here.
  • internal/cloud/remote/transport_headers_test.go#L186-L189: Use the thread-safe buffer here.
🛠️ Proposed thread-safe buffer helper

Add this helper to transport_headers_test.go and replace var buf bytes.Buffer with var buf safeBuffer in the affected tests:

type safeBuffer struct {
	mu sync.Mutex
	b  bytes.Buffer
}

func (s *safeBuffer) Write(p []byte) (n int, err error) {
	s.mu.Lock()
	defer s.mu.Unlock()
	return s.b.Write(p)
}

func (s *safeBuffer) String() string {
	s.mu.Lock()
	defer s.mu.Unlock()
	return s.b.String()
}
📍 Affects 1 file
  • internal/cloud/remote/transport_headers_test.go#L61-L64 (this comment)
  • internal/cloud/remote/transport_headers_test.go#L91-L94
  • internal/cloud/remote/transport_headers_test.go#L105-L108
  • internal/cloud/remote/transport_headers_test.go#L122-L125
  • internal/cloud/remote/transport_headers_test.go#L139-L142
  • internal/cloud/remote/transport_headers_test.go#L186-L189
🤖 Prompt for 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.

In `@internal/cloud/remote/transport_headers_test.go` around lines 61 - 64, In
internal/cloud/remote/transport_headers_test.go at lines 61-64, add a
mutex-protected safeBuffer implementing Write and String, importing sync as
needed; replace the local bytes.Buffer with safeBuffer at lines 61-64, 91-94,
105-108, 122-125, 139-142, and 186-189, preserving the existing global log
interception and assertions.

Comment on lines +213 to +228
// contains is a helper to avoid importing strings in the test name collision.
func contains(s, substr string) bool {
return len(s) >= len(substr) && (s == substr || len(substr) == 0 || containsStr(s, substr))
}

func containsStr(s, substr string) bool {
for i := 0; i <= len(s)-len(substr); i++ {
if s[i:i+len(substr)] == substr {
return true
}
}
return false
}

// Suppress unused import warning - os is used by other tests that may reference it.
var _ = os.Stderr

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove custom contains helpers and unused import suppressions.

Reinventing strings.Contains to avoid an import is an anti-pattern. If there were an actual naming collision with a local strings variable, the import could be aliased (e.g., import stdstrings "strings"). Additionally, bypassing the Go compiler's unused import checks with a dummy variable (var _ = os.Stderr) obfuscates dependencies.

Please import "strings", use strings.Contains throughout the test file, and remove the unused os import entirely.

♻️ Proposed refactor
-// contains is a helper to avoid importing strings in the test name collision.
-func contains(s, substr string) bool {
-	return len(s) >= len(substr) && (s == substr || len(substr) == 0 || containsStr(s, substr))
-}
-
-func containsStr(s, substr string) bool {
-	for i := 0; i <= len(s)-len(substr); i++ {
-		if s[i:i+len(substr)] == substr {
-			return true
-		}
-	}
-	return false
-}
-
-// Suppress unused import warning - os is used by other tests that may reference it.
-var _ = os.Stderr
🤖 Prompt for 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.

In `@internal/cloud/remote/transport_headers_test.go` around lines 213 - 228,
Replace the custom contains and containsStr helpers in the test file with the
standard strings.Contains function, importing strings or an alias if needed for
any naming collision. Update all call sites accordingly, then remove the unused
os import and the var _ = os.Stderr suppression.

Comment thread internal/cloud/remote/transport.go Outdated
Comment on lines +77 to +84
// sortStrings sorts a slice of strings in-place (insertion sort — small N, avoids importing sort).
func sortStrings(ss []string) {
for i := 1; i < len(ss); i++ {
for j := i; j > 0 && ss[j] < ss[j-1]; j-- {
ss[j], ss[j-1] = ss[j-1], ss[j]
}
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Prefer standard library sorting.

The net/http package already transitively imports sort (and slices), so avoiding the standard sort package provides no binary size benefit. Please use sort.Strings(keys) or slices.Sort(keys) (Go 1.21+) instead of maintaining a custom sorting implementation.

♻️ Proposed refactor

Replace sortStrings(keys) on line 71 with sort.Strings(keys), remove the sortStrings function entirely, and ensure sort is imported.

-// sortStrings sorts a slice of strings in-place (insertion sort — small N, avoids importing sort).
-func sortStrings(ss []string) {
-	for i := 1; i < len(ss); i++ {
-		for j := i; j > 0 && ss[j] < ss[j-1]; j-- {
-			ss[j], ss[j-1] = ss[j-1], ss[j]
-		}
-	}
-}
🤖 Prompt for 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.

In `@internal/cloud/remote/transport.go` around lines 77 - 84, Replace the call to
sortStrings with sort.Strings, add the standard library sort import, and remove
the custom sortStrings insertion-sort function. Preserve the existing in-place
sorting behavior for the string slice.

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Copilot AI review requested due to automatic review settings July 27, 2026 05:39

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

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

Comments suppressed due to low confidence (4)

internal/cloud/remote/transport.go:52

  • Malformed segments (missing :) are currently appended to the previous header value when currentKey is set. This contradicts the documented/expected “skip malformed pairs” behavior and can also cause accidental logging of secrets if a user forgets a colon. Consider always treating missing-colon segments as malformed: log a generic warning (no segment content) and skip without mutating any prior header value.
		if idx < 0 {
			if currentKey != "" {
				result[currentKey] += ", " + part
			} else {
				log.Printf("[cloud] WARN ENGRAM_CLOUD_EXTRA_HEADERS: skipping malformed segment %q (missing colon)", part)

internal/cloud/remote/transport_headers_test.go:86

  • This test currently expects malformed segments to be echoed back in logs (e.g. badpair). If logging is adjusted to avoid leaking potentially-sensitive raw segments/values, update the assertion to look for a generic warning message instead of the raw segment contents.
	// The log should reference the malformed pair "badpair"
	if !contains(logOut, "badpair") {
		t.Fatalf("expected log to mention 'badpair', got: %q", logOut)
	}

internal/cloud/remote/transport_headers_test.go:7

  • os is imported only to satisfy var _ = os.Stderr at the bottom of the file, but imports are file-scoped in Go, so this doesn’t help “other tests”. This adds noise and can confuse future maintenance; please drop the os import and the dummy reference.
import (
	"bytes"
	"log"
	"os"
	"testing"

internal/cloud/remote/transport_headers_test.go:228

  • The dummy os.Stderr reference (and its comment) should be removed once the os import is dropped; keeping it would break compilation.
// Suppress unused import warning - os is used by other tests that may reference it.
var _ = os.Stderr

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 2

🤖 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 `@internal/cloud/remote/transport.go`:
- Around line 37-54: Update the extra-header parsing loop around currentKey so
colonless segments are always treated as malformed: skip them and emit the
existing warning instead of appending them to the previous header value.
Preserve parsing of subsequent valid pairs and ensure
TestParseExtraHeaders_MalformedPairSkipped still yields X-A as "1"; document
that unescaped commas cannot appear in values unless an explicit quoting or
escaping rule is implemented.
- Around line 57-71: Update the extra-header parsing flow around currentKey and
result to validate both trimmed header names and values with net/http’s header
validation before storing them for Header.Set. Skip malformed entries, including
names or values containing control characters, spaces, or invalid bytes, and
emit only a key-focused warning without logging values; preserve the existing
Authorization override protection.
🪄 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: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 3a1b830f-dae1-4ff5-8b3b-0b589a16f4b8

📥 Commits

Reviewing files that changed from the base of the PR and between e5c6646 and ab90048.

📒 Files selected for processing (1)
  • internal/cloud/remote/transport.go

Comment on lines +37 to +54
parts := strings.Split(raw, ",")
result := make(map[string]string)
var currentKey string

for _, part := range parts {
part = strings.TrimSpace(part)
if part == "" {
continue
}

idx := strings.IndexByte(part, ':')
if idx < 0 {
if currentKey != "" {
result[currentKey] += ", " + part
} else {
log.Printf("[cloud] WARN ENGRAM_CLOUD_EXTRA_HEADERS: skipping malformed segment %q (missing colon)", part)
}
continue

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

This continuation heuristic breaks the malformed-pair contract.

For X-A: 1, badpair, X-B: 2, currentKey remains X-A, so badpair is appended to the value and no warning is emitted. The supplied TestParseExtraHeaders_MalformedPairSkipped expects X-A == "1" plus a warning, so this change makes the existing test fail. Define an explicit quoting/escaping rule for comma-containing values, or preserve skip-and-warn behavior and document the limitation.

🤖 Prompt for 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.

In `@internal/cloud/remote/transport.go` around lines 37 - 54, Update the
extra-header parsing loop around currentKey so colonless segments are always
treated as malformed: skip them and emit the existing warning instead of
appending them to the previous header value. Preserve parsing of subsequent
valid pairs and ensure TestParseExtraHeaders_MalformedPairSkipped still yields
X-A as "1"; document that unescaped commas cannot appear in values unless an
explicit quoting or escaping rule is implemented.

Comment on lines +57 to +71
key := strings.TrimSpace(part[:idx])
if key == "" {
currentKey = ""
continue
}

canonical := http.CanonicalHeaderKey(key)
if canonical == "Authorization" {
log.Printf("[cloud] WARN ENGRAM_CLOUD_EXTRA_HEADERS: refusing to override Authorization header")
currentKey = "" // Prevent appending trailing segments to a rejected header
continue
}

currentKey = canonical
result[currentKey] = strings.TrimSpace(part[idx+1:])

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Locate transport.go"
fd -a 'transport.go$' . | sed 's#^\./##'

echo
echo "Show internal/cloud/remote/transport.go around parser and callers"
sed -n '1,130p' internal/cloud/remote/transport.go

echo
echo "Search Env var usage and parsing"
rg -n "ENGRAM_CLOUD_EXTRA_HEADERS|ExtraHeaders|extra.headers|extra_headers|extra headers" internal/cloud/remote . -g '!vendor' | head -200

echo
echo "Go parser details for Header Values / Header validation across version if available"
go version 2>/dev/null || true
if command -v go >/dev/null 2>&1; then
  python3 - <<'PY'
import os, subprocess, tempfile
src = '''
package main
import (
    "net/http"
    "reflect"
    "regexp"
    "strings"
    "fmt"
)
func main() {
    cases := []struct{ name string }{
        {""}, {"A B"}, {"Bad\tHeader"}, {"Bad\rHeader"},
        {"Authorization Bad"}, {"Authorization", "A B C"},
    }
    for _, c := range cases {
        fmt.Printf("name:%q canonical:%q\n", c.name, http.CanonicalHeaderKey(c.name))
    }
    for _, name := range []string{"A B", "Bad\tHeader", "Bad\rHeader", "Authorization B"} {
        req, err := http.NewRequest("GET", "http://example.test", nil)
        req.Header[model.String] = map[string][]string{}
        req.Header[model.String][name] = []string{"value"}
        fmt.Printf("header %q err=%v req=%v\n", name, err != nil, req)
    }
    h := make(http.Header)
    h["Value\tBad"] = []string{"v"}
    h["Val\rue"] = []string{"v"}
    fmt.Printf("Header: %q\n", h.Get("Value\tBad"))
}
'''
# This won't run if dependencies missing; just avoid.
PY
fi

echo
echo "Parse behavior probe using repository copy as data? No runtime - use source references"
rg -n "http.CanonicalHeaderKey|addHeader|Header\." internal/cloud/remote/transport.go

Repository: Gentleman-Programming/engram

Length of output: 21269


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Behavioral probe with the Go stdlib if available, but keep it read-only and avoid running repository code.
if command -v go >/dev/null 2>&1; then
  tmp="$(mktemp -d)"
  cat > "$tmp/probe.go" <<'EOF'
package main

import (
	"fmt"
	"net/http"
)

func main() {
	cases := []string{"", "A B", "Bad\tHeader", "Bad\rHeader"}
	for _, name := range cases {
		fmt.Printf("name=%q canonical=%q\n", name, http.CanonicalHeaderKey(name))
	}

	var values = []string{"ok", "bad\tvalue", "bad\rv alue", "bad\t\rvalue"}
	for _, v := range values {
		req, err := http.NewRequest("GET", "http://example.test", nil)
		if err != nil {
			panic(err)
		}
		req.Header.Add("X-Test", v)
		b, _ := req.Header.WriteSubset(nil, map[string]bool{"Connection": true})
		fmt.Printf("value=%q\n", v)
		fmt.Printf("bytes contain tab=%v cr=%v\n", strings.Contains(string(b), "\t"), strings.Contains(string(b), "\r"))
	}
}
EOF
  (cd "$tmp" && go run .)
  rm -rf "$tmp"
fi

Repository: Gentleman-Programming/engram

Length of output: 260


🏁 Script executed:

#!/bin/bash
set -euo pipefail

tmp="$(mktemp -d)"
cat > "$tmp/module/probe.go" <<'EOF'
package main

import (
	"fmt"
	"net/http"
	"strings"
)

func main() {
	names := []string{"", "A B", "Bad\tHeader", "Bad\rHeader"}
	for _, name := range names {
		fmt.Printf("name=%q canonical=%q empty=%v\n", name, http.CanonicalHeaderKey(name), name == "")
	}

	values := []string{"ok", "bad\tvalue", "bad\rv alue", "bad\t\rvalue", "bad \"quote\""}
	for _, v := range values {
		req, err := http.NewRequest("GET", "http://example.test", nil)
		if err != nil {
			fmt.Printf("request_new_value=%q err=%v\n", v, err)
			continue
		}
		req.Header.Set("X-Test", v)
		b, _ := req.Header.WriteSubset(nil, map[string]bool{"Connection": true})
		wire := string(b)
		fmt.Printf("value=%q reqerr=%v wire_contains=%v\n", v, (err != nil), strings.Contains(wire, "\t"))
	}
}
EOF
go run "$tmp/module/probe.go"
rm -rf "$tmp"

Repository: Gentleman-Programming/engram

Length of output: 243


🏁 Script executed:

#!/bin/bash
set -euo pipefail

tmp="$(mktemp -d || exit 1)"
mkdir -p "$tmp/module"
cat > "$tmp/module/probe.go" <<'EOF'
package main

import (
	"fmt"
	"net/http"
	"strings"
)

func main() {
	names := []string{"", "A B", "Bad\tHeader", "Bad\rHeader"}
	for _, name := range names {
		fmt.Printf("name=%q canonical=%q empty=%v\n", name, http.CanonicalHeaderKey(name), name == "")
	}

	values := []string{"ok", "bad\tvalue", "bad\rv alue", "bad\t\rvalue", "bad \"quote\""}
	for _, v := range values {
		req, err := http.NewRequest("GET", "http://example.test", nil)
		if err != nil {
			fmt.Printf("request_new_value=%q err=%v\n", v, err)
			continue
		}
		req.Header.Set("X-Test", v)
		b, err := req.Header.WriteSubset(nil, map[string]bool{"Connection": true})
		wire := string(b)
		marshaledErr := ""
		if err != nil {
			marshaledErr = err.Error()
		}
		fmt.Printf("value=%q reqerr=%v writeerr=%s wire=%q contains_tab=%v contains_cr=%v\n",
			v, err != nil, marshaledErr, wire, strings.Contains(wire, "\t"), strings.Contains(wire, "\r"))
	}
}
EOF
cd "$tmp/module"
go run .

Repository: Gentleman-Programming/engram

Length of output: 260


🏁 Script executed:

#!/bin/bash
set -euo pipefail

tmp="$(mktemp -d || exit 1)"
mkdir -p "$tmp/module"
cd "$tmp/module" || exit 1

cat > go.mod <<'EOF'
module probe

go 1.26
EOF

cat > probe.go <<'EOF'
package main

import (
	"fmt"
	"net/http"
	"strings"
)

func main() {
	names := []string{"", "A B", "Bad\tHeader", "Bad\rHeader"}
	for _, name := range names {
		fmt.Printf("name=%q canonical=%q empty=%v\n", name, http.CanonicalHeaderKey(name), name == "")
	}

	values := []string{"ok", "bad\tvalue", "bad\rv alue", "bad\t\rvalue", "bad \"quote\""}
	for _, v := range values {
		req, err := http.NewRequest("GET", "http://example.test", nil)
		if err != nil {
			fmt.Printf("request_new_value=%q err=%v\n", v, err)
			continue
		}
		req.Header.Set("X-Test", v)
		b, err := req.Header.WriteSubset(nil, map[string]bool{"Connection": true})
		wire := string(b)
		marshaledErr := ""
		if err != nil {
			marshaledErr = err.Error()
		}
		fmt.Printf("value=%q reqerr=%v writeerr=%s wire=%q contains_tab=%v contains_cr=%v\n",
			v, err != nil, marshaledErr, wire, strings.Contains(wire, "\t"), strings.Contains(wire, "\r"))
	}
}
EOF

go run .

Repository: Gentleman-Programming/engram

Length of output: 268


🌐 Web query:

Go net/http Header WriteSubset request invalid header value tab NewRequest documentation

💡 Result:

In Go's net/http package, HTTP header values are strictly validated to ensure compliance with RFC standards [1][2]. While header values generally allow visible ASCII characters (0x21-0x7e) as well as horizontal tabs (HTAB, 0x09) and spaces (SP, 0x20), they must not contain control characters such as newlines (LF, 0x0a), carriage returns (CR, 0x0d), or null bytes (NUL, 0x00) [3][1]. If you attempt to set an HTTP header with an invalid character—such as a tab, which might be restricted depending on the specific validation context or Go version—the net/http library will reject it [2][4]. Specifically, methods like http.NewRequest and Header.Add/Set validate inputs; providing an invalid value will typically result in an error such as: net/http: invalid header field value [2][4]. Regarding Header.WriteSubset: 1. Purpose: This method is used to write header keys and values to an io.Writer in wire format, optionally excluding a subset of headers [5][6]. It is commonly used by internal tools like httputil.DumpRequest or proxies to filter out hop-by-hop headers [6]. 2. Validation: Because WriteSubset relies on the underlying header map, if you have bypassed initial validation (e.g., by manually manipulating the map) to insert invalid characters, the writing process may fail or produce malformed HTTP output [3][1]. 3. Documentation: Official documentation for Header.WriteSubset is available on the Go packages site [5][7]. If you find that your use case requires sending headers that are otherwise considered invalid by the standard library (such as including specific control characters or unconventional formatting), you cannot do so via the standard net/http request construction methods [4]. The common workaround is to manually construct the raw HTTP request byte stream and send it over a low-level net.Conn, bypassing the high-level net/http request validation logic entirely [4].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

tmp="$(mktemp -d || exit 1)"
mkdir -p "$tmp/module"
cd "$tmp/module" || exit 1

cat > go.mod <<'EOF'
module probe

go 1.26
EOF

cat > probe.go <<'EOF'
package main

import (
	"fmt"
	"net/http"
	"strings"
)

func main() {
	names := []string{"", "A B", "Bad\tHeader", "Bad\rHeader"}
	for _, name := range names {
		fmt.Printf("name=%q canonical=%q empty=%v\n", name, http.CanonicalHeaderKey(name), name == "")
	}

	values := []string{"ok", "bad\tvalue", "bad\rv alue", "bad\t\rvalue", "bad \"quote\""}
	for _, v := range values {
		req, err := http.NewRequest("GET", "http://example.test", nil)
		if err != nil {
			fmt.Printf("request_new_value=%q err=%v\n", v, err)
			continue
		}
		req.Header.Set("X-Test", v)
		b := req.Header.WriteSubset(nil, map[string]bool{"Connection": true})
		wire := string(b)
		fmt.Printf("value=%q reqerr=%v writeerr=%v wire=%q contains_tab=%v contains_cr=%v\n",
			v, err != nil, (err != nil), wire, strings.Contains(wire, "\t"), strings.Contains(wire, "\r"))
	}
}
EOF

go run .

Repository: Gentleman-Programming/engram

Length of output: 259


🏁 Script executed:

#!/bin/bash
set -euo pipefail

tmp="$(mktemp -d || exit 1)"
mkdir -p "$tmp/module"
cd "$tmp/module" || exit 1

cat > go.mod <<'EOF'
module probe

go 1.26
EOF

cat > probe.go <<'EOF'
package main

import (
	"fmt"
	"net/http"
	"strings"
)

func main() {
	names := []string{"", "A B", "Bad\tHeader", "Bad\rHeader"}
	for _, name := range names {
		fmt.Printf("name=%q canonical=%q empty=%v\n", name, http.CanonicalHeaderKey(name), name == "")
	}

	values := []string{"ok", "bad\tvalue", "bad\rv alue", "bad\t\rvalue", "bad \"quote\""}
	for _, v := range values {
		req, err := http.NewRequest("GET", "http://example.test", nil)
		fmt.Printf("request_new_value=%q reqerr=%v\n", v, (err != nil))
		if err != nil {
			continue
		}
		req.Header.Set("X-Test", v)
		err = req.Header.WriteSubset(nil, map[string]bool{"Connection": true})
		fmt.Printf("value=%q writeerr=%v\n", v, err != nil)
	}
}
EOF

go run .

Repository: Gentleman-Programming/engram

Length of output: 222


🏁 Script executed:

#!/bin/bash
set -euo pipefail

tmp="$(mktemp -d || exit 1)"
mkdir -p "$tmp/module"
cd "$tmp/module" || exit 1

cat > go.mod <<'EOF'
module probe

go 1.26
EOF

cat > probe.go <<'EOF'
package main

import (
	"fmt"
	"net/http"
	"strings"
)

func main() {
	names := []string{"", "A B", "Bad\tHeader", "Bad\rHeader"}
	for _, name := range names {
		canonical := http.CanonicalHeaderKey(name)
		fmt.Printf("name=%q canonical=%q empty=%v\n", name, canonical, name == "")
	}

	values := []string{"ok", "bad\tvalue", "bad\rv alue", "bad\t\rvalue", "bad \"quote\""}
	for _, v := range values {
		req, err := http.NewRequest("GET", "http://example.test", nil)
		fmt.Printf("request_new_value=%q reqerr=%v\n", v, (err != nil))
		if err != nil {
			continue
		}
		req.Header.Set("X-Test", v)
		err = req.Header.WriteSubset(nil, map[string]bool{"Connection": true})
		wire := strings.TrimSpace(fmt.Sprint(req.Header))
		fmt.Printf("value=%q writeerr=%v wire_contains_tab=%v wire_has_cr=%v\n",
			v, (err != nil), strings.Contains(wire, "\t"), strings.Contains(wire, "\r"))
	}
}
EOF

go run .

Repository: Gentleman-Programming/engram

Length of output: 904


Validate extra headers before applying them.

TrimSpace does not catch tab/carriage-return control characters in header names or values, and http.CanonicalHeaderKey leaves spaces or invalid bytes unchanged. Malformed ENGMT_CLOUD_EXTRA_HEADERS entries can make net/http fail to send the request; validate both parts before Header.Set, skip malformed entries with a key-only warning, and do not log values.

🤖 Prompt for 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.

In `@internal/cloud/remote/transport.go` around lines 57 - 71, Update the
extra-header parsing flow around currentKey and result to validate both trimmed
header names and values with net/http’s header validation before storing them
for Header.Set. Skip malformed entries, including names or values containing
control characters, spaces, or invalid bytes, and emit only a key-focused
warning without logging values; preserve the existing Authorization override
protection.

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.

Support custom HTTP headers for cloud sync (ENGRAM_CLOUD_EXTRA_HEADERS)

2 participants