From 125a8b44afaab4761df985b68a2bb12523668ac3 Mon Sep 17 00:00:00 2001 From: "a.bolonin" Date: Sat, 31 Oct 2026 14:15:37 +0300 Subject: [PATCH 1/2] perf: use byte search to split response headers --- cgi_test.go | 56 +++++++++++++++++++++++++++++++++++++++++++++++++++ frankenphp.go | 10 ++------- 2 files changed, 58 insertions(+), 8 deletions(-) diff --git a/cgi_test.go b/cgi_test.go index 81107c55c2..6675dd19e4 100644 --- a/cgi_test.go +++ b/cgi_test.go @@ -561,3 +561,59 @@ func TestAssignedWorkerScriptPath(t *testing.T) { }) } } + +func TestSplitRawHeader(t *testing.T) { + for _, tc := range []struct { + raw, key, value string + }{ + {"Content-Type: application/json", "Content-Type", "application/json"}, + {"X-Request-Id: abc:def", "X-Request-Id", "abc:def"}, + {"Access-Control-Allow-Credentials: true", "Access-Control-Allow-Credentials", "true"}, + {"X-Test: value", "X-Test", "value"}, + {"X-Test:\tvalue", "X-Test", "\tvalue"}, + {"X-Test:", "X-Test", ""}, + {"X-Test: ", "X-Test", ""}, + {"X-Test: a\x00b", "X-Test", "a\x00b"}, + {": value", "", "value"}, + {"missing separator", "", ""}, + {"", "", ""}, + } { + t.Run(tc.raw, func(t *testing.T) { + key, value := splitRawHeader(toUnsafeChar(tc.raw), len(tc.raw)) + assert.Equal(t, tc.key, key) + assert.Equal(t, tc.value, value) + }) + } +} + +func FuzzSplitRawHeader(f *testing.F) { + for _, raw := range []string{"", "X-Test: value", "X-Test:\tvalue", "X-Test: a\x00b", "missing separator", ": value"} { + f.Add(raw) + } + f.Fuzz(func(t *testing.T, raw string) { + wantKey, wantValue, found := strings.Cut(raw, ":") + if !found { + wantKey, wantValue = "", "" + } else { + wantValue = strings.TrimLeft(wantValue, " ") + } + key, value := splitRawHeader(toUnsafeChar(raw), len(raw)) + if key != wantKey || value != wantValue { + t.Fatalf("splitRawHeader(%q) = (%q, %q), want (%q, %q)", raw, key, value, wantKey, wantValue) + } + }) +} + +var responseHeaderKeyResult, responseHeaderValueResult string + +func BenchmarkSplitRawHeader(b *testing.B) { + for _, raw := range []string{"Content-Type: application/json", "X-Request-Id: abcdef0123456789", "Access-Control-Allow-Credentials: true", strings.Repeat("X", 128) + ": value", "missing separator"} { + b.Run(raw, func(b *testing.B) { + ptr := toUnsafeChar(raw) + b.ReportAllocs() + for b.Loop() { + responseHeaderKeyResult, responseHeaderValueResult = splitRawHeader(ptr, len(raw)) + } + }) + } +} diff --git a/frankenphp.go b/frankenphp.go index e560031e62..8a19df5fc2 100644 --- a/frankenphp.go +++ b/frankenphp.go @@ -623,14 +623,8 @@ func splitRawHeader(rawHeader *C.char, length int) (string, string) { buf := unsafe.Slice((*byte)(unsafe.Pointer(rawHeader)), length) // Search for the colon in 'Header-Key: value' - var i int - for i = 0; i < length; i++ { - if buf[i] == ':' { - break - } - } - - if i == length { + i := bytes.IndexByte(buf, ':') + if i < 0 { return "", "" // No colon found, invalid header } From e793238d9bbf7a28b715729b851dee20a04d8927 Mon Sep 17 00:00:00 2001 From: "a.bolonin" Date: Sat, 31 Oct 2026 14:20:04 +0300 Subject: [PATCH 2/2] fix: import bytes for response header scanning --- frankenphp.go | 1 + 1 file changed, 1 insertion(+) diff --git a/frankenphp.go b/frankenphp.go index 8a19df5fc2..b246e2dadb 100644 --- a/frankenphp.go +++ b/frankenphp.go @@ -20,6 +20,7 @@ package frankenphp // #include import "C" import ( + "bytes" "context" "errors" "fmt"