faraday-http-cache 0.0.1.dev
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +13 -0
- data/README.md +73 -0
- data/lib/faraday/http_cache/cache_control.rb +112 -0
- data/lib/faraday/http_cache/response.rb +170 -0
- data/lib/faraday/http_cache/storage.rb +71 -0
- data/lib/faraday/http_cache.rb +228 -0
- data/lib/faraday-http-cache.rb +1 -0
- data/spec/cache_control_spec.rb +107 -0
- data/spec/middleware_spec.rb +143 -0
- data/spec/response_spec.rb +150 -0
- data/spec/spec_helper.rb +27 -0
- data/spec/storage_spec.rb +41 -0
- data/spec/support/test_app.rb +72 -0
- data/spec/support/test_server.rb +64 -0
- metadata +197 -0
data/LICENSE
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
Copyright 2012 Plataformatec.
|
2
|
+
|
3
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
you may not use this file except in compliance with the License.
|
5
|
+
You may obtain a copy of the License at
|
6
|
+
|
7
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
|
9
|
+
Unless required by applicable law or agreed to in writing, software
|
10
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
See the License for the specific language governing permissions and
|
13
|
+
limitations under the License.
|
data/README.md
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
# Faraday Http Cache
|
2
|
+
a [Faraday](https://github.com/technoweenie/faraday) middleware that respects HTTP cache,
|
3
|
+
by checking expiration and validation of the stored responses.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add it to your Gemfile:
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
gem 'faraday-http-cache'
|
11
|
+
```
|
12
|
+
|
13
|
+
## Usage and configuration
|
14
|
+
|
15
|
+
You have to use the middleware in the Faraday instance that you want to. You can use the new
|
16
|
+
shortcut using a symbol or passing the middleware class
|
17
|
+
|
18
|
+
```ruby
|
19
|
+
client = Faraday.new do |builder|
|
20
|
+
builder.use :http_cache
|
21
|
+
# or
|
22
|
+
builder.use Faraday::HttpCache
|
23
|
+
|
24
|
+
builder.adapter Faraday.default_adapter
|
25
|
+
end
|
26
|
+
```
|
27
|
+
|
28
|
+
The middleware uses the `ActiveSupport::Cache` API to record the responses from the targeted
|
29
|
+
endpoints, and any extra configuration option will be used to setup the cache store.
|
30
|
+
|
31
|
+
```ruby
|
32
|
+
# Connect the middleware to a Memcache instance.
|
33
|
+
client = Faraday.new do |builder|
|
34
|
+
builder.use :http_cache, :mem_cache_store, "localhost:11211"
|
35
|
+
builder.adapter Faraday.default_adapter
|
36
|
+
end
|
37
|
+
|
38
|
+
# Or use the Rails.cache instance inside your Rails app.
|
39
|
+
client = Faraday.new do |builder|
|
40
|
+
builder.use :http_cache, Rails.cache
|
41
|
+
builder.adapter Faraday.default_adapter
|
42
|
+
end
|
43
|
+
```
|
44
|
+
|
45
|
+
The default store provided by ActiveSupport is the `MemoryStore` one, so it's important to
|
46
|
+
configure a proper one for your production environment.
|
47
|
+
|
48
|
+
### Logging
|
49
|
+
|
50
|
+
You can provide a `:logger` option that will be receive debug informations based on the middleware
|
51
|
+
operations:
|
52
|
+
|
53
|
+
```ruby
|
54
|
+
client = Faraday.new do |builder|
|
55
|
+
builder.use :http_cache, :logger => Rails.logger
|
56
|
+
builder.adapter Faraday.default_adapter
|
57
|
+
end
|
58
|
+
|
59
|
+
client.get('http://site/api/users')
|
60
|
+
# logs "HTTP Cache: [GET users] miss, store"
|
61
|
+
```
|
62
|
+
|
63
|
+
## See it live
|
64
|
+
|
65
|
+
You can clone this repository, install it's dependencies with Bundler (run `bundle install`) and
|
66
|
+
execute the `examples/twitter.rb` file to see a sample of the middleware usage - it's issuing
|
67
|
+
requests to the Twitter API and caching them, so the rate limit isn't reduced on every request by
|
68
|
+
the client object. After sleeping for 5 minutes the cache will expire and the client will hit the
|
69
|
+
Twitter API again.
|
70
|
+
|
71
|
+
## License
|
72
|
+
|
73
|
+
Copyright (c) 2012 Plataformatec. See LICENSE file.
|
@@ -0,0 +1,112 @@
|
|
1
|
+
module Faraday
|
2
|
+
class HttpCache < Faraday::Middleware
|
3
|
+
# Internal: a class to represent the 'Cache-Control' header options.
|
4
|
+
# This implementation is based on 'rack-cache' internals by Ryan Tomayko.
|
5
|
+
# It breaks the several directives into keys/values and stores them into
|
6
|
+
# a Hash.
|
7
|
+
class CacheControl
|
8
|
+
|
9
|
+
# Internal: Initialize a new CacheControl.
|
10
|
+
def initialize(string)
|
11
|
+
@directives = {}
|
12
|
+
parse(string)
|
13
|
+
end
|
14
|
+
|
15
|
+
# Internal: Checks if the 'public' directive is present.
|
16
|
+
def public?
|
17
|
+
@directives['public']
|
18
|
+
end
|
19
|
+
|
20
|
+
# Internal: Checks if the 'private' directive is present.
|
21
|
+
def private?
|
22
|
+
@directives['private']
|
23
|
+
end
|
24
|
+
|
25
|
+
# Internal: Checks if the 'no-cache' directive is present.
|
26
|
+
def no_cache?
|
27
|
+
@directives['no-cache']
|
28
|
+
end
|
29
|
+
|
30
|
+
# Internal: Checks if the 'no-store' directive is present.
|
31
|
+
def no_store?
|
32
|
+
@directives['no-store']
|
33
|
+
end
|
34
|
+
|
35
|
+
# Internal: Gets the 'max-age' directive as an Integer.
|
36
|
+
#
|
37
|
+
# Returns nil if the 'max-age' directive isn't present.
|
38
|
+
def max_age
|
39
|
+
@directives['max-age'].to_i if @directives.key?('max-age')
|
40
|
+
end
|
41
|
+
|
42
|
+
# Internal: Gets the 's-maxage' directive as an Integer.
|
43
|
+
#
|
44
|
+
# Returns nil if the 's-maxage' directive isn't present.
|
45
|
+
def shared_max_age
|
46
|
+
@directives['s-maxage'].to_i if @directives.key?('s-maxage')
|
47
|
+
end
|
48
|
+
alias_method :s_maxage, :shared_max_age
|
49
|
+
|
50
|
+
# Internal: Checks if the 'must-revalidate' directive is present.
|
51
|
+
def must_revalidate?
|
52
|
+
@directives['must-revalidate']
|
53
|
+
end
|
54
|
+
|
55
|
+
# Internal: Checks if the 'proxy-revalidate' directive is present.
|
56
|
+
def proxy_revalidate?
|
57
|
+
@directives['proxy-revalidate']
|
58
|
+
end
|
59
|
+
|
60
|
+
# Internal: Gets the String representation for the cache directives.
|
61
|
+
# Directives are joined by a '=' and then combined into a single String
|
62
|
+
# separated by commas. Directives with a 'true' value will omit the '='
|
63
|
+
# sign and their value.
|
64
|
+
#
|
65
|
+
# Returns the Cache Control string.
|
66
|
+
def to_s
|
67
|
+
booleans, values = [], []
|
68
|
+
|
69
|
+
@directives.each do |key, value|
|
70
|
+
if value == true
|
71
|
+
booleans << key
|
72
|
+
elsif value
|
73
|
+
values << "#{key}=#{value}"
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
(booleans.sort + values.sort).join(', ')
|
78
|
+
end
|
79
|
+
|
80
|
+
private
|
81
|
+
|
82
|
+
# Internal: Parses the Cache Control string into the directives Hash.
|
83
|
+
# Existing whitespaces are removed, and the string is splited on commas.
|
84
|
+
# For each segment, everything before a '=' will be treated as the key
|
85
|
+
# and the excedding will be treated as the value. If only the key is
|
86
|
+
# present the assigned value will defaults to true.
|
87
|
+
#
|
88
|
+
# Examples:
|
89
|
+
# parse("max-age=600")
|
90
|
+
# @directives
|
91
|
+
# # => { "max-age" => "600"}
|
92
|
+
#
|
93
|
+
# parse("max-age")
|
94
|
+
# @directives
|
95
|
+
# # => { "max-age" => true }
|
96
|
+
#
|
97
|
+
# Returns nothing.
|
98
|
+
def parse(string)
|
99
|
+
string = string.to_s
|
100
|
+
|
101
|
+
return if string.empty?
|
102
|
+
|
103
|
+
string.delete(' ').split(',').each do |part|
|
104
|
+
next if part.empty?
|
105
|
+
|
106
|
+
name, value = part.split('=', 2)
|
107
|
+
@directives[name.downcase] = (value || true) unless name.empty?
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
@@ -0,0 +1,170 @@
|
|
1
|
+
require 'time'
|
2
|
+
require 'faraday/http_cache/cache_control'
|
3
|
+
|
4
|
+
module Faraday
|
5
|
+
class HttpCache < Faraday::Middleware
|
6
|
+
# Internal: a class to represent a response from a Faraday request.
|
7
|
+
# It decorates the response hash into a smarter object that queries
|
8
|
+
# the response headers and status informations about how the caching
|
9
|
+
# middleware should handle this specific response.
|
10
|
+
class Response
|
11
|
+
# Internal: List of status codes that can be cached:
|
12
|
+
# * 200 - 'OK'
|
13
|
+
# * 203 - 'Non-Authoritative Information'
|
14
|
+
# * 300 - 'Multiple Choices'
|
15
|
+
# * 301 - 'Moved Permanently'
|
16
|
+
# * 302 - 'Found'
|
17
|
+
# * 404 - 'Not Found'
|
18
|
+
# * 410 - 'Gone'
|
19
|
+
CACHEABLE_STATUS_CODES = [200, 203, 300, 301, 302, 404, 410]
|
20
|
+
|
21
|
+
# Internal: Gets the actual response Hash (status, headers and body).
|
22
|
+
attr_reader :payload
|
23
|
+
|
24
|
+
# Internal: Gets the 'Last-Modified' header from the headers Hash.
|
25
|
+
attr_reader :last_modified
|
26
|
+
|
27
|
+
# Internal: Gets the 'ETag' header from the headers Hash.
|
28
|
+
attr_reader :etag
|
29
|
+
|
30
|
+
# Internal: Initialize a new Response with the response payload from
|
31
|
+
# a Faraday request.
|
32
|
+
#
|
33
|
+
# payload - the response Hash returned by a Faraday request.
|
34
|
+
# :status - the status code from the response.
|
35
|
+
# :response_headers - a 'Hash' like object with the headers.
|
36
|
+
# :body - the response body.
|
37
|
+
def initialize(payload = {})
|
38
|
+
@now = Time.now
|
39
|
+
@payload = payload
|
40
|
+
wrap_headers!
|
41
|
+
headers['Date'] ||= @now.httpdate
|
42
|
+
|
43
|
+
@last_modified = headers['Last-Modified']
|
44
|
+
@etag = headers['ETag']
|
45
|
+
end
|
46
|
+
|
47
|
+
# Internal: Checks the response freshness based on expiration headers.
|
48
|
+
# The calculated 'ttl' should be present and bigger than 0.
|
49
|
+
#
|
50
|
+
# Returns true if the response is fresh, otherwise false.
|
51
|
+
def fresh?
|
52
|
+
ttl && ttl > 0
|
53
|
+
end
|
54
|
+
|
55
|
+
# Internal: Checks if the Response returned a 'Not Modified' status.
|
56
|
+
#
|
57
|
+
# Returns true if the response status code is 304.
|
58
|
+
def not_modified?
|
59
|
+
@payload[:status] == 304
|
60
|
+
end
|
61
|
+
|
62
|
+
# Internal: Checks if the response can be cached by the client.
|
63
|
+
# This is validated by the 'Cache-Control' directives, the response
|
64
|
+
# status code and it's freshness or validation status.
|
65
|
+
#
|
66
|
+
# Returns false if the 'Cache-Control' says that we can't store the
|
67
|
+
# response, or if isn't fresh or it can't be revalidated with the origin
|
68
|
+
# server. Otherwise, returns true.
|
69
|
+
def cacheable?
|
70
|
+
return false if cache_control.private? || cache_control.no_store?
|
71
|
+
|
72
|
+
cacheable_status_code? && (validateable? || fresh?)
|
73
|
+
end
|
74
|
+
|
75
|
+
# Internal: Gets the response age in seconds.
|
76
|
+
#
|
77
|
+
# Returns the 'Age' header if present, or subtracts the response 'date'
|
78
|
+
# from the current time.
|
79
|
+
def age
|
80
|
+
(headers['Age'] || (@now - date)).to_i
|
81
|
+
end
|
82
|
+
|
83
|
+
# Internal: Calculates the 'Time to live' left on the Response.
|
84
|
+
#
|
85
|
+
# Returns the remaining seconds for the response, or nil the 'max_age'
|
86
|
+
# isn't present.
|
87
|
+
def ttl
|
88
|
+
max_age - age if max_age
|
89
|
+
end
|
90
|
+
|
91
|
+
# Internal: Parses the 'Date' header back into a Time instance.
|
92
|
+
#
|
93
|
+
# Returns the Time object.
|
94
|
+
def date
|
95
|
+
Time.httpdate(headers['Date'])
|
96
|
+
end
|
97
|
+
|
98
|
+
# Internal: Gets the response max age.
|
99
|
+
# The max age is extracted from one of the following:
|
100
|
+
# * The shared max age directive from the 'Cache-Control' header;
|
101
|
+
# * The max age directive from the 'Cache-Control' header;
|
102
|
+
# * The difference between the 'Expires' header and the response
|
103
|
+
# date.
|
104
|
+
#
|
105
|
+
# Returns the max age value in seconds or nil if all options above fails.
|
106
|
+
def max_age
|
107
|
+
cache_control.shared_max_age ||
|
108
|
+
cache_control.max_age ||
|
109
|
+
(expires && (expires - date))
|
110
|
+
end
|
111
|
+
|
112
|
+
# Internal: Creates a new 'Faraday::Response'.
|
113
|
+
#
|
114
|
+
# Returns a new instance of a 'Faraday::Response' with the payload.
|
115
|
+
def to_response
|
116
|
+
Faraday::Response.new(@payload)
|
117
|
+
end
|
118
|
+
|
119
|
+
private
|
120
|
+
|
121
|
+
# Internal: Checks if this response can be revalidated.
|
122
|
+
#
|
123
|
+
# Returns true if the 'headers' contains a 'Last-Modified' or an 'ETag'
|
124
|
+
# entry.
|
125
|
+
def validateable?
|
126
|
+
headers.key?('Last-Modified') || headers.key?('ETag')
|
127
|
+
end
|
128
|
+
|
129
|
+
# Internal: Validates the response status against the
|
130
|
+
# `CACHEABLE_STATUS_CODES' constant.
|
131
|
+
#
|
132
|
+
# Returns true if the constant includes the response status code.
|
133
|
+
def cacheable_status_code?
|
134
|
+
CACHEABLE_STATUS_CODES.include?(@payload[:status])
|
135
|
+
end
|
136
|
+
|
137
|
+
# Internal: Gets the 'Expires' in a Time object.
|
138
|
+
#
|
139
|
+
# Returns the Time object, or nil if the header isn't present.
|
140
|
+
def expires
|
141
|
+
headers['Expires'] && Time.httpdate(headers['Expires'])
|
142
|
+
end
|
143
|
+
|
144
|
+
# Internal: Gets the 'CacheControl' object.
|
145
|
+
def cache_control
|
146
|
+
@cache_control ||= CacheControl.new(headers['Cache-Control'])
|
147
|
+
end
|
148
|
+
|
149
|
+
# Internal: Converts the headers 'Hash' into 'Faraday::Utils::Headers'.
|
150
|
+
# Faraday actually uses a Hash subclass, `Faraday::Utils::Headers` to
|
151
|
+
# store the headers hash. When retrieving a serialized response,
|
152
|
+
# the headers object is decoded as a 'Hash' instead of the actual
|
153
|
+
# 'Faraday::Utils::Headers' object, so we need to ensure that the
|
154
|
+
# 'response_headers' is always a 'Headers' instead of a plain 'Hash'.
|
155
|
+
#
|
156
|
+
# Returns nothing.
|
157
|
+
def wrap_headers!
|
158
|
+
headers = @payload[:response_headers]
|
159
|
+
|
160
|
+
@payload[:response_headers] = Faraday::Utils::Headers.new
|
161
|
+
@payload[:response_headers].update(headers) if headers
|
162
|
+
end
|
163
|
+
|
164
|
+
# Internal: Gets the headers 'Hash' from the payload.
|
165
|
+
def headers
|
166
|
+
@payload[:response_headers]
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
require 'digest/sha1'
|
2
|
+
require 'active_support/cache'
|
3
|
+
require 'active_support/core_ext/hash/keys'
|
4
|
+
|
5
|
+
module Faraday
|
6
|
+
class HttpCache < Faraday::Middleware
|
7
|
+
# Internal: A Wrapper around a ActiveSupport::CacheStore to store responses.
|
8
|
+
#
|
9
|
+
# Examples
|
10
|
+
# # Creates a new Storage using a MemCached backend from ActiveSupport.
|
11
|
+
# Faraday::HttpCache::Storage.new(:mem_cache_store)
|
12
|
+
#
|
13
|
+
# # Reuse some other instance of a ActiveSupport::CacheStore object.
|
14
|
+
# Faraday::HttpCache::Storage.new(Rails.cache)
|
15
|
+
class Storage
|
16
|
+
attr_reader :cache
|
17
|
+
|
18
|
+
# Internal: Initialize a new Storage object with a cache backend.
|
19
|
+
#
|
20
|
+
# store - An ActiveSupport::CacheStore identifier (default: nil).
|
21
|
+
# options - The Hash options for the CacheStore backend (default: {}).
|
22
|
+
def initialize(store = nil, options = {})
|
23
|
+
@cache = ActiveSupport::Cache.lookup_store(store, options)
|
24
|
+
end
|
25
|
+
|
26
|
+
# Internal: Writes a response with a key based on the given request.
|
27
|
+
#
|
28
|
+
# request - The Hash containing the request information.
|
29
|
+
# :method - The HTTP Method used for the request.
|
30
|
+
# :url - The requested URL.
|
31
|
+
# :request_headers - The custom headers for the request.
|
32
|
+
# response - The Faraday::HttpCache::Response instance to be stored.
|
33
|
+
def write(request, response)
|
34
|
+
key = cache_key_for(request)
|
35
|
+
value = MultiJson.dump(response.payload)
|
36
|
+
|
37
|
+
cache.write(key, value)
|
38
|
+
end
|
39
|
+
|
40
|
+
# Internal: Reads a key based on the given request from the underlying cache.
|
41
|
+
#
|
42
|
+
# request - The Hash containing the request information.
|
43
|
+
# :method - The HTTP Method used for the request.
|
44
|
+
# :url - The requested URL.
|
45
|
+
# :request_headers - The custom headers for the request.
|
46
|
+
# klass - The Class to be instantiated with the recovered informations.
|
47
|
+
def read(request, klass = Faraday::HttpCache::Response)
|
48
|
+
key = cache_key_for(request)
|
49
|
+
value = cache.read(key)
|
50
|
+
|
51
|
+
if value
|
52
|
+
payload = MultiJson.load(value).symbolize_keys
|
53
|
+
klass.new(payload)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
# Internal: Generates a String key for a given request object.
|
60
|
+
# The request object is folded into a sorted Array (since we can't count
|
61
|
+
# on hashes order on Ruby 1.8), encoded as JSON and digested as a `SHA1`
|
62
|
+
# string.
|
63
|
+
#
|
64
|
+
# Returns the encoded String.
|
65
|
+
def cache_key_for(request)
|
66
|
+
array = request.stringify_keys.to_a.sort
|
67
|
+
Digest::SHA1.hexdigest(MultiJson.dump(array))
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,228 @@
|
|
1
|
+
require 'faraday'
|
2
|
+
require 'multi_json'
|
3
|
+
|
4
|
+
require 'active_support/core_ext/hash/slice'
|
5
|
+
|
6
|
+
require 'faraday/http_cache/storage'
|
7
|
+
require 'faraday/http_cache/response'
|
8
|
+
|
9
|
+
module Faraday
|
10
|
+
# Public: The middleware responsible for caching and serving responses.
|
11
|
+
# The middleware use the provided configuration options to establish a
|
12
|
+
# 'Faraday::HttpCache::Storage' to cache responses retrieved by the stack
|
13
|
+
# adapter. If a stored response can be served again for a subsequent
|
14
|
+
# request, the middleware will return the response instead of issuing a new
|
15
|
+
# request to it's server. This middleware should be the last attached handler
|
16
|
+
# to your stack, so it will be closest to the inner app, avoiding issues
|
17
|
+
# with other middlewares on your stack.
|
18
|
+
#
|
19
|
+
# Examples:
|
20
|
+
#
|
21
|
+
# # Using the middleware with a simple client:
|
22
|
+
# client = Faraday.new do |builder|
|
23
|
+
# builder.user :http_cache
|
24
|
+
# builder.adapter Faraday.default_adapter
|
25
|
+
# end
|
26
|
+
#
|
27
|
+
# # Attach a Logger to the middleware.
|
28
|
+
# client = Faraday.new do |builder|
|
29
|
+
# builder.use :http_cache, :logger => my_logger_instance
|
30
|
+
# builder.adapter Faraday.default_adapter
|
31
|
+
# end
|
32
|
+
#
|
33
|
+
# # Provide an existing CacheStore (for instance, from a Rails app)
|
34
|
+
# client = Faraday.new do |builder|
|
35
|
+
# builder.use :http_cache, Rails.cache
|
36
|
+
# end
|
37
|
+
class HttpCache < Faraday::Middleware
|
38
|
+
|
39
|
+
# Public: Initializes a new HttpCache middleware.
|
40
|
+
#
|
41
|
+
# app - the next endpoint on the 'Faraday' stack.
|
42
|
+
# arguments - aditional options to setup the logger and the storage.
|
43
|
+
#
|
44
|
+
# Examples:
|
45
|
+
#
|
46
|
+
# # Initialize the middleware with a logger.
|
47
|
+
# Faraday::HttpCache.new(app, :logger => my_logger)
|
48
|
+
#
|
49
|
+
# # Initialize the middleware with a FileStore at the 'tmp' dir.
|
50
|
+
# Faraday::HttpCache.new(app, :file_store, 'tmp')
|
51
|
+
def initialize(app, *arguments)
|
52
|
+
super(app)
|
53
|
+
|
54
|
+
if arguments.last.is_a? Hash
|
55
|
+
options = arguments.pop
|
56
|
+
@logger = options.delete(:logger)
|
57
|
+
else
|
58
|
+
options = arguments
|
59
|
+
end
|
60
|
+
|
61
|
+
store = arguments.shift
|
62
|
+
|
63
|
+
@storage = Storage.new(store, options)
|
64
|
+
end
|
65
|
+
|
66
|
+
# Public: Process the request into a duplicate of this instance to
|
67
|
+
# ensure that the internal state is preserved.
|
68
|
+
def call(env)
|
69
|
+
dup.call!(env)
|
70
|
+
end
|
71
|
+
|
72
|
+
# Internal: Process the stack request to try to serve a cache response.
|
73
|
+
# On a cacheable request, the middleware will attempt to locate a
|
74
|
+
# valid stored response to serve. On a cache miss, the middleware will
|
75
|
+
# forward the request and try to store the response for future requests.
|
76
|
+
# If the request can't be cached, the request will be delegated directly
|
77
|
+
# to the underlying app and does nothing to the response.
|
78
|
+
# The processed steps will be recorded to be logged once the whole
|
79
|
+
# process is finished.
|
80
|
+
#
|
81
|
+
# Returns a 'Faraday::Response' instance.
|
82
|
+
def call!(env)
|
83
|
+
@trace = []
|
84
|
+
@request = create_request(env)
|
85
|
+
|
86
|
+
response = nil
|
87
|
+
|
88
|
+
if can_cache?(@request[:method])
|
89
|
+
response = process(env)
|
90
|
+
else
|
91
|
+
trace :unacceptable
|
92
|
+
response = @app.call(env)
|
93
|
+
end
|
94
|
+
|
95
|
+
response.on_complete do
|
96
|
+
log_request
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
private
|
101
|
+
# Internal: Validates if the current request method is valid for caching.
|
102
|
+
#
|
103
|
+
# Returns true if the method is ':get' or ':head'.
|
104
|
+
def can_cache?(method)
|
105
|
+
method == :get || method == :head
|
106
|
+
end
|
107
|
+
|
108
|
+
# Internal: Tries to located a valid response or forwards the call to the stack.
|
109
|
+
# * If no entry is present on the storage, the 'fetch' method will forward
|
110
|
+
# the call to the remaining stack and return the new response.
|
111
|
+
# * If a fresh response is found, the middleware will abort the remaining
|
112
|
+
# stack calls and return the stored response back to the client.
|
113
|
+
# * If a response is found but isn't fresh anymore, the middleware will
|
114
|
+
# revalidate the response back to the server.
|
115
|
+
#
|
116
|
+
# env - the environment 'Hash' provided from the 'Faraday' stack.
|
117
|
+
#
|
118
|
+
# Returns the actual 'Faraday::Response' instance to be served.
|
119
|
+
def process(env)
|
120
|
+
entry = @storage.read(@request)
|
121
|
+
|
122
|
+
return fetch(env) if entry.nil?
|
123
|
+
|
124
|
+
if entry.fresh?
|
125
|
+
response = entry.to_response
|
126
|
+
trace :fresh
|
127
|
+
else
|
128
|
+
response = validate(entry, env)
|
129
|
+
end
|
130
|
+
|
131
|
+
response
|
132
|
+
end
|
133
|
+
|
134
|
+
# Internal: Tries to validated a stored entry back to it's origin server
|
135
|
+
# using the 'If-Modified-Since' and 'If-None-Match' headers with the
|
136
|
+
# existing 'Last-Modified' and 'ETag' headers. If the new response
|
137
|
+
# is marked as 'Not Modified', the previous stored response will be used
|
138
|
+
# and forwarded against the Faraday stack. Otherwise, the freshly new
|
139
|
+
# response will be stored (replacing the old one) and used.
|
140
|
+
#
|
141
|
+
# entry - a stale 'Faraday::HttpCache::Response' retrieved from the cache.
|
142
|
+
# env - the environment 'Hash' to perform the request.
|
143
|
+
#
|
144
|
+
# Returns the 'Faraday::HttpCache::Response' to be forwarded into the stack.
|
145
|
+
def validate(entry, env)
|
146
|
+
headers = env[:request_headers]
|
147
|
+
headers['If-Modified-Since'] = entry.last_modified if entry.last_modified
|
148
|
+
headers['If-None-Match'] = entry.etag if entry.etag
|
149
|
+
|
150
|
+
@app.call(env).on_complete do |env|
|
151
|
+
response = Response.new(env)
|
152
|
+
if response.not_modified?
|
153
|
+
trace :valid
|
154
|
+
env.merge!(entry.payload)
|
155
|
+
response = entry
|
156
|
+
end
|
157
|
+
store(response)
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
# Internal: Records a traced action to be used by the logger once the
|
162
|
+
# request/response phase is finished.
|
163
|
+
#
|
164
|
+
# operation - the name of the performed action, a String or Symbol.
|
165
|
+
#
|
166
|
+
# Returns nothing.
|
167
|
+
def trace(operation)
|
168
|
+
@trace << operation
|
169
|
+
end
|
170
|
+
|
171
|
+
# Internal: Stores the response into the storage.
|
172
|
+
# If the response isn't cacheable, a trace action 'invalid' will be
|
173
|
+
# recorded for logging purposes.
|
174
|
+
#
|
175
|
+
# response - a 'Faraday::HttpCache::Response' instance to be stored.
|
176
|
+
#
|
177
|
+
# Returns nothing.
|
178
|
+
def store(response)
|
179
|
+
if response.cacheable?
|
180
|
+
trace :store
|
181
|
+
@storage.write(@request, response)
|
182
|
+
else
|
183
|
+
trace :invalid
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
# Internal: Fetches the response from the Faraday stack and stores it.
|
188
|
+
#
|
189
|
+
# env - the environment 'Hash' from the Faraday stack.
|
190
|
+
#
|
191
|
+
# Returns the fresh 'Faraday::Response' instance.
|
192
|
+
def fetch(env)
|
193
|
+
trace :miss
|
194
|
+
@app.call(env).on_complete do |env|
|
195
|
+
response = Response.new(env)
|
196
|
+
store(response)
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
# Internal: Creates a new 'Hash' containing the request information.
|
201
|
+
#
|
202
|
+
# env - the environment 'Hash' from the Faraday stack.
|
203
|
+
#
|
204
|
+
# Returns a 'Hash' containing the ':method', ':url' and 'request_headers'
|
205
|
+
# entries.
|
206
|
+
def create_request(env)
|
207
|
+
@request = env.slice(:method, :url)
|
208
|
+
@request[:request_headers] = env[:request_headers].dup
|
209
|
+
@request
|
210
|
+
end
|
211
|
+
|
212
|
+
# Internal: Logs the trace info about the incoming request
|
213
|
+
# and how the middleware handled it.
|
214
|
+
# This method does nothing if theresn't a logger present.
|
215
|
+
#
|
216
|
+
# Returns nothing.
|
217
|
+
def log_request
|
218
|
+
return unless @logger
|
219
|
+
|
220
|
+
method = @request[:method].to_s.upcase
|
221
|
+
path = @request[:url].path
|
222
|
+
line = "HTTP Cache: [#{method} #{path}] #{@trace.join(', ')}"
|
223
|
+
@logger.debug(line)
|
224
|
+
end
|
225
|
+
end
|
226
|
+
end
|
227
|
+
|
228
|
+
Faraday.register_middleware :http_cache => lambda { Faraday::HttpCache }
|
@@ -0,0 +1 @@
|
|
1
|
+
require 'faraday/http_cache'
|