Skip to content

Commit

Permalink
feat(proxy): warn on unsupported encoding, fixes #692
Browse files Browse the repository at this point in the history
  • Loading branch information
a-h committed Apr 22, 2024
1 parent 3c47c10 commit 9ee8751
Show file tree
Hide file tree
Showing 4 changed files with 103 additions and 15 deletions.
2 changes: 1 addition & 1 deletion .version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.2.650
0.2.667
2 changes: 1 addition & 1 deletion cmd/templ/generatecmd/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -343,7 +343,7 @@ func (cmd *Generate) StartProxy(ctx context.Context) (p *proxy.Handler, err erro
if cmd.Args.ProxyBind == "" {
cmd.Args.ProxyBind = "127.0.0.1"
}
p = proxy.New(cmd.Args.ProxyBind, cmd.Args.ProxyPort, target)
p = proxy.New(cmd.Log, cmd.Args.ProxyBind, cmd.Args.ProxyPort, target)
go func() {
cmd.Log.Info("Proxying", slog.String("from", p.URL), slog.String("to", p.Target.String()))
if err := http.ListenAndServe(fmt.Sprintf("%s:%d", cmd.Args.ProxyBind, cmd.Args.ProxyPort), p); err != nil {
Expand Down
22 changes: 16 additions & 6 deletions cmd/templ/generatecmd/proxy/proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import (
"compress/gzip"
"fmt"
"io"
"log"
stdlog "log"
"log/slog"
"math"
"net/http"
"net/http/httputil"
Expand All @@ -27,6 +28,7 @@ var script string
const scriptTag = `<script src="/_templ/reload/script.js"></script>`

type Handler struct {
log *slog.Logger
URL string
Target *url.URL
p *httputil.ReverseProxy
Expand All @@ -45,7 +47,9 @@ func (pwc passthroughWriteCloser) Close() error {
return nil
}

func modifyResponse(r *http.Response) error {
const unsupportedContentEncoding = "Unsupported content encoding, hot reload script not inserted."

func (h *Handler) modifyResponse(r *http.Response) error {
if r.Header.Get("templ-skip-modify") == "true" {
return nil
}
Expand Down Expand Up @@ -75,6 +79,10 @@ func modifyResponse(r *http.Response) error {
newWriter = func(out io.Writer) io.WriteCloser {
return brotli.NewWriter(out)
}
case "":
// No content encoding.
default:
h.log.Warn(unsupportedContentEncoding, slog.String("encoding", r.Header.Get("Content-Encoding")))
}

// Read the encoded body.
Expand Down Expand Up @@ -110,21 +118,23 @@ func modifyResponse(r *http.Response) error {
return nil
}

func New(bind string, port int, target *url.URL) *Handler {
func New(log *slog.Logger, bind string, port int, target *url.URL) (h *Handler) {
p := httputil.NewSingleHostReverseProxy(target)
p.ErrorLog = log.New(os.Stderr, "Proxy to target error: ", 0)
p.ErrorLog = stdlog.New(os.Stderr, "Proxy to target error: ", 0)
p.Transport = &roundTripper{
maxRetries: 10,
initialDelay: 100 * time.Millisecond,
backoffExponent: 1.5,
}
p.ModifyResponse = modifyResponse
return &Handler{
h = &Handler{
log: log,
URL: fmt.Sprintf("http://%s:%d", bind, port),
Target: target,
p: p,
sse: sse.New(),
}
p.ModifyResponse = h.modifyResponse
return h
}

func (p *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
Expand Down
92 changes: 85 additions & 7 deletions cmd/templ/generatecmd/proxy/proxy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@ import (
"context"
"fmt"
"io"
"log/slog"
"net/http"
"net/http/httptest"
"net/url"
"strconv"
"strings"
"sync"
"testing"
"time"

Expand Down Expand Up @@ -58,7 +60,9 @@ func TestProxy(t *testing.T) {
r.Header.Set("Content-Length", "16")

// Act
err := modifyResponse(r)
log := slog.New(slog.NewJSONHandler(io.Discard, nil))
h := New(log, "127.0.0.1", 7474, &url.URL{Scheme: "http", Host: "example.com"})
err := h.modifyResponse(r)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
Expand Down Expand Up @@ -86,7 +90,9 @@ func TestProxy(t *testing.T) {
r.Header.Set("templ-skip-modify", "true")

// Act
err := modifyResponse(r)
log := slog.New(slog.NewJSONHandler(io.Discard, nil))
h := New(log, "127.0.0.1", 7474, &url.URL{Scheme: "http", Host: "example.com"})
err := h.modifyResponse(r)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
Expand Down Expand Up @@ -118,7 +124,9 @@ func TestProxy(t *testing.T) {
}

// Act
err := modifyResponse(r)
log := slog.New(slog.NewJSONHandler(io.Discard, nil))
h := New(log, "127.0.0.1", 7474, &url.URL{Scheme: "http", Host: "example.com"})
err := h.modifyResponse(r)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
Expand Down Expand Up @@ -148,7 +156,9 @@ func TestProxy(t *testing.T) {
r.Header.Set("Content-Length", "16")

// Act
err := modifyResponse(r)
log := slog.New(slog.NewJSONHandler(io.Discard, nil))
h := New(log, "127.0.0.1", 7474, &url.URL{Scheme: "http", Host: "example.com"})
err := h.modifyResponse(r)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
Expand Down Expand Up @@ -196,7 +206,10 @@ func TestProxy(t *testing.T) {
r.Header.Set("Content-Length", fmt.Sprintf("%d", expectedLength))

// Act
if err = modifyResponse(r); err != nil {
log := slog.New(slog.NewJSONHandler(io.Discard, nil))
h := New(log, "127.0.0.1", 7474, &url.URL{Scheme: "http", Host: "example.com"})
err = h.modifyResponse(r)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}

Expand Down Expand Up @@ -248,7 +261,10 @@ func TestProxy(t *testing.T) {
r.Header.Set("Content-Length", fmt.Sprintf("%d", expectedLength))

// Act
if err = modifyResponse(r); err != nil {
log := slog.New(slog.NewJSONHandler(io.Discard, nil))
h := New(log, "127.0.0.1", 7474, &url.URL{Scheme: "http", Host: "example.com"})
err = h.modifyResponse(r)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}

Expand All @@ -275,7 +291,8 @@ func TestProxy(t *testing.T) {
if err != nil {
t.Fatalf("unexpected error parsing URL: %v", err)
}
handler := New("0.0.0.0", 0, u)
log := slog.New(slog.NewJSONHandler(io.Discard, nil))
handler := New(log, "0.0.0.0", 0, u)
proxyServer := httptest.NewServer(handler)
defer proxyServer.Close()

Expand Down Expand Up @@ -353,4 +370,65 @@ func TestProxy(t *testing.T) {
t.Fatalf("timeout waiting for sse response")
}
})
t.Run("unsupported encodings result in a warning", func(t *testing.T) {
// Arrange
r := &http.Response{
Body: io.NopCloser(bytes.NewReader([]byte("<p>Data</p>"))),
Header: make(http.Header),
}
r.Header.Set("Content-Type", "text/html, charset=utf-8")
r.Header.Set("Content-Encoding", "weird-encoding")

// Act
lh := newTestLogHandler()
log := slog.New(lh)
h := New(log, "127.0.0.1", 7474, &url.URL{Scheme: "http", Host: "example.com"})
err := h.modifyResponse(r)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}

// Assert
if len(lh.records) != 1 {
t.Fatalf("expected 1 log entry, but got %d", len(lh.records))
}
record := lh.records[0]
if record.Message != unsupportedContentEncoding {
t.Errorf("expected warning message %q, got %q", unsupportedContentEncoding, record.Message)
}
if record.Level != slog.LevelWarn {
t.Errorf("expected warning, got level %v", record.Level)
}
})
}

func newTestLogHandler() *testLogHandler {
return &testLogHandler{
m: new(sync.Mutex),
records: nil,
}
}

type testLogHandler struct {
m *sync.Mutex
records []slog.Record
}

func (h *testLogHandler) Enabled(context.Context, slog.Level) bool {
return true
}

func (h *testLogHandler) Handle(ctx context.Context, r slog.Record) error {
h.m.Lock()
defer h.m.Unlock()
h.records = append(h.records, r)
return nil
}

func (h *testLogHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
return h
}

func (h *testLogHandler) WithGroup(name string) slog.Handler {
return h
}

0 comments on commit 9ee8751

Please sign in to comment.