From 029e280e4ba359f55b14a664d9486ae2baf4ba71 Mon Sep 17 00:00:00 2001 From: Philemon Ukane Date: Fri, 31 Oct 2025 23:27:44 +0100 Subject: [PATCH 1/2] cmd/dcrdata: add cache headers to improve page load on select pages This PR adds the ETag, Last-Modified and Cache-Control headers on select explorer changes to improve user experience and save resources. The ETag and Last-Modified are reset when *explorerUI receives a new block or mempool data. In the future, they might be reset in other call sites where update to backend cached data are made and API endpoints or pages depending on cached data should then use the ETagAndLastModifiedIntercept middleware. Signed-off-by: Philemon Ukane --- cmd/dcrdata/internal/explorer/explorer.go | 38 +++++++++++++++++++ .../internal/explorer/explorermiddleware.go | 27 +++++++++++++ cmd/dcrdata/main.go | 25 +++++++----- 3 files changed, 81 insertions(+), 9 deletions(-) diff --git a/cmd/dcrdata/internal/explorer/explorer.go b/cmd/dcrdata/internal/explorer/explorer.go index 0a6c5c9c1..f52ad11ef 100644 --- a/cmd/dcrdata/internal/explorer/explorer.go +++ b/cmd/dcrdata/internal/explorer/explorer.go @@ -8,8 +8,10 @@ package explorer import ( "context" + "crypto/rand" "fmt" "math" + "math/big" "net/http" "os" "os/signal" @@ -201,6 +203,8 @@ type pageData struct { BlockInfo *types.BlockInfo BlockchainInfo *chainjson.GetBlockChainInfoResult HomeInfo *types.HomeInfo + eTag string + lastModified time.Time } type explorerUI struct { @@ -359,6 +363,7 @@ func New(cfg *ExplorerConfig) *explorerUI { }, }, } + exp.resetETagAndLastModified() log.Infof("Mean Voting Blocks calculated: %d", exp.pageData.HomeInfo.Params.MeanVotingBlocks) @@ -447,6 +452,9 @@ func (exp *explorerUI) StoreMPData(_ *mempool.StakeData, _ []types.MempoolTx, in exp.invsMtx.Lock() exp.invs = inv exp.invsMtx.Unlock() + + exp.resetETagAndLastModified() + log.Debugf("Updated mempool details for the explorerUI.") } @@ -616,6 +624,8 @@ func (exp *explorerUI) Store(blockData *blockdata.BlockData, msgBlock *wire.MsgB }() } + exp.resetETagAndLastModified() + return nil } @@ -629,6 +639,21 @@ func (exp *explorerUI) ChartsUpdated() { exp.pageData.Unlock() } +// resetETagAndLastModified resets the eTag and last modified time to new +// values and is protected by the exp.pageData mutex. +func (exp *explorerUI) resetETagAndLastModified() { + exp.pageData.Lock() + exp.pageData.eTag = generateRandomString() + exp.pageData.lastModified = time.Now() + exp.pageData.Unlock() +} + +func (exp *explorerUI) eTagAndLastModified() (eTag string, lastModified time.Time) { + exp.pageData.RLock() + defer exp.pageData.RUnlock() + return exp.pageData.eTag, exp.pageData.lastModified +} + func (exp *explorerUI) updateDevFundBalance() { // yield processor to other goroutines runtime.Gosched() @@ -879,3 +904,16 @@ func indexPrice(index exchanges.CurrencyPair, indices map[string]map[exchanges.C } return price / nSources } + +const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" +const nLetters = len(letters) + +// generateRandomString creates a random alphanumeric string of length 16. +func generateRandomString() string { + bytes := make([]byte, 16) + for i := range bytes { + num, _ := rand.Int(rand.Reader, big.NewInt(int64(nLetters))) + bytes[i] = letters[num.Int64()] + } + return string(bytes) +} diff --git a/cmd/dcrdata/internal/explorer/explorermiddleware.go b/cmd/dcrdata/internal/explorer/explorermiddleware.go index 1ad9e4ad1..c663ce13c 100644 --- a/cmd/dcrdata/internal/explorer/explorermiddleware.go +++ b/cmd/dcrdata/internal/explorer/explorermiddleware.go @@ -189,6 +189,33 @@ func (exp *explorerUI) SyncStatusFileIntercept(next http.Handler) http.Handler { }) } +// ETagAndLastModifiedIntercept handles ETag and Last-Modified headers for caching purposes. +func (exp *explorerUI) ETagAndLastModifiedIntercept(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + eTag, lastModified := exp.eTagAndLastModified() + if match := r.Header.Get("If-None-Match"); match != "" { + if match == eTag { + w.WriteHeader(http.StatusNotModified) + return + } + } else if modifiedSince := r.Header.Get("If-Modified-Since"); modifiedSince != "" { + if t, err := time.Parse(http.TimeFormat, modifiedSince); err == nil { + if lastModified.Before(t.Add(1 * time.Second)) { + w.WriteHeader(http.StatusNotModified) + return + } + } + } + + // Set ETag and Last-Modified headers. + w.Header().Set("ETag", eTag) + w.Header().Set("Last-Modified", lastModified.Format(http.TimeFormat)) + w.Header().Set("Cache-Control", "private") + + next.ServeHTTP(w, r) + }) +} + func getBlockHashCtx(r *http.Request) string { hash, ok := r.Context().Value(ctxBlockHash).(string) if !ok { diff --git a/cmd/dcrdata/main.go b/cmd/dcrdata/main.go index 8bcf4e469..1d3cc5f5a 100644 --- a/cmd/dcrdata/main.go +++ b/cmd/dcrdata/main.go @@ -761,23 +761,15 @@ func _main(ctx context.Context) error { r.Get("/rejects", func(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, "/disapproved", http.StatusPermanentRedirect) }) - r.Get("/disapproved", explore.DisapprovedBlocks) - r.Get("/mempool", explore.Mempool) - r.Get("/parameters", explore.ParametersPage) r.With(explore.BlockHashPathOrIndexCtx).Get("/block/{blockhash}", explore.Block) r.With(explorer.TransactionHashCtx).Get("/tx/{txid}", explore.TxPage) r.With(explorer.TransactionHashCtx, explorer.TransactionIoIndexCtx).Get("/tx/{txid}/{inout}/{inoutid}", explore.TxPage) r.With(explorer.AddressPathCtx).Get("/address/{address}", explore.AddressPage) r.With(explorer.AddressPathCtx).Get("/addresstable/{address}", explore.AddressTable) - r.Get("/treasury", explore.TreasuryPage) - r.Get("/treasurytable", explore.TreasuryTable) - r.Get("/agendas", explore.AgendasPage) - r.With(explorer.AgendaPathCtx).Get("/agenda/{agendaid}", explore.AgendaPage) r.Get("/proposals", explore.ProposalsPage) r.With(explorer.ProposalPathCtx).Get("/proposal/{proposaltoken}", explore.ProposalPage) r.Get("/decodetx", explore.DecodeTxPage) r.Get("/search", explore.Search) - r.Get("/charts", explore.Charts) r.Get("/ticketpool", explore.Ticketpool) r.Get("/market", explore.MarketPage) r.Get("/stats", func(w http.ResponseWriter, r *http.Request) { @@ -786,9 +778,24 @@ func _main(ctx context.Context) error { // MenuFormParser will typically redirect, but going to the homepage as a // fallback. r.With(explorer.MenuFormParser).Post("/set", explore.Home) - r.Get("/attack-cost", explore.AttackCost) r.Get("/verify-message", explore.VerifyMessagePage) r.With(mw.Tollbooth(limiter)).Post("/verify-message", explore.VerifyMessageHandler) + + + // Pages that can be cached because they depend on block and/or mempool data cached by + // *explorer.explorerUI. This middleware sets ETag and Last-Modified headers that are + // reset if a new block or mempool change is detected. + withCache := r.With(explore.ETagAndLastModifiedIntercept) + withCache.Get("/", explore.Home) + withCache.Get("/disapproved", explore.DisapprovedBlocks) + withCache.Get("/mempool", explore.Mempool) + withCache.Get("/charts", explore.Charts) + withCache.Get("/treasury", explore.TreasuryPage) + withCache.Get("/treasurytable", explore.TreasuryTable) + withCache.Get("/parameters", explore.ParametersPage) + withCache.Get("/agendas", explore.AgendasPage) + withCache.With(explorer.AgendaPathCtx).Get("/agenda/{agendaid}", explore.AgendaPage) + withCache.Get("/attack-cost", explore.AttackCost) }) // Configure a page for the bare "/insight" path. This mounts the static From 8e5fcb86355452e4fb0ea33c189f1c8f092ee826 Mon Sep 17 00:00:00 2001 From: Philemon Ukane Date: Fri, 31 Oct 2025 23:42:17 +0100 Subject: [PATCH 2/2] format main.go Signed-off-by: Philemon Ukane --- cmd/dcrdata/main.go | 1 - 1 file changed, 1 deletion(-) diff --git a/cmd/dcrdata/main.go b/cmd/dcrdata/main.go index 1d3cc5f5a..b7718b57b 100644 --- a/cmd/dcrdata/main.go +++ b/cmd/dcrdata/main.go @@ -781,7 +781,6 @@ func _main(ctx context.Context) error { r.Get("/verify-message", explore.VerifyMessagePage) r.With(mw.Tollbooth(limiter)).Post("/verify-message", explore.VerifyMessageHandler) - // Pages that can be cached because they depend on block and/or mempool data cached by // *explorer.explorerUI. This middleware sets ETag and Last-Modified headers that are // reset if a new block or mempool change is detected.