From a659df14d1079c8c50131dc696db757f1b3118c3 Mon Sep 17 00:00:00 2001 From: sneak Date: Wed, 11 Nov 2026 04:42:42 +0000 Subject: [PATCH] Request log: write & < > as sent (closes #327) Request lines were encoded with json.Marshal, which writes &, < and > as &, < and >. A query string or referer then did not read as the visitor sent it, and grep for it found nothing. Lines are now written by a json.Encoder with HTML escaping off; they stay valid JSON. Model: opus-5-5 --- internal/requestlog/requestlog.go | 13 +++++++-- internal/requestlog/requestlog_test.go | 38 ++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 3 deletions(-) diff --git a/internal/requestlog/requestlog.go b/internal/requestlog/requestlog.go index d5a98b7..8bdfda3 100644 --- a/internal/requestlog/requestlog.go +++ b/internal/requestlog/requestlog.go @@ -5,6 +5,7 @@ package requestlog import ( + "bytes" "encoding/json" "fmt" "io" @@ -252,14 +253,20 @@ func WriteRProxyLine(w io.Writer, line *RProxyLine) error { return writeJSONLine(w, line) } -// writeJSONLine writes line to w as one JSON line. +// writeJSONLine writes line to w as one JSON line. It writes "&", "<" and +// ">" as they are, so a query string reads as it was sent. func writeJSONLine(w io.Writer, line any) error { - encoded, err := json.Marshal(line) + var encoded bytes.Buffer + + encoder := json.NewEncoder(&encoded) + encoder.SetEscapeHTML(false) + + err := encoder.Encode(line) if err != nil { return fmt.Errorf("encode the request log line: %w", err) } - _, err = w.Write(append(encoded, '\n')) + _, err = w.Write(encoded.Bytes()) if err != nil { return fmt.Errorf("write the request log line: %w", err) } diff --git a/internal/requestlog/requestlog_test.go b/internal/requestlog/requestlog_test.go index 79c50df..764fb86 100644 --- a/internal/requestlog/requestlog_test.go +++ b/internal/requestlog/requestlog_test.go @@ -67,6 +67,44 @@ func TestWriteWritesOneJSONLineMarkedRequest(t *testing.T) { } } +func TestRequestLinesHoldTheQueryAndRefererAsSent(t *testing.T) { + t.Parallel() + + var out bytes.Buffer + + err := requestlog.Write(&out, &requestlog.Line{ + Query: "a=1&b=<2>", + Referer: "https://example.com/?x=1&y=<2>", + }) + if err != nil { + t.Fatalf("write: %v", err) + } + + err = requestlog.WriteRProxyLine(&out, &requestlog.RProxyLine{Query: "a=1&b=<2>"}) + if err != nil { + t.Fatalf("write the rproxy line: %v", err) + } + + lines := strings.Split(strings.TrimSuffix(out.String(), "\n"), "\n") + if len(lines) != 2 { + t.Fatalf("wrote %q, want two lines", out.String()) + } + + for _, line := range lines { + if !json.Valid([]byte(line)) { + t.Errorf("%s is not valid JSON", line) + } + + if !strings.Contains(line, `"query":"a=1&b=<2>"`) { + t.Errorf("%s does not hold the query as sent", line) + } + } + + if !strings.Contains(lines[0], `"referer":"https://example.com/?x=1&y=<2>"`) { + t.Errorf("%s does not hold the referer as sent", lines[0]) + } +} + func TestWriteRProxyLineWritesOneJSONLineWithOnlyItsFields(t *testing.T) { t.Parallel()