Skip to content

Commit 303b8fc

Browse files
feat: reverse proxy handler with event stream
1 parent 1652da2 commit 303b8fc

1 file changed

Lines changed: 87 additions & 0 deletions

File tree

proxy.go

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
package main
2+
3+
import (
4+
"bufio"
5+
"errors"
6+
"fmt"
7+
"net"
8+
"net/http"
9+
"net/http/httputil"
10+
"net/url"
11+
"time"
12+
)
13+
14+
// Event is a single request going through the proxy.
15+
// It is published to a channel and consumed by whichever UI is active.
16+
type Event struct {
17+
Method string
18+
Path string
19+
Status int
20+
Duration time.Duration
21+
Time time.Time
22+
}
23+
24+
// newProxyHandler returns an http.Handler that reverse-proxies to the given target
25+
// and publishes an Event for each request to the events channel.
26+
// events may be nil (drops events on the floor).
27+
func newProxyHandler(targetPort int, events chan<- Event) http.Handler {
28+
target, _ := url.Parse(fmt.Sprintf("http://127.0.0.1:%d", targetPort))
29+
proxy := httputil.NewSingleHostReverseProxy(target)
30+
31+
// Preserve client's Host header — dev servers (Vite HMR, Next) check it.
32+
originalDirector := proxy.Director
33+
proxy.Director = func(r *http.Request) {
34+
host := r.Host
35+
originalDirector(r)
36+
r.Host = host
37+
}
38+
39+
proxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) {
40+
w.WriteHeader(http.StatusBadGateway)
41+
fmt.Fprintf(w, "upstream unreachable at localhost:%d\n", targetPort)
42+
}
43+
44+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
45+
start := time.Now()
46+
sw := &statusWriter{ResponseWriter: w, status: http.StatusOK}
47+
proxy.ServeHTTP(sw, r)
48+
if events != nil {
49+
// Non-blocking send — never let a slow UI stall the proxy.
50+
select {
51+
case events <- Event{
52+
Method: r.Method,
53+
Path: r.URL.Path,
54+
Status: sw.status,
55+
Duration: time.Since(start),
56+
Time: start,
57+
}:
58+
default:
59+
}
60+
}
61+
})
62+
}
63+
64+
// statusWriter captures the status code from an http.ResponseWriter.
65+
// Also implements http.Hijacker so WebSocket upgrades work.
66+
type statusWriter struct {
67+
http.ResponseWriter
68+
status int
69+
}
70+
71+
func (s *statusWriter) WriteHeader(code int) {
72+
s.status = code
73+
s.ResponseWriter.WriteHeader(code)
74+
}
75+
76+
func (s *statusWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
77+
if h, ok := s.ResponseWriter.(http.Hijacker); ok {
78+
return h.Hijack()
79+
}
80+
return nil, nil, errors.New("hijacking not supported")
81+
}
82+
83+
func (s *statusWriter) Flush() {
84+
if f, ok := s.ResponseWriter.(http.Flusher); ok {
85+
f.Flush()
86+
}
87+
}

0 commit comments

Comments
 (0)