From 1e70698cfcbe2fc29f8af21e63c60f93a5a58ecb Mon Sep 17 00:00:00 2001 From: sneak Date: Tue, 20 Oct 2026 13:43:52 +0000 Subject: [PATCH] An answer from the app without a Content-Type reaches the client without one (closes #127) Go's server adds a Content-Type it guesses from the body when the response's headers have no Content-Type key, which happened to some of the app's answers and not others, as the first part of the body or the headers went out first. When the app sends none, smallwebwaf now sets the key with no value on the client's response, which sends none. Model: opus-5-5 --- internal/proxy/passthrough_test.go | 35 ++++++++++++++++++++++++++++++ internal/proxy/request.go | 7 ++++++ 2 files changed, 42 insertions(+) diff --git a/internal/proxy/passthrough_test.go b/internal/proxy/passthrough_test.go index 46cb72d..80dfdb3 100644 --- a/internal/proxy/passthrough_test.go +++ b/internal/proxy/passthrough_test.go @@ -151,6 +151,41 @@ func wantRequestFields(t *testing.T, line logLine, host string, sent, received i } } +func TestAddsNoContentTypeTheAppDidNotSend(t *testing.T) { + t.Parallel() + + const typed = "text/plain" + + app := startApp(t, func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/typed" { + w.Header().Set("Content-Type", typed) + } else { + // The key with no value keeps Go's server from adding one. + w.Header()["Content-Type"] = nil + } + + _, _ = io.WriteString(w, "page") + }) + addr, _ := startProxy(t, app.URL, nil) + + // Go's server guesses a Content-Type for an answer without one only + // when the first part of the body goes out with the headers, which + // happens to some answers and not others: the test asks many times. + for range 50 { + got := get(t, addr, "/") + if got.header.Values("Content-Type") != nil { + t.Fatalf("client got Content-Type %q, want none", + got.header.Get("Content-Type")) + } + } + + got := get(t, addr, "/typed") + if got.header.Get("Content-Type") != typed { + t.Errorf("client got Content-Type %q, want %q", + got.header.Get("Content-Type"), typed) + } +} + func TestStreamsTheRequestBody(t *testing.T) { t.Parallel() diff --git a/internal/proxy/request.go b/internal/proxy/request.go index e67599c..922afa5 100644 --- a/internal/proxy/request.go +++ b/internal/proxy/request.go @@ -421,6 +421,13 @@ func (rq *request) modifyResponse(res *http.Response) error { return errResponseTooLarge } + // Go's server adds a Content-Type it guesses from the body to a + // response whose headers have no Content-Type key. The key with no + // value sends none, as the app sent none. + if res.Header.Get("Content-Type") == "" { + rq.out.Header()["Content-Type"] = nil + } + res.Body = &responseBody{body: limitBody(res.Body, maxBytes), rq: rq} rq.startClientResponseTimeout()