diff --git a/frankenphp.go b/frankenphp.go index 6888cad1..737874d8 100644 --- a/frankenphp.go +++ b/frankenphp.go @@ -481,14 +481,43 @@ 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])) - + 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 } + fc.responseWriter.Header().Add(key, val) +} - fc.responseWriter.Header().Add(parts[0], parts[1]) +// split the raw header coming from C with minimal allocations +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 { + return "", "" // No colon found, invalid header + } + + headerKey := C.GoStringN(rawHeader, C.int(i)) + + // skip whitespaces after the colon + j := i + 1 + for j < length && buf[j] == ' ' { + j++ + } + + // anything left is the header value + valuePtr := (*C.char)(unsafe.Pointer(uintptr(unsafe.Pointer(rawHeader)) + uintptr(j))) + headerValue := C.GoStringN(valuePtr, C.int(length-j)) + + return headerKey, headerValue } //export go_write_headers diff --git a/frankenphp_test.go b/frankenphp_test.go index 917c931a..24241e05 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 75d141ea..6728175d 100644 --- a/testdata/headers.php +++ b/testdata/headers.php @@ -5,6 +5,7 @@ require_once __DIR__.'/_executor.php'; 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);