Top Level Namespace
Defined Under Namespace
Classes: ResponseInputStream
Constant Summary collapse
- TEXT_MIME_TYPES =
[ 'application/json', 'application/javascript', 'application/xml', 'application/vnd.api+json', 'image/svg+xml' ].freeze
Instance Method Summary collapse
- #all_casings(input_string) ⇒ Object
- #base_path ⇒ Object
- #build_environ(event:, context:, headers:, body:) ⇒ Object
- #format_body(body:, headers:, text_mime_types:) ⇒ Object
- #format_grouped_headers(headers:) ⇒ Object
- #format_response(event:, status:, headers:, body:, text_mime_types:) ⇒ Object
- #format_split_headers(headers:) ⇒ Object
- #format_status_description(event:, status:) ⇒ Object
- #handle_request(app:, event:, context:, config: {}) ⇒ Object
- #keepalive_event?(event) ⇒ Boolean
- #parse_body(event) ⇒ Object
- #parse_headers(event) ⇒ Object
- #parse_http_headers(headers) ⇒ Object
- #parse_path_info(event) ⇒ Object
- #parse_query_string(event) ⇒ Object
- #parse_script_name(event, headers) ⇒ Object
- #text_mime_type?(headers:, text_mime_types:) ⇒ Boolean
Instance Method Details
#all_casings(input_string) ⇒ Object
195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 |
# File 'lib/serverless_rack.rb', line 195 def all_casings(input_string) # Permute all casings of a given string. # A pretty algoritm, via @Amber # http://stackoverflow.com/questions/6792803/finding-all-possible-case-permutations-in-python return enum_for(__method__, input_string) unless block_given? if input_string.empty? yield '' else first = input_string[0] if first.downcase == first.upcase all_casings(input_string[1..-1]) do |sub_casing| yield first + sub_casing end else all_casings(input_string[1..-1]) do |sub_casing| yield first.downcase + sub_casing yield first.upcase + sub_casing end end end end |
#base_path ⇒ Object
19 20 21 22 23 |
# File 'lib/serverless_rack.rb', line 19 def base_path return if ENV.fetch('API_GATEWAY_BASE_PATH', '').empty? "/#{ENV.fetch('API_GATEWAY_BASE_PATH')}" end |
#build_environ(event:, context:, headers:, body:) ⇒ Object
90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 |
# File 'lib/serverless_rack.rb', line 90 def build_environ(event:, context:, headers:, body:) { 'REQUEST_METHOD' => event['httpMethod'], 'SCRIPT_NAME' => parse_script_name(event, headers), 'PATH_INFO' => parse_path_info(event), 'QUERY_STRING' => parse_query_string(event), 'SERVER_NAME' => headers['Host'] || 'lambda', 'SERVER_PORT' => headers['X-Forwarded-Port'] || '80', 'CONTENT_LENGTH' => body.bytesize.to_s, 'CONTENT_TYPE' => headers['Content-Type'] || '', 'SERVER_PROTOCOL' => 'HTTP/1.1', 'REMOTE_ADDR' => (event['requestContext']['identity'] || {})['sourceIp'] || '', 'REMOTE_USER' => (event['requestContext']['authorizer'] || {})['principalId'] || '', 'rack.version' => Rack::VERSION, 'rack.url_scheme' => headers['X-Forwarded-Proto'] || 'http', 'rack.input' => StringIO.new(body), 'rack.errors' => $stderr, 'rack.multithread' => false, 'rack.multiprocess' => false, 'rack.run_once' => false, 'serverless.event' => event, 'serverless.context' => context, 'serverless.authorizer' => event['requestContext']['authorizer'] }.merge(parse_http_headers(headers)) end |
#format_body(body:, headers:, text_mime_types:) ⇒ Object
166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 |
# File 'lib/serverless_rack.rb', line 166 def format_body(body:, headers:, text_mime_types:) response_data = '' begin if body.respond_to?(:each) body.each { |part| response_data += part } elsif body.respond_to?(:to_ary) body.to_ary.each { |part| response_data += part } elsif body.respond_to?(:call) response_data = ResponseInputStream[body] end ensure body.close if body.respond_to?(:close) end return {} if response_data.empty? if text_mime_type?(headers: headers, text_mime_types: text_mime_types) { 'body' => response_data, 'isBase64Encoded' => false } else { 'body' => Base64.strict_encode64(response_data), 'isBase64Encoded' => true } end end |
#format_grouped_headers(headers:) ⇒ Object
237 238 239 |
# File 'lib/serverless_rack.rb', line 237 def format_grouped_headers(headers:) { 'multiValueHeaders' => headers.transform_values { |v| v.is_a?(Array) ? v : [v] } } end |
#format_response(event:, status:, headers:, body:, text_mime_types:) ⇒ Object
241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 |
# File 'lib/serverless_rack.rb', line 241 def format_response(event:, status:, headers:, body:, text_mime_types:) response = { 'statusCode' => status } if event.include? 'multiValueHeaders' response.merge!(format_grouped_headers(headers: headers)) else response.merge!(format_split_headers(headers: headers)) end response.merge!( format_status_description(event: event, status: status) ) response.merge!( format_body( body: body, headers: headers, text_mime_types: text_mime_types ) ) response end |
#format_split_headers(headers:) ⇒ Object
218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 |
# File 'lib/serverless_rack.rb', line 218 def format_split_headers(headers:) # Rack::Headers will automatically lower-case new keys # Use (and return) a regular hash literal instead headers_hash = {} # If there are headers multiple occurrences, e.g. Set-Cookie, create # case-mutated variations in order to pass them through APIGW. # This is a hack that's currently needed. headers.each do |key, value| if value.is_a?(Array) value.zip(all_casings(key)).each { |v, casing| headers_hash[casing] = v } else headers_hash[key] = value end end { 'headers' => headers_hash } end |
#format_status_description(event:, status:) ⇒ Object
116 117 118 119 120 121 122 123 |
# File 'lib/serverless_rack.rb', line 116 def format_status_description(event:, status:) return {} unless event['requestContext']['elb'] # If the request comes from ALB we need to add a status description description = Rack::Utils::HTTP_STATUS_CODES[status] { 'statusDescription' => "#{status} #{description}" } end |
#handle_request(app:, event:, context:, config: {}) ⇒ Object
265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 |
# File 'lib/serverless_rack.rb', line 265 def handle_request(app:, event:, context:, config: {}) return {} if keepalive_event?(event) status, headers, body = app.call( build_environ( event: event, context: context, headers: parse_headers(event), body: parse_body(event) ) ) format_response( event: event, status: status, headers: headers, body: body, text_mime_types: TEXT_MIME_TYPES + config['text_mime_types'].to_a ) end |
#keepalive_event?(event) ⇒ Boolean
25 26 27 |
# File 'lib/serverless_rack.rb', line 25 def keepalive_event?(event) ['aws.events', 'serverless-plugin-warmup'].include?(event['source']) end |
#parse_body(event) ⇒ Object
58 59 60 61 62 63 64 |
# File 'lib/serverless_rack.rb', line 58 def parse_body(event) if event['isBase64Encoded'] Base64.decode64(event['body']) else event['body'] || '' end end |
#parse_headers(event) ⇒ Object
66 67 68 69 70 71 72 73 74 75 76 |
# File 'lib/serverless_rack.rb', line 66 def parse_headers(event) if event.include? 'multiValueHeaders' Rack::Headers[ (event['multiValueHeaders'] || {}).transform_values do |value| value.join("\n") end ] else Rack::Headers[event['headers'] || {}] end end |
#parse_http_headers(headers) ⇒ Object
78 79 80 81 82 83 84 85 86 87 88 |
# File 'lib/serverless_rack.rb', line 78 def parse_http_headers(headers) headers = headers.map do |key, value| ["HTTP_#{key.upcase.tr('-', '_')}", value] end headers = headers.reject do |key, _value| %w[HTTP_CONTENT_TYPE HTTP_CONTENT_LENGTH].include?(key) end headers.to_h end |
#parse_path_info(event) ⇒ Object
37 38 39 40 41 42 43 44 45 46 |
# File 'lib/serverless_rack.rb', line 37 def parse_path_info(event) # If a user is using a custom domain on API Gateway, they may have a base # path in their URL. This allows us to strip it out via an optional # environment variable. if base_path && event['path'].start_with?(base_path) event['path'][base_path.length..-1] else event['path'] end end |
#parse_query_string(event) ⇒ Object
48 49 50 51 52 53 54 55 56 |
# File 'lib/serverless_rack.rb', line 48 def parse_query_string(event) if event.include? 'multiValueQueryStringParameters' Rack::Utils.unescape( Rack::Utils.build_query(event['multiValueQueryStringParameters'] || {}) ) else Rack::Utils.unescape(Rack::Utils.build_query(event['queryStringParameters'] || {})) end end |
#parse_script_name(event, headers) ⇒ Object
29 30 31 32 33 34 35 |
# File 'lib/serverless_rack.rb', line 29 def parse_script_name(event, headers) if base_path.nil? && (headers['Host'] || '').include?('amazonaws.com') "/#{event['requestContext']['stage']}" else base_path.to_s end end |
#text_mime_type?(headers:, text_mime_types:) ⇒ Boolean
125 126 127 128 129 130 131 132 133 |
# File 'lib/serverless_rack.rb', line 125 def text_mime_type?(headers:, text_mime_types:) mime_type = headers['Content-Type'] || 'text/plain' return false if headers['Content-Encoding'] return true if mime_type.start_with?('text/') return true if text_mime_types.include?(mime_type) false end |