@@ -10,6 +10,7 @@ import (
1010 "io"
1111 "log/slog"
1212 "net/http"
13+ "strconv"
1314 "strings"
1415 "time"
1516
@@ -394,14 +395,14 @@ func (p *Proxy) FetchOrCacheMetadata(ctx context.Context, ecosystem, cacheKey, u
394395 }
395396
396397 // Try upstream
397- body , contentType , etag , err := p .fetchUpstreamMetadata (ctx , upstreamURL , entry , accept )
398+ body , contentType , etag , lastModified , err := p .fetchUpstreamMetadata (ctx , upstreamURL , entry , accept )
398399 if errors .Is (err , errStale304 ) {
399400 // 304 but cached file is gone; retry without ETag
400- body , contentType , etag , err = p .fetchUpstreamMetadata (ctx , upstreamURL , nil , accept )
401+ body , contentType , etag , lastModified , err = p .fetchUpstreamMetadata (ctx , upstreamURL , nil , accept )
401402 }
402403 if err == nil {
403404 if p .CacheMetadata {
404- p .cacheMetadataBlob (ctx , ecosystem , cacheKey , storagePath , body , contentType , etag )
405+ p .cacheMetadataBlob (ctx , ecosystem , cacheKey , storagePath , body , contentType , etag , lastModified )
405406 }
406407 return body , contentType , nil
407408 }
@@ -435,11 +436,13 @@ func (p *Proxy) FetchOrCacheMetadata(ctx context.Context, ecosystem, cacheKey, u
435436}
436437
437438// fetchUpstreamMetadata fetches metadata from upstream, using ETag for conditional revalidation.
438- // Returns the body, content type, ETag, and any error.
439- func (p * Proxy ) fetchUpstreamMetadata (ctx context.Context , upstreamURL string , entry * database.MetadataCacheEntry , accept string ) ([]byte , string , string , error ) {
439+ // Returns the body, content type, ETag, upstream Last-Modified time, and any error.
440+ func (p * Proxy ) fetchUpstreamMetadata (ctx context.Context , upstreamURL string , entry * database.MetadataCacheEntry , accept string ) ([]byte , string , string , time.Time , error ) {
441+ var zeroTime time.Time
442+
440443 req , err := http .NewRequestWithContext (ctx , http .MethodGet , upstreamURL , nil )
441444 if err != nil {
442- return nil , "" , "" , fmt .Errorf ("creating request: %w" , err )
445+ return nil , "" , "" , zeroTime , fmt .Errorf ("creating request: %w" , err )
443446 }
444447 req .Header .Set ("Accept" , accept )
445448
@@ -449,38 +452,42 @@ func (p *Proxy) fetchUpstreamMetadata(ctx context.Context, upstreamURL string, e
449452
450453 resp , err := p .HTTPClient .Do (req )
451454 if err != nil {
452- return nil , "" , "" , fmt .Errorf ("fetching metadata: %w" , err )
455+ return nil , "" , "" , zeroTime , fmt .Errorf ("fetching metadata: %w" , err )
453456 }
454457 defer func () { _ = resp .Body .Close () }()
455458
456459 // 304 Not Modified -- our cached copy is still good
457460 if resp .StatusCode == http .StatusNotModified && entry != nil {
458461 cached , readErr := p .Storage .Open (ctx , entry .StoragePath )
459462 if readErr != nil {
460- return nil , "" , "" , errStale304
463+ return nil , "" , "" , zeroTime , errStale304
461464 }
462465 defer func () { _ = cached .Close () }()
463466 data , readErr := ReadMetadata (cached )
464467 if readErr != nil {
465- return nil , "" , "" , errStale304
468+ return nil , "" , "" , zeroTime , errStale304
466469 }
467470 ct := contentTypeJSON
468471 if entry .ContentType .Valid {
469472 ct = entry .ContentType .String
470473 }
471- return data , ct , entry .ETag .String , nil
474+ lm := zeroTime
475+ if entry .LastModified .Valid {
476+ lm = entry .LastModified .Time
477+ }
478+ return data , ct , entry .ETag .String , lm , nil
472479 }
473480
474481 if resp .StatusCode == http .StatusNotFound {
475- return nil , "" , "" , ErrUpstreamNotFound
482+ return nil , "" , "" , zeroTime , ErrUpstreamNotFound
476483 }
477484 if resp .StatusCode != http .StatusOK {
478- return nil , "" , "" , fmt .Errorf ("upstream returned %d" , resp .StatusCode )
485+ return nil , "" , "" , zeroTime , fmt .Errorf ("upstream returned %d" , resp .StatusCode )
479486 }
480487
481488 body , err := ReadMetadata (resp .Body )
482489 if err != nil {
483- return nil , "" , "" , fmt .Errorf ("reading response: %w" , err )
490+ return nil , "" , "" , zeroTime , fmt .Errorf ("reading response: %w" , err )
484491 }
485492
486493 contentType := resp .Header .Get ("Content-Type" )
@@ -489,11 +496,17 @@ func (p *Proxy) fetchUpstreamMetadata(ctx context.Context, upstreamURL string, e
489496 }
490497
491498 etag := resp .Header .Get ("ETag" )
492- return body , contentType , etag , nil
499+
500+ var lastModified time.Time
501+ if lm := resp .Header .Get ("Last-Modified" ); lm != "" {
502+ lastModified , _ = http .ParseTime (lm )
503+ }
504+
505+ return body , contentType , etag , lastModified , nil
493506}
494507
495508// cacheMetadataBlob stores metadata bytes in storage and updates the database.
496- func (p * Proxy ) cacheMetadataBlob (ctx context.Context , ecosystem , cacheKey , storagePath string , data []byte , contentType , etag string ) {
509+ func (p * Proxy ) cacheMetadataBlob (ctx context.Context , ecosystem , cacheKey , storagePath string , data []byte , contentType , etag string , lastModified time. Time ) {
497510 if p .DB == nil || p .Storage == nil {
498511 return
499512 }
@@ -505,13 +518,14 @@ func (p *Proxy) cacheMetadataBlob(ctx context.Context, ecosystem, cacheKey, stor
505518 }
506519
507520 _ = p .DB .UpsertMetadataCache (& database.MetadataCacheEntry {
508- Ecosystem : ecosystem ,
509- Name : cacheKey ,
510- StoragePath : storagePath ,
511- ETag : sql.NullString {String : etag , Valid : etag != "" },
512- ContentType : sql.NullString {String : contentType , Valid : contentType != "" },
513- Size : sql.NullInt64 {Int64 : size , Valid : true },
514- FetchedAt : sql.NullTime {Time : time .Now (), Valid : true },
521+ Ecosystem : ecosystem ,
522+ Name : cacheKey ,
523+ StoragePath : storagePath ,
524+ ETag : sql.NullString {String : etag , Valid : etag != "" },
525+ ContentType : sql.NullString {String : contentType , Valid : contentType != "" },
526+ Size : sql.NullInt64 {Int64 : size , Valid : true },
527+ LastModified : sql.NullTime {Time : lastModified , Valid : ! lastModified .IsZero ()},
528+ FetchedAt : sql.NullTime {Time : time .Now (), Valid : true },
515529 })
516530}
517531
@@ -537,7 +551,44 @@ func (p *Proxy) ProxyCached(w http.ResponseWriter, r *http.Request, upstreamURL,
537551 return
538552 }
539553
554+ // Look up cache entry to get ETag and upstream Last-Modified for conditional response headers
555+ var etag string
556+ var lastModified time.Time
557+ if p .DB != nil {
558+ if entry , err := p .DB .GetMetadataCache (ecosystem , cacheKey ); err == nil && entry != nil {
559+ if entry .ETag .Valid {
560+ etag = entry .ETag .String
561+ }
562+ if entry .LastModified .Valid {
563+ lastModified = entry .LastModified .Time
564+ }
565+ }
566+ }
567+
568+ // Honor client conditional request headers
569+ if etag != "" {
570+ if match := r .Header .Get ("If-None-Match" ); match != "" && match == etag {
571+ w .WriteHeader (http .StatusNotModified )
572+ return
573+ }
574+ }
575+ if ! lastModified .IsZero () {
576+ if ims := r .Header .Get ("If-Modified-Since" ); ims != "" {
577+ if t , err := http .ParseTime (ims ); err == nil && ! lastModified .After (t ) {
578+ w .WriteHeader (http .StatusNotModified )
579+ return
580+ }
581+ }
582+ }
583+
540584 w .Header ().Set ("Content-Type" , contentType )
585+ w .Header ().Set ("Content-Length" , strconv .Itoa (len (body )))
586+ if etag != "" {
587+ w .Header ().Set ("ETag" , etag )
588+ }
589+ if ! lastModified .IsZero () {
590+ w .Header ().Set ("Last-Modified" , lastModified .UTC ().Format (http .TimeFormat ))
591+ }
541592 w .WriteHeader (http .StatusOK )
542593 _ , _ = w .Write (body )
543594}
0 commit comments