Something went wrong. Try again.
This repository has no description
Something went wrong. Try again.
3.1 kB · 88 lines
Go
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889package api
import ( "context" _ "embed" "html" "net" "net/http" "net/url" "strings" "time"
"github.com/julienschmidt/httprouter" "stream.place/streamplace/pkg/log")
//go:embed dev-frontend-proxy.htmlvar devFrontendProxyHTML string
// devFrontendProxyStatusPath is handled locally by the node and never proxied,// so the loading page has something to poll while the dev server is down.const devFrontendProxyStatusPath = "/__dev_frontend_proxy/status"
// upstreamPortUp reports whether the dev server's TCP port accepts connections.// That is exactly the condition whose failure makes httputil.ReverseProxy answer// with a bare 502, so it is what the loading page polls for.func upstreamPortUp(u *url.URL) bool { port := u.Port() if port == "" { if u.Scheme == "https" { port = "443" } else { port = "80" } } conn, err := net.DialTimeout("tcp", net.JoinHostPort(u.Hostname(), port), 500*time.Millisecond) if err != nil { return false } _ = conn.Close() return true}
// HandleDevFrontendProxyStatus answers 200 once the proxied dev server is// accepting connections and 503 until then.func (a *StreamplaceAPI) HandleDevFrontendProxyStatus(ctx context.Context, u *url.URL) httprouter.Handle { return func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { w.Header().Set("Cache-Control", "no-store") if upstreamPortUp(u) { w.WriteHeader(http.StatusOK) return } w.WriteHeader(http.StatusServiceUnavailable) }}
// renderDevFrontendProxyPage bakes the proxy destination into the loading page// once at startup.func renderDevFrontendProxyPage(upstream string) []byte { return []byte(strings.Replace(devFrontendProxyHTML, "UPSTREAM_REPLACE_ME", html.EscapeString(upstream), 1))}
// isDocumentNavigation reports whether a request is a browser page load that// should get the loading page. Assets, fetches, and XRPC calls keep a plain// error so tooling fails loudly instead of trying to parse HTML.func isDocumentNavigation(r *http.Request) bool { return r.Method == http.MethodGet && strings.Contains(r.Header.Get("Accept"), "text/html")}
// devFrontendProxyErrorHandler replaces the reverse proxy's default 502 with a// page that polls until the dev server is up and then reloads. Without it, a// slow `pnpm app start` leaves the browser on a flat error page until the// developer manually retries.func devFrontendProxyErrorHandler(ctx context.Context, page []byte) func(http.ResponseWriter, *http.Request, error) { return func(w http.ResponseWriter, r *http.Request, err error) { if !isDocumentNavigation(r) { http.Error(w, "frontend dev server unavailable", http.StatusServiceUnavailable) return } log.Debug(ctx, "frontend dev server unreachable, serving loading page", "path", r.URL.Path, "error", err) w.Header().Set("Content-Type", "text/html; charset=utf-8") w.Header().Set("Cache-Control", "no-store") w.WriteHeader(http.StatusServiceUnavailable) if _, werr := w.Write(page); werr != nil { log.Error(ctx, "error writing dev frontend proxy loading page", "error", werr) } }}