Skip to content

Latest commit

 

History

History
580 lines (418 loc) · 16.5 KB

File metadata and controls

580 lines (418 loc) · 16.5 KB

Cache Management in Folio

This document describes Folio's cache management system, including HTTP cache headers, fragment caching, and development tools integration.

Overview

Folio provides a caching system with:

  • HTTP Cache Headers - Smart browser and CDN caching (opt-in via config)
  • Fragment Caching - View-level caching for performance
  • Development Integration - MiniProfiler management
  • Flexible Configuration - ENV-based overrides for development

Important: Cache headers are disabled by default in Folio Engine. You must explicitly enable them in your application.

HTTP Cache Headers

Automatic Header Management

Folio after enabling automatically sets appropriate Cache-Control headers based on content type and publishing status:

# Published content - cacheable
Cache-Control: max-age=60, must-revalidate, stale-while-revalidate=15, stale-if-error=300

# Unpublished content - not cacheable
Cache-Control: no-store

# 404 error pages - same TTL as regular pages (attack prevention)
Cache-Control: max-age=60, must-revalidate

# Server errors (500+) - shorter TTL
Cache-Control: max-age=15, must-revalidate

# Admin/console paths - never cached
Cache-Control: private, no-store

# Signed-in users - private cache
Cache-Control: private, max-age=60

Configuration

Cache headers are configured in config/initializers/cache_headers.rb with ENV variable support:

# Core settings
config.folio_cache_headers_enabled = true             # MASTER SWITCH - enables entire system
config.folio_cache_headers_default_ttl = 60           # Default TTL in seconds

# Header inclusion  
config.folio_cache_headers_include_etag = true        # Include ETag headers
config.folio_cache_headers_include_last_modified = true # Include Last-Modified headers  
config.folio_cache_headers_include_cache_tags = false # Include cache tags

# Advanced settings
config.folio_cache_headers_stale_while_revalidate = 15  # Stale-while-revalidate seconds
config.folio_cache_headers_stale_if_error = 300        # Stale-if-error seconds

All settings can be overridden via ENV variables (see Development Environment Controls below).

Enabling Cache Headers in Your Application

Cache headers are disabled by default in Folio Engine. To enable them in your application:

Option 1: Create Initializer (Recommended)

Generate the cache headers initializer:

rails generate folio:cache_headers

This creates config/initializers/cache_headers.rb with cache headers enabled.

Option 2: Manual Configuration

Add to your config/initializers/cache_headers.rb:

# Enable cache headers system
Rails.application.config.folio_cache_headers_enabled = true

# Configure other settings as needed
Rails.application.config.folio_cache_headers_default_ttl = 60

Option 3: Environment Variable

For testing or temporary enabling:

FOLIO_CACHE_HEADERS_ENABLED=true rails server

Controller Usage

In your controllers, use set_cache_control_headers:

class ArticlesController < ApplicationController
  def show
    @article = Article.find(params[:id])
    set_cache_control_headers(record: @article)
  end
end

Custom Cache Headers in Controllers

If you need to set custom cache headers that differ from the automatic system, you can call the cache header methods directly in your action. The automatic after_action callback will detect that headers are already set and skip:

class Api::ThumbnailsController < Api::BaseController
  def show
    cache_ttl = calculate_custom_ttl
    
    # Set public cache headers early in the action
    set_public_cache_headers(cache_ttl)
    
    # ... rest of action logic ...
  end
end

How it works:

  • Setting headers early in the action ensures they're in place before the after_action callback runs
  • The automatic cache headers system checks if Cache-Control is already set (line 30 in Folio::HttpCache::Headers)
  • If headers are present, it skips and logs: "cache_control_already_set"
  • This prevents the automatic system from adding private for signed-in users or other automatic behavior

Available methods:

  • set_public_cache_headers(ttl) - Sets public cache headers with Vary headers
  • set_private_cache_headers_with_ttl(ttl) - Sets private cache headers with TTL
  • set_private_cache_headers - Sets no-cache headers

Fragment Caching

Folio Cache Patterns

Folio provides specific patterns for action and view caching:

Action Caching

# In controllers - cache entire action logic
folio_run_unless_cached(["blog/articles#show", params[:id]] + cache_key_base) do
  @article = Article.find(params[:id])
  set_meta_variables(@article)
end

View Caching with Base Key

/ In views - cache with computed base key
- cache @cache_key
  .content
    = render @article

Base Cache Key: Controllers include Folio::CacheMethods and implement cache_key_base returning an array with user state, site context, and other cache-busting factors:

def cache_key_base
  [request.host, user_signed_in? ? "logged_in" : "logged_out", I18n.locale]
end

Development Fragment Caching

Enable fragment caching in development:

# Enable caching (creates tmp/caching-dev.txt)
rails dev:cache

# Disable caching (removes tmp/caching-dev.txt)  
rails dev:cache

Skip caching for debugging:

http://localhost:3000/articles/123?skip_global_cache=1

Component Session Requirements

When cache optimization is active (FOLIO_CACHE_SKIP_SESSION=true), components that need session state (forms, interactive elements) can declare their requirements using the polymorfic ComponentSessionHelper concern. This automatically disables session skipping when needed.

The system uses polymorphic override pattern - components that need session include a concern and declare their requirements, which are automatically detected by the cache headers system.

Form Components Implementation

Components that need session state include the helper concern and override the session_requirement_reason method:

class MyFormComponent < ApplicationComponent
  include Folio::ComponentSessionHelper

  def session_requirement_reason
    "contact_form_csrf"
  end
end

Automatic Detection

The HTTP cache headers system automatically detects components with session requirements:

# In Folio::HttpCache::Headers
def should_skip_session_for_cache?
  # Global configuration check first
  unless Rails.application.config.folio_cache_skip_session_for_public
    return false
  end

  # Automatic detection of ComponentSessionRequirements concern
  # No manual class name checking needed!
  super if defined?(super)
end

Component Session Requirements Concern

Controllers automatically get the concern via ApplicationControllerBase:

# Automatically included in all controllers
include Folio::ComponentSessionRequirements

# Provides polymorphic override
def should_skip_session_for_cache?
  # Auto-analyze @page if exists
  if defined?(@page) && @page&.respond_to?(:atoms)
    analyze_page_session_requirements(@page)
  end
  
  # Check if any component requires session
  return false if component_requires_session?

  # Delegate to parent (Headers concern)
  super if defined?(super)
end

How It Works

  1. Components declare their session needs via session_requirement_reason method
  2. Headers concern automatically analyzes page atoms for session requirements
  3. Polymorphic override in ComponentSessionRequirements prevents session skipping if needed
  4. Cache optimization is disabled automatically when forms are present
  5. Result: Forms work correctly on cached pages with clean architecture

See Component Session Requirements Documentation for detailed implementation guide.

Development Tools Integration

Smart MiniProfiler Management

Folio automatically manages MiniProfiler to prevent interference with cache testing:

  • Cache ENABLED → MiniProfiler AUTO-DISABLED
  • Cache DISABLED → MiniProfiler AUTO-ENABLED

This prevents MiniProfiler from overriding cache headers during development testing.

Manual Overrides

Force MiniProfiler behavior via environment variables:

# Force enable MiniProfiler (may interfere with cache testing)
MINI_PROFILER_FORCE_ENABLED=true rails server

# Force disable MiniProfiler
MINI_PROFILER_ENABLED=false rails server

Development Environment Controls

Cache Headers Control

Control cache headers in development via ENV variables:

# Enable cache headers in development
FOLIO_CACHE_HEADERS_ENABLED=true rails server

# Disable cache headers (default in development)
FOLIO_CACHE_HEADERS_ENABLED=false rails server

# Custom TTL
FOLIO_CACHE_HEADERS_TTL=120 rails server

# Disable ETag headers
FOLIO_CACHE_HEADERS_ETAG=false rails server

# Custom stale-while-revalidate
FOLIO_CACHE_HEADERS_SWR=30 rails server

Emergency Cache Control

Control cache behavior via emergency ENV variables:

# Disable all caching immediately (sets Cache-Control: no-store)
FOLIO_CACHE_TTL_MULTIPLIER=0 rails server

# Reduce cache TTL by half
FOLIO_CACHE_TTL_MULTIPLIER=0.5 rails server

# Double cache TTL
FOLIO_CACHE_TTL_MULTIPLIER=2 rails server

Development Banners

When starting the development server, Folio displays helpful banners:

Cache Enabled

✅ FOLIO DEVELOPMENT CACHE: ENABLED
Store: Memory Store
Fragment cache logging: ON
Public file headers: 172800s TTL
MiniProfiler: Auto-disabled (prevents interference)
To disable: rails dev:cache
Documentation: docs/cache.md

Cache Disabled

❌ FOLIO DEVELOPMENT CACHE: DISABLED
Store: :null_store (no caching)
Fragment caching is OFF
Cache headers are OFF
MiniProfiler: Auto-enabled
To enable: rails dev:cache
This will create tmp/caching-dev.txt
Documentation: docs/cache.md

Cloudflare Cache Optimization

Problem: Set-Cookie Headers Cause BYPASS

Cloudflare automatically sets cf-cache-status: BYPASS for responses with Set-Cookie headers, preventing CDN caching. Rails applications commonly send:

  • Session cookies (_session_id) - Rails default behavior
  • Log cookies (s_for_log, u_for_log) - Folio tracking cookies

Solution: Session Skip for Public Responses

Enable cache-friendly mode that skips session cookies for anonymous users:

# Enable in production for Cloudflare optimization
FOLIO_CACHE_SKIP_SESSION=true rails server

What it does:

  • Skips Rails session cookies for public cache responses
  • Skips Folio log cookies for anonymous users
  • Allows Cloudflare to cache instead of BYPASS
  • Maintains cookies for signed-in users (private cache)
  • Note: Business-specific cookies (like UTM tracking) maintain their own logic

Safety:

  • ✅ Safe for anonymous content that doesn't need session state
  • ✅ CSRF protection still works via meta tags
  • ✅ User authentication unaffected
  • ✅ Flash messages work normally for signed-in users

Configuration

# config/initializers/cache_headers.rb
Rails.application.config.folio_cache_skip_session_for_public = true

# or via ENV
ENV["FOLIO_CACHE_SKIP_SESSION"] = "true"

Testing Cloudflare Cache

# Without session skip (will get BYPASS)
curl -I https://yoursite.com/ | grep -E "set-cookie|cf-cache"

# With session skip enabled  
FOLIO_CACHE_SKIP_SESSION=true rails server
curl -I https://yoursite.com/ | grep -E "set-cookie|cf-cache"
# Should see no set-cookie headers for anonymous requests

Form and Interactive Components

Built-in Session Management

Folio automatically handles session requirements for form controllers:

# Folio::LeadsController - automatically requires session for form submissions
class Folio::LeadsController < Folio::ApplicationController
  include Folio::RequiresSession
  requires_session_for :form_functionality, only: [:create]
end

# Folio::Api::NewsletterSubscriptionsController - session for newsletter signups
class Folio::Api::NewsletterSubscriptionsController < Folio::Api::BaseController
  include Folio::RequiresSession
  requires_session_for :newsletter_subscription, only: [:create]
end

Component Integration

Form components automatically declare session requirements using the polymorphic pattern:

# Lead forms require session for CSRF and flash messages
class Folio::Leads::FormComponent < ApplicationComponent
  include Folio::ComponentSessionHelper

  def initialize(lead: nil)
    @lead = lead || Folio::Lead.new
  end

  def session_requirement_reason
    "lead_form_csrf_and_flash"
  end
end

# Newsletter forms require session for CSRF and Turnstile
class Folio::NewsletterSubscriptions::FormComponent < ApplicationComponent
  include Folio::ComponentSessionHelper

  def initialize(newsletter_subscription: nil, view_options: {})
    @newsletter_subscription = newsletter_subscription || Folio::NewsletterSubscription.new
    @view_options = view_options
  end

  def session_requirement_reason
    "newsletter_subscription_csrf_and_turnstile"
  end
end

Common Development Workflows

Testing Cache Headers

  1. Enable cache headers:

    FOLIO_CACHE_HEADERS_ENABLED=true rails server
  2. Check headers in browser DevTools or curl:

    curl -I http://localhost:3000/some-page
  3. Notice MiniProfiler is automatically disabled

Testing Fragment Caching

  1. Enable Rails caching:

    rails dev:cache
  2. Check logs for cache hits/misses:

    Cache read: views/articles/1-20231201123456/article
    Cache write: views/articles/1-20231201123456/article
    
  3. Notice MiniProfiler is automatically disabled

Debug Cache Issues

  1. Enable detailed logging:

    FOLIO_CACHE_HEADERS_ENABLED=true rails server
  2. Check logs for cache decisions:

    [Cache Headers] ArticlesController -> public (get_request_signed_out_2xx_with_record)
    Headers: Cache-Control: max-age=60, must-revalidate, stale-while-revalidate=15
    

Force MiniProfiler During Cache Testing

# If you need MiniProfiler while testing cache
MINI_PROFILER_FORCE_ENABLED=true FOLIO_CACHE_HEADERS_ENABLED=true rails server

⚠️ Warning: This may interfere with cache header testing as MiniProfiler injects its own headers.

Best Practices

Development

  1. Test with cache enabled - Periodically enable caching to catch issues early
  2. Check logs - Cache decision logging helps debug issues
  3. Use ENV overrides - Quickly test different cache configurations

Production

  1. Enable cache headers - They're enabled by default in production
  2. Monitor cache hit rates - Use CDN analytics to track effectiveness
  3. Test emergency overrides - Ensure FOLIO_CACHE_TTL_MULTIPLIER=0 works
  4. Validate ETag/Last-Modified - Check that conditional requests work

Content Management

  1. Use set_cache_control_headers(record: @model) - Automatic unpublished detection
  2. Test preview mode - Ensure unpublished content isn't cached
  3. Consider cache invalidation - Plan for content update workflows
  4. Monitor stale content - Use stale-while-revalidate appropriately

Troubleshooting

Headers Not Appearing

  1. Check if cache headers are enabled:

    Rails.application.config.folio_cache_headers_enabled
  2. Verify the path should be cached:

    # Console/admin paths are never cached
    request.path.starts_with?("/console") # Should be false
  3. Check for existing headers:

    # Headers set earlier take precedence
    response.headers["Cache-Control"] # Should be nil before setting

MiniProfiler Interference

  1. Check if auto-disabled:

    [MiniProfiler] 🔄 Auto-disabled (caching active)
    
  2. Force enable if needed:

    FORCE_MINI_PROFILER=true rails server
  3. Verify in browser that cache headers are correct (MiniProfiler may add its own)

Fragment Cache Not Working

  1. Ensure caching is enabled:

    rails dev:cache
  2. Check for cache store:

    Rails.cache.class # Should not be NullStore
  3. Verify cache key generation:

    <% cache @article do %>
      <!-- This generates a key like: views/articles/1-20231201123456/article -->
    <% end %>

Related Documentation