Skip to content

Commit 25d2847

Browse files
authored
Merge pull request #16 from jmalloc/sse
Add a route that sends "server-sent events".
2 parents 1a1b959 + 18b9972 commit 25d2847

File tree

3 files changed

+119
-10
lines changed

3 files changed

+119
-10
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ The format is based on [Keep a Changelog], and this project adheres to
99
[Keep a Changelog]: https://keepachangelog.com/en/1.0.0/
1010
[Semantic Versioning]: https://semver.org/spec/v2.0.0.html
1111

12+
## [Unreleased]
13+
14+
- Add the `/.see` route
15+
1216
## [0.2.0] - 2021-06-03
1317

1418
- Add support for logging HTTP headers to stdout (thanks [@arulrajnet])

README.md

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
11
# Echo Server
22

3-
A very simple HTTP echo server with support for websockets.
3+
A very simple HTTP echo server with support for websockets and server-sent
4+
events (SSE).
5+
6+
The server is designed for testing HTTP proxies and clients. It echoes
7+
information about HTTP request headers and bodies back to the client.
48

59
## Behavior
610

7-
- Any messages sent from a websocket client are echoed
8-
- Visit `/.ws` for a basic UI to connect and send websocket messages
9-
- Requests to any other URL will return the request headers and body
11+
- Any messages sent from a websocket client are echoed as a websocket message
12+
- Visit `/.ws` in a browser for a basic UI to connect and send websocket messages
13+
- Request `/.sse` to receive the echo response via server-sent events
14+
- Request any other URL to receive the echo response in plain text
1015

1116
## Configuration
1217

cmd/echo-server/main.go

Lines changed: 106 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ import (
88
"io/ioutil"
99
"net/http"
1010
"os"
11+
"strconv"
12+
"strings"
13+
"time"
1114

1215
"github.com/gorilla/websocket"
1316
"golang.org/x/net/http2"
@@ -41,6 +44,7 @@ var upgrader = websocket.Upgrader{
4144
}
4245

4346
func handler(wr http.ResponseWriter, req *http.Request) {
47+
defer req.Body.Close()
4448

4549
if os.Getenv("LOG_HTTP_BODY") != "" || os.Getenv("LOG_HTTP_HEADERS") != "" {
4650
fmt.Printf("-------- %s | %s %s\n", req.RemoteAddr, req.Method, req.URL)
@@ -80,6 +84,8 @@ func handler(wr http.ResponseWriter, req *http.Request) {
8084
wr.Header().Add("Content-Type", "text/html")
8185
wr.WriteHeader(200)
8286
io.WriteString(wr, websocketHTML) // nolint:errcheck
87+
} else if req.URL.Path == "/.sse" {
88+
serveSSE(wr, req)
8389
} else {
8490
serveHTTP(wr, req)
8591
}
@@ -143,15 +149,109 @@ func serveHTTP(wr http.ResponseWriter, req *http.Request) {
143149
fmt.Fprintf(wr, "Server hostname unknown: %s\n\n", err.Error())
144150
}
145151

146-
fmt.Fprintf(wr, "%s %s %s\n", req.Proto, req.Method, req.URL)
147-
fmt.Fprintln(wr, "")
148-
fmt.Fprintf(wr, "Host: %s\n", req.Host)
152+
writeRequest(wr, req)
153+
}
154+
155+
func serveSSE(wr http.ResponseWriter, req *http.Request) {
156+
if _, ok := wr.(http.Flusher); !ok {
157+
http.Error(wr, "Streaming unsupported!", http.StatusInternalServerError)
158+
return
159+
}
160+
161+
var echo strings.Builder
162+
writeRequest(&echo, req)
163+
164+
wr.Header().Set("Content-Type", "text/event-stream")
165+
wr.Header().Set("Cache-Control", "no-cache")
166+
wr.Header().Set("Connection", "keep-alive")
167+
wr.Header().Set("Access-Control-Allow-Origin", "*")
168+
169+
var id int
170+
171+
// Write an event about the server that is serving this request.
172+
if host, err := os.Hostname(); err == nil {
173+
writeSSE(
174+
wr,
175+
req,
176+
&id,
177+
"server",
178+
host,
179+
)
180+
}
181+
182+
// Write an event that echoes back the request.
183+
writeSSE(
184+
wr,
185+
req,
186+
&id,
187+
"request",
188+
echo.String(),
189+
)
190+
191+
// Then send a counter event every second.
192+
ticker := time.NewTicker(1 * time.Second)
193+
defer ticker.Stop()
194+
195+
for {
196+
select {
197+
case <-req.Context().Done():
198+
return
199+
case t := <-ticker.C:
200+
writeSSE(
201+
wr,
202+
req,
203+
&id,
204+
"time",
205+
t.Format(time.RFC3339),
206+
)
207+
}
208+
}
209+
}
210+
211+
// writeSSE sends a server-sent event and logs it to the console.
212+
func writeSSE(
213+
wr http.ResponseWriter,
214+
req *http.Request,
215+
id *int,
216+
event, data string,
217+
) {
218+
*id++
219+
writeSSEField(wr, req, "event", event)
220+
writeSSEField(wr, req, "data", data)
221+
writeSSEField(wr, req, "id", strconv.Itoa(*id))
222+
fmt.Fprintf(wr, "\n")
223+
wr.(http.Flusher).Flush()
224+
}
225+
226+
// writeSSEField sends a single field within an event.
227+
func writeSSEField(
228+
wr http.ResponseWriter,
229+
req *http.Request,
230+
k, v string,
231+
) {
232+
for _, line := range strings.Split(v, "\n") {
233+
fmt.Fprintf(wr, "%s: %s\n", k, line)
234+
fmt.Printf("%s | sse | %s: %s\n", req.RemoteAddr, k, line)
235+
}
236+
}
237+
238+
// writeRequest writes request headers to w.
239+
func writeRequest(w io.Writer, req *http.Request) {
240+
fmt.Fprintf(w, "%s %s %s\n", req.Proto, req.Method, req.URL)
241+
fmt.Fprintln(w, "")
242+
243+
fmt.Fprintf(w, "Host: %s\n", req.Host)
149244
for key, values := range req.Header {
150245
for _, value := range values {
151-
fmt.Fprintf(wr, "%s: %s\n", key, value)
246+
fmt.Fprintf(w, "%s: %s\n", key, value)
152247
}
153248
}
154249

155-
fmt.Fprintln(wr, "")
156-
io.Copy(wr, req.Body) // nolint:errcheck
250+
var body bytes.Buffer
251+
io.Copy(&body, req.Body) // nolint:errcheck
252+
253+
if body.Len() > 0 {
254+
fmt.Fprintln(w, "")
255+
body.WriteTo(w) // nolint:errcheck
256+
}
157257
}

0 commit comments

Comments
 (0)