Class: HTTP::Features::Caching

Inherits:
HTTP::Feature show all
Defined in:
lib/http/features/caching.rb,
lib/http/features/caching/entry.rb,
lib/http/features/caching/in_memory_store.rb,
sig/http.rbs

Overview

HTTP caching feature that stores and reuses responses according to RFC 7234. Only GET and HEAD responses are cached. Supports Cache-Control, Expires, ETag, and Last-Modified for freshness checks and conditional revalidation.

Examples:

Basic usage with in-memory cache

HTTP.use(:caching).get("https://example.com/")

With a shared store across requests

store = HTTP::Features::Caching::InMemoryStore.new
client = HTTP.use(caching: { store: store })
client.get("https://example.com/")

Defined Under Namespace

Classes: Entry, InMemoryStore

Constant Summary collapse

CACHEABLE_METHODS =

Returns:

  • (Set[Symbol])
Set.new(%i[get head]).freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods inherited from HTTP::Feature

#on_error, #on_request, #wrap_request

Constructor Details

#initialize(store: InMemoryStore.new) ⇒ Caching

Initializes the Caching feature

Examples:

Caching.new(store: InMemoryStore.new)

Parameters:

  • store (#lookup, #store) (defaults to: InMemoryStore.new)

    cache store instance

  • store: (Object) (defaults to: InMemoryStore.new)


44
45
46
# File 'lib/http/features/caching.rb', line 44

def initialize(store: InMemoryStore.new)
  @store = store
end

Instance Attribute Details

#store#lookup, #store (readonly)

The cache store instance

Examples:

feature.store

Returns:

  • (#lookup, #store)

    the cache store



34
35
36
# File 'lib/http/features/caching.rb', line 34

def store
  @store
end

Instance Method Details

#add_conditional_headers(request, entry) ⇒ HTTP::Request

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Add conditional headers from a cached entry to the request

Parameters:

Returns:



177
178
179
180
181
182
183
184
185
186
187
188
189
190
# File 'lib/http/features/caching.rb', line 177

def add_conditional_headers(request, entry)
  headers = request.headers.dup
  headers[Headers::IF_NONE_MATCH] = entry.headers[Headers::ETAG] # steep:ignore
  headers[Headers::IF_MODIFIED_SINCE] = entry.headers[Headers::LAST_MODIFIED] # steep:ignore

  Request.new(
    verb:    request.verb,
    uri:     request.uri,
    headers: headers,
    proxy:   request.proxy,
    body:    request.body,
    version: request.version
  )
end

#around_request(request) {|arg0| ... } ⇒ HTTP::Response

Wraps the HTTP exchange with caching logic

Checks the cache before making a request. Returns a cached response if fresh; otherwise adds conditional headers and revalidates. Stores cacheable responses for future use.

Examples:

feature.around_request(request) { |req| perform_exchange(req) }

Parameters:

Yields:

  • Executes the HTTP exchange

Yield Parameters:

Yield Returns:

Returns:



62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
# File 'lib/http/features/caching.rb', line 62

def around_request(request)
  return yield(request) unless cacheable_request?(request)

  entry = store.lookup(request)

  return yield(request) unless entry

  return build_cached_response(entry, request) if entry.fresh?

  response = yield(add_conditional_headers(request, entry))

  return revalidate_entry(entry, response, request) if response.status.not_modified?

  response
end

#build_cached_response(entry, request) ⇒ HTTP::Response

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Build a response from a cached entry

Parameters:

Returns:



195
196
197
198
199
200
201
202
203
204
# File 'lib/http/features/caching.rb', line 195

def build_cached_response(entry, request)
  Response.new(
    status:        entry.status,
    version:       entry.version,
    headers:       entry.headers,
    proxy_headers: entry.proxy_headers,
    body:          entry.body,
    request:       request
  )
end

#build_entry(response, body_string) ⇒ Entry

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Build a cache entry from a response

Parameters:

  • response (Response)
  • body_string (String)

Returns:



124
125
126
127
128
129
130
131
132
133
134
# File 'lib/http/features/caching.rb', line 124

def build_entry(response, body_string)
  Entry.new(
    status:        response.code,
    version:       response.version,
    headers:       response.headers.dup,
    proxy_headers: response.proxy_headers,
    body:          body_string,
    request_uri:   response.uri,
    stored_at:     now
  )
end

#cacheable_request?(request) ⇒ Boolean

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Check whether this request method is cacheable

Parameters:

Returns:

  • (Boolean)


139
140
141
# File 'lib/http/features/caching.rb', line 139

def cacheable_request?(request)
  CACHEABLE_METHODS.include?(request.verb)
end

#cacheable_response?(response) ⇒ Boolean

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Check whether this response is cacheable

Parameters:

Returns:

  • (Boolean)


146
147
148
149
150
151
152
153
154
# File 'lib/http/features/caching.rb', line 146

def cacheable_response?(response)
  return false if response.status < 200
  return false if response.status >= 400

  directives = parse_cache_control(response.headers)
  return false if directives.include?("no-store")

  freshness_info?(response, directives)
end

#freshness_info?(response, directives) ⇒ Boolean

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Whether the response carries enough information to determine freshness

Parameters:

  • response (Response)
  • directives (Array[String])

Returns:

  • (Boolean)


159
160
161
162
163
164
165
# File 'lib/http/features/caching.rb', line 159

def freshness_info?(response, directives)
  return true if directives.any? { |d| d.start_with?("max-age=") }
  return true if response.headers.include?(Headers::EXPIRES)
  return true if response.headers.include?(Headers::ETAG)

  response.headers.include?(Headers::LAST_MODIFIED)
end

#nowTime

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Current time (extracted for testability)

Returns:

  • (Time)


209
210
211
# File 'lib/http/features/caching.rb', line 209

def now
  Time.now
end

#parse_cache_control(headers) ⇒ Array<String>

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Parse Cache-Control header into a list of directives

Parameters:

Returns:

  • (Array<String>)


170
171
172
# File 'lib/http/features/caching.rb', line 170

def parse_cache_control(headers)
  String(headers[Headers::CACHE_CONTROL]).downcase.split(",").map(&:strip)
end

#revalidate_entry(entry, response, request) ⇒ HTTP::Response

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Revalidate a cached entry with a 304 response

Parameters:

Returns:



98
99
100
101
102
# File 'lib/http/features/caching.rb', line 98

def revalidate_entry(entry, response, request)
  entry.update_headers!(response.headers)
  entry.revalidate!
  build_cached_response(entry, request)
end

#store_and_freeze_response(response) ⇒ HTTP::Response

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Store response in cache and return a new response with eagerly-read body

Parameters:

Returns:



107
108
109
110
111
112
113
114
115
116
117
118
119
# File 'lib/http/features/caching.rb', line 107

def store_and_freeze_response(response)
  body_string = String(response)
  store.store(response.request, build_entry(response, body_string))

  Response.new(
    status:        response.code,
    version:       response.version,
    headers:       response.headers,
    proxy_headers: response.proxy_headers,
    body:          body_string,
    request:       response.request
  )
end

#wrap_response(response) ⇒ HTTP::Response

Stores cacheable responses in the cache

Examples:

feature.wrap_response(response)

Parameters:

Returns:



86
87
88
89
90
91
# File 'lib/http/features/caching.rb', line 86

def wrap_response(response)
  return response unless cacheable_request?(response.request)
  return response unless cacheable_response?(response)

  store_and_freeze_response(response)
end