@@ -11,6 +11,7 @@ import (
1111 "net/http"
1212 "os"
1313 "path/filepath"
14+ "strings"
1415
1516 "go.viam.com/rdk/components/generic"
1617 "go.viam.com/rdk/logging"
@@ -68,6 +69,51 @@ func resolveCacheRoot(configured string) string {
6869 return filepath .Join (base , "viam-chartplotter" )
6970}
7071
72+ // withCookiePathRoot wraps an http.Handler so any Set-Cookie headers
73+ // it writes that don't already specify a Path get `Path=/` appended.
74+ // Required because vmodutils's cookie middleware doesn't set Path
75+ // and Go's default-Path-from-request-URL behaviour fans out the same
76+ // cookie into a copy per tile path.
77+ func withCookiePathRoot (next http.Handler ) http.Handler {
78+ return http .HandlerFunc (func (w http.ResponseWriter , r * http.Request ) {
79+ next .ServeHTTP (& cookiePathRootWriter {ResponseWriter : w }, r )
80+ })
81+ }
82+
83+ type cookiePathRootWriter struct {
84+ http.ResponseWriter
85+ wroteHeader bool
86+ }
87+
88+ func (w * cookiePathRootWriter ) fixCookies () {
89+ cookies := w .Header ().Values ("Set-Cookie" )
90+ if len (cookies ) == 0 {
91+ return
92+ }
93+ w .Header ().Del ("Set-Cookie" )
94+ for _ , c := range cookies {
95+ if ! strings .Contains (strings .ToLower (c ), "path=" ) {
96+ c = c + "; Path=/"
97+ }
98+ w .Header ().Add ("Set-Cookie" , c )
99+ }
100+ }
101+
102+ func (w * cookiePathRootWriter ) WriteHeader (code int ) {
103+ if ! w .wroteHeader {
104+ w .wroteHeader = true
105+ w .fixCookies ()
106+ }
107+ w .ResponseWriter .WriteHeader (code )
108+ }
109+
110+ func (w * cookiePathRootWriter ) Write (b []byte ) (int , error ) {
111+ if ! w .wroteHeader {
112+ w .WriteHeader (http .StatusOK )
113+ }
114+ return w .ResponseWriter .Write (b )
115+ }
116+
71117// StartChartplotterServer wires the static frontend, the NOAA WMS caching proxy, and
72118// the ENC catalog/store handlers, and starts an HTTP server on the given port.
73119// draftFt is the boat's draft in feet — drives the depth-shading bands at
@@ -87,6 +133,15 @@ func StartChartplotterServer(
87133 if err != nil {
88134 return nil , err
89135 }
136+ // vmodutils.PrepInModuleServer installs a cookie middleware that
137+ // calls http.SetCookie(w, &http.Cookie{Name, Value}) without
138+ // setting Path — Go then fills in the request URL's directory as
139+ // the default Path. That means every tile URL gets its own copy of
140+ // `api-key` / `api-key-id` / `host` cookies, fanning out into
141+ // hundreds of duplicates per session. Wrap the server handler so
142+ // any outgoing Set-Cookie gets a global Path=/ if it doesn't
143+ // already specify one.
144+ server .Handler = withCookiePathRoot (server .Handler )
90145
91146 root := resolveCacheRoot (cacheRoot )
92147
@@ -128,6 +183,18 @@ func StartChartplotterServer(
128183 encHandlers .Register (mux )
129184 logger .Infof ("noaa enc store: %s (default draft=%.1f ft)" , encDir , draftFt )
130185
186+ // NOAA GFS weather cache. Serves /noaa-weather/gfs/latest.json which
187+ // the frontend wind layer (ol-wind) consumes. Disk cache lives under
188+ // <root>/noaa-weather/.
189+ weatherDir := filepath .Join (root , "noaa-weather" )
190+ weatherCache , err := NewWeatherCache (weatherDir , logger .Sublogger ("weather" ))
191+ if err != nil {
192+ logger .Warnf ("weather cache disabled: %v" , err )
193+ } else {
194+ weatherCache .Register (mux )
195+ logger .Infof ("noaa weather cache: %s" , weatherDir )
196+ }
197+
131198 // Per-process instance ID. The frontend polls /version and reloads when it
132199 // changes, so the browser picks up a new build/restart without manual refresh.
133200 instanceID := newInstanceID ()
0 commit comments