I have an api written in rails which on each request responds with a JSON response.
The response could be huge, so i need to compress the JSON response using gzip.
Wondering how to do this in rails controller?
I have added the line
use Rack::Deflater
in config.ru
Should I also be changing something in the line which renders JSON?
render :json => response.to_json()
Also, how do i check if the response is in gzip format or not..??
I did a curl request from terminal, I see only the normal plain JSON.
My post Content Compression with Rack::Deflater describes a couple of ways to integrate Rack::Deflater. The easiest would be to just update config/application.rb with:
module YourApp
class Application < Rails::Application
config.middleware.use Rack::Deflater
end
end
and you'll automatically compress all controller responses with deflate / gzip if the client explicitly says they can handle it.
For the response to be in gzip format we don't have to change the render method call.
If the request has the header Accept-Encoding: gzip, Rails will automatically compress the JSON response using gzip.
If you don't want the user to send a request with preset header., you can add the header to the request manually in the controller before rendering the response:
request.env['HTTP_ACCEPT_ENCODING'] = 'gzip'
render :json => response.to_json()
You can query Curl by setting a custom header to get gzipped response
$ curl -H "Accept-Encoding: gzip, deflate" localhost:3000/posts.json > posts_json.gz
then, then decompress it to view the actual response json
$ gzip -d posts_json.gz
$ cat posts_json
If it doesn't work. post back with output of rake middlewares to help us troubleshoot further.
In some cases you can consider to write huge response into a file and gzip it:
res = {} # huge data hash
json = res.to_json
Zlib::GzipWriter.open('public/api/huge_data.json.gz') { |gz| gz.write json }
and update this file regularly
Consider not putting Rack middlewares in config.ru when using Rails
Rails has it's own middleware stack manager since Rails 2.
The correct way is:
# config/application.rb or config/environment.rb depends on your Rails version
config.middleware.use Rack::Deflater
Don't use #djcp's solution when using Rack::ETag
Short answer:
module MyApp
class Application < Rails::Application
config.middleware.insert_before Rack::ETag, Rack::Deflater
end
end
The order of Rack::Deflater and Rack::ETag matters because Rack::Deflater uses Zlib::GzipWriter to compress the response body and it would compress with a timestamp by default, which means the compressed response body would change every second even if the original response body is the same.
To reproduce this problem, run the following script:
require 'rack/etag'
require 'rack/deflater'
require 'rack/content_length'
#app = Rack::Builder.new do
use Rack::ETag
use Rack::Deflater
use Rack::ContentLength
run ->(*) { [200, {}, ['hello world']] }
end
def puts_etag
puts #app.call({ 'HTTP_ACCEPT_ENCODING' => 'gzip' })[1]['ETag']
end
puts_etag
sleep 1
puts_etag
One can simply swap the lines of Rack::ETag and Rack::Deflater and get the expected output.
Rails uses Rack::ETag by default and config.middleware.use is just appending. To insert Rack::Deflater before Rack::Etag, use config.middleware.insert_before instead.
🍻
Related
I have a Rails + React app that is ~3mb (too big!)
About 2mb of that is a single instance variable, #songs, that's passed to the react component like this:
<%= react_component('SongApp', songData: #songs, songId: #song_id, allBooks: Book.reactify) %>
By passing the data in like that, I can immediately use it as this.props.songData in the component.
I want to compress this data (which is a JSON string)—so the client device doesn't need to download so much—and decompress within the React component on the client device. How can I do this?
Use Rack::Deflater
Rack::Deflater middleware compresses responses at runtime using deflate or trusty ol’ gzip. Inserted correctly into your Rack app, it can drastically reduce the size of your HTML / JSON controller responses
Add it to config/application.rb thusly
module YourApp
class Application < Rails::Application
config.middleware.use Rack::Deflater
end
end
Answer Number 2
For the response to be in gzip format we don't have to change the render method call.
If the request has the header Accept-Encoding: gzip, Rails will automatically compress the JSON response using gzip.
If you don't want the user to send a request with preset header., you can add the header to the request manually in the controller before rendering the response:
request.env['HTTP_ACCEPT_ENCODING'] = 'gzip'
render :json => response.to_json()enter code here
check this gist
I'm using Net::HTTP with Ruby to crawl an URL.
I don't want to crawl streaming audio such as: http://listen2.openstream.co/334
in fact i only want to crawl Html content, so no pdfs, video, txt..
Right now, I have both open_timeout and read_timeout set to 10, so even if I do crawl these streaming audio pages they will timeout.
url = 'http://listen2.openstream.co/334'
path = uri.path
req= Net::HTTP::Get.new(path, {'Accept' => '*/*', 'Content-Type' => 'text/plain; charset=utf-8', 'Connection' => 'keep-alive','Accept-Encoding' => 'Identity'})
uri = Addressable::URI.parse(url)
resp = Net::HTTP.start(uri.host, uri.inferred_port) do |httpRequest|
httpRequest.open_timeout = 10
httpRequest.read_timeout = 10
#how can I read the headers here before it's streaming the body and then exit b/c the content type is audio?
httpRequest.request(req)
end
However, is there a way to check the header BEFORE I read the body of a http response to see if it's an audio? I want to do so without sending a separate HEAD request.
net/http supports streaming, you can use this to read the header before the body.
Code example,
url = URI('http://stackoverflow.com/questions/41306082/ruby-nethttp-read-the-header-before-the-body-without-head-request')
Net::HTTP.start(url.host, url.port) do |http|
request = Net::HTTP::Get.new(url)
http.request(request) do |response|
# check headers here, body has not yet been read
# then call read_body or just body to read the body
if true
response.read_body do |chunk|
# process body chunks here
end
end
end
end
I will add a ruby example later tonight. However, for a quick response. There is a simple trick to do this.
You can use HTTP Range header to indicate if which range of bytes you want to receive from the server. Here is an example:
curl -XGET http://www.sample-videos.com/audio/mp3/crowd-cheering.mp3 -v -H "Range: bytes=0-1"
The above example means the server will return data from 0 to 1 byte range.
See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Range_requests
Since I did not find a way to properly do this in Net::HTTP, and I saw that you're using the addressable gem as an external dependency already, here's a solution using the wonderful http gem:
require 'http'
response = HTTP.get('http://listen2.openstream.co/334')
# Here are the headers
puts response.headers
# Everything ok? Start streaming the response
body = response.body
body.stream!
# now just call `readpartial` on the body until it returns `nil`
# or some other break condition is met
Sorry if you're required to use Net::HTTP, hopefully someone else will find an answer. A separate HEAD request might indeed be the way to go in that case.
You can do a whole host of net related things without using a gem. Just use the net/http module.
require 'net/http'
url = URI 'http://listen2.openstream.co/334'
Net::HTTP.start(url.host, url.port){|conn|
conn.request_get(url){|resp|
resp.each{|k_header, v_header|
# process headers
puts "#{k_header}: #{v_header}"
}
#
# resp.read_body{|body_chunk|
# # process body
# }
}
}
Note: while processing headers, just make sure to check the content-type header. For audio related content it would normally contain audio/mpeg value.
Hope, it helped.
I am using Swagger-UI to browse my own API, built with grape and automatically documented with grape-swagger.
I've googled and tried every suggestion I can find, but I cannot get POST to work. Here's my headers:
header "Access-Control-Allow-Origin", "*"
header "Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, PATCH, DELETE"
header "Access-Control-Request-Method", "*"
header "Access-Control-Max-Age", "1728000"
header "Access-Control-Allow-Headers", "api_key, Content-Type"
I just threw in everything suggested. I've enabled all the HTTP methods in supportedSubmitMethods and I have tested the API using the POSTMAN Chrome extension and it works perfectly. Creates a user properly and returns the correct data.
However all I get with swagger post is the server reporting:
Started OPTIONS "/v1/users.json" for 127.0.0.1 at 2012-12-21 04:07:13 -0800
and swagger response looking like this:
Request URL
http://api.lvh.me:3000/v1/users.json
Response Body
Response Code
0
Response Headers
I have also tested the OPTIONS response with POSTMAN and it is below:
Allow →OPTIONS, GET, POST
Cache-Control →no-cache
Date →Fri, 21 Dec 2012 12:14:27 GMT
Server →Apache-Coyote/1.1
X-Request-Id →9215cba8da86824b97c6900fb6d97aec
X-Runtime →0.170000
X-UA-Compatible →IE=Edge
I had the same problem and just solved it, hope this helps somebody.
Swagger-UI accepts multiple parameters through POST only through a 'form' paramType, not 'body' paramType, referenced in this issue https://github.com/wordnik/swagger-ui/issues/72.
I used the branch :git => 'git://github.com/Digication/grape-swagger.git' changing 'post' request paramType to 'form'. Generated xml output for swagger_doc (probably at path/swagger_doc/api or similar) should look something like this:
<api>
<path>/api/v2/...</path>
<operations type="array">
...
<httpMethod>POST</httpMethod>
<parameters type="array">
<parameter>
<paramType>form</paramType>
...More
Not
<paramType>body</paramType>
...More
I used the grape-swagger-rails gem to automatically install swagger-ui on localhost (files can also be downloaded from the swagger-ui site), and everything works!!
Had the same problem. Fixed by adding CORS
add into Gemfile:
gem 'rack-cors', :require => 'rack/cors'
add into application.rb
config.middleware.use Rack::Cors do
allow do
origins '*'
# location of your API
resource '/*', :headers => :any, :methods => [:get, :post, :options, :put]
end
end
be sure that you've changed location of your API here.
Nice to hear you are using grape-swagger: I think it is awesome :)
I am not entirely sure you are having the same problem, but when testing locally from the browser it will try to check if the origin is the same as requested, so to make sure I do not get that error, I created a small middleware that will tell the browser we allow all origin.
I am using a rails process (created with the awesome rails-api gem), so I create a new file in lib/middleware/access_control_allow_all_origin.rb with the following content:
module Middleware
class AccessControlAllowAllOrigin
def initialize(app)
#app = app
end
def call(env)
status, headers, body = #app.call(env)
allow_all_origin!(headers)
[status, headers, body]
end
private
def allow_all_origin!(headers)
headers['Access-Control-Allow-Origin'] = '*'
headers['Access-Control-Request-Method'] = '*'
end
end
end
and at the bottom of my application.rb I just add the middleware as follows:
require 'middleware/access_control_allow_all_origin'
config.middleware.insert_after Rack::ETag, Middleware::AccessControlAllowAllOrigin
Hope this helps.
I do not know about the solution for ruby-on-rails as I am using Swagger with play framework 2.0.2.
I provided a domain name to it and changed the basePath to domain name in application.conf file as swagger.api.basepath="domain-name" and it worked.
You can change the basePath in api-docs to domain-name. I read about the api-docs on
api-docs.
And does your web server hijack headers? If you are using NGinx for example, your "OPTIONS" request might not send the appropriate values as a response, in some cases.
What is your OPTIONS request response? Can you dump it out here? I'll tell you if it can be that.
I'd like to debug the request my Rails app makes with RestClient. The RestClient docs say:
To enable logging you can
set RestClient.log with a ruby Logger
or set an environment variable to avoid modifying the code (in this case you can use a file name, “stdout” or “stderr”):
$ RESTCLIENT_LOG=stdout path/to/my/program
Either produces logs like this:
RestClient.get "http://some/resource"
=> 200 OK | text/html 250 bytes
RestClient.put "http://some/resource", "payload"
=> 401 Unauthorized | application/xml 340 bytes
Note that these logs are valid Ruby, so you can paste them into the restclient shell or a >script to replay your sequence of rest calls.
How do I do get these logs included in my Rails apps log folder?
from: https://gist.github.com/jeremy/1383337
require 'restclient'
# RestClient logs using << which isn't supported by the Rails logger,
# so wrap it up with a little proxy object.
RestClient.log =
Object.new.tap do |proxy|
def proxy.<<(message)
Rails.logger.info message
end
end
Create a file in config/initializers:
RestClient.log = 'log/a_log_file.log'
Or just put last in console
https://github.com/adelevie/rest-client/commit/5a7ed325eaa091809141d3ef6e31087569614e9d
This worked for me, running on RestClient 1.8 and Rails 4.2.1:
::RestClient.log = Rails.logger
You can use this gem:
https://github.com/uswitch/rest-client-logger
It works out of the box just by adding "gem 'rest-client-logger'" to your Gemfile.
may be so: RestClient.log = Rails.logger
I've setup unicorn in rails 3.1 and http streaming works until I enable Rack::Deflater.
I've tried both with and without use Rack::Chunked. In curl I can see my response while in chrome I get the following errror: ERR_INVALID_CHUNKED_ENCODING
The result is same in other browsers (firefox, safari) and between development (osx) and production (heroku).
config.ru:
require ::File.expand_path('../config/environment', __FILE__)
use Rack::Chunked
use Rack::Deflater
run Site::Application
unicorn.rb:
listen 3001, :tcp_nopush => false
worker_processes 1 # amount of unicorn workers to spin up
timeout 30 # restarts workers that hang for 30 seconds
controller:
render "someview", :stream => true
Thanks for any help.
The problem is that Rails ActionController::Streaming renders directly into a Chunked::Body. This means the content is first chunked and then gzipped by the Rack::Deflater middleware, instead of gzipped and then chunked.
According to the HTTP/1.1 RFC 6.2.1, chunked must be last applied encoding to a transfer.
Since "chunked" is the only transfer-coding required to be understood
by HTTP/1.1 recipients, it plays a crucial role in delimiting messages
on a persistent connection. Whenever a transfer-coding is applied to a
payload body in a request, the final transfer-coding applied must be
"chunked".
I fixed it for us by monkey patching ActionController::Streaming _process_options and _render_template methods in an initializer so it does not wrap the body in a Chunked::Body, and lets the Rack::Chunked middleware do it instead.
module GzipStreaming
def _process_options(options)
stream = options[:stream]
# delete the option to stop original implementation
options.delete(:stream)
super
if stream && env["HTTP_VERSION"] != "HTTP/1.0"
# Same as org implmenation except don't set the transfer-encoding header
# The Rack::Chunked middleware will handle it
headers["Cache-Control"] ||= "no-cache"
headers.delete('Content-Length')
options[:stream] = stream
end
end
def _render_template(options)
if options.delete(:stream)
# Just render, don't wrap in a Chunked::Body, let
# Rack::Chunked middleware handle it
view_renderer.render_body(view_context, options)
else
super
end
end
end
module ActionController
class Base
include GzipStreaming
end
end
And leave your config.ru as
require ::File.expand_path('../config/environment', __FILE__)
use Rack::Chunked
use Rack::Deflater
run Roam7::Application
Not a very nice solution, it will probably break some other middlewares that inspect/modify the body. If someone has a better solution I'd love to hear it.
If you are using new relic, its middleware must also be disabled when streaming.