first commit

This commit is contained in:
2026-08-24 13:06:29 +05:30
commit 7d12ebef3d
86 changed files with 39996 additions and 0 deletions

View File

@@ -0,0 +1,104 @@
package httpserver
import (
"net/http"
"strconv"
"strings"
)
// Cross-origin access, for local development.
//
// In Phase 2D the frontend fetches this API directly from the Vite dev server,
// which is a different origin (http://localhost:5173 → http://127.0.0.1:8080).
// Without these headers the browser makes the request and then refuses to let
// the page read the response, which surfaces in the app as an opaque "Failed to
// fetch" with a perfectly healthy 200 in the server log.
//
// This is a transport concern only. No endpoint, request shape, response shape
// or status code in docs/api-contract.md changes because of it.
// corsMaxAge is how long a browser may cache a preflight result. Ten minutes
// keeps preflight off the hot path without making an allowlist change take an
// awkwardly long time to be noticed in development.
const corsMaxAge = 600
// allowedCORSMethods is every method the router actually registers, plus
// OPTIONS for the preflight itself. It is a fixed list rather than something
// derived per path: the browser asks about one method at a time and only needs
// to know it is permitted in general.
var allowedCORSMethods = []string{
http.MethodGet, http.MethodPost, http.MethodPatch,
http.MethodDelete, http.MethodOptions,
}
// cors answers preflights and marks cross-origin responses as readable.
//
// Origins are matched exactly against the allowlist and echoed back one at a
// time — never "*" — so adding credentials later does not require rewriting
// this. A request whose Origin is not on the list is served normally, with no
// CORS headers: the API does not refuse it, the browser simply will not hand
// the response to the page. That distinction matters, because curl, the health
// checker and any server-to-server caller send no Origin at all and must not be
// affected by this middleware.
//
// With an empty allowlist the middleware is not installed at all (see New), so
// the same-origin deployment pays nothing for it.
func cors(origins []string) func(http.Handler) http.Handler {
allowed := make(map[string]bool, len(origins))
for _, o := range origins {
allowed[o] = true
}
methods := strings.Join(allowedCORSMethods, ", ")
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
origin := r.Header.Get("Origin")
// Vary on Origin whether or not this particular origin matched: the
// response differs by Origin, so a cache that ignored it could hand
// one origin's headers to another.
w.Header().Add("Vary", "Origin")
if origin == "" || !allowed[origin] {
if isPreflight(r) {
// A preflight is never a real request. Answering it with
// the router's 404 for "OPTIONS /api/v1/…" would be
// misleading; 403 says plainly that the origin was refused.
w.WriteHeader(http.StatusForbidden)
return
}
next.ServeHTTP(w, r)
return
}
w.Header().Set("Access-Control-Allow-Origin", origin)
if isPreflight(r) {
w.Header().Add("Vary", "Access-Control-Request-Method")
w.Header().Add("Vary", "Access-Control-Request-Headers")
w.Header().Set("Access-Control-Allow-Methods", methods)
// Echo the requested headers rather than listing them. The
// frontend sends only Content-Type today; echoing means a
// future header does not need a change here to be allowed from
// an origin that is already trusted.
if h := r.Header.Get("Access-Control-Request-Headers"); h != "" {
w.Header().Set("Access-Control-Allow-Headers", h)
} else {
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
}
w.Header().Set("Access-Control-Max-Age", strconv.Itoa(corsMaxAge))
w.WriteHeader(http.StatusNoContent)
return
}
next.ServeHTTP(w, r)
})
}
}
// isPreflight identifies the browser's OPTIONS probe. A bare OPTIONS with no
// Access-Control-Request-Method is not a preflight and is left to the router.
func isPreflight(r *http.Request) bool {
return r.Method == http.MethodOptions &&
r.Header.Get("Access-Control-Request-Method") != ""
}