package grpcapi import ( "net/http" ) // withCORS wraps next so that CORS preflight (OPTIONS) requests with an // allow-listed Origin receive 204 plus the `Access-Control-Allow-*` // headers Connect-Web needs, and actual requests get the matching // `Access-Control-Allow-Origin` header echoed back. Origins are // compared exactly: scheme, host, and port must match. An empty // allow-list passes through untouched — the production posture when // the UI and the gateway share one hostname. // // The wrapper mirrors `restapi.withCORS` but speaks plain `net/http` // because the Connect handler is mounted on a `http.ServeMux`, not a // gin engine. Connect-Web POSTs use `Content-Type: application/connect+json` // which triggers a browser preflight; without these headers the // browser surfaces "Load failed" before the Connect handler even sees // the request. func withCORS(allowedOrigins []string, next http.Handler) http.Handler { allowed := make(map[string]struct{}, len(allowedOrigins)) for _, origin := range allowedOrigins { allowed[origin] = struct{}{} } if len(allowed) == 0 { return next } return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { origin := r.Header.Get("Origin") if origin == "" { next.ServeHTTP(w, r) return } if _, ok := allowed[origin]; !ok { next.ServeHTTP(w, r) return } w.Header().Set("Access-Control-Allow-Origin", origin) w.Header().Add("Vary", "Origin") w.Header().Set("Access-Control-Allow-Credentials", "true") if r.Method == http.MethodOptions { w.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS") if reqHeaders := r.Header.Get("Access-Control-Request-Headers"); reqHeaders != "" { w.Header().Set("Access-Control-Allow-Headers", reqHeaders) } else { // Defaults cover the Connect-Web preflight set: protocol // version, content type, timeout, and the signed-request // metadata the gateway interceptor expects. w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Connect-Protocol-Version, Connect-Timeout-Ms, Authorization") } // Expose the response headers Connect-Web needs to read on // the client (e.g. trailers folded into headers for unary). w.Header().Set("Access-Control-Expose-Headers", "Connect-Protocol-Version, Grpc-Status, Grpc-Message") w.Header().Set("Access-Control-Max-Age", "3600") w.WriteHeader(http.StatusNoContent) return } // Expose the same response headers on the actual call. w.Header().Set("Access-Control-Expose-Headers", "Connect-Protocol-Version, Grpc-Status, Grpc-Message") next.ServeHTTP(w, r) }) }