labstack / echo

High performance, minimalist Go web framework
https://echo.labstack.com
MIT License
29.01k stars 2.21k forks source link

proxy middleware should use httputil.ReverseProxy for SSE requests #2624

Closed aldas closed 2 months ago

aldas commented 2 months ago

Relates to #1172

Use httputil.ReverseProxy to proxy SSE requests as it has support for streaming responses. See: https://github.com/golang/go/blob/b107d95b9a66bfe7150fd4f2915e9bb876a6999a/src/net/http/httputil/reverseproxy.go#L601


can be tested with

  1. create separate package and execute this code to start serving proxy application at port 8080 that proxies requests to localhost:8081
package main

import (
    "errors"
    "github.com/labstack/echo/v4"
    "github.com/labstack/echo/v4/middleware"
    "log"
    "net/http"
    "net/url"
)

func main() {
    e := echo.New()
    e.Use(middleware.Logger())
    e.Use(middleware.Recover())

    tmpURL, err := url.Parse("http://localhost:8081")
    if err != nil {
        log.Fatal(err)
    }
    e.Use(middleware.Proxy(middleware.NewRoundRobinBalancer([]*middleware.ProxyTarget{{URL: tmpURL}})))

    if err := e.Start(":8080"); err != nil && !errors.Is(err, http.ErrServerClosed) {
        log.Fatal(err)
    }
}
  1. Create application for serving SSE

Go file for application

package main

import (
    "bytes"
    "errors"
    "fmt"
    "github.com/labstack/echo/v4"
    "github.com/labstack/echo/v4/middleware"
    "log"
    "net/http"
    "time"
)

func main() {
    e := echo.New()

    e.Use(middleware.Logger())
    e.Use(middleware.Recover())
    e.File("/", "./index.html")

    e.GET("/sse", func(c echo.Context) error {
        log.Printf("SSE client connected, ip: %v", c.RealIP())

        w := c.Response()
        w.Header().Set("Content-Type", "text/event-stream")
        w.Header().Set("Cache-Control", "no-cache")
        w.Header().Set("Connection", "keep-alive")

        ticker := time.NewTicker(1 * time.Second)
        defer ticker.Stop()
        for {
            select {
            case <-c.Request().Context().Done():
                log.Printf("SSE client disconnected, ip: %v", c.RealIP())
                return nil
            case <-ticker.C:
                event := Event{
                    Data: []byte("ping: " + time.Now().Format(time.RFC3339Nano)),
                }
                if err := event.WriteTo(w); err != nil {
                    return err
                }
                w.Flush()
            }
        }
    })

    if err := e.Start(":8081"); err != nil && !errors.Is(err, http.ErrServerClosed) {
        log.Fatal(err)
    }
}

// Event structure is defined here: https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#event_stream_format
type Event struct {
    ID      []byte
    Data    []byte
    Event   []byte
    Retry   []byte
    Comment []byte
}

// WriteTo writes Event to given ResponseWriter
func (ev *Event) WriteTo(w http.ResponseWriter) error {
    // Marshalling part is taken from: https://github.com/r3labs/sse/blob/c6d5381ee3ca63828b321c16baa008fd6c0b4564/http.go#L16
    if len(ev.Data) == 0 && len(ev.Comment) == 0 {
        return nil
    }

    if len(ev.Data) > 0 {
        if _, err := fmt.Fprintf(w, "id: %s\n", ev.ID); err != nil {
            return err
        }

        sd := bytes.Split(ev.Data, []byte("\n"))
        for i := range sd {
            if _, err := fmt.Fprintf(w, "data: %s\n", sd[i]); err != nil {
                return err
            }
        }

        if len(ev.Event) > 0 {
            if _, err := fmt.Fprintf(w, "event: %s\n", ev.Event); err != nil {
                return err
            }
        }

        if len(ev.Retry) > 0 {
            if _, err := fmt.Fprintf(w, "retry: %s\n", ev.Retry); err != nil {
                return err
            }
        }
    }

    if len(ev.Comment) > 0 {
        if _, err := fmt.Fprintf(w, ": %s\n", ev.Comment); err != nil {
            return err
        }
    }

    if _, err := fmt.Fprint(w, "\n"); err != nil {
        return err
    }

    return nil
}

in the same folder as app create index.html

<!DOCTYPE html>
<html>
<body>

<h1>Getting server updates</h1>
<div id="result"></div>

<script>
  // Example taken from: https://www.w3schools.com/html/html5_serversentevents.asp
  if (typeof (EventSource) !== "undefined") {
    const source = new EventSource("/sse");
    source.onmessage = function (event) {
      document.getElementById("result").innerHTML += event.data + "<br>";
    };
  } else {
    document.getElementById("result").innerHTML = "Sorry, your browser does not support server-sent events...";
  }
</script>

</body>
</html>
  1. Open http://localhost:8080 in your browser. You should see Ping messages streamed, assuming proxy middleware handles SSE requests as raw proxy