package httpclient import ( "net/http" ) type corsConfig struct { origin string // "" = mirror request Origin; "*" = wildcard (no credentials) methods string headers string exposeHeaders string } // CORSOption customises the CORS middleware. type CORSOption func(*corsConfig) // WithOrigin sets the Access-Control-Allow-Origin value. Use "*" for public // APIs that don't need credentials. The default (empty string) mirrors the // request's Origin header and enables credentials. func WithOrigin(origin string) CORSOption { return func(c *corsConfig) { c.origin = origin } } // WithMethods sets Access-Control-Allow-Methods. Default: "GET, POST, PUT, OPTIONS". func WithMethods(methods string) CORSOption { return func(c *corsConfig) { c.methods = methods } } // WithHeaders sets Access-Control-Allow-Headers. Default: "*". func WithHeaders(headers string) CORSOption { return func(c *corsConfig) { c.headers = headers } } // WithExposeHeaders sets Access-Control-Expose-Headers. Default: empty. func WithExposeHeaders(headers string) CORSOption { return func(c *corsConfig) { c.exposeHeaders = headers } } // CORS returns middleware that sets CORS response headers and handles // preflight (OPTIONS) requests. By default it mirrors the request Origin, // enables credentials, and allows GET/POST/PUT/OPTIONS with any request // headers. // // Use WithOrigin("*") for public APIs that don't need credentials (per the // CORS spec, credentials and wildcard origin are mutually exclusive). // // Common patterns: // // // Public API (wildcard origin, no credentials): // mux := http.NewServeMux() // handler := httpclient.CORS( // httpclient.WithOrigin("*"), // httpclient.WithMethods("GET, POST, OPTIONS"), // )(mux) // // // Authenticated API (origin reflection, credentials): // handler := httpclient.CORS()(mux) func CORS(opts ...CORSOption) func(http.Handler) http.Handler { cfg := corsConfig{ methods: "GET, POST, PUT, OPTIONS", headers: "*", } for _, o := range opts { o(&cfg) } return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { setCORSHeaders(w, r, cfg) if r.Method == http.MethodOptions { w.Header().Set("Access-Control-Max-Age", "86400") w.WriteHeader(http.StatusNoContent) return } next.ServeHTTP(w, r) }) } } func setCORSHeaders(w http.ResponseWriter, r *http.Request, cfg corsConfig) { origin := cfg.origin if origin == "" { origin = r.Header.Get("Origin") } if origin == "" { return } w.Header().Set("Access-Control-Allow-Origin", origin) // Per spec, credentials can't be used with wildcard origin. if origin != "*" { w.Header().Set("Access-Control-Allow-Credentials", "true") } if cfg.exposeHeaders != "" { w.Header().Set("Access-Control-Expose-Headers", cfg.exposeHeaders) } if cfg.methods != "" { w.Header().Set("Access-Control-Allow-Methods", cfg.methods) } if cfg.headers != "" { w.Header().Set("Access-Control-Allow-Headers", cfg.headers) } }