From 29ff0b4efe3f83ff917b4b639c83427482320a30 Mon Sep 17 00:00:00 2001 From: Alliballibaba Date: Sat, 12 Jul 2025 15:49:19 +0200 Subject: [PATCH 1/4] Allows headers without whitespace after colon. --- frankenphp.go | 14 +++++++++++--- frankenphp_test.go | 1 + testdata/headers.php | 1 + 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/frankenphp.go b/frankenphp.go index 6888cad129..5439925a68 100644 --- a/frankenphp.go +++ b/frankenphp.go @@ -482,13 +482,21 @@ func go_apache_request_headers(threadIndex C.uintptr_t) (*C.go_string, C.size_t) func addHeader(fc *frankenPHPContext, cString *C.char, length C.int) { parts := strings.SplitN(C.GoStringN(cString, length), ": ", 2) - if len(parts) != 2 { - fc.logger.LogAttrs(context.Background(), slog.LevelDebug, "invalid header", slog.String("header", parts[0])) + if len(parts) == 2 { + fc.responseWriter.Header().Add(parts[0], parts[1]) return } - fc.responseWriter.Header().Add(parts[0], parts[1]) + // also check for the format "Header:Value" without a space + parts = strings.SplitN(C.GoStringN(cString, length), ":", 2) + if len(parts) == 2 { + fc.responseWriter.Header().Add(parts[0], parts[1]) + + return + } + + fc.logger.LogAttrs(context.Background(), slog.LevelDebug, "invalid header", slog.String("header", parts[0])) } //export go_write_headers diff --git a/frankenphp_test.go b/frankenphp_test.go index 917c931a34..24241e058b 100644 --- a/frankenphp_test.go +++ b/frankenphp_test.go @@ -242,6 +242,7 @@ func testHeaders(t *testing.T, opts *testOptions) { assert.Equal(t, 201, resp.StatusCode) assert.Equal(t, "bar", resp.Header.Get("Foo")) assert.Equal(t, "bar2", resp.Header.Get("Foo2")) + assert.Equal(t, "bar3", resp.Header.Get("Foo3"), "header without whitespace after colon") assert.Empty(t, resp.Header.Get("Invalid")) assert.Equal(t, fmt.Sprintf("%d", i), resp.Header.Get("I")) }, opts) diff --git a/testdata/headers.php b/testdata/headers.php index 75d141eafc..6728175df8 100644 --- a/testdata/headers.php +++ b/testdata/headers.php @@ -5,6 +5,7 @@ return function () { header('Foo: bar'); header('Foo2: bar2'); + header('Foo3:bar3'); // no space after colon (also valid, not recommended) header('Invalid'); header('I: ' . ($_GET['i'] ?? 'i not set')); http_response_code(201); From 6f30e88bfcfe97b3c4c848ca670192ab1434eead Mon Sep 17 00:00:00 2001 From: Alliballibaba Date: Sat, 12 Jul 2025 19:03:32 +0200 Subject: [PATCH 2/4] Makes headers faster. --- frankenphp.go | 46 ++++++++++++++++++++++++++++++++++++---------- 1 file changed, 36 insertions(+), 10 deletions(-) diff --git a/frankenphp.go b/frankenphp.go index 5439925a68..d3281416de 100644 --- a/frankenphp.go +++ b/frankenphp.go @@ -481,22 +481,48 @@ func go_apache_request_headers(threadIndex C.uintptr_t) (*C.go_string, C.size_t) } func addHeader(fc *frankenPHPContext, cString *C.char, length C.int) { - parts := strings.SplitN(C.GoStringN(cString, length), ": ", 2) - if len(parts) == 2 { - fc.responseWriter.Header().Add(parts[0], parts[1]) - + key, val := splitHeader(cString) + if key == "" { + fc.logger.LogAttrs(context.Background(), slog.LevelDebug, "invalid header", slog.String("header", C.GoStringN(cString, length))) return } + fc.responseWriter.Header().Add(key, val) +} - // also check for the format "Header:Value" without a space - parts = strings.SplitN(C.GoStringN(cString, length), ":", 2) - if len(parts) == 2 { - fc.responseWriter.Header().Add(parts[0], parts[1]) +// split the raw header coming from C with minimal allocations +func splitHeader(raw *C.char) (string, string) { + ptr := unsafe.Pointer(raw) + var i int + + // Scan for ':' + for { + b := *(*byte)(unsafe.Pointer(uintptr(ptr) + uintptr(i))) + if b == 0 { + // Reached end without finding ':' + return "", "" + } + if b == ':' { + break + } + i++ + } - return + key := C.GoStringN(raw, C.int(i)) + + // Skip whitespace after ':' + j := i + 1 + for { + b := *(*byte)(unsafe.Pointer(uintptr(ptr) + uintptr(j))) + if b != ' ' { + break + } + j++ } - fc.logger.LogAttrs(context.Background(), slog.LevelDebug, "invalid header", slog.String("header", parts[0])) + valuePtr := (*C.char)(unsafe.Pointer(uintptr(ptr) + uintptr(j))) + value := C.GoString(valuePtr) + + return key, value } //export go_write_headers From e83164b20f066399b068ebaff565339740b09208 Mon Sep 17 00:00:00 2001 From: Alliballibaba Date: Sat, 12 Jul 2025 19:38:00 +0200 Subject: [PATCH 3/4] Optimizes header splitting. --- frankenphp.go | 40 ++++++++++++++++++---------------------- 1 file changed, 18 insertions(+), 22 deletions(-) diff --git a/frankenphp.go b/frankenphp.go index d3281416de..2132cf61bc 100644 --- a/frankenphp.go +++ b/frankenphp.go @@ -481,7 +481,7 @@ func go_apache_request_headers(threadIndex C.uintptr_t) (*C.go_string, C.size_t) } func addHeader(fc *frankenPHPContext, cString *C.char, length C.int) { - key, val := splitHeader(cString) + key, val := splitRawHeader(cString, int(length)) if key == "" { fc.logger.LogAttrs(context.Background(), slog.LevelDebug, "invalid header", slog.String("header", C.GoStringN(cString, length))) return @@ -490,37 +490,33 @@ func addHeader(fc *frankenPHPContext, cString *C.char, length C.int) { } // split the raw header coming from C with minimal allocations -func splitHeader(raw *C.char) (string, string) { - ptr := unsafe.Pointer(raw) - var i int +func splitRawHeader(rawHeader *C.char, length int) (string, string) { + buf := unsafe.Slice((*byte)(unsafe.Pointer(rawHeader)), length) - // Scan for ':' - for { - b := *(*byte)(unsafe.Pointer(uintptr(ptr) + uintptr(i))) - if b == 0 { - // Reached end without finding ':' - return "", "" - } - if b == ':' { + // Search for the colon in 'Header-Key: value' + var i int + for i = 0; i < length; i++ { + if buf[i] == ':' { break } - i++ } - key := C.GoStringN(raw, C.int(i)) + if i == length { + return "", "" // No colon found, invalid header + } - // Skip whitespace after ':' + key := C.GoStringN(rawHeader, C.int(i)) + + // skip whitespaces after the colon j := i + 1 - for { - b := *(*byte)(unsafe.Pointer(uintptr(ptr) + uintptr(j))) - if b != ' ' { - break - } + for j < length && buf[j] == ' ' { j++ } - valuePtr := (*C.char)(unsafe.Pointer(uintptr(ptr) + uintptr(j))) - value := C.GoString(valuePtr) + // anything left is the header value + valueLen := length - j + valuePtr := (*C.char)(unsafe.Pointer(uintptr(unsafe.Pointer(rawHeader)) + uintptr(j))) + value := C.GoStringN(valuePtr, C.int(valueLen)) return key, value } From 52f2c673c66477fd49fb86acd318994869189810 Mon Sep 17 00:00:00 2001 From: Alliballibaba Date: Sat, 12 Jul 2025 19:45:10 +0200 Subject: [PATCH 4/4] Formatting. --- frankenphp.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/frankenphp.go b/frankenphp.go index 2132cf61bc..737874d89c 100644 --- a/frankenphp.go +++ b/frankenphp.go @@ -505,7 +505,7 @@ func splitRawHeader(rawHeader *C.char, length int) (string, string) { return "", "" // No colon found, invalid header } - key := C.GoStringN(rawHeader, C.int(i)) + headerKey := C.GoStringN(rawHeader, C.int(i)) // skip whitespaces after the colon j := i + 1 @@ -514,11 +514,10 @@ func splitRawHeader(rawHeader *C.char, length int) (string, string) { } // anything left is the header value - valueLen := length - j valuePtr := (*C.char)(unsafe.Pointer(uintptr(unsafe.Pointer(rawHeader)) + uintptr(j))) - value := C.GoStringN(valuePtr, C.int(valueLen)) + headerValue := C.GoStringN(valuePtr, C.int(length-j)) - return key, value + return headerKey, headerValue } //export go_write_headers