@@ -28,6 +28,7 @@ import (
2828 "github.com/prometheus/client_golang/prometheus"
2929 "github.com/prometheus/client_golang/prometheus/promhttp"
3030 tphttp "willnorris.com/go/imageproxy/third_party/http"
31+ tphc "willnorris.com/go/imageproxy/third_party/httpcache"
3132)
3233
3334// Maximum number of redirection-followings allowed.
@@ -91,6 +92,10 @@ type Proxy struct {
9192 // PassRequestHeaders identifies HTTP headers to pass from inbound
9293 // requests to the proxied server.
9394 PassRequestHeaders []string
95+
96+ // MinimumCacheDuration is the minimum duration to cache remote images.
97+ // This will override cache-control instructions from the remote server.
98+ MinimumCacheDuration time.Duration
9499}
95100
96101// NewProxy constructs a new proxy. The provided http RoundTripper will be
@@ -118,6 +123,7 @@ func NewProxy(transport http.RoundTripper, cache Cache) *Proxy {
118123 proxy .logf (format , v ... )
119124 }
120125 },
126+ updateCacheHeaders : proxy .updateCacheHeaders ,
121127 },
122128 Cache : cache ,
123129 MarkCachedResponses : true ,
@@ -128,6 +134,39 @@ func NewProxy(transport http.RoundTripper, cache Cache) *Proxy {
128134 return proxy
129135}
130136
137+ // updateCacheHeaders updates the cache-control headers in the provided headers.
138+ // It sets the cache-control max-age value to the maximum of the minimum cache
139+ // duration, the expires header, and the max-age header. It also removes the
140+ // expires header.
141+ func (p * Proxy ) updateCacheHeaders (hdr http.Header ) {
142+ if p .MinimumCacheDuration == 0 {
143+ return
144+ }
145+ cc := tphc .ParseCacheControl (hdr )
146+
147+ var expiresDuration time.Duration
148+ var maxAgeDuration time.Duration
149+
150+ if maxAge , ok := cc ["max-age" ]; ok {
151+ maxAgeDuration , _ = time .ParseDuration (maxAge + "s" )
152+ }
153+ if date , err := httpcache .Date (hdr ); err == nil {
154+ if expiresHeader := hdr .Get ("Expires" ); expiresHeader != "" {
155+ if expires , err := time .Parse (time .RFC1123 , expiresHeader ); err == nil {
156+ expiresDuration = expires .Sub (date )
157+ }
158+ }
159+ }
160+
161+ maxAge := max (p .MinimumCacheDuration , expiresDuration , maxAgeDuration )
162+ cc ["max-age" ] = fmt .Sprintf ("%d" , int (maxAge .Seconds ()))
163+ delete (cc , "no-cache" )
164+ delete (cc , "no-store" )
165+
166+ hdr .Set ("Cache-Control" , cc .String ())
167+ hdr .Del ("Expires" )
168+ }
169+
131170// ServeHTTP handles incoming requests.
132171func (p * Proxy ) ServeHTTP (w http.ResponseWriter , r * http.Request ) {
133172 if r .URL .Path == "/favicon.ico" {
@@ -475,6 +514,8 @@ type TransformingTransport struct {
475514 CachingClient * http.Client
476515
477516 log func (format string , v ... any )
517+
518+ updateCacheHeaders func (hdr http.Header )
478519}
479520
480521// RoundTrip implements the http.RoundTripper interface.
@@ -484,7 +525,11 @@ func (t *TransformingTransport) RoundTrip(req *http.Request) (*http.Response, er
484525 if t .log != nil {
485526 t .log ("fetching remote URL: %v" , req .URL )
486527 }
487- return t .Transport .RoundTrip (req )
528+ resp , err := t .Transport .RoundTrip (req )
529+ if err == nil && t .updateCacheHeaders != nil {
530+ t .updateCacheHeaders (resp .Header )
531+ }
532+ return resp , err
488533 }
489534
490535 f := req .URL .Fragment
0 commit comments